什麼是 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()
,而StringBuilder
的toString()
方法又在底層建立了一個String
物件
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
所以 s3 == s4
為 false
字串常量拼接
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 ab
和 6: ldc #4 // String ab
指向的是字串常量池中相同的字串常量 #4
,說明 javac
在編譯期間進行了優化,結果已經在編譯期確定為 ab
所以 s3 == s5
為 true
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"
物件,則返回串池中的物件引用給變數s2
,s
依然指向堆中的"ab"
物件 s2 == x
為true
s == x
為false
如果調換一下位置
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"
物件,則返回串池中的物件引用給變數s2
,s
指向串池中的"ab"
物件 s2 == x
為true
s == x
為true
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
執行緒通過Cleaner
的clean
方法呼叫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()
,直接記憶體不能被即時釋放,可以通過直接呼叫 Unsafe
的 freeMemory
方法手動管理回收直接記憶體。
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);
}
}