【JVM】JVM 概述、記憶體結構、溢位、調優(基礎結構+StringTable+Unsafe+ByteBuffer)

gonghr發表於2021-11-18

什麼是 JVM ?

定義

  • Java Virtual Machine - java 程式的執行環境(java 二進位制位元組碼的執行環境)

好處

  • 一次編寫,到處執行

  • 自動記憶體管理,垃圾回收功能

  • 陣列下標越界檢查

  • 多型

  • jvm jre jdk

常見的 JVM

整體結構

記憶體結構

程式計數器

定義

  • Program Counter Register 程式計數器(暫存器)
  • 作用
    • 是記住下一條 jvm 指令的執行地址,也就是執行緒當前要執行的指令地址
  • 特點
    • 執行緒私有
    • 不會存在記憶體溢位(唯一)

虛擬機器棧

定義

  • 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

問題

  • 垃圾回收是否涉及棧記憶體?
    不涉及。每一次方法呼叫之後棧幀會被彈出,釋放記憶體,不需要垃圾回收。

  • 棧記憶體分配越大越好嗎?
    不。計算機總的實體記憶體有限,棧記憶體越大,棧的數量就越少,能夠開啟的執行緒就越少

  • 方法內的區域性變數是否執行緒安全?

    • 如果方法內區域性變數沒有逃離方法的作用訪問,它是執行緒安全的
    • 如果是區域性變數引用了物件,並逃離方法的作用範圍,需要考慮執行緒安全

棧記憶體溢位

  • 棧幀過多導致棧記憶體溢位
  • 棧幀過大導致棧記憶體溢位
    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();
    }
21187
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 = 0;
        try {
            while (true) {
                s += "a";
                array.add(s);
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(count);
        }
    }
60311
Exception in thread "main" java.lang.OutOfMemoryError: 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("1...");
        Thread.sleep(30000);
        byte[] bytes = new byte[1024 * 1024 * 10];
        System.out.println("2...");
        Thread.sleep(30000);
        bytes = null;
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }

三次輸入 jmap -heap pid 之後輸出的部分內容如下

1️⃣ 第一次:程式剛開始

Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 7990344 (7.620185852050781MB)
   free     = 58594232 (55.87981414794922MB)
   12.000292680394931% used

2️⃣ 第二次:建立 10 MB byte 陣列之後

Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 18476120 (17.620201110839844MB)
   free     = 48108456 (45.879798889160156MB)
   27.748348206046998% used

注意到 used 大小擴大了 10 MB

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

Eden Space:
   capacity = 66584576 (63.5MB)
   used     = 1331712 (1.27001953125MB)
   free     = 65252864 (62.22998046875MB)
   2.0000307578740157% used

發現 used 減小明顯。

? 還可以使用 jconsole 圖形化工具

程式執行之後終端輸入 jconsole 即可

? 使用 jvisualvm 獲取更詳細的堆記憶體描述:

jvisualvm  // 終端輸入

使用 堆 Dump 可以檢視堆內具體資訊。

方法區

定義

  • 方法區(method area)只是 JVM 規範中定義的一個概念,用於儲存類資訊、常量池、靜態變數、JIT編譯後的程式碼等資料,不同的實現可以放在不同的地方。

  • 邏輯上是堆的一部分,但不同廠商具體實現起來是不一樣的,不強制位置

  • hotspot 虛擬機器使得在 jdk1.8 之前方法區由永久代實現,在jdk1.8之後由元空間實現(本地記憶體)

  • 執行緒共享

  • 會導致記憶體溢位

方法區記憶體溢位

  • jdk1.8 元空間記憶體溢位

因為虛擬機器預設使用本機記憶體作為元空間,記憶體較大,所以要調小一下元空間的大小。

輸入引數

-XX:MaxMetaspaceSize=10m
public class Test extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Test test = new Test();
            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 物件
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(j);
        }
    }
}
3331
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space

和預想的不太一樣,Compressed class space 是什麼呢?

在 64 位平臺上,HotSpot 使用了兩個壓縮優化技術,Compressed Object Pointers (“CompressedOops”) 和 Compressed Class Pointers。
壓縮指標,指的是在 64 位的機器上,使用 32 位的指標來訪問資料(堆中的物件或 Metaspace 中的後設資料)的一種方式。
這樣有很多的好處,比如 32 位的指標佔用更小的記憶體,可以更好地使用快取,在有些平臺,還可以使用到更多的暫存器。

-XX:+UseCompressedOops 允許物件指標壓縮。

-XX:+UseCompressedClassPointers 允許類指標壓縮。

它們預設都是開啟的,可以手動關閉它們。

VM options中輸入

-XX:-UseCompressedOops
-XX:-UseCompressedClassPointers

再次執行結果如下

9344
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 test02 {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
E:\Project\JavaProject\Practice\out\production\Practice\demo04>javap -v test02.class
Classfile /E:/Project/JavaProject/Practice/out/production/Practice/demo04/test02.class
  Last modified 2021-11-18; size 535 bytes
  MD5 checksum 6da0b7066cec4b7beb4be01700bf3897
  Compiled from "test02.java"
public class demo04.test02
  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            // demo04/test02
   #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               Ldemo04/test02;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               test02.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               demo04/test02
  #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 demo04.test02();                    // 構造方法
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ldemo04/test02;

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

  • 常量池可以給虛擬機器指令提供一些常量符號,可以通過查表的方式查到。

StringTable

StringTable 的資料結構

  • hash表(陣列+連結串列)
  • 不可擴容
  • 存字串常量,唯一不重複
  • 每個陣列單元稱為一個雜湊桶
  • 大小至少是 1009

面試題

String s1 = "a"; 
String s2 = "b"; 
String s3 = "a" + "b"; 
String s4 = s1 + s2; 
String s5 = "ab"; 
String s6 = s4.intern(); 
// 問 
System.out.println(s3 == s4); 
System.out.println(s3 == s5); 
System.out.println(s3 == s6); 
String x2 = new String("c") + new String("d"); 
String x1 = "cd"; 
x2.intern(); 
// 問,如果調換了【最後兩行程式碼】的位置呢,如果是jdk1.6呢 
// x2.intern(); 
// String x1 = "cd"; 
System.out.println(x1 == x2);
false
true
true
false
// 調換後,true

解析

  • 常量池中的字串僅是符號,第一次用到時才變為物件
  • 利用串池的機制,來避免重複建立字串物件
  • 字串變數拼接的原理是 StringBuilder (1.8)
  • 字串常量拼接的原理是編譯期優化
  • 可以使用 intern 方法,主動將串池中還沒有的字串物件放入串池
  • jdk1.8 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的物件返回
  • jdk1.6 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有會把此物件複製一份,放入串池, 會把串池中的物件返回
字串常量
    String s1 = "a"; 
    String s2 = "b"; 
    String s3 = "ab"; 

反編譯後的執行過程:

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

    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
    ...
    常量池中的資訊,都會被載入到執行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字串物件
    ldc #2 會把 a 符號變為 "a" 字串物件
    ldc #3 會把 b 符號變為 "b" 字串物件
    ldc #4 會把 ab 符號變為 "ab" 字串物件

? 字串延遲載入

字串變數拼接
    String s1 = "a"; // 懶惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;

反編譯結果

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

字串拼接的過程 new StringBilder().append("a").append("b").toString(),而StringBuildertoString()方法又在底層建立了一個String物件

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

所以 s3 == s4false

字串常量拼接
    String s1 = "a"; // 懶惰的
    String s2 = "b";
    String s3 = "ab";
    String s4 = s1 + s2;
    String s5 = "a" + "b";

反編譯結果

    Code:
      stack=2, locals=6, 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: ldc           #4                  // String ab
        31: astore        5
        33: return

注意 29: ldc #4 // String ab6: ldc #4 // String ab
指向的是字串常量池中相同的字串常量 #4,說明 javac 在編譯期間進行了優化,結果已經在編譯期確定為 ab

所以 s3 == s5true

intern 方法
        String s = new String("a") + new String("b");

反編譯結果

Constant pool:
   ...
   #5 = String             #30            // a
   ...
   #8 = String             #33            // b
   ...
Code:
      stack=4, locals=2, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String a
        13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #8                  // String b
        25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: 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 s2 = s.intern(); 

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

? 過程:

  • 字串常量 "ab" 放入串池
  • "a""b" 放入串池
  • s 指向堆中建立的 "ab" 物件
  • 串池中已經有 "ab" 物件,則返回串池中的物件引用給變數 s2s 依然指向堆中的 "ab" 物件
  • s2 == xtrue
  • s == xfalse

如果調換一下位置

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

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

? 過程:

  • "a""b" 放入串池
  • s 指向堆中建立的 "ab" 物件
  • 串池中沒有 "ab" 物件,則返回串池中的物件引用給變數 s2s 指向串池中的 "ab" 物件
  • s2 == xtrue
  • s == xtrue

StringTable 的位置

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

  • jdk1.8 StringTable 放在堆中

StringTable 垃圾回收

  • StringTable 會發生垃圾回收
-Xmx10m -XX:+PrintStringTableStatistics
-XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) throws InterruptedException {
        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);
        }
    }
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->676K(9728K), 0.0010489 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
...
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :      4388 =    105312 bytes, avg  24.000
Number of literals      :      4388 =    284264 bytes, avg  64.782
Total footprint         :           =    869680 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 = 0; i < 10; i++) {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
                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)/1000000);
            }
        }
        System.in.read();
    }

直接記憶體

定義

  • Direct Memory
  • 常見於 NIO 操作時,用於資料緩衝區
  • 分配回收成本較高,但讀寫效能高
  • 不受 JVM 記憶體回收管理

Java 本身不具有磁碟讀寫能力,需要呼叫作業系統提供的函式。

當 CPU 從使用者態切換為核心態時,作業系統中會劃分出一個系統緩衝區,Java 無法直接訪問系統緩衝區,而堆中存在 Java 緩衝區,資料進入系統緩衝區再進入 Java 緩衝區就可以被 Java 訪問。

兩個緩衝區直接存在不必要的資料複製。

直接記憶體可以使系統緩衝區和 Java 緩衝區共享,使 Java 可以直接訪問系統緩衝區的資料,減少了不必要的資料複製,適合檔案的 IO 操作。

public class Demo1_9 {
    static final String FROM = "E:\\程式設計資料\\第三方教學視訊\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
    static final String TO = "E:\\a.mp4";
    static final int _1Mb = 1024 * 1024;

    public static void main(String[] args) {
        io();           // io 用時:1535.586957 1766.963399 1359.240226
        directBuffer(); // directBuffer 用時:479.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(_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);
    }
}

分配和回收原理

  • ByteBuffer 使用了 Unsafe 物件完成直接記憶體的分配回收,並且回收需要主動呼叫 freeMemory 方法

  • ByteBuffer 的實現類內部,使用了 Cleaner (虛引用)來監測 ByteBuffer 物件,一旦 ByteBuffer 物件被垃圾回收,那麼就會由 ReferenceHandler 執行緒通過 Cleanerclean 方法呼叫 freeMemory 來釋放直接記憶體

? ByteBuffer 的 allocateDirect 方法

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

? DirectByteBuffer 物件

    // Primary constructor
    //
    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);   // 呼叫了 unsafe 類的 allocateMemory 方法
        } 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));  // Cleaner 虛引用監控 DirectByteBuffer 物件
        att = null;
    }

? Cleanr 物件的 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;
                    }
                });
            }

        }
    }

? 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); 
        }

    }

DirectByteBuffer 這個 Java 物件被垃圾回收器呼叫的時候,會觸發虛引用物件 Cleaner 中的 clean 方法,執行任務物件 Deallocator,呼叫任務物件中的 freeMemory 去釋放直接記憶體。

禁用顯式垃圾回收

? 禁用顯式垃圾回收

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

System.gc() 觸發的是 Full GC,回收新生代和老年代,程式暫停時間長,JVM 調優的時候可能會禁用掉,防止無意使用 System.gc()

但是禁用顯式的 System.gc() ,直接記憶體不能被即時釋放,可以通過直接呼叫 UnsafefreeMemory 方法手動管理回收直接記憶體。

    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();
    }

    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);
        }
    }

相關文章