垃圾收集器與記憶體分配策略_記憶體分配策略

z1340954953發表於2018-03-31

物件的記憶體分配策略

測試環境jdk1.6 32位

物件的記憶體分配,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在

tlab上分配。少數情況下也可能會直接分配在老年代中,分配的規則不是百分百確定,其細節取決於當前使用的是那種垃圾收集器組合,還有虛擬機器中於記憶體相關的引數配置,下面介紹的是幾條普遍的記憶體分配規則。

物件優先在Eden分配

大多數的物件在新生代Eden區中分配記憶體空間,如果Eden空間不夠,虛擬機器將自動發起一次Minor GC 垃圾回收

測試工具:eclipse

1. 測試程式碼

package cn.erong.test;

public class Jtest {
	private static final int _1M = 1024*1024;
	public static void main(String[] args) {
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[2*_1M];
		allocation2 = new byte[2*_1M];
		allocation3 = new byte[2*_1M];
		allocation4 = new byte[4*_1M];
	}
}	

2. 配置jvm引數

-XX:+PrintGCTimeStamps --列印gc時間資訊
-XX:+PrintGCDetails --列印GC詳細資訊
-verbose:gc --開啟gc日誌
-Xloggc:d:/gc.log --設定gc日誌的存放位置 
-Xms20M --設定java堆最小為20M
-Xmx20M --設定java堆最大為20M
-Xmn10M --設定新生代記憶體大小10M
-XX:SurvivorRatio=8 --設定新生代記憶體區域中Eden區域和Survivor區域的比例

3. JVM引數加入到Run configurations->對應的application->Arguments->Vm arguments,然後run 執行


GC日誌如下:

Java HotSpot(TM) Client VM (25.151-b12) for windows-x86 JRE (1.8.0_151-b12), built on Sep  5 2017 19:31:49 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 3567372k(1058760k free), swap 7133056k(3189624k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:-UseLargePagesIndividualAllocation 
0.096: [GC (Allocation Failure) 0.096: [DefNew: 6963K->483K(9216K), 0.0058886 secs] 6963K->6627K(19456K), 0.0060831 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4743K [0x03c00000, 0x04600000, 0x04600000)
  eden space 8192K,  52% used [0x03c00000, 0x040290e8, 0x04400000)
  from space 1024K,  47% used [0x04500000, 0x04578d98, 0x04600000)
  to   space 1024K,   0% used [0x04400000, 0x04400000, 0x04500000)
 tenured generation   total 10240K, used 6144K [0x04600000, 0x05000000, 0x05000000)
   the space 10240K,  60% used [0x04600000, 0x04c00030, 0x04c00200, 0x05000000)
 Metaspace       used 84K, capacity 2242K, committed 2368K, reserved 4480K

注意:

1. 新生代GC(Minor GC):指的是發生在新生代中的垃圾收集動作,很頻繁

2. 老年代GC(Major GC/Full GC):指的發生在老年代中的GC,出現了Major GC,經常伴隨至少一次Minor GC(但非絕對),速度上比上面的Minor GC要慢10倍以上

3. Allocation Failure 表示引起GC的原因是因為年輕代中沒有足夠的記憶體空間存放物件而觸發的,這裡的GC表示Minor GC 

日誌解讀:

可以看出發生了一次Minor GC,發生前allocation1-allocation3是放在新生代中,從日誌看出大概是佔用了6M的空間,後面在建立allocation4由於記憶體空間不夠(allocation4是4M,新生代中可用的為9M),這樣空間不夠會觸發一次複製演算法,另一個Survivor記憶體空間(用來存放存活物件的記憶體區域)為1M,這樣無法放入,由於分擔機制,將會把allocation1->allocation3放入到老年代中。最後的allocation4存放在Eden記憶體區域中。

從後面的記憶體區域的佔有率可以驗證這點

 def new generation   total 9216K, used 4743K [0x03c00000, 0x04600000, 0x04600000) ---新生代
  eden space 8192K,  52% used [0x03c00000, 0x040290e8, 0x04400000)  --eden使用記憶體區域為4M
  from space 1024K,  47% used [0x04500000, 0x04578d98, 0x04600000)
  to   space 1024K,   0% used [0x04400000, 0x04400000, 0x04500000)
 tenured generation   total 10240K, used 6144K [0x04600000, 0x05000000, 0x05000000)--老年代 使用為6M

大物件直接進入老年代

大物件指的是,需要大量連續記憶體空間的Java物件,最典型的大物件是很長的字串和陣列。

對於虛擬機器而言,出現大物件容易導致記憶體空間還有不少空間就提前出發垃圾收集,這樣耗費系統資源

虛擬機器提供一個-XX:PretenureSizeThreshold引數,令大於這個設定值的物件直接分配到老年代中。這樣避免了新生代的多次的記憶體複製(新生代採用複製演算法收集記憶體)

注意: PretenureSizeThreshold引數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個收集器,Parrallel Scavenge收集器一般不需要設定,如果遇到必須使用此引數,可以考慮ParNew和CMS收集器的組合

測試程式碼:

public class Jtest {
	private static final int _1M = 1024*1024;
	public static void main(String[] args) {
		byte[] allocation4;
		allocation4 = new byte[4*_1M];
	}
}

JVM引數設定,在上面的案例的基礎上加上:-XX:PretenureSizeThreshold=3145728

Heap
 def new generation   total 9216K, used 984K [0x03c00000, 0x04600000, 0x04600000)
  eden space 8192K,  12% used [0x03c00000, 0x03cf6010, 0x04400000)
  from space 1024K,   0% used [0x04400000, 0x04400000, 0x04500000)
  to   space 1024K,   0% used [0x04500000, 0x04500000, 0x04600000)
 tenured generation   total 10240K, used 4096K [0x04600000, 0x05000000, 0x05000000) --從這裡看出,4M物件直接放入老年代中
   the space 10240K,  40% used [0x04600000, 0x04a00010, 0x04a00200, 0x05000000)
 Metaspace       used 84K, capacity 2242K, committed 2368K, reserved 4480K

長期存活的物件將進入老年代

虛擬機器是採用分代收集的思想來決定記憶體使用何種收集演算法進行記憶體回收的。那麼就需要區分哪些物件放在新生代中,哪些物件放在老年代中。為了做到這點,虛擬機器給每個物件定義了一個物件年齡(Age)計數器。

如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能夠被Survivor容納的話,將移動到Survivor空間中,並且物件年齡設為1,並且沒熬過一次Minor GC,年齡就會增加一歲,預設增加到15歲,自動晉升到老年代中

預設年齡可以設定:  -XX:MaxTenuringThreshold

測試程式碼:

public class MaxAgeTest {
	private static final int _1M = 1024*1024;
	public static void main(String[] args) {
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[_1M/4];//在第二次GC時候進入到老年代中
		allocation2 = new byte[4*_1M];
		allocation3 = new byte[4*_1M];//觸發一次Minor GC,allocation3放入Eden區
		allocation3 = null;//手動觸發一次GC,Eden區清空了
		allocation4 = new byte[4*_1M];//Eden再次放入一個4M物件
	}
}

jvm引數:

-Xms20M
-Xmx20M
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDetails 
-verbose:gc 
-Xloggc:d:/gc.log
-Xmn10M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1

動態物件年齡判定

虛擬機器並不是永遠要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代中,如果Survivor空間中相同年齡的物件的總和大於Survivor空間的一半,就將年齡大於等於該年齡的物件直接放入到老年代中

測試程式碼:

public class Jtest {
	private static final int _1M = 1024*1024;
	public static void main(String[] args) {
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[_1M/4];
		allocation2 = new byte[_1M/4];
		allocation3 = new byte[4*_1M];
		allocation4 = new byte[4*_1M];
		allocation4 = null;
		allocation4 = new byte[4*_1M];
		
	}
}	

JVM引數:

-Xms20m
-Xmx20m
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDetails 
-verbose:gc 
-Xloggc:d:/gc.log
-Xmn10M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15

GC日誌:

0.095: [GC (Allocation Failure) 0.095: [DefNew: 5427K->995K(9216K), 0.0037095 secs] 5427K->5091K(19456K), 0.0039170 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.099: [GC (Allocation Failure) 0.099: [DefNew: 5091K->0K(9216K), 0.0013279 secs] 9187K->5090K(19456K), 0.0014083 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4260K [0x03e00000, 0x04800000, 0x04800000)
  eden space 8192K,  52% used [0x03e00000, 0x042290e8, 0x04600000)
  from space 1024K,   0% used [0x04600000, 0x04600000, 0x04700000)
  to   space 1024K,   0% used [0x04700000, 0x04700000, 0x04800000)
 tenured generation   total 10240K, used 5090K [0x04800000, 0x05200000, 0x05200000)
   the space 10240K,  49% used [0x04800000, 0x04cf8ac0, 0x04cf8c00, 0x05200000)
 Metaspace       used 84K, capacity 2242K, committed 2368K, reserved 4480K

從日誌的結果可以看出,最後2個1/4M的物件和一個4M的物件放入到老年代中,一個4M的放入在新生代的Eden區中,為什麼會這樣?

第一次的GC,由於allocation4的建立而觸發的,由於此時Eden區域中已經存放了allocation1->allocation3,記憶體空間不夠。

這樣理論上是allocation3是分擔進入到老年代中,但是為什麼allocation1,allocation2也是直接進入到了老年代中,而沒有等到15歲呢,首次不是應該放在Survivor區域中嗎?

因為兩個物件加起來達到了Survivor的一半,並且是同年的,就直接進入老年代了

第二次GC,觸發前allocation4在新生代中,手動將引用置null,觸發一次GC,後面又建立了allocation4,最後還是放在Eden中

空間分配擔保

前面有介紹過,進行新生代的垃圾收集Minor GC時候,將存活物件移動到另一塊的Survivor區域中,如果該記憶體區域空間不夠,就會通過擔保機制,放入到老年代中

現在說下,這個擔保細節?

1. 在JDK 1.6 update 24版本前,如果發生Minor GC,虛擬機器會檢查老年代最大可用連續空間是否大於新生代物件總和,如果成立的話, 那麼Minor GC可以確保是安全的,不過不是,則是檢查引數HandlePromotionFailure是否允許擔保失敗,如果允許,那麼會檢查老年代的最大可用連續空間是否大於歷次晉升的物件的平均值,如果是,那麼就會嘗試進行一次可能有風險的Minor GC,如果小於或者HandlerPromotionFailure不允許冒險,就會進行一次老年代垃圾收集 Full GC.

2. 在JDK 1.6 update 24後,HandlerPromotionFailure引數是失敗的,此時只要老年代的連續可用空間大於新生代物件總大小或者歷次晉升的物件平均值就會進行Minor GC,否則進行Full GC.


總結下,JVM的垃圾收集策略

1. 物件優先在Eden區域分配,如果空間不夠,觸發一次 Minor GC 

2. 大物件直接進入到老年代中,可以通過引數設定,只要物件大於這個引數-XX:PretenureSizeThreshold的值,直接進入老年代

3. 長期存活的物件將進入老年代,只要物件的年齡等於這個引數-XX:MaxTenuringThreshold的值,下次gc進入老年代

4. 如果Survivor年齡相同的物件的總空間大於Survivor空間的一半,只要年齡大於等於這個年齡的物件,直接進入老年代

5. 執行新生代垃圾收集時候,存在一個擔保原則,老年代的連續可用空間大於新生代物件總大小或者歷次晉升的物件平均值就會進行Minor GC,否則進行Full GC.

相關文章