JVM-09自动内存管理机制【内存分配和回收策略】
文章目錄
- 思維導圖
- 對象優先在eden區域分配
- 理論
- 案例
- 虛擬機參數設置及參數說明
- 代碼
- GC結果分析
- 大對象直接進入老年代
- 理論
- 案例
- 虛擬機參數設置及參數說明
- 代碼
- GC日志
- 長期存活的對象將進入老年代
- 理論
- 案例
- 虛擬機參數設置及參數說明
- 代碼
- XX:MaxTenuringThreshold=1時的 GC日志
- XX:MaxTenuringThreshold=15時的 GC日志
- 動態對象年齡判定
- 理論
- 案例
- 虛擬機參數設置及參數說明
- 代碼
- GC日志
- 空間分配擔保
- 理論
思維導圖
對象優先在eden區域分配
理論
大多數情況下,對象在新生代的eden區中分配,當eden區沒有足夠的空間進行分配時,虛擬機將進行一次Minor GC。
虛擬機提供-XX:+PrintGCDetails參數,告訴虛擬機在發生垃圾收集行為時打印內存回收日志,并且在進程退出的時候輸出當前的內存各區域分配情況。
實際應用中,GC日志一般都是輸出到文件中,使用GC日志分析工具來進行分析。
案例
虛擬機參數設置及參數說明
JDK1.6
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails-
-Xms20M 初始化堆內存 20M
-
-Xmx20M 最大堆內存20M,結合-Xms20M 即為堆內存不可擴展
-
-Xmn10M 新生代內存分配10M,結合-Xms -Xmx 可知 老年代也是10M
-
-XX:SurvivorRatio=8 默認值,可不配置。 新生代中Eden區與一個Survivor區的比例為8:1,即 Eden: from Survivor:to Survivor = 8:1:1,即8MB:1MB:1MB,新生代的可用空間為9MB。
-
-XX:+UseSerialGC 指定年輕代使用Serial垃圾收集器
-
-verbose:gc 和 -XX:+PrintGCDetails 發生垃圾回收時,打印GC日志
代碼
package com.artisan.gc;public class EdenAllocationGC {private int _1M = 1024 * 1024;/*** * @Title: testGCAllocation* * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails* * @return: void*/@SuppressWarnings("unused")private void testGCAllocation() {// 如下的分配,僅僅是為了占用些內存空間,方便觀察GC回收情況byte[] object1 = new byte[2 * _1M];byte[] object2 = new byte[2 * _1M];byte[] object3 = new byte[2 * _1M];byte[] object4 = new byte[4 * _1M];}public static void main(String[] args) {new EdenAllocationGC().testGCAllocation();}}GC結果分析
[GC [DefNew: 6487K->159K(9216K), 0.0052344 secs] 6487K->6303K(19456K), 0.0052735 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heapdef new generation total 9216K, used 4582K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)eden space 8192K, 54% used [0x00000000f9a00000, 0x00000000f9e51f98, 0x00000000fa200000)from space 1024K, 15% used [0x00000000fa300000, 0x00000000fa327c28, 0x00000000fa400000)to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)compacting perm gen total 21248K, used 2995K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0eccf8, 0x00000000fb0ece00, 0x00000000fc2c0000) No shared spaces configured.因為內容較少可以直接閱讀 。 當然了也可以傳到http://gceasy.io 在線生成一份GC分析報告。
這里我們直接來分析下這個GC日志
結合JVM參數的設置,通過GC日志的驗證,符合設置。
def new generation total 9216K ...... eden space 8192K ...... from space 1024K ...... to space 1024K ......def new generation 是通過-XX:+UseSerialGC指定的垃圾回收器,顯示名稱是由收集器決定的。
- 如果是用的Serial收集器,新生代名為“Default New Generation”,所以顯示“[DefNew”。
- 如果是用的ParNew收集器,新生代名為“Parallel New Generation”,所以顯示“[ParNew”。
- 如果是用的Parallel Scavenge收集器,新生代名則顯示為“[PSYongGen”
新生代分配了10M,又因為SurvivorRatio = 8 。 所以 Eden: from Survivor:to Survivor = 8192K:1024K :1024K
通過new創建對象的方式,虛擬機會將對象的實例分配到堆內存中,具體的說是分配object1 、object2 、object3 三個對象到 Eden區+Survivor From,3個對象占6MB空間,而 Eden + Survivor From 的大小為9M,空間足夠,優先分配到Eden區。 所以Eden區的內存被占用6M
分配object4時的時候,發現Eden+Survivor From剩余空間只有3M,而object4占用4M的內存,這個時候就會觸發一次Minor GC ,輸出的信息如下
[GC [DefNew: 6487K->159K(9216K), 0.0052344 secs] 6487K->6303K(19456K), 0.0052735 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]可以看到6487K->159K(9216K) ,新生代的內存由6487K變為了159K。 而堆內存 6487K->6303K基本沒有發生變化,是因為 object1,object2,object3都是存活的對象,無法被GC回收。
GC期間又發現已有的3個2MB的對象都無法放入Survivor To空間(1MB),所以通過擔保機制提前轉移到老年代區(3個2MB的對象),此時Eden區恢復到8MB空間,然后將object4分配到Eden空間。
GC結束后,4M的object4被順利的分配到了Eden區中,Survivor空閑。 老年代 tenured generation占用6M(object1,object2,object3占用)。 通過如下日志也可以眼睜這個結論
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)大對象直接進入老年代
理論
需要大量連續內存空的Java對象,一般稱之為大對象。
PretenureSizeThreshold參數,可以在新生代直接分配的對象最大值,0表示沒有最大值 。 可以使大于這個值的對象直接在老年代分配,避免在Eden區和Survivor區發生大量的內存復制,該參數只對Serial和ParNew收集器有效,Parallel Scavenge并不認識該參數
使用方法:-XX:PretenureSizeThreshold=1000000
案例
虛擬機參數設置及參數說明
JDK1.6
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728- -XX:PretenureSizeThreshold=3145728 : 單位是byte, 3145728 = 3M,大于3M的對象直接在老年代分配,避免在Eden區和Survivor區發生大量的內存復制.
代碼
package com.artisan.gc;public class PretenureSizeThresholdTest {private int _1M = 1024 * 1024;/*** * @Title: testPretenureSizeThreshold* * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails* -XX:PretenureSizeThreshold=3145728* * @return: void*/@SuppressWarnings("unused")private void testPretenureSizeThreshold() {// 如下的分配,僅僅是為了占用些內存空間,方便觀察GC回收情況byte[] object4 = new byte[4 * _1M];}public static void main(String[] args) {new PretenureSizeThresholdTest().testPretenureSizeThreshold();} }GC日志
JDK1.6
Heapdef new generation total 9216K, used 507K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)eden space 8192K, 6% used [0x00000000f9a00000, 0x00000000f9a7ee98, 0x00000000fa200000)from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)compacting perm gen total 21248K, used 2985K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0ea690, 0x00000000fb0ea800, 0x00000000fc2c0000) No shared spaces configured.可以看到這里并沒有發生Minor GC ,僅僅是打印了堆內存信息。 通過-XX:PretenureSizeThreshold=3145728的設置,4M大小的object4 大于設置的3M閥值,直接分配到了老年代。
tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)長期存活的對象將進入老年代
理論
現在商用虛擬機都采用分代收集的思想來管理內存,那么內存回收就必須能識別哪些對象應該放在新生代,哪些對象應該放在老年代。
為了做到這一點,虛擬機給每個對象定義了一個對象年齡計數器。 如果對象在Eden畜生并經過第一次Minor GC后仍然存活,并且能夠被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設置為1。 對象在Survivor區中沒經歷過一次Minor GC且存活下來,年齡就增加1歲。當它的年齡增加到一定程度(默認15歲),就將會被晉升到老年代中。
對象晉升老年代的年齡可以通過-XX:MaxTenuringThreshold設置
案例
我們分別將-XX:MaxTenuringThreshold=1 和 -XX:MaxTenuringThreshold=15 來看下GC日志的區別。
虛擬機參數設置及參數說明
JDK1.6
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1結合這個虛擬機參數設置,我們來構造符合預期的數據
-Xms20M -Xmx20M:java堆內存初始化值和最大值均為20M,不可擴展。
-Xmn10M :同時給新生代分配10M內存,可以推算出老年代也是20-10=10M
通過-XX:SurvivorRatio=8可知,新生代中Eden : Survivor From : Survivor To = 8:1:1 ,所以新生代能用的最大的內存為9M。
通過-XX:MaxTenuringThreshold設置對象在新生代存活的最大年齡。
根據Eden : Survivor From : Survivor To = 8:1:1 來構造對象的大小 。 一個256KB的對象,確保在不符合MaxTenuringThreshold的情況下,Survivor To 區能夠有足夠的空間存放這個256KB的對象。
代碼
package com.artisan.gc;public class MaxTenuringThresholdTest {private static final int _1M = 1024 * 1024;/*** * * @Title: testMaxTenuringThreshold* * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails* -XX:MaxTenuringThreshold=1* * * @return: void*/public void testMaxTenuringThreshold() {// 根據JVM參數的設置,分配合理的大小,達到測試的目的byte[] object1 = new byte[_1M / 4];byte[] object2 = new byte[_1M * 4];// 什么時候進入老年代取決于-XX:MaxTenuringThresholdbyte[] object3 = new byte[_1M * 4];object3 = null;byte[] object4 = new byte[_1M * 4];}public static void main(String[] args) {new MaxTenuringThresholdTest().testMaxTenuringThreshold();}}XX:MaxTenuringThreshold=1時的 GC日志
堆內存新生代可用空間為9M, 首先在堆內存中分配了object1 256KB的內存,緊接著分配了object2 占用4M內存空間,此時新生代中還剩余8M-( 256KB+4M ) 的內存空間, object3 占用一個4M的內存空間,空間已經不夠,提前觸發了一次Minor GC
[GC [DefNew: 4695K->415K(9216K), 0.0056811 secs] 4695K->4511K(19456K), 0.0057238 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]發生Minor GC,Survivor From 只有1M的空間可用,也不夠存放object2, 所以出發了擔保機制,將該對象放到了老年代(10M),可以存放的下4M的object2 。但object1只有256KB,所以Survivor From可以存放的下object1。 同時 MaxTenuringThreshold變為1 。
將object3置為null(這個時候已經沒有引用,對象已經死亡,GC可以回收),分配object4 的時候又觸發了一次Minor GC 。 此時object1已經達到了MaxTenuringThreshold,符合清理到老年代的要求,可以看到新生代from space已經被清為0了。
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)XX:MaxTenuringThreshold=15時的 GC日志
from space 使用了40% ,存放的是object1對象,因沒達到XX:MaxTenuringThreshold,暫時還沒有清理到老年代。
動態對象年齡判定
理論
為了能更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
案例
虛擬機參數設置及參數說明
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails -XX:MaxTenuringThreshold=15代碼
package com.artisan.gc;public class MaxTenuringThresholdTest {private static final int _1M = 1024 * 1024;/*** * * @Title: testMaxTenuringThreshold* * @Description: -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8* -XX:+UseSerialGC -verbose:gc -XX:+PrintGCDetails* -XX:MaxTenuringThreshold=15* * * @return: void*/public void testMaxTenuringThreshold() {// 根據JVM參數的設置,分配合理的大小,達到測試的目的byte[] object1 = new byte[_1M / 4];byte[] object2 = new byte[_1M / 4];// 什么時候進入老年代取決于-XX:MaxTenuringThresholdbyte[] object3 = new byte[_1M * 4];byte[] object4 = new byte[_1M * 4];object4 = null;object4 = new byte[_1M * 4];}public static void main(String[] args) {new MaxTenuringThresholdTest().testMaxTenuringThreshold();}}GC日志
設置了MaxTenuringThreshold=15,會發現運行結果中Survivor的空間占用仍然為0%,而老年代比預期增加了6%【和上個案例的結果比對】,也就是說,object1、object2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。因為這兩個對象加起來已經到達了512KB,并且它們是同年的,滿足同年對象達到Survivor空間的一半規則。
如果我們只要注釋掉其中一個對象new操作,就會發現另外一個就不會晉升到老年代中去了
空間分配擔保
理論
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。
如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。
如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。
JDK1.60 Update24之后HandlePromotionFailure參數不會影響虛擬機的空間分配擔保策略了。
總結
以上是生活随笔為你收集整理的JVM-09自动内存管理机制【内存分配和回收策略】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM-08垃圾收集Garbage Co
- 下一篇: 实战SSM_O2O商铺_41【前端展示】