Java applications have a reputation for consuming a lot of memory. In his talk “Principles of Memory Management in Java”, software engineer Ron Pressler argues that this observation misses the more important question: How efficiently does an application use RAM and CPU together?
Java’s garbage collectors do not manage memory like malloc and free in C or C++. Allocating an object is usually little more than advancing a pointer. When a collection becomes necessary, the garbage collector identifies reachable objects and compacts them. It does not individually free every dead object.
This distinction matters because the cost of a moving collector depends primarily on the live set, the allocation rate, and the available heap headroom. Giving the JVM more headroom allows it to collect less frequently and therefore spend less CPU time on memory management. A smaller heap reduces the application’s footprint, but usually at the cost of more garbage-collection work.
The JDK’s collectors are also generational. This exploits the empirical observation that most objects die young. Short-lived objects are collected in the young generation, while objects that survive several cycles move into the old generation. Consequently, the young generation normally has a small live set, while the old generation has a low allocation rate. This combination makes memory management particularly efficient.
That model fits typical Java and Spring business applications remarkably well. Processing an HTTP request may create DTOs, validation results, JSON structures, security contexts, database projections, and numerous framework-internal objects. Most become unreachable almost immediately after the request completes. Long-lived objects such as configuration, metadata, and application caches naturally behave differently and are handled in the older generation.
The practical lesson is not simply to give every Spring application a huge heap. It is to stop treating the smallest possible memory footprint as the only measure of efficiency. An application that uses less RAM but substantially more CPU may be more expensive to run—especially when cloud instances and Kubernetes resources package memory and processing capacity together.
Heap sizing should therefore reflect the actual deployment unit and workload. Set a deliberate maximum heap size instead of relying blindly on JVM defaults, while leaving sufficient room for metaspace, thread stacks, direct buffers, and other native memory. When increasing CPU capacity to handle more traffic, also test whether additional heap headroom reduces garbage-collection overhead.
Pressler also warns against drawing conclusions from synthetic benchmarks. Allocation patterns, object lifetimes, caches, I/O, and surrounding application code all influence JVM behaviour. For a real Spring application, measure allocation rate, live-set size, garbage-collection CPU, latency, and throughput under a representative workload. JMX and Java Flight Recorder provide the necessary visibility.
The broader point is reassuring: Java does not merely tolerate using additional memory. Its moving, generational collectors can deliberately turn RAM into saved CPU cycles. What looks like bloat in an isolated memory chart may be an efficient use of the machine as a whole.