JVM 概述、内存结构、溢出、调优

Java
192
0
0
2023-11-19
标签   JVM

什么是 JVM ?

定义

  • Java Virtual Machine – java 程序的运行环境(java 二进制 字节码的运行环境)

好处

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态
  • jvm jre jdk

VM 概述、内存结构、溢出、调优

常见的 JVM

VM 概述、内存结构、溢出、调优

整体结构

VM 概述、内存结构、溢出、调优

内存结构

程序计数器

VM 概述、内存结构、溢出、调优

定义

  • Program Counter Register 程序计数器(寄存器)
  • 作用
  • 是记住下一条 j VM 指令的执行地址,也就是 线程 当前要执行的指令地址
  • 特点
  • 线程私有
  • 不会存在内存溢出(唯一)

VM 概述、内存结构、溢出、调优

虚拟机栈

VM 概述、内存结构、溢出、调优

定义

  • Java Virtual Machine Stacks ( Java 虚拟机栈)
  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 栈的大小
  • Linux /x64(64-bit):1024 KB
  • maxOS(64-bit):1024 KB
  • Oracle Solaris/x64(64-bit):1024 KB
  • Windows:The default value depends on virtual memory

VM 概述、内存结构、溢出、调优

问题

  • 垃圾回收是否涉及栈内存?
  • 不涉及。每一次方法调用之后栈帧会被弹出,释放内存,不需要垃圾回收。
  • 栈内存分配越大越好吗?
  • 不。计算机总的物理内存有限,栈内存越大,栈的数量就越少,能够开启的线程就越少
  • 方法内的局部变量是否线程安全?如果方法内局部变量没有逃离方法的作用访问,它是线程安全的如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出
 public  static   void  main(String[] args) throws  Exception {
        try {
            method();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(count);
        }
    }

    public static void method() {
        count++;
        method();
    }
Exception in thread "main" java.lang.StackOverflowError 

本地方法栈

定义

  • 管理本地方法,即非 Java 语言编写的方法( C语言 )的调用
  • Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库
  • 线程私有
  • HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一
 // Object 类中有大量的本地方法

    public final native Class<?> getClass();

    public native int hashCode();

    protected native Object clone() throws CloneNotSupportedException;

    public final native void notify();

    public final native void notifyAll();

    public final native void wait(long timeout) throws InterruptedException; 

定义

  • 通过 new 关键字,创建对象都会使用堆内存
  • 线程共享的,堆中对象都需要考虑线程安全的问题
  • 垃圾回收机制

堆内存溢出

  • 创建的对象被虚拟机认为有用,不被回收,最后可能造成 OOM
  • 注意不一定非得 new 对象的时候才会出现。
 public static void main(String[] args) throws Exception {
        String s = "a";
        ArrayList<String> array = new ArrayList<>();
        int count =;
        try {
            while (true) {
                s += "a";
                array.add(s);
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(count);
        }
    }
Exception in thread "main" java.lang. OutOfMemory Error: Java heap space 

堆内存诊断

  • jps 工具
  • 查看当前系统中有哪些 java 进程
  • jmap 工具
  • 查看堆内存占用情况 jmap – heap 进程id
  • jconsole 工具
  • 图形界面的,多功能的监测工具,可以连续监测
  • jvisualvm 工具
  • 更强大的可视化工具

实例:

  • 输出 1… 之后,线程休眠 30 秒
  • 终端输入 jps ,查看进程 id,寻找到 Main 线程的 pid
  • 终端输入 jmap -heap pid
  • 程序创建一个 10 MB 大小的 byte 数组,
  • 输出 2… 之后,线程休眠 30 秒
  • 终端输入 jmap -heap pid
  • 垃圾回收,释放数组内存
  • 输出 3… 之后,线程休眠
  • 终端输入 jmap -heap pid
 public static void main(String[] args) throws Exception {
        System.out.println("...");
        Thread.sleep();
         byte [] bytes = new byte[1024 * 1024 * 10];
        System.out.println("...");
        Thread.sleep();
        bytes = null;
        System.gc();
        System.out.println("...");
        Thread.sleep(L);
    } 

三次输入 jmap -heap pid 之后输出的部分内容如下

1️⃣ 第一次:程序刚开始

 Eden Space:
   capacity = (63.5MB)
   used     = (7.620185852050781MB)
   free     = (55.87981414794922MB)
.000292680394931% used 

2️⃣ 第二次:创建 10 MB byte 数组之后

 Eden Space:
   capacity = (63.5MB)
   used     = (17.620201110839844MB)
   free     = (45.879798889160156MB)
.748348206046998% used 

注意到 used 大小扩大了 10 MB

3️⃣ 第三次:垃圾回收之后

 Eden Space:
   capacity = (63.5MB)
   used     = (1.27001953125MB)
   free     = (62.22998046875MB)
.0000307578740157% used 

发现 used 减小明显。

还可以使用 jconsole 图形化工具

程序运行之后终端输入 jconsole 即可

VM 概述、内存结构、溢出、调优

使用 jvisualvm 获取更详细的堆内存描述:

 jvisualvm  // 终端输入 

VM 概述、内存结构、溢出、调优

使用 堆 Dump 可以查看堆内具体信息。

方法区

定义

  • 方法区(method area)只是 JVM 规范 中定义的一个 概念 ,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,不同的实现可以放在不同的地方。
  • 逻辑上是堆的一部分,但不同厂商具体实现起来是不一样的,不强制位置
  • hotspot 虚拟机使得在 jdk 1.8 之前方法区由 永久代 实现,在jdk1.8之后由 元空间 实现(本地内存)
  • 线程共享
  • 会导致内存溢出

VM 概述、内存结构、溢出、调优

VM 概述、内存结构、溢出、调优

方法区内存溢出

  • jdk1.8 元空间内存溢出

因为虚拟机默认使用本机内存作为元空间,内存较大,所以要调小一下元空间的大小。

VM 概述、内存结构、溢出、调优

VM 概述、内存结构、溢出、调优

输入参数

 -XX:MaxMetaspaceSize=m
public class Test  extends  ClassLoader {
    public static void main(String[] args) {
        int j =;
        try {
            Test test = new Test();
            for (int i =; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter();
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test. define Class("Class" + i, code, 0, code.length); // Class 对象
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space 

和预想的不太一样,Compressed class space 是什么呢?

-XX:+UseCompressedOops 允许对象指针压缩。

-XX:+UseCompressedClassPointers 允许类指针压缩。

它们默认都是开启的,可以手动关闭它们。

在VM options中输入

 -XX:-UseCompressedOops
-XX:-UseCompressedClassPointers 

再次运行结果如下

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace 

表明元空间内存溢出。

  • jdk1.6 永久代内存溢出

相同的代码和虚拟机参数配置,结果如下

 Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 

表明永久代内存溢出

运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

反编译 字节码 命令(终端先 cd 进入 out 目录下相应字节码文件的目录)

 javap -v Class.class 
  • 二进制字节码:由类基本信息、常量池、类方法定义、虚拟机指令组成
 public class test {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
:ProjectJavaProjectPracticeoutproductionPracticedemo>javap -v test02.class
Classfile /E:/Project/JavaProject/Practice/out/production/Practice/demo/test02.class
  Last modified-11-18; size 535 bytes
  MD checksum 6da0b7066cec4b7beb4be01700bf3897
  Compiled from "test.java"
public class demo.test02
  minor version:
  major version:
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:                            // 常量池
   # = Methodref          #6.#20         // java/lang/Object."<init>":()V
   # = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   # = String             #23            // hello world
   # = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   # = Class              #26            // demo04/test02
   # = Class              #27            // java/lang/Object
   # = Utf8               <init>
   # = Utf8               ()V
   # = Utf8               Code
  # = Utf8               LineNumberTable
  # = Utf8               LocalVariableTable
  # = Utf8               this
  # = Utf8               Ldemo04/test02;
  # = Utf8               main
  # = Utf8               ([Ljava/lang/String;)V
  # = Utf8               args
  # = Utf8               [Ljava/lang/String;
  # = Utf8               SourceFile
  # = Utf8               test02.java
  # = NameAndType        #7:#8          // "<init>":()V
  # = Class              #28            // java/lang/System
  # = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  # = Utf8               hello world
  # = Class              #31            // java/io/PrintStream
  # = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  # = Utf8               demo04/test02
  # = Utf8               java/lang/Object
  # = Utf8               java/lang/System
  # = Utf8               out
  # = Utf8               Ljava/io/PrintStream;
  # = Utf8               java/io/PrintStream
  # = Utf8               println
  # = Utf8               (Ljava/lang/String;)V
{
  public demo.test02();                    //  构造方法 
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=, locals=1, args_size=1
    : aload_0
    : invokespecial #1                  // Method java/lang/Object."<init>":()V
    : return
      LineNumberTable:
        line: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature       5     0  this   Ldemo04/test02;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=, locals=1, args_size=1
    : getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    : ldc           #3                  // String hello world
    : invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    : return
      LineNumberTable:
        line: 0
        line: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature       9     0  args   [Ljava/lang/String;
}
SourceFile: "test.java" 
  • 常量池可以给虚拟机指令提供一些常量符号,可以通过查表的方式查到。

StringTable

StringTable 的数据结构

  • hash表(数组+ 链表 )
  • 不可扩容
  • 存 字符串 常量,唯一不重复
  • 每个数组单元称为一个哈希桶
  • 大小至少是 1009

面试题

 String s = "a"; 
String s = "b"; 
String s = "a" + "b"; 
String s = s1 + s2; 
String s = "ab"; 
 String  s6 = s4.intern(); 
// 问 
System.out.println(s == s4); 
System.out.println(s == s5); 
System.out.println(s == s6); 
String x = new String("c") + new String("d"); 
String x = "cd"; 
x.intern(); 
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk.6呢 
// x.intern(); 
// String x = "cd"; 
System.out.println(x == x2);
false
true
true
false
// 调换后,true 

解析

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是 StringBuilder (1.8)
  • 字符串常量 拼接的原理是编译期优化
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
  • jdk1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
  • jdk1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回

字符串常量

 String s = "a"; 
    String s = "b"; 
    String s = "ab"; 

反编译后的执行过程:

 Constant pool:
       # = Methodref          #6.#24         // java/lang/Object."<init>":()V
       # = String             #25            // a
       # = String             #26            // b
       # = String             #27            // ab
    ...

    Code:
      stack=, locals=4, args_size=1
: ldc           #2                  // String a
: astore_1
: ldc           #3                  // String b
: astore_2
: ldc           #4                  // String ab
: astore_3
: return
    ...
常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    ldc # 会把 a 符号变为 "a" 字符串对象
    ldc # 会把 b 符号变为 "b" 字符串对象
    ldc # 会把 ab 符号变为 "ab" 字符串对象 

字符串延迟加载

字符串变量拼接

 String s = "a"; // 懒惰的
    String s = "b";
    String s = "ab";
    String s = s1 + s2; 

反编译结果

 Code:
      stack=, locals=5, args_size=1
: ldc           #2                  // String a
: astore_1
: ldc           #3                  // String b
: astore_2
: ldc           #4                  // String ab
: astore_3
: new           #5                  // class java/lang/StringBuilder
: dup
: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
: aload_1
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: aload_2
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
: astore        4
: return 

字符串拼接的过程 new StringBilder().append(“a”).append(“b”).toString(),而StringBuilder的toString()方法又在底层创建了一个String对象

 @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value,, count);
    } 

所以 s3 == s4 为 false

字符串常量拼接

 String s = "a"; // 懒惰的
    String s = "b";
    String s = "ab";
    String s = s1 + s2;
    String s = "a" + "b"; 

反编译结果

 Code:
      stack=, locals=6, args_size=1
: ldc           #2                  // String a
: astore_1
: ldc           #3                  // String b
: astore_2
: ldc           #4                  // String ab
: astore_3
: new           #5                  // class java/lang/StringBuilder
: dup
: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
: aload_1
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: aload_2
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
: astore        4
: ldc           #4                  // String ab
: astore        5
: return 

注意 29: ldc #4 // String ab 和 6: ldc #4 // String ab

指向的是字符串常量池中相同的字符串常量 #4,说明 javac 在编译期间进行了优化,结果已经在编译期确定为 ab

所以 s3 == s5 为 true

intern 方法

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

反编译结果

 Constant pool:
   ...
   # = String             #30            // a
   ...
   # = String             #33            // b
   ...
Code:
      stack=, locals=2, args_size=1
: new           #2                  // class java/lang/StringBuilder
: dup
: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
: new           #4                  // class java/lang/String
: dup
: ldc           #5                  // String a
: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: new           #4                  // class java/lang/String
: dup
: ldc           #8                  // String b
: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
: astore_1
: return
  ... 

可以发现,创建了三个对象,”a”,”b” 以及StringBuilder.toString()创建的 “ab”。

字符串常量 “a”,”b” 进入串池,”ab” 是动态拼接出的一个字符串,没有被放入串池。

s 是一个变量指向堆中的 “ab” 字符串对象

调用 String.intern() 方法可以将这个字符串对象尝试放入串池,如果有则并不会放入,把串池中的对象返回;如果没有则放入串池, 再把串池中的对象返回。

注意这里说的返回是指调用 String.intern() 方法后返回的值。比如 String ss = s.intern() , ss 接收返回的对象,与 s 无关。而 s 只与对象本身有关,与返回值无关。
 String x = "ab";
        String s = new String("a") + new String("b");
        String s = s.intern(); 

        System.out.println(s == x);
        System.out.println(s == x); 

过程:

  • 字符串常量 “ab” 放入串池
  • “a” “b” 放入串池
  • s 指向堆中创建的 “ab” 对象
  • 串池中已经有 “ab” 对象,则返回串池中的对象引用给变量 s2 s 依然指向堆中的 “ab” 对象
  • s2 == x true
  • s == x false

如果调换一下位置

 String s = new String("a") + new String("b");
        String s = s.intern(); 
        String x = "ab";

        System.out.println( s == x);
        System.out.println( s == x ); 

过程:

  • “a” “b” 放入串池
  • s 指向堆中创建的 “ab” 对象
  • 串池中没有 “ab” 对象,则返回串池中的对象引用给变量 s2 s 指向串池中的 “ab” 对象
  • s2 == x true
  • s == x true

StringTable 的位置

VM 概述、内存结构、溢出、调优

  • jdk1.6 StringTable 放在永久代中,与常量池放在一起
  • jdk1.8 StringTable 放在堆中

StringTable 垃圾回收

  • StringTable 会发生垃圾回收
 -Xmxm -XX:+PrintStringTableStatistics
-XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws InterruptedException {
        int i =;
        try {
            for (int j =; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            System.out.println(i);
        }
    }
[GC (Allocation Failure) [PSYoungGen:K->488K(2560K)] 2048K->676K(9728K), 0.0010489 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
...
StringTable statistics:
Number of buckets       : =    480104 bytes, avg   8.000
Number of entries       : =    105312 bytes, avg  24.000
Number of literals      : =    284264 bytes, avg  64.782
Total footprint         :           = bytes 

可以看到 entries 的个数小于 10000,从第一行也可以看出发生了 GC。

StringTable 调优

调整 StringTable 的大小

 -XX:StringTableSize=桶个数 
  • 哈希桶越多,分布越分散,发生哈希冲突的可能性越低,效率越高
  • 字符串常量多的话,可以调大 StringTable 的大小,能增加哈希桶的个数,提供效率

考虑字符串是否入池

  • 使用 String.intern() 方法使重复字符串常量入池,减少堆的内存占用
 public static void main(String[] args) throws IOException {
        List<String> address = new ArrayList<>();
        System.in.read();
        for (int i =; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-"))) {
                String line = null;
                long start = System.nanoTime();
                while (true) {
                    line = reader.readLine();
                    if(line == null) {
                        break;
                    }
                    address.add(line.intern());  // 字符串常量放入串池
                }
                System.out.println("cost:" +(System.nanoTime()-start)/);
            }
        }
        System.in.read();
    } 

直接内存

定义

  • Direct Memory
  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

VM 概述、内存结构、溢出、调优

Java 本身不具有磁盘读写能力,需要调用操作系统提供的函数。

当 CPU 从用户态切换为内核态时,操作系统中会划分出一个系统缓冲区,Java 无法直接访问系统缓冲区,而堆中存在 Java 缓冲区,数据进入系统缓冲区再进入 Java 缓冲区就可以被 Java 访问。

两个缓冲区直接存在不必要的数据复制。

VM 概述、内存结构、溢出、调优

直接内存可以使系统缓冲区和 Java 缓冲区共享,使 Java 可以直接访问系统缓冲区的数据,减少了不必要的数据复制,适合文件的 IO 操作。

 public class Demo_9 {
    static final String FROM = "E:编程资料第三方教学视频youtubeGetting Started with Spring Boot-sbPSjItt10.mp4";
    static final String TO = "E:a.mp";
    static final int _Mb = 1024 * 1024;

    public static void main(String[] args) {
        io();           // io 用时:.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用时:.295165 702.291454 562.56592
    }

    private static void directBuffer() {
        long start = System.nanoTime();
        try (FileChannel from = new FileInputStream(FROM).getChannel();
             FileChannel to = new FileOutputStream(TO).getChannel();
        ) {
            ByteBuffer bb = ByteBuffer.allocateDirect(_Mb);
            while (true) {
                int len = from.read(bb);
                if (len == -) {
                    break;
                }
                bb.flip();
                to.write(bb);
                bb.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) /_000.0);
    }

    private static void io() {
        long start = System.nanoTime();
        try (FileInputStream from = new FileInputStream(FROM);
             FileOutputStream to = new FileOutputStream(TO);
        ) {
            byte[] buf = new byte[_Mb];
            while (true) {
                int len = from.read(buf);
                if (len == -) {
                    break;
                }
                to.write(buf,, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) /_000.0);
    }
} 

分配和回收原理

  • ByteBuffer 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner clean 方法调用 freeMemory 来释放直接内存

ByteBuffer 的 allocateDirect 方法

 public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    } 

DirectByteBuffer 对象

 // Primary constructor
    //
    DirectByteBuffer(int cap) {                   // package-private

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

        long base =;
        try {   
            base = unsafe.allocateMemory(size);   // 调用了 unsafe 类的 allocateMemory 方法
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte));    
        if (pa && (base % ps !=)) {
            // Round up to page boundary
            address = base + ps - (base & (ps -));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));  // Cleaner 虚引用监控 DirectByteBuffer 对象
        att = null;
    } 

Cleanr 对象的 clean 方法

 public void clean() {
        if (remove(this)) {
            try {
                this.thunk.run();   // 执行任务对象
            } catch (final Throwable var) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var)).printStackTrace();
                        }

                        System.exit();
                        return null;
                    }
                });
            }

        }
    } 

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 !=);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }

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

    } 

DirectByteBuffer 这个 Java 对象被垃圾回收器调用的时候,会触发虚引用对象 Cleaner 中的 clean 方法,执行任务对象 Deallocator,调用任务对象中的 freeMemory 去释放直接内存。

禁用显式垃圾回收

禁用显式垃圾回收

 -XX:+DisableExplicitGC // 禁用显式的 System.gc() 

System.gc() 触发的是 Full GC,回收新生代和老年代,程序暂停时间长,JVM 调优的时候可能会禁用掉,防止无意使用 System.gc() 。

但是禁用显式的 System.gc() ,直接内存不能被即时释放,可以通过直接调用 Unsafe 的 freeMemory 方法手动管理回收直接内存。

 static int _Gb = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_Gb);
        unsafe.setMemory(base, _Gb, (byte) 0);
        System.in.read();

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

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