Understanding the JVM Architecture and Memory Management

The JVM Memory Model

The JVM architecture primarily consists of the class loader system, memory management, bytecode execution engine, and garbage collection mechanisms.

Class Loading Mechanism

The class loader hierarchy includes the Bootstrap ClassLoader, Extension ClassLoader, and Application ClassLoader.

The Bootstrap ClassLoader handles core Java classes, typically found in rt.jar.

The Extension ClassLoader manages classes located in the java.ext directory.

The Application ClassLoader loads user-defined classes.

The loading strategy employs both the parenet delegation model and the full-delegation principle.

In the parent delegation model, when loading a class, the current loader first checks its parent for availability. If the parent cannot load it, the request is passed down until the Bootstrap ClassLoader attempts to load core classes. Since it can't load application classes, the responsibility returns up the chain until the Application ClassLoader loads them.

The full-delegation principle means that if no explicit delegation occurs, all classes within a loader's scope are processed.

To override the parent delegation mechanism, one can examine the source code:

 1 protected Class<?> loadClass(String name, boolean resolve)
 2         throws ClassNotFoundException
 3     {
 4         synchronized (getClassLoadingLock(name)) {
 5             // First, check if the class has already been loaded
 6             Class<?> c = findLoadedClass(name);
 7             if (c == null) {
 8                 long t0 = System.nanoTime();
 9                 try {
10                     if (parent != null) {
11                         c = parent.loadClass(name, false);
12                     } else {
13                         c = findBootstrapClassOrNull(name);
14                     }
15                 } catch (ClassNotFoundException e) {
16                     // ClassNotFoundException thrown if class not found
17                     // from the non-null parent class loader
18                 }
19 
20                 if (c == null) {
21                     // If still not found, then invoke findClass in order
22                     // to find the class.
23                     long t1 = System.nanoTime();
24                     c = findClass(name);

25                     // this is the defining class loader; record the stats
26                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
27                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
28                     sun.misc.PerfCounter.getFindClasses().increment();
29                 }
30             }
31             if (resolve) {
32                 resolveClass(c);
33             }
34             return c;
35         }
36     }

The parent.loadClass call indicates where to modify the behavior by overriding loadClass and findClass.

JVM Memory Areas

Let's analyze the operand stack and local variable table with a simple example:

 1 //
 2 // Source code recreated from a .class file by IntelliJ IDEA
 3 // (powered by Fernflower decompiler)
 4 //
 5 
 6 package com.tuling;
 7 
 8 public class Main {
 9     public Main() {
10     }
11 
12     public int add() {
13         int a = 1;
14         int b = 2;
15         int c = (a + b) * 10;
16         return c;
17     }
18 
19     public static void main(String[] args) {
20         Main main = new Main();
21         main.add();
22         System.out.println("aaa");
23     }
24 }

By disassembling main.class with javap -c, we get the following bytecode:

  1 Classfile /E:/йљПЁгъ/01java-vip/tuling-vip-spring/springannopriciple01/target/test-classes/com/tuling/Main.class
  2   Last modified 2019-9-8; size 714 bytes
  3   MD5 checksum 316510b260c590e9dd45038da671e84e
  4   Compiled from "Main.java"
  5 public class com.tuling.Main
  6   minor version: 0
  7   major version: 52
  8   flags: ACC_PUBLIC, ACC_SUPER
  9 Constant pool:
 10    #1 = Methodref          #8.#28         // java/lang/Object."<init>":()V
 11    #2 = Class              #29            // com/tuling/Main
 12    #3 = Methodref          #2.#28         // com/tuling/Main."<init>":()V
 13    #4 = Methodref          #2.#30         // com/tuling/Main.add:()I
 14    #5 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
 15    #6 = String             #33            // aaa
 16    #7 = Methodref          #34.#35        // java/io/PrintStream.println:(Ljava/lang/String;)V
 17    #8 = Class              #36            // java/lang/Object
 18    #9 = Utf8               <init>
 19   #10 = Utf8               ()V
 20   #11 = Utf8               Code
 21   #12 = Utf8               LineNumberTable
 22   #13 = Utf8               LocalVariableTable
 23   #14 = Utf8               this
 24   #15 = Utf8               Lcom/tuling/Main;
 25   #16 = Utf8               add
 26   #17 = Utf8               ()I
 27   #18 = Utf8               a
 28   #19 = Utf8               I
 29   #20 = Utf8               b
 30   #21 = Utf8               c
 31   #22 = Utf8               main
 32   #23 = Utf8               ([Ljava/lang/String;)V
 33   #24 = Utf8               args
 34   #25 = Utf8               [Ljava/lang/String;
 35   #26 = Utf8               SourceFile
 36   #27 = Utf8               Main.java
 37   #28 = NameAndType        #9:#10         // "<init>":()V
 38   #29 = Utf8               com/tuling/Main
 39   #30 = NameAndType        #16:#17        // add:()I
 40   #31 = Class              #37            // java/lang/System
 41   #32 = NameAndType        #38:#39        // out:Ljava/io/PrintStream;
 42   #33 = Utf8               aaa
 43   #34 = Class              #40            // java/io/PrintStream
 44   #35 = NameAndType        #41:#42        // println:(Ljava/lang/String;)V
 45   #36 = Utf8               java/lang/Object
 46   #37 = Utf8               java/lang/System
 47   #38 = Utf8               out
 48   #39 = Utf8               Ljava/io/PrintStream;
 49   #40 = Utf8               java/io/PrintStream
 50   #41 = Utf8               println
 51   #42 = Utf8               (Ljava/lang/String;)V
 52 {
 53   public com.tuling.Main();
 54     descriptor: ()V
 55     flags: ACC_PUBLIC
 56     Code:
 57       stack=1, locals=1, args_size=1
 58          0: aload_0
 59          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 60          4: return
 61       LineNumberTable:
 62         line 10: 0
 63       LocalVariableTable:
 64         Start  Length  Slot  Name   Signature
 65             0       5     0  this   Lcom/tuling/Main;
 66 
 67   public int add();
 68     descriptor: ()I
 69     flags: ACC_PUBLIC
 70     Code:
 71       stack=2, locals=4, args_size=1
 72          0: iconst_1
 73          1: istore_1
 74          2: iconst_2
 75          3: istore_2
 76          4: iload_1
 77          5: iload_2
 78          6: iadd
 79          7: bipush        10
 80          9: imul
 81         10: istore_3
 82         11: iload_3
 83         12: ireturn
 84       LineNumberTable:
 85         line 13: 0
 86         line 14: 2
 87         line 15: 4
 88         line 16: 11
 89       LocalVariableTable:
 90         Start  Length  Slot  Name   Signature
 91             0      13     0  this   Lcom/tuling/Main;
 92             2      11     1     a   I
 93             4       9     2     b   I
 94            11       2     3     c   I
 95 
 96   public static void main(java.lang.String[]);
 97     descriptor: ([Ljava/lang/String;)V
 98     flags: ACC_PUBLIC, ACC_STATIC
 99     Code:
100       stack=2, locals=2, args_size=1
101          0: new           #2                  // class com/tuling/Main
102          3: dup
103          4: invokespecial #3                  // Method "<init>":()V
104          7: astore_1
105          8: aload_1
106          9: invokevirtual #4                  // Method add:()I
107         12: pop
108         13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
109         16: ldc           #6                  // String aaa
110         18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
111         21: return
112       LineNumberTable:
113         line 20: 0
114         line 21: 8
115         line 22: 13
116         line 23: 21
117       LocalVariableTable:
118         Start  Length  Slot  Name   Signature
119             0      22     0  args   [Ljava/lang/String;
120             8      14     1  main   Lcom/tuling/Main;
121 }
122 SourceFile: "Main.java"

Focusing on the add method, we see bytecode instructions like:

iconst_1: Pushes integer value 1 onto the stack.

istore_1: Stores the top stack value into local variable slot 1.

This stack refers to the operand stack.

Garbage Collection Algorithms: Mark-Sweep, Mark-Compact, Copying, Generational

Mark-Sweep: Involves marking objects to be collected followed by sweeping and deleting marked objects. It's the fundamental approach.

Mark-Compact: Similar to Mark-Sweep but moves live objects toward one end before cleaning the rest.

Copying: Divides memory into two equal halves. When one half fills, live objects are copied to the other half, clearing the first.

Why Garbage Collection?

Examining heap structure reveals memory usage patterns.

To inspect your application's heap, use jps to identify the process ID, then run jmap -heap [PID] to view heap details, such as:

 1 Attaching to process ID 7964, please wait...
 2 Debugger attached successfully.
 3 Server compiler detected.
 4 JVM version is 25.73-b02
 5 
 6 using thread-local object allocation.
 7 Parallel GC with 4 thread(s)
 8 
 9 Heap Configuration:
10    MinHeapFreeRatio         = 0
11    MaxHeapFreeRatio         = 100
12    MaxHeapSize              = 2128609280 (2030.0MB)
13    NewSize                  = 44564480 (42.5MB)
14    MaxNewSize               = 709361664 (676.5MB)
15    OldSize                  = 89653248 (85.5MB)
16    NewRatio                 = 2
17    SurvivorRatio            = 8
18    MetaspaceSize            = 21807104 (20.796875MB)
19    CompressedClassSpaceSize = 1073741824 (1024.0MB)
20    MaxMetaspaceSize         = 17592186044415 MB
21    G1HeapRegionSize         = 0 (0.0MB)
22 
23 Heap Usage:
24 PS Young Generation
25 Eden Space:
26    capacity = 34078720 (32.5MB)
27    used     = 4771760 (4.5507049560546875MB)
28    free     = 29306960 (27.949295043945312MB)
29    14.002169095552885% used
30 From Space:
31    capacity = 5242880 (5.0MB)
32    used     = 0 (0.0MB)
33    free     = 5242880 (5.0MB)
34    0.0% used
35 To Space:
36    capacity = 5242880 (5.0MB)
37    used     = 0 (0.0MB)
38    free     = 5242880 (5.0MB)
39    0.0% used
40 PS Old Generation
41    capacity = 89653248 (85.5MB)
42    used     = 0 (0.0MB)
43    free     = 89653248 (85.5MB)
44    0.0% used

If the old generation isn't full, a Full GC won't occur. Young GC happens upon filling the young generation, with out stopping the world.

Available Garbage Collectors

Types include Serial, ParNew, Parallel Scavenge, Serial Old, Parallel Old, CMS, and G1 collectors.

Serial Collector: Uses copying in the young generation and mark-compact in the old generation. Operates single-threaded, pausing all threads during collection.

ParNew Collector: Copies in the young generation and marks-compact in the old. Utilizes multiple threads for GC but pauses application threads.

Parallel Scavenge Collector: Similar to ParNew but focuses on throughput.

Serial Old and Parallel Old Collectors: Serve as older-generation versions of Serial and Parallel Scavenge collectors respectively, often combined with other collectors.

CMS Collector: Reduces stop-the-world time by marking reachable nodes concurrently, then performs final marking and cleanup with minimal pause.

G1 Collector: Divides heap into regions. Unlike tradisional generations, it maintains logical separation. Large objects go directly into Humongous regions to avoid frequent Full GCs. It allows users to specify collection time goals, prioritizing efficient collection based on cost-benefit analysis.

Tags: JVM garbage collection ClassLoader heap memory Bytecode

Posted on Mon, 31 Aug 2026 16:47:31 +0000 by kevinfwb