## 前提環境
筆者JDK版本如下,如果不做指定的話,64為虛擬機1.8版本默認使用的`ParallelGC`垃圾收集器。
```
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
```
使用`-XX:+UseSerialGC`選擇Serial+ Serial Old的垃圾收集器組合來驗證如下內存分配和回收策略。
其中顯示默認使用的是。
## 對象優先在Eden區分配
多數情況下,對象優先在Eden區分配。當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次`Minor GC`。
可以添加`-XX:+PrintGCDetails`打印內存回收日志。
實例:(空的main方法在jdk1.8上運行得到如下日志)
```
# 堆大小為20M,新生代為10M,打印GC詳細信息
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC
```
日志如下:
```
Heap
def new generation total 9216K, used 1996K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 24% used [0x00000007bec00000, 0x00000007bedf3198, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
Metaspace used 3012K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 330K, capacity 388K, committed 512K, reserved 1048576K
```
通過日志可以知道:
* 堆里年輕代共9216K(9M=Eden+S),其中Eden為8192K(8M),2個S區為1024K(1M)。其中已使用1996K,**說明虛擬機啟動后,其他線程已經占用了eden區1996K,也能說明對象是優先在Eden區分配的**。
* 堆里老年代共10240K(10M),已使用0K。
* 元數據區共4496K,已使用3208K,其中類空間容量388K,已使用353K。
## 大對象直接進入老年代
虛擬機提供了一個`-XX:PretenureSizeThreshold`參數,**令大于這個設置值的對象直接在老年代分配**,避免大對象在Eden和2個Survivor區來回復制。
例子:(設置大對象閥值為3兆)
~~~
/**
* 大對象直接進入老年代,對Serial和ParNew垃圾收集有效
* vm參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
*/
public static void bigObj2old(){
byte[] a;
a = new byte[4 * _1MB];
}
~~~
GC日志如下:
```
Heap
def new generation total 9216K, used 1996K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 24% used [0x00000007bec00000, 0x00000007bedf3198, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
Metaspace used 3199K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
```
通過日志可以知道,在設置了大對象閥值后,4M的對象確實被分配到了老年代。
**注意:參數XX:PretenureSizeThreshold只對Serial和ParNew收集器有效**
## 長期存活的對象將進入老年代
虛擬機給每個對象定義了對象年齡計數器(Age,在對象頭的`Mark Word`里),當對象到達15歲時(`-XX:MaxTenuringThreshold`默認值15),就會被晉升到老年代。
如果對象在Eden出生并且經過第一次Minor GC后存活且能被Survivor容納,就會被移動到Survivor空間,則對象年齡加1。
例子:(設置年齡閥值為1,且打印新生代各個對象年齡信息)
~~~
/**
* 長期存活對象進入老年代
* vm參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC
* -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public static void liveObj2old(){
byte[] a,b,c;
a = new byte[_1MB / 4];
b = new byte[4 * _1MB];
c = new byte[4 * _1MB];
}
~~~
```
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 685504 bytes, 685504 total
: 6187K->669K(9216K), 0.0054269 secs] 6187K->4765K(19456K), 0.0054705 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4847K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 51% used [0x00000007bec00000, 0x00000007bf014930, 0x00000007bf400000)
from space 1024K, 65% used [0x00000007bf500000, 0x00000007bf5a75c0, 0x00000007bf600000)
to space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
tenured generation total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
Metaspace used 3209K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
```
## 動態對象年齡判斷
Survivor空間內相同年齡所有對象大小總和大于Survivor空間的一半,年齡大于等于的對象可以直接進入老年代,無須等到默認15歲。
例子:
~~~
/**
* vm參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC
* -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
*/
public static void dynamicAgeObj2old(){
byte[] a,b,c,d;
a = new byte[_1MB / 4];
b = new byte[_1MB / 4];
// a + b 大于survivor空間一半
c = new byte[4 * _1MB];
d = new byte[4 * _1MB];
d = null;
d = new byte[4 * _1MB];
}
~~~
GC日志如下:
```
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 921984 bytes, 921984 total
: 6440K->900K(9216K), 0.0061563 secs] 6440K->4996K(19456K), 0.0061984 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age 1: 1288 bytes, 1288 total
: 5080K->1K(9216K), 0.0018378 secs] 9176K->4983K(19456K), 0.0018675 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 4235K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 51% used [0x00000007bec00000, 0x00000007bf022a48, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400508, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
tenured generation total 10240K, used 4982K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
the space 10240K, 48% used [0x00000007bf600000, 0x00000007bfadda80, 0x00000007bfaddc00, 0x00000007c0000000)
Metaspace used 3191K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
```
通過日志可以知道,survivor沒有被占用,說明a和b對象直接進入了老年代,并沒有等年齡到15。
## 空間分配擔保

在GC開始前,會先判斷老年代最大可連續空間是否大于新生代所有對象總和,如果大于則說明無風險并執行Minor GC;如果不大于,再判斷是否開啟了空間分配擔保失敗,如果開啟了則再判斷老年代最大可以連續空間是否大于歷次晉升老年代對象的平均大小,若大于則冒著風險執行一次Minor GC;若不大于且不允許空間分配擔保失敗,則直接執行Full GC。
虛擬機使用`-XX:+HandlePromotionFailure`設置允許擔保失敗,該參數開啟后虛擬機在老年代最大可用連續空間大于歷次晉升老年代對象平均值情況下嘗試先執行Minor GC,其目的是為了盡可能避免Full GC。
`JDK 6 Update 24`之 后的規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小,就會進行Minor GC,否則將進行Full GC。
## 參考資料
* 周志明 * 《深入理解Java虛擬機》
- 面試突擊
- Java虛擬機
- 認識字節碼
- 000Java發展歷史
- 000Macos10.15.7上編譯OpenJDK8u
- 001熟悉Java內存區域
- 002熟悉HotSpot中的對象
- 003Java如何計算對象大小
- 004垃圾判定算法與4大引用
- 005回收堆和方法區中對象
- 006垃圾收集算法
- 007HotSpot虛擬機垃圾算法實現篇1
- 007HotSpot虛擬機垃圾算法實現篇2
- 007HotSpot虛擬機垃圾算法實現篇3
- 008垃圾收集器
- 009內存分配與回收策略
- 010Java虛擬機相關工具
- 011調優案例分析
- 012一次IDEA的啟動速度調優
- 013類文件Class的結構
- 014熟悉字節碼指令
- 015類加載機制(過程)
- 016類加載器
- IDEA的JVM參數
- Java基礎
- Java自動裝箱與拆箱
- Java基礎數據類型
- Java方法的參數傳遞
- Java并發
- 001走入并行的世界
- 002并行程序基礎
- 003熟悉Java內存模型JMM
- 004Java并發之volatile關鍵字
- 005線程池入門到精通
- 006Java多線程間的同步控制方法
- 007Java維基準測試框架JMH
- 008Java并發容器
- 009Java的線程實現
- 010Java關鍵字synchronized
- 011一些并行模式的熟悉
- 單例模式和不變模式
- 生產者消費者模式
- Future模式
- 012一些并行算法的熟悉
- 面試總結
- 長亮一面