一 內存分配與回收策略概述
對象的內存分配往大方向講,就是在堆上分配(但也可能經過JIT編譯後被拆散爲標量類型並間接地棧上分配),
對象主要分配在新生代的Eden區上,如果啓用了本地線程分配緩衝,將按線程優先在TLAB上分配。少數情況下
也可能直接分配在老年代中,分配的規則並不是百分百固定的,分配細節取決於垃圾收集器組合,還有虛擬機中
與內存相關的參數設置。
HotSpot分代收集內存劃分圖:
以下內容實例分析基於JVM server模式下驗證,因爲用的是jdk8 64位默認就是server模式,不能切換到client模式,
就不換32 jdk,來回切換JVM的client和server模式了,不過要特別注意Client模式和Server模式分配策略有些不同。
二 對象優先在Eden區分配
對象通常在新生代Eden區中分配,當Eden區沒有足夠空間分配時,虛擬機將發生一次Minor GC。
與Minor GC對應的是Major GC、Full GC。
新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因爲Java對象大多都具備朝生夕滅的特性,
所以Minor GC非常頻繁,一般回收速度也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現Major GC,經常會伴隨至少一次的Minor GC(
但非絕對的,在Parallel Scavenge收集器的可以配置Major GC收集策略)。Major GC的速度一般會比Minor GC
慢10倍以上。
eg1:
相關參數:
-verbose:gc 輸出顯示虛擬機運行信息;
-XX:+PrintGCDetails 打印內存回收日誌;
-Xms20M -Xmx20M -Xmn10M 限制堆大小爲20M,不可以擴展,10M分配給新生代,剩下10分配給老年代;
-XX:SurvivorRatio=8 配置Eden區與一個Survivor區的比例,這裏是默認的8:1,不用顯示配置也可以;
代碼示例:
package com.lanhuigu.jvm.gc;
public class AllocationTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public static void main(String[] args) {
byte[] allocation = new byte[4 * _1MB];
}
}
結果:
Heap
PSYoungGen total 9216K, used 6414K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc43b58,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
從GC日誌可以看到,新生代eden區佔用78%,from、to兩個Survivor區和ParOldGen老年代都沒有使用,
驗證了對象優先在Eden區分配的事實。
按理說,eden區分配只是4M,應該佔50%纔對,事實分配後佔用的比實際分配的要多,主要是因爲Java對象
並不是一個人在戰鬥,其它部分也佔用了內存。
eg2:
把上面代碼實現修改如下:
package com.lanhuigu.jvm.gc;
public class AllocationTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public static void main(String[] args) {
byte[] allocation1 = new byte[2 * _1MB];
byte[] allocation2 = new byte[2 * _1MB];
byte[] allocation3 = new byte[5 * _1MB];
}
}
結果:
Heap
PSYoungGen total 9216K, used 6579K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 80% used [0x00000000ff600000,0x00000000ffc6ccb8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 5120K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 50% used [0x00000000fec00000,0x00000000ff100010,0x00000000ff600000)
Metaspace used 3333K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
程序執行後,eden區用了80%,from 未佔用,to未佔用,而ParOldGen老年代佔用50%。
當allocation1,allocation2分配的時候,eden區佔用80%,咱們分配4M,實際佔用了6.4M,
這個時候再分配allocation3是5M,eden區剩餘內存不夠5M,然後allocation3直接分配在老年代,
因爲分配的內存>=Eden大小的一半,就直接放入了老年代。
三 大對象直接進入老年代
大對象是指需要大量連續內存空間的Java對象,最經典的對象就是那種很長的字符串以及數組。
大對象對虛擬機內存分配來說就是一個壞消息,經常出現大對象容易導致內存還有不少空間時就
提前觸發垃圾收集以獲取足夠的連續空間來存儲大對象。
虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。
這樣做的目的是爲了避免在Eden區和兩個Survivor區之間發生大量的內存複製。
eg1:
相關參數:
-verbose:gc 輸出顯示虛擬機運行信息;
-XX:+PrintGCDetails 打印內存回收日誌;
-Xms20M -Xmx20M -Xmn10M 限制堆大小爲20M,不可以擴展,10M分配給新生代,剩下10分配給老年代;
-XX:SurvivorRatio=8 配置Eden區與一個Survivor區的比例,這裏是默認的8:1,不用顯示配置也可以;
-XX:PretenureSizeThreshold=3145728 (3M)設置大對象的閥值,大於該值,直接進入老年代;
-XX:+UseSerialGC 指定收集器爲Serial
注意:PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,我這裏用的是Jdk8,
默認使用Parallel Scavenge,一般不需要設置,如果非得需要,可以考慮ParNew和CMS收集器組合。
代碼示例:
package com.lanhuigu.jvm.gc;
public class PretenureSizeThresholdTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
*/
public static void main(String[] args) {
byte[] allocation = new byte[4 * _1MB];
}
}
結果:
Heap
def new generation total 9216K, used 2319K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee43d48, 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 3235K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K
從結果可以看出eden區沒怎麼使用,from、to兩個Survivor去未使用,老年代用了40%。
因爲分配對象爲4M,大於設置的3M閥值,直接在老年代進行分配。
四 長期存活的對象將進入老年代
虛擬機爲每一個對象定義了一個對象年齡(Age)計數器。如果對象在Eden區出生經過第一次Minor GC
後仍然存活,並能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設置爲1。對存活在
Survivor中的對象,每"熬過"一次Minor GC,年齡就增加1,當它的年齡增加到一定年齡閥值(默認15歲),
就將會被晉升 到老年代中。對老年代年齡閥值可以通過-XX:MaxTenuringThreshold設置。
相關參數:
-verbose:gc 輸出顯示虛擬機運行信息;
-XX:+PrintGCDetails 打印內存回收日誌;
-Xms20M -Xmx20M -Xmn10M 限制堆大小爲20M,不可以擴展,10M分配給新生代,剩下10分配給老年代;
-XX:SurvivorRatio=8 配置Eden區與一個Survivor區的比例,這裏是默認的8:1,不用顯示配置也可以;
-XX:MaxTenuringThreshold=1 年齡閥值設置,默認爲15歲。
代碼示例:
package com.lanhuigu.jvm.gc;
public class TenuringThresholdTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
}
結果:
Heap
def new generation total 9216K, used 4150K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 50% used [0x00000000fec00000, 0x00000000ff00dbf8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5140K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 50% used [0x00000000ff600000, 0x00000000ffb05210, 0x00000000ffb05400, 0x0000000100000000)
Metaspace used 3345K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
從執行結果可以看出eden使用50%,from、to兩個Survivor區未使用,老年代使用50%。
按照程序內存分配,從上到下分析,allocation1分配_1MB/4進入eden區,allocation2分配4MB進入eden區,
當第一次分配allocation3時,發現eden區內存不夠,直接觸發Minor GC,allocation1和allocation2按理說都進入
Survivor區,但是Survivor區只有1MB,只能容得下allocation1,allocation1進入Survivor區並且年齡爲1,
將在下一次GC時晉升到老年代,而allocation2通過擔保機制直接進入老年代,allocation3分配4MB則在eden區。
當第二次分配allocation3時,分配4M,這樣eden區不夠,因爲上一次分配allocation3是4MB,要比實際大,
現在又來4M,eden區只有8MB,當然eden區內存就不夠了,這個時候又觸發了一次Minor GC,allocation1年齡加1,
晉升到老年代,allocation2也還在老年代,上一次的allocation3因爲被設置爲null,直接被清除,eden區變爲8MB內存,
第二次的allocation3被分配到eden區。
最終結果就是: allocation1, allocation2分配在老年區,allocation3分配在新生代的eden區。
五 動態對象年齡判斷
對象的年齡到達了MaxTenuringThreshold可以進入老年代,同時,如果在survivor區中相同年齡所有對象
大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就可以直接進入老年代。
無需等到MaxTenuringThreshold中要求的年齡。
相關參數:
-verbose:gc 輸出顯示虛擬機運行信息;
-XX:+PrintGCDetails 打印內存回收日誌;
-Xms20M -Xmx20M -Xmn10M 限制堆大小爲20M,不可以擴展,10M分配給新生代,剩下10分配給老年代;
-XX:SurvivorRatio=8 配置Eden區與一個Survivor區的比例,這裏是默認的8:1,不用顯示配置也可以;
-XX:MaxTenuringThreshold=15 年齡閥值設置,默認爲15歲。
代碼示例:
package com.lanhuigu.jvm.gc;
public class TenuringThresholdTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
}
結果:
Heap
def new generation total 9216K, used 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff022828, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400618, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 5371K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 52% used [0x00000000ff600000, 0x00000000ffb3ec10, 0x00000000ffb3ee00, 0x0000000100000000)
Metaspace used 3339K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K
從結果可以看出eden使用51%,from、to兩個Survivor未使用,老年代使用了52%。
allocation1、allocation2、allocation3分配進入Eden區。
當第一次給allocation4分配內存時,eden區內存不夠,發生一次Minor GC,
此時allocation1、allocation2將會進入survivor區,而allocation3通過擔保機制將會進入老年代。
第二次發生在給allocation4分配內存時,此時,survivor區的allocation1、allocation2達到了survivor區容量的一半,
將會進入老年代,此次GC可以清理出allocation4原來的4MB空間,並將allocation4分配在Eden區。
最終,allocation1、allocation2、allocation3在老年代,allocation4在Eden區。
六 空間分配擔保
在發生Minor GC時,虛擬機會檢查老年代連續的空閒區域是否大於新生代所有對象的總和,
若成立,則說明Minor GC是安全的,否則,虛擬機需要查看HandlePromotionFailure的值,看是否運行擔保失敗,
若允許,則虛擬機繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,
若大於,將嘗試進行一次Minor GC;
若小於或者HandlePromotionFailure設置不運行冒險,那麼此時將改成一次Full GC,
以上是JDK Update 24之前的策略,之後的策略改變了,只要老年代的連續空間大於新生代對象總大小
或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。冒險是指經過一次Minor GC後有大量對象存活,
而新生代的survivor區很小,放不下這些大量存活的對象,所以需要老年代進行分配擔保,
把survivor區無法容納的對象直接進入老年代。
代碼示例:
package com.lanhuigu.jvm.gc;
public class HandlePromotionTest {
private static final int _1MB = 1024 * 1024;
/**
* VM參數: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:+HandlePromotionFailture
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4,
allocation5, allocation6, allocation7, allocation8;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation1 = null;
allocation4 = new byte[2 * _1MB];
allocation5 = new byte[2 * _1MB];
allocation6 = new byte[2 * _1MB];
allocation4 = null;
allocation5 = null;
allocation6 = null;
allocation7 = new byte[2 * _1MB];
}
}
結果:
Heap
def new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000)
Metaspace used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 275K, capacity 386K, committed 512K, reserved 1048576K
一開始allocation1、allocation2、allocation3分配在eden區,同時allocation1被置爲無效。
當第一次給allocation4分配內存時,eden區內存不夠,發生一次Minor GC,
由於老年代的連續可用空間大於存活的對象總和,所以allocation2、allocation3將會進入老年代,
allocation1的空間將被回收,此時整個eden區被清空,又變爲8MB,有空間讓allocation4分配在eden區;
接下來allocation5、allocation6接着往eden區分配,同時allocation4、allocation5、allocation6被置爲無效。
當第二次給allocation7分配內存時,eden區內存不夠,發生一次Minor GC,allocation4、allocation5、
allocation6所佔的內存全部回收,把整個eden區清空,變爲8MB,然後將allocation7分配在新生代eden區。
最後,allocation2、allocation3在老年代,allocation7在新生代。