Runtime memory economics · July 18, 2026
Java memory in containers: why an idle application can still cost gigabytes
Heap used, heap committed, metaspace, direct buffers, thread stacks, page cache, and container RSS are different numbers. This guide explains what a memory graph means, why garbage collection may retain capacity, and how flexible RAM billing turns slack into cost.
Method
What this comparison measures
- Separate JVM heap used, committed and maximum from process RSS and cgroup memory.
- Model a 30-day continuously running service; published Railway memory pricing is used only as a transparent cost example.
- Treat memory timelines as illustrative workload shapes, not universal Java-versus-Node benchmarks.
- Evaluate performance and cost together because shrinking heap can trade RAM for GC CPU, latency, and OOM risk.
Heap used is not the container bill
Oracle's MemoryUsage model distinguishes init, used, committed, and max. Committed memory is guaranteed to the JVM and is always at least used. The JVM can request and release memory over time, but unused committed capacity is not the same as an unreachable object leak.[1]
The process also owns class metadata, JIT code, thread stacks, direct byte buffers, native libraries, allocator arenas, and mapped pages. Container and platform graphs commonly report a process or cgroup view closer to RSS, which includes more than live Java objects.[2][6]
The first chart is a modeled 2.8 GB resident process after a burst. The categories are deliberately visible so a reader can replace them with Native Memory Tracking, JMX, cgroup, and profiler evidence from a real application.
The JVM sizes itself against a boundary
Current Java documents that the available-memory basis is the smaller of physical memory and an environment constraint such as a container. MaxRAMPercentage controls how much of that basis can become heap; Oracle documents a default of 25 percent for the option in Java 25.[3]
A manually oversized -Xmx can leave too little headroom for native memory. A very small heap can increase collection frequency and tail latency. A container with no memory limit can consume node memory and become a stronger OOM-kill candidate; a container above its limit can be terminated reactively under memory pressure.[3][4][5]
The resource limit, JVM heap ceiling, thread count, direct-memory ceiling, and workload allocation rate therefore form one configuration. Copying a heap flag from a large bare-metal server into a 1 GB PaaS container is not neutral.
| Signal | What it measures | Useful question | Common mistake |
|---|---|---|---|
| Heap used after GC | Live Java objects | Is retained application state growing? | Calling every high RSS value a heap leak |
| Heap committed | Memory guaranteed to JVM heap | How much capacity is retained for reuse? | Assuming free committed heap costs nothing |
| Allocation rate | Bytes created per second | How hard is GC working? | Looking only at a static heap snapshot |
| Native memory | Metaspace, code, stacks, buffers, libraries | What exists outside heap? | Setting Xmx equal to container limit |
| Container RSS / working set | Kernel-accounted resident memory | What boundary is the platform billing or enforcing? | Comparing it directly with heap used |
| GC pause and CPU | Collection cost | Did the smaller heap damage latency? | Optimizing RAM without an SLA |
Memory retained after a burst is not automatically a leak
The JVM can keep committed heap after traffic falls because future allocation can reuse it cheaply. Oracle exposes MaxHeapFreeRatio, MinHeapFreeRatio, and incremental heap shrinking controls, while warning that lower values can trade footprint for performance and results vary by application.[3]
A leak has a different signature: the post-GC floor rises across comparable cycles, retained object paths grow, or native allocations never return. A single sawtooth that settles above startup proves only that the runtime warmed up.
The timeline below is an illustrative shape. Java and Node values are not vendor benchmarks; they show why startup, peak, immediate post-load, and settled measurements all belong in a capacity report.
Flexible memory pricing makes slack visible
Railway currently publishes RAM at $10 per GB per month of measured usage. A service that settles at 4 GB instead of 2 GB therefore carries roughly a $20 monthly memory difference before replicas, databases, and traffic. Other platforms bill provisioned tiers instead, but the economic question is the same.[7]
Do not tune from idle alone. Run startup, steady, burst, allocation-heavy, and recovery phases under the exact cgroup limit. Record OOM events, restarts, throughput, CPU, p95/p99 latency, GC pauses, and heap after GC.
Source register
Specifications and prices change. The links make this snapshot auditable.
Sources and commercial facts were checked on 2026-07-18. Prices exclude tax unless the source says otherwise.
- MemoryUsage ↗Oracle · product
- On-heap and off-heap memory ↗Oracle · product
- The java command and advanced runtime options ↗Oracle · product
- Assign memory resources to containers ↗Kubernetes · product
- Resource management for Pods and containers ↗Kubernetes · product
- process.memoryUsage ↗Node.js · product
- Railway pricing ↗Railway · pricing
- JVM metrics ↗Micrometer · observability