一、簡介
JVM采用分代垃圾回收。在JVM的內存空間中把堆空間分為年老代和年輕代。將大量(據說是90%以上)創建了沒多久就會消亡的對象存儲在年輕代,而年老代中存放生命周期長久的實例對象。年輕代中又被分為Eden區(聖經中的伊甸園)、和兩個Survivor區。新的對象分配是首先放在Eden區,Survivor區作為Eden區和Old區的緩沖,在Survivor區的對象經歷若干次收集仍然存活的,就會被轉移到年老區。

簡單講,就是生命期短的對象放在一起,將少數生命期長的對象放在一起,分別采用不同的回收策略。生命期短的對象回收頻率比較高,生命期長的對象采用比較低回收頻率,生命期短的對象被嘗試回收幾次發現還存活,則被移到另外一個地方去存起來。就像現在夏天了,勤勞的douma把doudou和douba常穿的衣服放在順手的地方,把冬天的衣服打包放在櫃子另一個地方。雖然把doudou的小衣服類比成虛擬機里的對象有點不合適,大致意思應該就是這樣。
本文中通過最簡單的一個例子來demo下這個過程。包括對象的分配和回收。設定上包括對堆中年輕代(年輕代中eden區和survivor區)、年老代大小的設定,以及設置閾值控制年輕代到年老代的晉升。
二、示例代碼
下面是最簡單的代碼,通過代碼的每一步的執行來剖析其中的規則。
1 package com.idouba.jvm.demo; 2 3 /** 4 * @author idouba 5 * Use shortest code demo jvm allocation, gc, and someting in gc. 6 * 7 * In details 8 * 1) sizing of young generation (eden space,survivor space),old generation. 9 * 2) allocation in eden space, gc in young generation, 10 * 3) working with survivor space and with old generation. 11 * 12 */ 13 public class SimpleJVMArg { 14 15 /** 16 * @param args 17 */ 18 public static void main(String[] args) 19 { 20 demo(); 21 } 22 23 /** 24 * VM arg:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution 25 * 26 */ 27 @SuppressWarnings("unused") 28 public static void demo() { 29 30 final int tenMB = 10* 1024 * 1024; 31 32 byte[] alloc1, alloc2, alloc3; 33 34 alloc1 = new byte[tenMB / 5]; 35 alloc2 = new byte[5 * tenMB]; 36 alloc3 = new byte[4 * tenMB]; 37 alloc3 = null; 38 alloc3 = new byte[6 * tenMB]; 39 } 40 }
三、執行輸出
通過jvm 參數設定幾個區域的大小,結合代碼執行可以觀察到對象在堆上分配和回收的過程。執行參數如下:
-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+PrintTenuringDistribution
通過設這-Xms200M -Xmx200M 設置Java堆大小為200M,不可擴展,-Xmn100M設置其中100M分配給新生代,則200-100=100M,即剩下的100M分配給老年代。-XX:SurvivorRatio=8設置了新生代中eden與survivor的空間比例是1:8。
執行上述代碼結果如下:
1 [GC [DefNew 2 Desired survivor size 5242880 bytes, new threshold 15 (max 15) 3 - age 1: 2237152 bytes, 2237152 total 4 : 54886K->2184K(92160K), 0.0508477 secs] 54886K->53384K(194560K), 0.0508847 secs] [Times: user=0.03 sys=0.03, real=0.06 secs] 5 [GC [DefNew 6 Desired survivor size 5242880 bytes, new threshold 15 (max 15) 7 - age 2: 2237008 bytes, 2237008 total 8 : 43144K->2184K(92160K), 0.0028660 secs] 94344K->53384K(194560K), 0.0028957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 9 Heap 10 def new generation total 92160K, used 65263K [0x1a1d0000, 0x205d0000, 0x205d0000) 11 eden space 81920K, 77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000) 12 from space 10240K, 21% used [0x1f1d0000, 0x1f3f2250, 0x1fbd0000) 13 to space 10240K, 0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000) 14 tenured generation total 102400K, used 51200K [0x205d0000, 0x269d0000, 0x269d0000) 15 the space 102400K, 50% used [0x205d0000, 0x237d0010, 0x237d0200, 0x269d0000) 16 compacting perm gen total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000) 17 the space 12288K, 2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000) 18 ro space 8192K, 66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000) 19 rw space 12288K, 52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)
從中可以看到eden 大小為81920K, Survivor中from區域和to區域大小都是10240k。新生代總的92160K指的是eden和一個Survivor區域的和。
即原始的內存如圖:
為了演示年輕代對象晉級到年老代的過程。需要設置一個VM參數, 這里設置MaxTenuringThreshold=1。前面不設置的時候,默認MaxTenuringThreshold取值15。當設置不同的閾值,jvm在內存處理會有不同。我們重點觀察觀察alloc1 這么小塊區域在不同的MaxTenuringThreshold參數設置下的遭遇。
這時候JVM的參數中加上MaxTenuringThreshold=1如下:
-verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
可以看到輸出結果是:
1 [GC [DefNew 2 Desired survivor size 5242880 bytes, new threshold 1 (max 1) 3 - age 1: 2237152 bytes, 2237152 total 4 : 54886K->2184K(92160K), 0.0641037 secs] 54886K->53384K(194560K), 0.0641390 secs] [Times: user=0.03 sys=0.03, real=0.06 secs] 5 [GC [DefNew 6 Desired survivor size 5242880 bytes, new threshold 1 (max 1) 7 : 43144K->0K(92160K), 0.0036114 secs] 94344K->53384K(194560K), 0.0036418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 8 Heap 9 def new generation total 92160K, used 63078K [0x1a1d0000, 0x205d0000, 0x205d0000) 10 eden space 81920K, 77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000) 11 from space 10240K, 0% used [0x1f1d0000, 0x1f1d0000, 0x1fbd0000) 12 to space 10240K, 0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000) 13 tenured generation total 102400K, used 53384K [0x205d0000, 0x269d0000, 0x269d0000) 14 the space 102400K, 52% used [0x205d0000, 0x239f2260, 0x239f2400, 0x269d0000) 15 compacting perm gen total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000) 16 the space 12288K, 2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000) 17 ro space 8192K, 66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000) 18 rw space 12288K, 52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)
四、過程解析
下面觀察每一步語句執行后,jvm內存的變化情況,並給出解析。
1)在執行第一個語句,alloc1分配2M空間
alloc1 = new byte[tenMB / 5];
后,根據分代策略,在新生代的eden區分配2M的空間存儲對象。
2)在執行第二語句,alloc2分配50M
alloc2 = new byte[5 * tenMB];
前面alloc1分配2M后,因為eden的80M空間還有80-2=78M還可以容納下allocation2要求的50M空間,因此接着在eden區域分配。
3)當執行第三句,alloc3分配40M
alloc3 = new byte[4 * tenMB];
還是嘗試在eden上分配,但是eden空間只剩下28M,不能容納alloc3要求的40M空間。於是觸發在新生代上的一次gc,將Eden區的存活對象轉移到Survivor區。在這個里先將2M的alloc1對象存放(其實是copy,參見java 垃圾回收策略的描述)到from區,然后copy 50M的alloc2對象,顯然survivor區不能容納下alloc2對象,該對象被直接copy到年老代。需要說明的是復制到Survivor區的對象在經歷一次gc后期對象年齡會被加一。
在eden區gc后騰出空間可以存放allocation3的40M對象,則alloc3分配40M對象如圖:
4)執行第四句,將alloc3置空
alloc3 = null;
這是eden上alloc3分配的的40M對象則變成可被回收狀態。
5)執行第5句,對alloc重新分配60M空間
allocation3 = new byte[6 * tenMB];
還是嘗試先在eden區上分配,發現超出了eden區域的容量,則再次觸發新生代上的一次gc。首先eden上分配的40M對象因為沒有被再使用,則直接被回收。而根據的設置不同,這次gc的行為會稍有不同。
先看MaxTenuringThreshold不設置,即取默認值15的時候。eden區上無用的40M回收后,再考察Survivor區域的對象是否滿足對象晉升老年代的年齡閾值,發現from中的2M對象,年齡是1,不滿足晉升條件,則不被處理,只是把Survivor區域的經歷這次回收未被處理的對象age加一,即新的age為2.如圖:
通過輸出日志也顯示:經過這次回收年輕代大小,由43114K變為2184k,總的大小由94344k變為53384k,即反映出回收了40M無用對象。
Desired survivor size 5242880 bytes, new threshold 15 (max 15) - age 2: 2237008 bytes, 2237008 total : 43144K->2184K(92160K), 0.0028660 secs] 94344K->53384K(194560K), 0.0028957 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
在年輕代上gc后騰出空間后,新的alloc3的60M空間被分配到eden 區域上。分配后堆如下:
以上是不設置晉升閾值MaxTenuringThreshold情況下進行的gc,以及gc后alloc3的分配。
再看看當MaxTenuringThreshold設置為1的情況。同樣eden區上無用的40M回收后,再考察Survivor區域的對象是否滿足對象晉升老年代的年齡閾值,發現from中的2M對象,年齡是1,滿足晉升條件,則Survivor區域滿足年齡的對象被拷貝到年老區。
通過日志顯示年輕代的大小被清0了,表示survivor的存活對象因為滿足晉升條件被移到被移到年老代了。
[GC [DefNew Desired survivor size 5242880 bytes, new threshold 1 (max 1) : 43144K->0K(92160K), 0.0036114 secs] 94344K->53384K(194560K), 0.0036418 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
同樣的,gc完后會在eden上分配空間來存儲alloc3對象,這種情況下堆結構如圖:
比較上面兩個圖,發現差別就僅僅在於survivor中的2M對象是否被認為生存時間足夠長科院被移到年老代中去。從上面日志高亮部分from區域的最終存儲也可反映出了這個差別。
比較前面兩個日志可以看到:總的大小和上面設置和不設置MaxTenuringThreshold(其實是MaxTenuringThreshold設置1還是15)沒有關系,都是由94344k變為53384k,即都是回收了40M eden區域無用對象。第N次gc時存活的滿足晉升條件則由survivor移到年老代,不滿足的還留在survivor區域,堆的總的大小沒有變。
五、最后
上面通過最簡單的例子示意了下在jvm堆上對象是如果分配的,當空間不足時,是如何調整回收的。希望可以對jvm的堆上結構和gc思路有個基本的了解。當然相關參數(其實反映的是機制)遠比這個復雜,有挺多細節,更多的是在實踐中來體會。
附:主要參數
JVM啟動設置的參數很多,可以參照中Java HotSpot VM Options說明。例子中涉及的是最最基礎的參數,這里只是對涉及到的幾個參數進行說明。
| 參數名稱 | 含義 | 默認值 | 說明 |
| -Xms | 初始堆大小 | 物理內存的1/64(<1GB) | 默認(MinHeapFreeRatio參數可以調整)空余堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制. |
| -Xmx | 最大堆大小 | 物理內存的1/4(<1GB) | 默認(MaxHeapFreeRatio參數可以調整)空余堆內存大於70%時,JVM會減少堆直到 -Xms的最小限制 |
| -Xmn | 年輕代大小 | 注意:此處的大小是(eden+ 2 survivor space).與jmap -heap中顯示的New gen是不同的。 整個堆大小=年輕代大小 + 年老代大小 + 持久代大小. 增大年輕代后,將會減小年老代大小.此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8 |
|
| -XX:SurvivorRatio | Eden區與Survivor區的大小比值 | 設置為8,則兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區占整個年輕代的1/10 | |
| -XX:+PrintGC | 開啟GC日志打印 | 默認不啟用 | 開啟GC日志打印。打印格式例如: [Full GC 131115K->7482K(1015808K), 0.1633180 secs] 該選項可通過 com.sun.management.HotSpotDiagnosticMXBean API 和 Jconsole 動態啟用。 詳見 http://java.sun.com/developer/technicalArticles/J2SE/monitoring/#Heap_Dump |
| -XX:+PrintGCDetails | 打印GC回收的細節。 | 1.4.0引入,默認不啟用 | 打印格式例如: [Full GC (System) [Tenured: 0K->2394K(466048K), 0.0624140 secs] 30822K->2394K(518464K), [Perm : 10443K->10443K(16384K)], 0.0625410 secs] [Times: user=0.05 sys=0.01, real=0.06 secs] 該選項可通過 com.sun.management.HotSpotDiagnosticMXBean API 和 Jconsole 動態啟用 詳見 http://java.sun.com/developer/technicalArticles/J2SE/monitoring/#Heap_Dump |
| XX:+PrintTenuringDistribution | 查看每次minor GC后新的存活周期的閾值 | 打印對象的存活期限信息。打印格式例如: [GC Desired survivor size 4653056 bytes, new threshold 32 (max 32) - age 1: 2330640 bytes, 2330640 total - age 2: 9520 bytes, 2340160 total 204009K->21850K(515200K), 0.1563482 secs] Age1 2表示在第1和2次GC后存活的對象大小。 |
|
| -XX:MaxTenuringThreshold | 垃圾最大年齡。Survivor區經過該閾值次回收依然存活的對象會被移到年老代。 | 如果設置為0的話,則年輕代對象不經過Survivor區,直接進入年老代. 對於年老代比較多的應用,可以提高效率.如果將此值設置為一個較大值,則年輕代對象會在Survivor區進行多次復制,這樣可以增加對象再年輕代的存活 時間,增加在年輕代即被回收的概率 |
附配圖可編輯件:jvm-allocation-and-gc
原創文章。為了維護文章的版本一致、最新、可追溯,轉載請注明: 轉載自idouba
本文鏈接地址: 最簡單例子圖解JVM內存分配和回收











