1. 程式人生 > >JVM筆記4:Java記憶體分配策略及配置引數

JVM筆記4:Java記憶體分配策略及配置引數

簡單來說,物件記憶體分配主要是在堆中分配。但是分配的規則並不是固定的,取決於使用的收集器組合以及JVM記憶體相關引數的設定

以下介紹幾條基本規則(使用的ParNew+Serial Old收集器組合):

一,物件優先在新生代Eden區分配

//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails
public class test {
	static int mb = 1024*1024;
	
	public static void main(String[] args) {
		byte[] b1 = new byte[2*mb];
		System.out.println("b1 over");
		byte[] b2 = new byte[2*mb];
		System.out.println("b2 over");
		byte[] b3 = new byte[2*mb];
		System.out.println("b3 over");//GC
		byte[] b4 = new byte[4*mb];
		System.out.println("b4 over");
	}
}

堆記憶體大小為20M,不可自動擴充套件,新生代記憶體大小為10M,根據預設值,Eden區:Survivor區為8:1,Eden區大小應為:10M*8/10=8129KB,Survivor區大小應為1024KB,新生代總可用記憶體應為9216KB

當b3分配完成後,新生代將使用6M記憶體(6144KB,b1+b2+b3),同時申請b4的4M=4096KB記憶體,此時新生代的可用記憶體為9216-6144=3072KB,不足以分配b4的空間,則觸發一次Minor GC回收新生代記憶體空間,由於b1、b2以及b3都為存活狀態,並且剩餘的一個Survivor區無法裝下b1、b2和b3,則新生代會租借老年代的區域,並將b1、b2和b3移動至租借區域,然後新生代完成Minor GC。由於此時新生代已經沒有物件存放其中,剩餘大量記憶體,則b4將在新生代中分配

b1 over
b2 over
b3 over
{Heap before GC invocations=0 (full 0):
 par new generation   total 9216K, used 6487K [0x03b30000, 0x04530000, 0x04530000)//b1+b2+b3,佔6M
  eden space 8192K,  79% used [0x03b30000, 0x04185f60, 0x04330000)
  from space 1024K,   0% used [0x04330000, 0x04330000, 0x04430000)
  to   space 1024K,   0% used [0x04430000, 0x04430000, 0x04530000)
 tenured generation   total 10240K, used 0K [0x04530000, 0x04f30000, 0x04f30000)//老年代為空
   the space 10240K,   0% used [0x04530000, 0x04530000, 0x04530200, 0x04f30000)
 compacting perm gen  total 12288K, used 2105K [0x04f30000, 0x05b30000, 0x08f30000)
   the space 12288K,  17% used [0x04f30000, 0x0513e478, 0x0513e600, 0x05b30000)
No shared spaces configured.
[GC [ParNew: 6487K->150K(9216K), 0.0092952 secs] 6487K->6294K(19456K), 0.0093314 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]//物件仍處於存活狀態,新生代無足夠的空間完成Minor GC,只能租借老年代的空間,將b1、b2和b3移動至老年代 
Heap after GC invocations=1 (full 0):
 par new generation   total 9216K, used 150K [0x03b30000, 0x04530000, 0x04530000)//新生代幾乎被清空
  eden space 8192K,   0% used [0x03b30000, 0x03b30000, 0x04330000)
  from space 1024K,  14% used [0x04430000, 0x04455a10, 0x04530000)
  to   space 1024K,   0% used [0x04330000, 0x04330000, 0x04430000)
 tenured generation   total 10240K, used 6144K [0x04530000, 0x04f30000, 0x04f30000)//b1+b2+b3
   the space 10240K,  60% used [0x04530000, 0x04b30030, 0x04b30200, 0x04f30000)
 compacting perm gen  total 12288K, used 2105K [0x04f30000, 0x05b30000, 0x08f30000)
   the space 12288K,  17% used [0x04f30000, 0x0513e478, 0x0513e600, 0x05b30000)
No shared spaces configured.
}
b4 over
Heap
 par new generation   total 9216K, used 4410K [0x03b30000, 0x04530000, 0x04530000)//b4
  eden space 8192K,  54% used [0x03b30000, 0x03f82008, 0x04330000)
  from space 1024K,  14% used [0x04430000, 0x04455a10, 0x04530000)
  to   space 1024K,   0% used [0x04330000, 0x04330000, 0x04430000)
 tenured generation   total 10240K, used 6144K [0x04530000, 0x04f30000, 0x04f30000)//b1+b2+b3
   the space 10240K,  60% used [0x04530000, 0x04b30030, 0x04b30200, 0x04f30000)
 compacting perm gen  total 12288K, used 2116K [0x04f30000, 0x05b30000, 0x08f30000)
   the space 12288K,  17% used [0x04f30000, 0x051413c8, 0x05141400, 0x05b30000)
No shared spaces configured.

二,大物件直接進入老年代

為了避免記憶體回收時大物件在Eden區和2個Survivor區之間的拷貝(ParNew收集器使用複製演算法),同時為了避免為了提供足夠的記憶體空間而提前觸發的GC,虛擬機器提供了-XX:PretenureSizeThreshold該設定只對Serial和ParNew收集器生效)引數,大於該引數設定值的物件將直接在老年代分配

//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails 
//-XX:PretenureSizeThreshold=2097152
public class test {
	static int mb = 1024*1024;
	
	public static void main(String[] args) {
		byte[] b1 = new byte[3*mb];
		System.out.println("b1 over");
	}
}

由於設定超過2M(2*1024*1024=2097152B)的物件直接在老年代分配,故b1將分配在老年代上

b1 over
Heap
 par new generation   total 9216K, used 507K [0x03b50000, 0x04550000, 0x04550000)//新生代幾乎為空
  eden space 8192K,   6% used [0x03b50000, 0x03bcef00, 0x04350000)
  from space 1024K,   0% used [0x04350000, 0x04350000, 0x04450000)
  to   space 1024K,   0% used [0x04450000, 0x04450000, 0x04550000)
 tenured generation   total 10240K, used 3072K [0x04550000, 0x04f50000, 0x04f50000)//老年代使用了3*1024K記憶體
   the space 10240K,  30% used [0x04550000, 0x04850010, 0x04850200, 0x04f50000)
 compacting perm gen  total 12288K, used 2110K [0x04f50000, 0x05b50000, 0x08f50000)
   the space 12288K,  17% used [0x04f50000, 0x0515f8c8, 0x0515fa00, 0x05b50000)
No shared spaces configured.

三,長期存活物件將進入老年代
由於虛擬機器垃圾收集是基於“分代演算法”的,故虛擬機器必須能夠識別哪些物件存放在新生代,哪些物件應該存放在老年代

虛擬機器設計了一個物件年齡計數器,如果物件在Eden區出生並且經過第一次Minor GC後依然存活,並且可以被Survivor區容納,就會被複制至Survivor區並將物件年齡設定為1。以後物件每熬過一次Minor GC,物件年齡便+1。當物件年齡超過物件晉升老年代的年齡閥值(該閥值預設為15)時,便會晉升至老年代,何時晉升,我們接下來研究

虛擬機器提供了-XX:MaxTenuringThreshold引數設定晉升閥值

//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails 
//-XX:MaxTenuringThreshold=1
public class test {
	static int mb = 1024*1024;
	
	public static void main(String[] args) {
		System.out.println("step 1");
		byte[] b1 = new byte[1*mb/4];
		System.out.println("step 2");
		byte[] b2 = new byte[4*mb];
		System.out.println("step 3");
		byte[] b3 = new byte[4*mb];//GC
		System.out.println("step 4");
		b3 = null;
		System.out.println("step 5");
		b3 = new byte[4*mb];//GC
	}
}

b1、b2正常分配。在step3,新生代將沒有足夠的記憶體分配b3所需的4M空間,故引發一次Minor GC。b1只有256KB,可以放置在Survivor區中,故複製b1到Survivor區中,b2為4M,無法放置到Survivor區中,故租借老年代4M記憶體放置b2,回收新生代記憶體空間,b1經歷了一次Minor GC後依然存活,故年齡變為1。

在step4,分配給b3物件的記憶體空間依然被佔用,只是將b3物件的引用置為空,由於不涉及到記憶體分配,故而不涉及到GC,因此物件的年齡也不會發生變化

在step5,重新給b3物件分配4M空間,由於新生代沒有足夠記憶體,故引發Minor GC,step3分配給b3的4M記憶體空間由於不再與存活物件相關聯,將被回收,同時,由於b1的年齡到達物件晉升老年代的年齡設定,b1將被移動至老年代

step 1
step 2
step 3
{Heap before GC invocations=0 (full 0):
 par new generation   total 9216K, used 4695K [0x03b80000, 0x04580000, 0x04580000)//b1+b2
  eden space 8192K,  57% used [0x03b80000, 0x04015f50, 0x04380000)
  from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//此時老年代為空
   the space 10240K,   0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)
 compacting perm gen  total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)
No shared spaces configured.
[GC [ParNew: 4695K->409K(9216K), 0.0049519 secs] 4695K->4505K(19456K), 0.0049944 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=1 (full 0):
 par new generation   total 9216K, used 409K [0x03b80000, 0x04580000, 0x04580000)//b1
  eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)
  from space 1024K,  39% used [0x04480000, 0x044e6610, 0x04580000)
  to   space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
 tenured generation   total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2
   the space 10240K,  40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)
 compacting perm gen  total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)
No shared spaces configured.
}
step 4
step 5
{Heap before GC invocations=1 (full 0):
 par new generation   total 9216K, used 4669K [0x03b80000, 0x04580000, 0x04580000)//b1+b3(step3)
  eden space 8192K,  52% used [0x03b80000, 0x03fa9098, 0x04380000)
  from space 1024K,  39% used [0x04480000, 0x044e6610, 0x04580000)
  to   space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
 tenured generation   total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2
   the space 10240K,  40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)
 compacting perm gen  total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)
No shared spaces configured.
[GC [ParNew: 4669K->43K(9216K), 0.0008256 secs] 8765K->4548K(19456K), 0.0008701 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=2 (full 0):
 par new generation   total 9216K, used 43K [0x03b80000, 0x04580000, 0x04580000)//step3分配的b3物件空間被回收
  eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)
  from space 1024K,   4% used [0x04380000, 0x0438ad90, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2
   the space 10240K,  43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)
 compacting perm gen  total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)
No shared spaces configured.
}
Heap
 par new generation   total 9216K, used 4303K [0x03b80000, 0x04580000, 0x04580000)//b3(step5)
  eden space 8192K,  52% used [0x03b80000, 0x03fa8fe0, 0x04380000)
  from space 1024K,   4% used [0x04380000, 0x0438ad90, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2
   the space 10240K,  43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)
 compacting perm gen  total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x051913c8, 0x05191400, 0x05b80000)
No shared spaces configured.

如果修改MaxTenuringThreshold的值為2,從列印日誌中可以發現,最終老年代的記憶體使用量為4096KB=4M,也就是說b1沒有晉升至老年代

上面是Minor GC的執行狀況,如果是Full GC呢:

//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails 
//-XX:MaxTenuringThreshold=1
public class test {
	static int mb = 1024*1024;
	
	public static void main(String[] args) {
		byte[] b1 = new byte[1*mb/4];
		System.gc();
	}
}

這裡我們使用的是Full GC。

Full GC通常至少伴隨著一次Minor GC(並非絕對),看下面日誌,這裡的Minor GC應該至少發生了2次,一次Minor GC是不會把b1移動至老年代的

{Heap before GC invocations=0 (full 0):
 par new generation   total 9216K, used 599K [0x03b80000, 0x04580000, 0x04580000)//b1
  eden space 8192K,   7% used [0x03b80000, 0x03c15f40, 0x04380000)
  from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//老年代為空
   the space 10240K,   0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)
 compacting perm gen  total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)
No shared spaces configured.
[Full GC (System) [Tenured: 0K->404K(10240K), 0.0069434 secs] 599K->404K(19456K), [Perm : 2104K->2104K(12288K)], 0.0069992 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=1 (full 1):
 par new generation   total 9216K, used 0K [0x03b80000, 0x04580000, 0x04580000)//新生代為空
  eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)
  from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)//b1
   the space 10240K,   3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)
 compacting perm gen  total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)
No shared spaces configured.
}
Heap
 par new generation   total 9216K, used 327K [0x03b80000, 0x04580000, 0x04580000)
  eden space 8192K,   4% used [0x03b80000, 0x03bd1f98, 0x04380000)
  from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)
  to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)
 tenured generation   total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)
   the space 10240K,   3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)
 compacting perm gen  total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)
   the space 12288K,  17% used [0x04f80000, 0x05191190, 0x05191200, 0x05b80000)
No shared spaces configured.

四:動態物件年齡判定

為了使記憶體分配更加靈活,虛擬機器並不要求物件年齡達到MaxTenuringThreshold才晉升老年代

如果Survivor區中相同年齡所有物件大小的總和大於Survivor區空間的一半,年齡大於或等於該年齡的物件在Minor GC時將複製至老年代

//-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m  -XX:MaxTenuringThreshold=10
//-XX:+PrintTenuringDistribution
public class Test {
	static int mb = 1024*1024;
	
	public static void main(String[] args) {
		System.out.println("step 1");
		byte[] b1 = new byte[1*mb/4];
		byte[] b3 = new byte[4*mb];
		byte[] b4 = new byte[4*mb];//GC
		System.out.println("step 2");
		byte[] b2 = new byte[1*mb/4];//可以嘗試1*mb/2,然後觀察日誌
		b4 = null;
		System.out.println("step 3");
		b4 = new byte[4*mb];//GC
		System.out.println("step 4");
		b4 = null;
		b4 = new byte[4*mb];//GC
	}
}

先來介紹一個設定-XX:+PrintTenuringDistribution,這個引數很有意思,會在Minor GC時列印Survivor區記憶體容量的一半,晉升老年代年齡閥值,Survivor區中的物件大小以及物件年齡

根據啟動引數的設定,Survivor大小的一半是524288B,也就是512KB。第一次GC後,b1依然存活,故年齡變為1。第二次GC後,b1和b2依然存活,故b1的年齡變為2,b2的年齡為1。b1+b2的大小加起來超過了Survivor區容量的一半,此時會修改Survivor區晉升老年代年齡閥值為2(如果移動年齡為2的物件可以使Survivor去的記憶體使用降至512KB以內,則只移動年齡為2的物件,否則將會同時移動年齡為1的物件)。第三次GC時,將年齡等於晉升閥值的物件移動至老年代,執行GC,GC結束後,b1依然在Survivor區(當然可能從Survivor from區拷貝至了Survivor to區),此時b1的年齡變為2。這時Survivor區的使用記憶體沒有達到512M,修改Survivor區晉升老年代年齡閥值為引數設定的10。

step 1

Desired survivor size 524288 bytes, new threshold 10 (max 10)
- age   1:     412800 bytes,     412800 total
step 2
step 3

Desired survivor size 524288 bytes, new threshold 2 (max 10)
- age   1:     262160 bytes,     262160 total
- age   2:     412800 bytes,     674960 total
step 4

Desired survivor size 524288 bytes, new threshold 10 (max 10)
- age   1:        136 bytes,        136 total
- age   2:     262160 bytes,     262296 total

最後,為什麼在第三次GC後,Survivor區還存在一個大小為136B,年齡為1的被使用記憶體空間?

我猜測,雖然Minor GC時Survivor區沒有足夠的空間完成GC時會租借老年代的記憶體,但是在Survivor區依然儲存了一個指向老年代租借記憶體起始地址的引用

五:空間分配擔保

這個前面已經出現過多次了,由於新生代使用複製演算法,當Minor GC時如果存活物件過多,無法完全放入Survivor區,就會向老年代借用記憶體存放物件,以完成Minor GC

在觸發Minor GC時,虛擬機器會先檢測之前GC時租借的老年代記憶體的平均大小是否大於老年代的剩餘記憶體,如果大於,則將Minor GC變為一次Full GC,如果小於,則檢視虛擬機器是否允許擔保失敗(-XX:+/-HandlePromotionFailure。從jdk6.0開始,允許擔保失敗已變為HotSpot虛擬機器所有收集器預設設定,虛擬機器將不再識別該引數設定,詳見JDK-6990095 : Deprecate and eliminate -XX:-HandlePromotionFailure),如果允許擔保失敗,則只執行一次Minor GC,否則也要將Minor GC變為一次Full GC(直到GC結束時才能確定到底有多少物件需要被移動至老年代,所以在GC前,只能使用粗略的平均值進行判斷)