跳到正文
返回

JVM 内存结构

发表于 更新于
浏览量: --
系列文章: JVM 快速入门 1 / 4
  1. JVM 内存结构 当前文章
  2. JVM 垃圾回收
  3. 字节码与类加载
  4. Java 内存模型

封面来源:由博主个人绘制,如需使用请联系博主。

参考链接:黑马程序员JVM快速入门

本文涉及的代码:mofan212/jvm-demo

除特别注明外,本文内容都基于 JDK 1.8

1. 程序计数器

Program Counter Register,程序计数器(寄存器)。

作用:记住下一条 JVM 指令的执行地址

特点:

JVM内存结构之程序计数器

下面是 JVM 指令与对应 Java 代码的一个示例:

0: getstatic     #20                        // PrintStream out = System.out; 
3: astore_1                                 // -- 
4: aload_1                                  // out.println(1); 
5: iconst_1                                 // -- 
6: invokevirtual #26                        // -- 
9: aload_1                                  // out.println(2); 
10: iconst_2                                // -- 
11: invokevirtual #26                       // -- 
14: aload_1                                 // out.println(3); 
15: iconst_3                                // -- 
16: invokevirtual #26                       // -- 
19: aload_1                                 // out.println(4); 
20: iconst_4                                // -- 
21: invokevirtual #26                       // -- 
24: aload_1                                 // out.println(5); 
25: iconst_5                                // -- 
26: invokevirtual #26                       // -- 
29: return

Java 源代码经过编译后得到二进制字节码,字节码中包含许多的 JVM 指令。

CPU 并不能直接执行 JVM 指令,在这中间还需要一个「解释器」。「解释器」将 JVM 指令解释为机器码,之后再由 CPU 执行这些机器码。

上述示例的 JVM 指令前都有一个数字,它们是 JVM 指令的执行地址。

当前一条指令执行完成后,解释器会去「程序计数器」中取得下一条 JVM 指令的执行地址,然后再执行。

Java 程序支持多线程运行。当系统中有多个线程正在运行时,CPU 会为每个线程分配时间片。如果一个线程中的逻辑在一个时间片内未执行完,CPU 会将该线程的状态进行暂存,然后切换到另一个线程并执行它的逻辑,后续又轮到第一个线程执行时,能够继续执行 剩余 逻辑。

由于「程序计数器」是线程私有的,线程在时间片轮转的过程中能够轻松得知下一条指令的执行地址,完成剩余逻辑的执行。

2. 虚拟机栈

2.1 栈与栈帧

Java Virtual Machine Stacks,Java 虚拟机栈。

JVM内存结构之虚拟机栈

每个 Java 线程在运行时需要一定的内存空间,这个内存空间就是「虚拟机栈」。如果有多个运行的线程,也对应着多个「虚拟机栈」。

「虚拟机栈」中存放的元素是「栈帧」,一个「栈帧」对应着一次「方法调用」,是每个方法运行时需要的内存。方法可以接受参数,内部可以定义局部变量,还可以有返回值,这些信息都需要占用内存,因此在方法执行前需要预先分配一定的内存空间。

Java 线程在执行一个方法时,会为该方法划分一段栈帧空间,并将「栈帧」压入「虚拟机栈」内;当方法执行完成后,再将对应「栈帧」从「虚拟机栈」中移除。

2.2 问题辨析

垃圾回收是否涉及栈内存?

不涉及,栈中的数据在作用域结束后自行释放,垃圾回收只会回收堆中数据。

栈内存分配越大越好吗?

可以通过 -Xss 虚拟机参数来指定栈内存。64 位的 Linux 和 macOS 系统下,其默认值为 1024KB,而在 Windows 操作系统下,其默认值取决于虚拟内存。

下面示例展示了使用不同单位设置虚拟机栈大小为 1m:

-Xss1m
-Xss1024k
-Xss1048576

在 IDEA 中,可以在程序的 VM options 中设置,设置方式与上述一致。

每个线程在创建的时候都会创建一个「虚拟机栈」,由于物理内存的大小是一定的,当虚拟机栈的大小越大时,可供使用的线程数就会越少。

方法内的局部变量是否是线程安全的?

存在 m1()m2()m3() 共三个方法,判断每个方法使用的 sb 对象是否是线程安全的:

public static void m1() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
}

public static void m2(StringBuilder sb) {
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
}

public static StringBuilder m3() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    return sb;
}

2.3 栈内存溢出

什么情况下会导致栈内存溢出(java.lang.StackOverflowError)?

绝大多数栈内存溢出都是由于 栈帧过多 导致,比如:

2.4 线程运行诊断

CPU 占用过多

【下篇】Linux 基础 中的 「排查 Java 进程导致 CPU 飙升」

程序运行很长时间没有效果

这往往是因为产生了「死锁」,借助 jstack pid 查看对应进程下 JVM 中当前时刻的线程快照。

仔细阅读最后的 Java stack information for the threads listed above: 信息即可定位到问题代码。

3. 本地方法栈

Native Method Stack,本地方法栈。

用于管理通过 JNI(Java Native Interface) 调用的非 Java 代码(如 C/C++)的执行。

与虚拟机栈类似,只不过虚拟机栈用于处理 Java 方法的调用,本地方法栈用于处理 Native 方法的调用。

本地方法栈也是线程私有的。

在某些 JVM 的实现中,会将本地方法栈和虚拟机栈合并为一个栈,以简化实现。

JVM内存结构之本地方法栈

4. 堆

4.1 堆的特点

Heap,堆。

通过 new 关键字创建的对象都会使用堆内存。

特点:

JVM内存结构之堆

4.2 堆内存溢出

堆内存溢出的异常信息:java.lang.OutofMemoryError :java heap space。比如:

public class Demo_1 {
    public static void main(String[] args) {
        int i = 0;

        try {
            ArrayList<String> list = new ArrayList<>();
            String a = "hello";
            // 不断向 list 中添加对象
            while (true) {
                list.add(a);
                a = a + a;
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

堆内存溢出OOM示例

与使用 -Xss 设置栈内存大小类似,可以使用 -Xmx 来指定最大堆内存大小。

4.3 堆内存诊断

jps 工具:查看当前系统中有哪些 Java 进程

jmap 工具:查看某一时刻堆内存占用情况,比如 jmap -heap pid

jconsole 工具:多功能图形界面监测工具,可以连续监测

jvisualvm 工具:图形化的虚拟机工具,可以利用它的「堆转储」(dump)功能分析某一时刻堆的使用情况。注意,在 JDK9 之后的版本中已经不再自带该工具,需要额外下载

5. 方法区

5.1 定义与组成

JVM内存结构之方法区

以 JDK8 为例,JVM 规范中对于 方法区 的定义如下:

JVM规范中对方法区的定义

用 DeepSeek 翻译下主要内容:

JDK 1.8 版本之前方法区使用堆的内存,叫永久代;JDK 1.8 之后用操作系统的内存,叫元空间。

Java6中JVM的堆与方法区

Java8中JVM的堆与方法区.

5.2 方法区内存溢出

不同 Java 版本中方法区的实现不相同,因此方法区内存溢出时的错误信息也有所区别:

Java 8 中元空间的大小默认与系统内存相关,为方便演示内存溢出,执行下列演示代码前需要先设置 JVM 参数 -XX:MaxMetaspaceSize=10m,指定最大元空间的大小为 10m:

import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=10m
 *
 * @author mofan
 * @date 2025/5/3 21:07
 */
public class Demo_2 extends ClassLoader { // 用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo_2 test = new Demo_2();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 用于生产类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号,public,类名,包名,所属父类,实现的接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回字节码的 byte[]
                byte[] code = cw.toByteArray();
                // 执行类的加载
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

方法区内存溢出OOM示例

5.3 运行时常量池

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

要运行上述代码,需要将其编译成二进制字节码,它主要包括:

  1. 类的基本信息
  2. 常量池
  3. 类的方法定义(包含虚拟机指令)

使用 javap -v HelloWorld.class 反编译字节码:

// ------------- 以下是「类的基本信息」-------------
Classfile /D:/Code/Java/MyCode/jvm-demo/structure/target/classes/indi/mofan/HelloWorld.class
  Last modified 2025-5-3; size 555 bytes
  MD5 checksum ac4ed814f06117795da5bc9fe3c3c0ef
  Compiled from "HelloWorld.java"
public class indi.mofan.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// ------------- 以上是「类的基本信息」-------------
  
// ------------- 以下是「常量池」-------------
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // indi/mofan/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lindi/mofan/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               indi/mofan/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
// ------------- 以上是「常量池」-------------

// ------------- 以下是「类的方法定义」------------- 
{
  public indi.mofan.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lindi/mofan/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
// ------------- 以上是「类的方法定义」-------------

SourceFile: "HelloWorld.java"

每条虚拟机指令都会对应常量池表中一个地址(比如 getstatic #2 中的 #2 就是常量池中的一个地址),常量池表中的地址可能对应着一个类名、方法名、参数类型等信息。

6. StringTable

6.1 特性

StringTable(字符串常量池) 有如下特性:

public class Demo_3_1 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
    }
}

编译上述代码后,使用 javap -v 反编译 Demo_3_1.class

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return

StringTable 是一个 Hashtable 结构,并且不能扩容,程序刚运行时,StringTable 内没有任何元素。

在加载 Demo_3_1.class 时,其常量池中的信息会被加载到「运行时常量池」中,这时 abab 都是常量池中的符号,不是 Java 字符串对象。

当执行到 ldc #2 时,会把符号 a 变为 "a" 字符串对象,bab 也是如此(字符串延迟加载)。

main() 末尾再增加一句代码:

public static void main(String[] args) {
    String s1 = "a";
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
}

再使用 javap 反编译字节码:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4
        29: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  args   [Ljava/lang/String;
            3      27     1    s1   Ljava/lang/String;
            6      24     2    s2   Ljava/lang/String;
            9      21     3    s3   Ljava/lang/String;
           29       1     4    s4   Ljava/lang/String;

0:8: 与原先一样,在 9: 处使用了 new 关键字,然后在 13: 处执行了 StringBuilder<init> 方法,也就是执行了 StringBuilder 的构造方法,对应 Java 代码是 new StringBuilder()

接着执行 16: aload_1,这会从 LocalVariableTable 中加载 Slot = 1 处的变量,也就是 s1;然后在 17: 处执行了 StringBuilderappend() 方法,并将刚刚加载的 s1 作为参数传入,对应 Java 代码是 append(s1)

后续使用同样的方式加载 s2,并将其作为参数调用 append() 方法。

来到 24: 处,这里调用了 StringBuildertoString() 方法。

结合前面内容,整体对应的 Java 代码为:

(new StringBuilder()).append(s1).append(s2).toString();

最后在 27: 处执行 astore 4,表示将 toString() 方法的结果放入 LocalVariableTableSlot = 4 的位置。

经过上述步骤得到的 s4 并不在 StringTable 中,与其中的 ab 显然不是同一个对象,因此:

System.out.println(s3 == s4); // false

如果在 main() 方法后再加一句呢?

String s5 = "a" + "b";

同样反编译字节码:

        6: ldc            #4                  // String ab
        8: astore_3
		
        29: ldc           #4                  // String ab
        31: astore        5

这时执行的是 ldc 指令,从运行时常量池中加载 #4 位置的符号,对应 ab。没有加载符号 a,也没有加载符号 b,而是直接加载的 ab

String s3 = "ab"; 对应的字节码进行对比,它们几乎一样。

这时 javac 在编译期间的优化,s5 由常量 ab 进行拼接,其值能够直接在编译期间确定,因此编译器对其进行优化。那么:

System.out.println(s3 == s5); // true

JDK 1.8 中的 String#intern()

先来句代码:

String s = new String("a") + new String("b");

执行这句代码后会得到 6 个对象:

得到的 s 的值是 ab,注意此时 ab 并不在 StringTable 中。

加一句代码:

String s2 = s.intern();

尝试将字符串对象 s 放入 StringTable 中(如果 StringTable 内已经有了对应的字符串,则不会放入),然后返回 StringTable 中对象的引用。

在这时,字符串对象 s 对应的 ab 字符串已经存在于 StringTable 中。

private static void intern1() {
    String s = new String("a") + new String("b");
    String s2 = s.intern();
    // true
    System.out.println(s2 == "ab");
    // true
    System.out.println(s == "ab");
}

那如果这样呢?

private static void intern2() {
    String x = "ab";
    String s = new String("a") + new String("b");
    String s2 = s.intern();
    System.out.println(s2 == x);
    System.out.println(s == x);
}

首先将 ab 添加到 StringTable 中,然后又会将 ab 添加到 StringTable 中。

执行 s.intern() 时,由于 StringTable 中已经有字符串 ab 了,那么不会再把 s 对象添加到 StringTable 中,intern() 方法返回 StringTable 中 ab 字符串的引用。

因此:

6.2 所在位置

Java6中JVM的堆与方法区

Java8中JVM的堆与方法区

6.3 StringTable 的垃圾回收

先介绍几个待会要用的虚拟机参数:

添加以上虚拟机参数后,运行下面的 main() 方法:

public class Demo_5 {
    public static void main(String[] args) {
        int i  = 0;
        try {
            for (int j = 0; j < 10000; j++) {
                String.valueOf(i).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
}

控制台输出类似以下信息:

[GC (Allocation Failure) [PSYoungGen: 2046K->496K(2560K)] 2046K->872K(9728K), 0.0007887 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

这证明触发了 GC。

在堆内存空间不足时,会触发 StringTable 的垃圾回收。

6.4 性能调优

使用 -XX:StringTableSize 参数

StringTable 的底层由 Hashtable 实现。当 Hashtable 中的桶数量太少,而添加到 StringTable 中的字符串太多时,会增加哈希碰撞的几率,也会增加将字符串添加到 StringTable 中使用的时间。

可以使用以下虚拟机参数调整桶个数:

-XX:StringTableSize=桶个数

可选的桶个数范围是 [1009, 2305843009213693951]

尝试将字符串添加到 StringTable

当系统中存在大量、可重复的字符串时,可以考虑调用 String#intern() 方法将字符串添加到 StringTable 中,以减少堆内存的使用。

7. 直接内存

7.1 定义

Direct Memory,直接内存。

Java 本身并不具备磁盘读写的能力,需要调用操作系统提供的函数来实现磁盘读写:

使用Java读取磁盘文件

要想提高效率,那么数据就不能复制两次。

「直接内存」是操作系统和 Java 代码都可以直接访问的一块区域,不用再将系统内存中的数据复制到 Java 堆内存中,相比前一种方式减少了一次数据复制,进而提高了效率。

Java使用直接内存读取磁盘文件

7.2 内存溢出

使用 ByteBuffer.allocateDirect() 方法来分配一块直接内存。

由于直接内存不受 JVM 的内存回收管理,因此使用不当可能会导致内存溢出。

public class Demo_6_1 {
    static int _100MB = 1024 * 1024 * 100;

    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        int i = 0;
        try {
            while (true) {
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100MB);
                list.add(byteBuffer);
                i++;
            }
        } finally {
            System.out.println(i);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:695)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at indi.mofan.Demo_6.main(Demo_6.java:21)

7.3 内存释放原理

直接内存不受 JVM 的内存回收管理。

做一个实验:

public class Demo_6_2 {

    static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
        System.out.println("分配完毕...");
        System.in.read();
        System.out.println("开始释放...");
        byteBuffer = null;
        System.gc();
        System.in.read();
    }
}

在 Windows 下使用任务管理器查看系统使用内存,运行上述 main() 方法后,发现新增一个 Java 进程,它占用了 1 GB 左右的内存。

在 IDEA 控制台键入回车后,刚刚新增的 Java 进程在任务管理器中消失,并且使用的系统内存下降了 1 GB。

不是说直接内存不受 JVM 的内存回收管理吗?怎么在执行 System.gc() 方法后,使用的系统内存下降了 1 GB?消失的这 1 GB 内存是因为触发了垃圾回收吗?

内存释放原理

开门见山地说,直接内存的分配与 Unsafe 类相关:

public class Demo_6_3 {
    static int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 获取分配的直接内存地址
        long base = unsafe.allocateMemory(_1GB);
        // 分配内存
        unsafe.setMemory(base, _1GB, (byte) 0);
        System.in.read();

        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }

    static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

查看 ByteBuffer#allocateDirect() 方法的源码:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 获取分配的直接内存地址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;



}

可以看到确实调用了 Unsafe#allocateMemory() 方法来申请内存,但并没有释放内存的代码。

关键在 Cleaner#create() 方法中,先看传入的 Deallocator 对象:

private static class Deallocator
    implements Runnable
{

    private static Unsafe unsafe = Unsafe.getUnsafe();

    private long address;
    private long size;
    private int capacity;

    private Deallocator(long address, long size, int capacity) {
        assert (address != 0);
        this.address = address;
        this.size = size;
        this.capacity = capacity;
    }

    public void run() {
        if (address == 0) {
            // Paranoia
            return;
        }
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
    }

}

Deallocator 实现了 Runnable 接口,是一个「回调任务」对象,在重写的 run() 方法中调用了 Unsafe#freeMemory() 方法。

那么是怎么使用 Cleaner 将它们串联起来呢?

Cleaner 是虚引用类型,当其关联的对象被回收时,就会触发它的 clean() 方法。

在调用 Cleaner#create() 方法时,传入的第一个参数就是它关联的对象:

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

this 表示 DirectByteBuffer 对象。

也就是当 DirectByteBuffer 对象被垃圾回收时,就会触发 Cleaner#clean() 方法:

public void clean() {
    if (!remove(this))
        return;
    try {
        // thunk 对象就是调用 create() 方法传入的 Deallocator 对象
        thunk.run();
    } catch (final Throwable x) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    if (System.err != null)
                        new Error("Cleaner terminated abnormally", x)
                            .printStackTrace();
                    System.exit(1);
                    return null;
                }});
    }
}

禁用显式垃圾回收对直接内存的影响

调用 System.gc() 方法显式地触发的垃圾回收是一次 Full GC,这里不展开 Full GC 的内容,知道 Full GC 是一种比较影响性能的垃圾回收即可。

为防止程序员在代码中使用了 System.gc() 方法,在进行 JVM 调优时可以使用 -XX:+DisableExplicitGC 参数来禁用这种显式地垃圾回收。

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
// --snip--
byteBuffer = null;
System.gc();

在使用 -XX:+DisableExplicitGC 参数后,那么上述代码中的 System.gc() 方法会失效,无法触发垃圾回收。尽管没有引用 byteBuffer 的地方(byteBuffer = null;),但在堆内存充裕的情况下,byteBuffer 对象还会一直存活,那么其对应的直接内存也无法及时得到释放,只有等到 JVM 回收 byteBuffer 对象时,这块直接内存才会被释放。

既然这样,那把 -XX:+DisableExplicitGC 参数去掉呗?

这肯定也不行。

如果要想及时释放不再使用的直接内存,应当使用 Unsafe 类来分配直接内存,在使用完之后调用 Unsafe#freeMemory() 方法释放直接内存。

如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
Functors, Applicatives and Monads In Pictures
下一篇
实现 Dubbo 默认服务