关于jvm:JVM笔记一-内存结构

6次阅读

共计 12795 个字符,预计需要花费 32 分钟才能阅读完成。

1. 什么是 JVM?

1.1 定义

JVM,即 Java Virtual Machine,JAVA 程序的 运行环境(JAVA 二进制字节码的运行环境)

1.2 益处

  1. 一次编写,到处运行的基石
  2. 主动内存治理,垃圾回收性能
  3. 数据下标越界查看
  4. 多态

1.3 JVM、JRE、JDK 比拟

1.4 常见的 JVM

JVM 只是套标准,基于这套标准的实现常见如下几种:

2. 内存构造

2.1 整体架构

JVM 内存构造次要蕴含:程序计数器 虚拟机栈 本地办法栈 办法区 ,其中前三者是 线程公有 的,后两者是 线程共享 的。整体架构如下图:

2.2 程序计数器

2.2.1 作用

用于保留 JVM 中下一条要执行的指令的地址

2.2.2 特点

  • 线程公有

    • CPU 会为每个线程调配工夫片,当以后线程的工夫片应用完当前,CPU 就会去执行另一个线程的代码。程序计数器是每个线程所公有的,当另一个线程的工夫片用完,又返回来执行以后线程的代码时,通过程序计数器能够晓得应该执行哪一条指令
  • 不会存在内存溢出

    • 因为永远只存储一个指令地址,JVM 标准中惟一一个不存在 OutOfMemoryError 的区域

2.3 虚拟机栈

2.3.1 定义

每个 线程运行须要的内存空间 ,称为 虚拟机栈 。每个栈由多个 栈帧 组成,对应着每次调用办法时所占用的内存。每个线程只能有 一个流动栈帧 ,对应着 以后正在执行的办法

应用 IDE 调试演示,从控制台能够看到办法进入虚拟机栈后的样子,如下:

2.3.2 特点

  • 线程公有的
  • 存在栈内存溢出

2.3.3 问题辨析

  • 垃圾回收是否波及栈内存?

    • 不须要。因为虚拟机栈中是由一个个栈帧组成的,在办法执行结束后,对应的栈帧就会被弹出栈,内存会主动开释。所以无需通过垃圾回收机制去回收栈内存
  • 栈内存的调配越大越好吗?

    • 不是。因为 物理内存是肯定的,栈内存越大,能够反对更多的递归调用,然而可执行的线程数就会越少
  • 办法内的局部变量是否是线程平安的?

    • 如果办法内 局部变量没有逃离办法的作用范畴 ,则是 线程平安
    • 如果 局部变量援用了对象 ,并 逃离了办法的作用范畴,则须要思考线程平安问题
    • 代码示例:
          // 是线程平安的,因为局部变量 sb 只在 m1()办法内应用
          public static void m1() {StringBuilder sb = new StringBuilder();
              sb.append(1);
              sb.append(2);
              sb.append(3);
              System.out.println(sb.toString());
          }
        
          // 不是线程平安的,因为局部变量 sb 是作为参数传过来的,m2()执行的同时可能会被其余线程批改
          public static void m2(StringBuilder sb) {sb.append(1);
              sb.append(2);
              sb.append(3);
              System.out.println(sb.toString());
          }
        
          // 不是线程平安的,因为局部变量 sb 是作为返回值,可能会被多个其余线程拿到并同时批改
          public static StringBuilder m3() {StringBuilder sb = new StringBuilder();
              sb.append(1);
              sb.append(2);
              sb.append(3);
              return sb;
          }

2.3.4 栈内存溢出

Java.lang.stackOverflowError 栈内存溢出

产生起因

  • 虚拟机栈中,栈帧过多(比方有限递归)
  • 每个 栈帧所占用内存过大

2.3.5 线程运行诊断

  • CPU 占用过高。Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时须要定位占用 CPU 过高的线程

    1. top 命令,查看哪个 过程 占用 CPU 过高
    2. 拿到占用 CPU 过高的过程 ID 后,再通过 ps 查看哪个线程占用 CPU 过高。” ps H -eo pid, tid, %cpu | grep 查到的占用 CPU 过高的过程 ID
    3. 通过 “jstack 过程 id” 命令查看过程中线程 ID(nid),与上一步查出的线程 ID(tid)来 比照定位有问题的线程以及源码行号 。留神 jstack 查找出的线程 ID(nid) 是 16 进制的须要转换。
  • 程序运行很长时间没有后果 。个别可能是产生了 线程死锁,这时须要找到死锁的线程

    • 找到以后运行的 java 程序的过程 ID(也能够应用 jps 命令)
    • 通过 “jstack 过程 id” 命令查看过程中是否有死锁线程
    • 演示

      class A{}
      class B{}
      public class Demo004 {static A a = new A();
          static B b = new B();
      
          public static void main(String[] args) throws InterruptedException {new Thread(()->{synchronized (a) {
                      try {Thread.sleep(2000);
                      } catch (InterruptedException e) {e.printStackTrace();
                      }
                      synchronized (b) {System.out.println("我取得了 a 和 b");
                      }
                  }
              }).start();
              
              Thread.sleep(1000);
              
              new Thread(()->{synchronized (b) {synchronized (a) {System.out.println("我取得了 a 和 b");
                      }
                  }
              }).start();}
      
      }

2.4 本地办法栈

一些带有 native 关键字 的办法就是须要 JAVA 去调用本地的 C 或者 C ++ 办法,因为 JAVA 有时候没法间接和操作系统底层交互,所以须要用到本地办法。

本地办法栈也是 线程公有的

2.5 堆

2.5.1 定义

通过 new 关键字 创立的对象 都会被放在堆内存

2.5.2 特点

  • 所有线程共享。堆内存中的对象都须要 思考线程平安问题
  • 有垃圾回收机制
  • 存在堆内存溢出

2.5.3 堆内存溢出

java.lang.OutofMemoryError:java heap space 堆内存溢出

2.5.4 堆内存诊断工具

  • jps 工具:查看以后零碎中有哪些 java 过程
  • jmap 工具:java 内存映像工具。如查看堆内存占用状况 ,jmap -heap pid
  • jconsole 工具:图形界面的,Java 监督与治理控制台
  • jvisualvm 工具:图形界面的,多合一故障解决工具

2.6 办法区

2.6.1 定义

办法区用于存储每个类的构造,比方运行时常量池、字段和办法数据,以及办法和构造函数的代码,包含在类和实例初始化以及接口初始化中应用的非凡办法。办法区域能够是固定大小,也能够依据计算的须要进行扩大,如果不须要更大的办法区域,则能够膨胀。

2.6.2 组成

办法区是一个抽象概念,JDK1.8 以前办法区由 永恒代 实现,JDK1.8 当前办法区由 元空间 实现

2.6.3 办法区内存溢出

  • JDK1.8 之前会导致永恒代内存溢出

    • 设置永恒代最大内存参数:-XX:MaxPerSize=<size>
    • 异样信息:java.lang.OutOfMemoryError: PerGen space
  • JDK1.8 之后会导致元空间内存溢出

    • 设置元空间最大内存参数:-XX:MaxMetaspaceSize=<size>
    • 异样信息:java.lang.OutOfMemoryError: Metaspace
    • 演示

      /**
       * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 */
public class Demo006 extends ClassLoader { // 继承 ClassLoader,能够用来加载类的二进制字节码
    
    public static void main(String[] args) {
        int j = 0;
        try {Demo006 test = new Demo006();
            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); // Class 对象
            }
        } finally {System.out.println(j);
        }
    }
}
```

![image](/img/bVbZOEr)

尽管咱们本人编写的程序没有大量应用动静加载类,但如果咱们在应用内部一些框架时,可能大量动静加载类,就可能会导致元空间内存溢出。如 spring、mybatis 的动静代理

2.6.4 常量池

  • 常量池 :Class 文件中除了类的版本、字段、办法、接口等形容信息,还有 常量池 ,用于 寄存编译期生成的各种字面量和符号援用。就是一张表,虚拟机指令依据这张常量表找到要执行的类名、办法名、参数类型、字面量等信息。
  • 运行时常量池 :常量池是 class 文件中的,当该类被加载,它的 常量池信息就会放入运行时常量池,并把外面的符号地址翻译为实在地址

较于 Class 文件常量池,运行时常量池具备动态性 ,在运行期间也能够将新的常量放入常量池中,而不是肯定要在编译时确定的常量能力放入。最次要的使用便是 String 类的 intern() 办法

常量池与 JVM 字节码演示

public class Demo007 {public static void main(String[] args) {System.out.println("hello world");
    }
}

通过 “javap -v <class 文件 >” 反编译,查看二进制字节码,其中蕴含了 类根本信息 常量池 类办法定义 (蕴含虚拟机指令)
进入 Demo007.class 所在目录执行,javap -v Demo007.class,输入后果如下,

Classfile /E:/codes/java_demos/out/production/jvm/indi/taicw/jvm/structure/methodarea/constantpool/Demo007.class
  Last modified 2020-10-7; size 622 bytes
  MD5 checksum 1356d96bb349e8a95e8c5df1cb36040c
  Compiled from "Demo007.java"
public class indi.taicw.jvm.structure.methodarea.constantpool.Demo007
  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/taicw/jvm/structure/methodarea/constantpool/Demo007
   #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/taicw/jvm/structure/methodarea/constantpool/Demo007;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Demo007.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/taicw/jvm/structure/methodarea/constantpool/Demo007
  #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.taicw.jvm.structure.methodarea.constantpool.Demo007();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lindi/taicw/jvm/structure/methodarea/constantpool/Demo007;

  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 8: 0
        line 9: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;}
SourceFile: "Demo007.java"

2.6.5 字符串常量池 StringTable

字符串常量池 是常量池中专门用来寄存常量字符串的一块区域,应用 StringTable 进行存储,底层数据结构是一个哈希表。

StringTable 地位

  • JDK1.6 版本中,字符串常量池是在永恒代
  • JDK1.7 版本及当前版本,JVM 曾经将字符串常量池从办法区中移了进去,在 Java 堆(Heap)中开拓了一块区域寄存字符串常量池

StringTable 个性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象并放入字符串常量池
  • 利用字符串常量池的机制,来防止反复创立字符串对象
  • 字符串变量拼接的原理是 StringBuilder
  • 字符串常量拼接的原理是编译期优化
  • 能够应用 String#intern() 办法,被动将以后字符串退出到字符串常量池中,并返回常量池中该字符串对象的援用

    • JDK1.7 及当前版本中调用 intern() 办法将这个字符串对象尝试放入串池,如果串池有则不会放入,如果没有则放入串池,并把串池中的对象援用返回
    • JDK1.6 版本中调用 intern() 办法将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,并把串池中的对象援用返回,而之前那个字符串对象还是存在的

字符串拼接与字符串常量池相干问题

public class Demo008 {public static void main(String[] args) {String s1 = "a";    // 字符串 "a" 放入串池,StringTable ["a"]
        String s2 = "b";    // 字符串 "b" 放入串池,StringTable ["a", "b"]
        String s3 = "ab";   // 字符串 "ab" 放入串池,StringTable ["a", "b", "ab"]

        // 因为 s1、s2 是变量,只有运行期间能力确定具体值,所以底层实现是 new StringBuilder().append("a").append("b").toString(),返回一个新的 "ab" 字符串对象
        String s4 = s1 + s2;
        // javac 在编译期间的优化,后果曾经在编译期确定为 "ab",所以等价于 s5 = "ab",此时常量池已存在不必再创立新的 "ab" 对象
        String s5 = "a" + "b";
        // s4 调用 intern()办法尝试把 "ab" 放入串池中,此时串池已存在 "ab", 并返回串池中的 "ab" 对象援用赋值给 s6
        String s6 = s4.intern();

        //false,起因:s3 是串池中 "ab" 字符串对象的援用,s4 是堆中 "ab" 对象的援用
        System.out.println(s3 == s4);
        //true,起因:s3、s5 都是串池中 "ab" 字符串对象的援用
        System.out.println(s3 == s5);
        //true,起因:s3、s6 都是串池中 "ab" 字符串对象的援用
        System.out.println(s3 == s6);


        String x2 = new String("c") + new String("d");
        x2.intern(); // 把字符串 "cd" 放入串池,StringTable ["a", "b", "ab", "cd"]
        String x1 = "cd"; // 间接把串池中的 "cd" 对象援用赋值给 x1
        
        //JDK1.7 及之后版本,后果为 true, 起因:x2 调用 intern()办法尝试把 "cd" 放入串池,此时串池不存在 "cd" 放入胜利,此时串池中的 "cd" 对象与堆中的是同一个
        //JDK1.6 版本,后果为 false, 起因:x2 调用 intern()办法尝试把 "cd" 放入串池,此时串池不存在 "cd" 字符串,会复制一个新的 "cd" 对象放入串池中,此时串池中的 "cd" 对象与堆中的不是同一个
        System.out.println(x1 == x2);

        String x4 = new String("e") + new String("f");
        String x3 = "ef"; // 把字符串 "cd" 放入串池,StringTable ["a", "b", "ab", "cd", "ef"]
        x4.intern();
        // 后果为 false, 起因:x4 调用 intern()办法尝试把 "ef" 放入串池,此时串池已存在 "cd" 放入失败,此时 x3 是串池中 "ef" 对象的援用,而 x4 是堆中的 "ef" 对象援用
        System.out.println(x3 == x4);
    }
}

2.6.6 StringTable 垃圾回收

因为在 jdk1.7 版本当前,字符串常量池是放在堆中,如果堆空间有余,字符串常量池也会进行垃圾回收。如下演示代码,设置最大堆内存为 10m 并打印 gc 信息

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Demo009 {public static void main(String[] args) {
        int i = 0;
        try {for (int j = 0; j < 100000; j++) { // j=100, j=10000
                String.valueOf(j).intern();
                i++;
            }
        } catch (Throwable e) {e.printStackTrace();
        } finally {System.out.println(i);
        }
    }
}

2.6.7 StringTable 性能调优

  • StringTable 的数据结构是哈希表,所以能够适当减少哈希表桶的大小,来缩小字符长放入 StringTable 所需的工夫

    • 设置 StringTable 桶大小参数:-XX:StringTableSize=< 桶个数 >
  • 对于有些反复的字符串能够思考放入常量池,能够缩小内存占用

2.7 间接内存

2.7.1 定义

间接内存并不是 JVM 内存区域的一部分,不受 JVM 内存回收治理 常见于 NIO 操作用于数据缓存区 调配回收老本高但读写性能高

java 程序资源文件读取过程中,未应用间接内存和应用间接内存区别如下:

  • 未应用间接内存

    • 须要从用户态向内核态申请资源,内核态会创立零碎缓冲区,用户态会创立一个 java 缓冲区 byte[],而后数据从零碎缓存区复制到 java 缓存区
  • 应用间接内存

    • 须要从用户态向内核态申请资源,即内核态会创立一块间接内存 direct memory,这块 direct memory 内存能够在用户态、内核态应用。间接内存是操作系统和 Java 代码都能够拜访的一块区域,无需将数据从零碎内存复制到 Java 堆内存,从而进步了效率

应用一般内存形式与应用间接内存形式读取大文件效率比拟,演示代码如下:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 演示 一般内存与间接内存读取文件效率
 */
public class Demo011 {
    static final String FROM = "C:\\Users\\taichangwei\\Downloads\\CentOS-6.10-x86_64-bin-DVD2.iso"; // 一个 2.03G 的文件
    static final String TO = "E:\\a.iso";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {io();
        directBuffer();}

    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(_1Mb);
            while (true) {int len = from.read(bb);
                if (len == -1) {break;}
                bb.flip();
                to.write(bb);
                bb.clear();}
        } catch (IOException e) {e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("directBuffer 用时:" + (end - start) / 1000_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[_1Mb];
            while (true) {int len = from.read(buf);
                if (len == -1) {break;}
                to.write(buf, 0, len);
            }
        } catch (IOException e) {e.printStackTrace();
        }
        long end = System.nanoTime();
        System.out.println("io 用时:" + (end - start) / 1000_000.0);
    }
}

/*
执行三次别离用时
io 用时:4944.8368
directBuffer 用时:1927.7912

io 用时:4989.7547
directBuffer 用时:2287.7028

io 用时:5022.2618
directBuffer 用时:2453.5307
 */

2.7.2 间接内存溢出

间接内存尽管不受 JVM 内存治理,然而物理内存是无限的,所以间接内存还是会存在内存溢出,java.lang.OutOfMemoryError: Direct buffer memory

演示间接内存溢出,代码如下:

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * 演示 间接内存溢出
 */
public class Demo012 {
    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);
        }
    }
}

循环了 36 次,应用了 3.6G 的内存导致内存溢出了

2.7.3 间接内存的调配及回收原理

  • 底层应用了 UnSafe 对象实现间接内存的 调配及回收 ,并且回收须要被动调用freeMemory() 办法

    • 间接内存调配及回收的底层原理演示,代码如下:

      import sun.misc.Unsafe;
      import java.io.IOException;
      import java.lang.reflect.Field;
      
      public class Demo014 {
      
          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);
              // 暂定期待。此时查看工作管理器(windows 零碎),会发现有一个占用 1G 内存的 java 程序
              System.in.read();
      
              // 开释内存
              unsafe.freeMemory(base);
              // 暂定期待。此时查看工作管理器(windows 零碎),会发现方才占用 1G 内存的 java 程序内存开释了
              System.in.read();}
      
          public static Unsafe getUnsafe() {
              try {
                  //Unsafe 构造方法是私有化的,只能通过反射取得
                  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);
              }
          }
      }
  • 通常状况是咱们是应用 “ByteBuffer.allocateDirect(int capacity)” 来申请间接内存就能够了。ByteBuffer的实现类外部应用了 Cleaner(虚援用) 来检测 ByteBuffer一旦 ByteBuffer 被垃圾回收,那么会由 ReferenceHandler 来调用 Cleaner 的 clean()办法调用 freeMemory()来开释内存

    • allocateDirect(int capacity) 办法外部实现:

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

      DirectByteBuffer(int cap) 办法外部实现

          DirectByteBuffer(int cap) {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;
          }

      这里调用了一个 Cleaner 的 create()办法,且后盾线程还会对虚援用的对象监测,如果虚援用的理论对象(这里指的是 DirectByteBuffer 对象)被回收当前,就会调用 Cleaner 的 clean()办法,来革除间接内存中占用的内存

          public void clean() {if (remove(this)) {
                  try {this.thunk.run();
                  } catch (final Throwable var2) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {if (System.err != null) {(new Error("Cleaner terminated abnormally", var2)).printStackTrace();}
      
                              System.exit(1);
                              return null;
                          }
                      });
                  }
      
              }
          }

      其中 “this.thunk” 是上一步传入的 Deallocator 对象,它的 run 办法外部实现如下:

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