(四)學習JVM —— 內存分配與回收策略

(一)學習JVM ——運行時數據區域 java

(二)學習JVM —— 垃圾回收機制 算法

(三)學習JVM —— 垃圾回收器 數組

(四)學習JVM —— 內存分配與回收策略安全

內存分配與回收

對象的回收已經經過介紹回收算法與虛擬機,大體學習了一次。bash

對象的內存分配,往大方向講,就是在堆上分配對象,主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按縣城優先在TLAB上分配。學習

JVM在內存新生代Eden Space中開闢了一小塊線程私有的區域,稱做TLAB(Thread-local allocation buffer)。默認設定爲佔用Eden Space的1%。在Java程序中不少對象都是小對象且用過即丟,它們不存在線程共享也適合被快速GC,因此對於小對象一般JVM會優先分配在TLAB上,而且TLAB上的分配因爲是線程私有因此沒有鎖開銷。所以在實踐中分配多個小對象的效率一般比分配一個大對象的效率要高。
也就是說,Java中每一個線程都會有本身的緩衝區稱做TLAB(Thread-local allocation buffer),每一個TLAB都只有一個線程能夠操做,TLAB結合bump-the-pointer技術能夠實現快速的對象分配,而不須要任何的鎖進行同步,也就是說,在對象分配的時候不用鎖住整個堆,而只須要在本身的緩衝區分配便可。測試

少數狀況下,也可能會直接分配在老年代中,分配規則不是百分百固定的,其細節取決於使用的是哪種垃圾收集器組合,還有虛擬機中的參數設置。spa

理解GC日誌

先看看GC日誌是什麼格式,在後面的例子中會對日誌進行分析。.net

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(1024K), 0.01149142 secs] 4603K->210K(19456K), [Perm: 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00 real=0.02 secs]

最前面的數字33.125和100.667,表明GC發生的時間,這個數字的含義是從虛擬機啓動以來通過的秒數。線程

GC日誌開頭的"[GC"和"[Full GC"說明了此次垃圾回收的停頓類型,若是有Full表明發生了STW。

接下來"[DefNew"、"[Tenured"和"[Perm"表示GC發生的區域,DefNew=Default New Generation,表明新生代,若是用ParNew回收器,新生代叫"[ParNew"=Parallel New Generation,若是採用Parallel Scavenge回收器,新生代叫"PSYoungGen",老年代同理。

後面方括號裏的3324K->152K(3712K)的意思是「GC前該內存區域已使用的容量」->"GC後該內存區域已使用的容量(該內存區域總容量)"。

方括號外面的3324K->152K(11904K)表示"GC前堆已使用的容量"->"GC後堆已使用的容量(堆總容量)"

再日後的"0.0031680 secs"表示本次GC所佔用的時間,單位是秒。

有的回收器會帶有"[Times: user=0.01 sys=0.00 real=0.02 secs]",這種輸入與Linux的time命令輸出一致,分別表明用戶態消耗CPU時間,內核態消耗CPU時間,操做從開始到結束通過的牆鍾時間(Wall Clock Time)。

對象在Eden分配

大多數狀況下,對象在新生代Eden區分配。當Eden區沒有足夠空間時進行分配時,虛擬機將會發起一次Minor GC。

新生代GC (Minor GC)

指發生在新生代的垃圾回收動做,由於Java對象大多都具有朝生夕滅的特性,因此MinorGC很是頻繁,通常回收速度也比較快。

虛擬機提供了-XX:+PrintGCDetails參數,告訴咱們在發生垃圾回收行爲時,打印內存回收日誌,並在線程退出的時候輸出當前內存各區域的分配狀況,具體測試看下面代碼,和GC日誌:

package test;

public class Test1 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM參數:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * @param args
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3, c4;
		c1 = new byte[2 * _1MB];
		c2 = new byte[2 * _1MB];
		c3 = new byte[2 * _1MB];
		c4 = new byte[4 * _1MB]; // 出現 Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 7292K->556K(9216K), 0.0021306 secs] 7292K->6700K(19456K), 0.0021570 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4734K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  54% used [0x00000000ff500000, 0x00000000ff58b018, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

main方法中,嘗試分配3個2M對象和1個4MB對象,根據設置,eden 8M,兩個survivor分別是1M,新生代的可用空間是eden + 1個survivor = 9M。

在分配c4的時候會發生一次Minor GC,這個GC的結果是(日誌第一行),新生代7292K->556K(9216K),內存佔用幾乎沒有減小(由於c一、c二、c3都是存活的對象)。此次發生的緣由是,分配c4的時候,發展Eden已經佔用了6MB,剩餘的空間不夠分配4MB內存,所以發生Minor GC。GC期間,發現c一、c二、c3對象都是2MB,沒法放入Survivor空間(由於Survivor只有1MB),因此只好經過分配擔保將這3個2MB的對象轉移到老年代。

GC結束後,c4順利分配在Eden中,所以程序線程結束後,結果爲"eden space ... 51%",老年代"the space 10240K ... 60%"

大對象在老年代分配

因此大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。大對象堆虛擬機的分配來講是一個壞消息(更壞的消息就是遇到一羣朝生夕陽滅的短命大對象),常常出現大對象容易致使內存還有很多空間,就提早出發垃圾回收來獲取足夠的連續空間分配它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個值的對象直接在老年代分配,避免Eden和Survivor發生大量的內存複製,具體測試看下面代碼,和GC日誌:

package test;

public class Test2 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM參數:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
	 */
	public static void main(String[] args) {

		byte[] c1;
		c1 = new byte[4 * _1MB];

	}

}
Heap
 def new generation   total 9216K, used 1312K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed481e8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 2718K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

代碼中參數-XX:PretenureSizeThreshold=3145728是說大於3MB的對象直接分配到老年代(3145728=3*1024*1024),看日誌中 "the space 1024K, 40% used"說明對象c1直接分配到了老年代。

長期存活的對象晉升到老年代

虛擬機採用分代收集的思想管理內存,在內存回收時就必需要能識別那些對象該分配在新生代,哪些對象該分配在老年代。虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生並通過一次MinorGC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor並將年齡設置爲1歲。對象在Survivor區每熬過一次Minor GC,就增長1歲,當它的年齡增長到必定程度(默認15歲),就晉升到老年代。對象晉升老年代的閥值,能夠經過參數-XX:MaxTenuringThreshold設置。

測試將該參數設置爲1,並觀察代碼與GC日誌:

package test;

public class Test3 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM參數:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3;
		c1 = new byte[_1MB / 4];
		c2 = new byte[4 * _1MB];
		c3 = new byte[4 * _1MB]; // Minor GC
		c3 = null;
		c3 = new byte[4 * _1MB]; // Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 5500K->811K(9216K), 0.0016828 secs] 5500K->4908K(19456K), 0.0017091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4908K->0K(9216K), 0.0005337 secs] 9004K->4906K(19456K), 0.0005447 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4906K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffacabc8, 0x00000000ffacac00, 0x0000000100000000)
 Metaspace       used 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

一共發生了2次GC。分配c1只須要256KB,在第一次GC時,Survivor能夠容納c1,當第二次GC時,熬過1此GC的Survivor中的c1晉升到了老年代,因此結果爲eden 51% used(存放c3),老年代10240K 47% used(存放c1和c2)。

動態對象年齡斷定

爲了能更好的適應不一樣程序的內存狀況,虛擬機並非永遠的要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象的大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無需等到參數要求的年齡。

具體看代碼與GC日誌:

package test;

public class Test4 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM參數:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3;
		c1 = new byte[_1MB / 4];
		c2 = new byte[4 * _1MB];
		c3 = new byte[4 * _1MB]; // Minor GC
		c3 = null;
		c3 = new byte[4 * _1MB]; // Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 5500K->811K(9216K), 0.0017106 secs] 5500K->4908K(19456K), 0.0017381 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4908K->0K(9216K), 0.0005325 secs] 9004K->4906K(19456K), 0.0005422 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4906K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffacabc8, 0x00000000ffacac00, 0x0000000100000000)
 Metaspace       used 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

代碼中MaxTenuringThreshold參數已經設置成了15,可是發如今通過第二次GC後,Survivor中的c1依舊晉升到了老年代,這就是由於,c1與c2加起來大於Survivor空間的通常(大於512KB),而且它們是同年代的對象,知足同年對象達到Survivor空間的通常這種規則。若是註釋到最後一行c3,就會發現結果不同。

空間分配擔保

在發生MinorGC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。

只要老年代的連續空間大於新生代對象總大小,或歷次晉升的平均大小就會進行MinorGC,不然進行FullGC。

取平均晉升大小的值進行比較實際上是一種動態機率的手段,也就是說,若是某次MinorGC內存後的對象突增,遠遠高於平均值的話,那就只好在從新發起一次Full GC。

(一)學習JVM ——運行時數據區域

(二)學習JVM —— 垃圾回收機制

(三)學習JVM —— 垃圾回收器

(四)學習JVM —— 內存分配與回收策略

相關文章
相關標籤/搜索