Runtime Data Areas Deep-Dive
Explore allocation, survivor copying, promotion, compaction, method-frame execution, class metadata lifetimes, and the configuration boundaries behind JVM memory behavior.
Young generation: optimize for short-lived objects
Many programs create numerous temporary objects that become unreachable quickly. A generational collector exploits this pattern by collecting younger objects more frequently instead of repeatedly processing the entire heap in the same way.
The source's Eden, S0, S1, and old-generation layout is a useful traditional copying-collector model. It is not a physical layout required by the JVM specification. G1 uses regions with changing roles, and other collectors organize generations or relocation differently. The diagrams here deliberately illustrate the traditional model.
Eden: the usual entry point in the traditional model
In this model, ordinary new allocations usually begin in Eden. When the collector needs more allocation capacity, a young collection can reclaim unreachable young objects and copy surviving ones elsewhere.
Do not infer that every new expression physically allocates in Eden. Large-object policies, collector design, allocation slow paths, and JIT allocation elimination can change what actually happens. Eden becoming full is a useful introductory trigger, not the only possible reason for a collection.
Survivor spaces: preserve only the live young objects
A traditional young copying collection visits live objects in Eden and the current from-survivor space. Objects that remain young are copied into the empty to-survivor space; unreachable objects are not copied, so their former storage can be reused.
The survivor spaces exchange from/to roles after collection. This is not an application copying the same object back and forth explicitly, and it does not guarantee that every object passes through both spaces: some die immediately and some are promoted early because of age or capacity conditions.
- Before collection: new objects occupy Eden; older young survivors may occupy the from-space.
- During collection: trace live objects, copy retained young objects, and update references.
- After collection: the destination becomes the next from-space; the other survivor space is available for a later collection.
Old generation: long-lived does not mean immortal
Objects that remain reachable through multiple young collections may be promoted into the old generation. Caches, application state, and long-lived service objects often contribute to old-generation occupancy, but their source-code type does not decide their generation.
A static reference can keep an object reachable while the associated class remains live. That can make long survival likely, but static does not mean immediate old-generation allocation. Old objects can still become unreachable and be reclaimed according to the collector's policies.
Allocation establishes instance state and a reference
Conceptually, new Student() ensures required class initialization has occurred, obtains instance storage, establishes default field values, and invokes constructor logic. The expression yields a reference that the caller can store in a local slot, a field, or an array element.
In HotSpot, many ordinary allocations use a thread-local allocation buffer inside the shared heap. A TLAB reduces contention on the allocation path; it does not make the resulting objects permanently private to that thread. Allocation outside a TLAB or allocation elimination are possible.
class Student {
final int id;
Student(int id) {
this.id = id;
}
}
Student local = new Student(101);
// local is a reference; the Student instance has its own field state.Promotion depends on survival and collector policy
A collector can track the age of objects surviving young collections and move sufficiently old survivors into the old generation. Age is a collection-survival concept, not elapsed wall-clock time or a count of method calls.
Promotion thresholds and early-promotion conditions depend on the collector and available survivor capacity. An application cannot rely on one fixed number of collections for every object. High survival rates can fill old space quickly even when allocation in Eden remains fast.
Fragmentation: enough free space, but not one suitable span
After some objects are reclaimed, free memory may be scattered between live objects. In a simplified six-slot heap containing O1, O2, a hole, O4, a hole, and O6, there are two free slots in total but no two-slot contiguous span.
A two-slot allocation cannot fit in those separated holes in this model. Compaction moves the live objects together, yielding one two-slot free span. The example deliberately requests two slots: two free slots cannot satisfy a three-slot allocation even after compaction.
Collectors can copy, compact, or relocate live objects and update references to limit fragmentation. Region-based heaps and large-object allocation rules make real allocation constraints more complex than the drawing, so fragmentation must be diagnosed in the context of the selected collector.
Stack frames are created for method invocations
Each logical method invocation has its own frame with local-variable slots, an operand stack, and runtime support for linking, exceptions, and returning to the caller. Two calls to the same method have separate invocation state.
When main calls display and display calls calculate, three frames are active in the conceptual stack. Only calculate executes until it returns or throws; its caller's frame remains available to resume. A JIT can inline calls, so a physical native stack need not literally match every box in this teaching model.
Local-variable slots hold values, including reference values
Parameters and method-local values occupy the frame's local-variable array in the JVM execution model. For an instance method, slot zero initially holds this; static methods have no implicit this parameter. Values of type long and double occupy two local slots.
For int x = 10 and String name = "John", the local values are the integer and a reference. The String object is on the heap, possibly reused through literal interning. Leaving the method discards its local frame state, not necessarily the referenced String or any other escaped object.
The operand stack evaluates expressions
The operand stack is working storage inside a frame, distinct from the local-variable array and from the thread's stack of method frames. Bytecode instructions push values, consume operands, and produce results.
For a static add(int a, int b) method, iload_0 pushes a, iload_1 pushes b, iadd consumes both integers and pushes their sum, and ireturn transfers the result to the caller. With inputs 5 and 10, the intermediate stack contains 5, then 5 with 10 on top, then 15.
This is the JVM's logical instruction model. Compiled execution may keep values in registers or eliminate intermediate operations while preserving the required result.
static int add(int a, int b) {
return a + b;
}
// Representative bytecode for this static method:
// 0: iload_0
// 1: iload_1
// 2: iadd
// 3: ireturnReturn continuation: resume the suspended caller
After calculate returns, display must continue after its invocation instruction rather than restart from its first line. The runtime preserves enough caller state to restore that continuation and transfer a return value when one exists.
The source calls this a return address, which is a useful conceptual label. Do not assume the specification requires a universal named return-address slot in every frame. The JVM returnAddress data type is historically associated with jsr/ret subroutines and should not be confused with ordinary modern method-return bookkeeping.
Trace call, return, and exception unwinding
In the example, main invokes display, and display invokes calculate. calculate prints first; after its return, display prints; after display returns, main prints. The observed output follows the call structure, not the order in which method declarations appear in the source.
On normal completion, each callee's frame is discarded and control returns to its caller. If calculate throws an exception that display does not handle, the runtime can discard display's frame too while searching for a handler. A return and an unhandled exception are different completion paths.
public class Demo {
public static void main(String[] args) {
display();
System.out.println("Back to main");
}
static void display() {
calculate();
System.out.println("Inside display");
}
static void calculate() {
System.out.println("Inside calculate");
}
}
// Output:
// Inside calculate
// Inside display
// Back to mainStackOverflowError: call depth exceeds stack capacity
An endlessly recursive fun() that immediately calls fun() creates more pending invocations without returning. Eventually the permitted stack cannot support the next call and the JVM reports StackOverflowError. Valid but excessively deep recursion can fail for the same reason.
This is primarily a call-stack capacity problem, not evidence that the heap is full. Adding a base case, bounding recursion, or using an iterative algorithm addresses the structure of the work. Raising -Xss can sometimes accommodate legitimate depth, but it does not fix unbounded recursion and can increase per-thread memory costs.
The source's stack examples assume ordinary platform-thread execution. Actual physical frame size and supported depth depend on the VM, compilation state, platform, and thread implementation; no fixed recursion count is portable.
static void broken() {
broken(); // No base case: eventually StackOverflowError.
}
static long factorial(int n) {
if (n < 0 || n > 20) {
throw new IllegalArgumentException("Use 0..20 for long");
}
long result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}Class metadata is shared by instances of a loaded class
Loading Employee makes its name, field descriptions, method structures, constructor information, access flags, superclass, and interfaces available to the runtime. These describe what an Employee is, rather than storing the id and name values of every Employee instance.
Sharing one loaded definition avoids duplicating method and field descriptions in each object. The defining class loader is part of class identity; loading the same name through separate defining loaders can create separate metadata and static state.
Runtime constant pool is not the String intern pool
Each loaded class or interface has a runtime constant pool derived from its class-file constant pool. It contains constants and symbolic information used to resolve classes, fields, and methods, with implementation-specific runtime structures backing resolution.
A literal such as "Hello" is represented through constant-pool information, but the resolved String is a heap object. The intern pool and the per-class runtime constant pool are different concepts. Similarly, a final local variable is not automatically a separate runtime constant-pool entry merely because it is declared final.
Static fields belong to the class, not each instance
Student.college represents class-associated state, shared by instances of the same loaded Student class. Changing it does not update a separate college field inside every Student object.
In HotSpot, static field values are stored with the heap-resident class mirror; metadata describes those fields in native structures. A static reference may point to another heap object and keep it reachable while the class is live. Calling all static values 'Metaspace contents' obscures this distinction.
class Student {
static String college = "ABC";
int id;
}
Student one = new Student();
Student two = new Student();
Student.college = "XYZ";
// One class-associated value; not one college copy per instance.Metaspace grows with loaded class metadata
Modern HotSpot allocates class metadata from native memory rather than using the old PermGen mechanism. Loading more distinct classes consumes metadata capacity even when the application creates relatively few instances.
Metadata reclamation depends on class unloading and class-loader reachability. Retaining obsolete loaders can retain their loaded classes, so dynamically generated classes, redeployments, or plugin systems can create metadata pressure. More heap does not directly raise a configured Metaspace ceiling.
MetaspaceSize is a threshold, not a preallocated slab
HotSpot's -XX:MetaspaceSize influences the initial metadata high-water mark for triggering collection associated with metadata pressure. The threshold can be adjusted by the VM; it is not equivalent to -Xms and does not mean that amount of Metaspace is eagerly allocated at startup.
-XX:MaxMetaspaceSize limits Metaspace growth. The source's example values of 128 MiB and 512 MiB are illustrative, not universal recommendations. Account for the JDK's metadata layout and class-space handling, and verify effective values for the runtime in use.
Before raising a limit, inspect loaded-class growth and whether old class loaders can be collected. A higher ceiling may delay failure without correcting unbounded class generation or loader retention.
// Illustrative HotSpot launch configuration, not a sizing recommendation:
// java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m HelloInstruction position follows control flow
A non-native method's PC state identifies its current JVM instruction. Sequential instructions advance through bytecode, branches select different offsets, and calls temporarily transfer execution to a callee before returning to a continuation.
A JVM bytecode offset is not generally the same number as a native machine-code address. JIT compilation can transform the physical instruction sequence while maintaining enough execution information to preserve Java behavior and support runtime services.
Independent threads need independent positions
Thread A may be at bytecode offset 25 while thread B is at offset 80, even in the same method. Their stacks and PC state record independent execution histories while both can access shared class definitions and heap objects.
The runtime and scheduler preserve execution state as threads progress or are suspended. This does not make unsynchronized updates safe: execution-state independence and shared-data visibility are different concerns.
Native execution leaves the bytecode instruction model
A native declaration such as native void openCamera() requires an implementation outside Java bytecode. Native code can call platform APIs, reuse C/C++ libraries, or interact with devices, but it must satisfy the platform's calling conventions and integration rules.
The JVM specification leaves the Java PC value undefined during a native method. Concrete runtimes preserve the call state needed to return safely, potentially using the same underlying stack for both Java and native work. Native memory corruption or invalid pointers are not prevented by Java bytecode verification.
JNI calls cross ownership and deployment boundaries
JNI connects Java and native code through defined operations rather than treating Java references as freely usable raw pointers. Native code can use local and global JNI references; their lifetimes affect which Java objects remain reachable.
An application using a native library must ship a compatible binary for each supported operating system and CPU architecture, and release native resources according to that library's rules. Native allocation failures can occur even with free capacity under -Xmx.
System.out.println illustrates a portable Java API eventually reaching platform output facilities. It does not establish that every println, every library API, or every JVM operation uses an identical JNI sequence. Keep that implementation detail separate from the conceptual Java-to-OS boundary.