1. 程式人生 > >最簡單例子圖解JVM記憶體分配和回收

最簡單例子圖解JVM記憶體分配和回收

一、簡介

JVM採用分代垃圾回收。在JVM的記憶體空間中把堆空間分為年老代和年輕代。將大量(據說是90%以上)建立了沒多久就會消亡的物件儲存在年輕代,而年老代中存放生命週期長久的例項物件。年輕代中又被分為Eden區(聖經中的伊甸園)、和兩個Survivor區。新的物件分配是首先放在Eden區,Survivor區作為Eden區和Old區的緩衝,在Survivor區的物件經歷若干次收集仍然存活的,就會被轉移到年老區。

jvm-memory-generation

簡單講,就是生命期短的物件放在一起,將少數生命期長的物件放在一起,分別採用不同的回收策略。生命期短的物件回收頻率比較高,生命期長的物件採用比較低迴收頻率,生命期短的物件被嘗試回收幾次發現還存活,則被移到另外一個地方去存起來。就像現在夏天了,勤勞的

doumadoudoudouba常穿的衣服放在順手的地方,把冬天的衣服打包放在櫃子另一個地方。雖然把doudou的小衣服類比成虛擬機器裡的物件有點不合適,大致意思應該就是這樣。

doudou-closet

本文中通過最簡單的一個例子來demo下這個過程,程式碼很短,很簡單,希望剖析的細一點,包括每一步操作後物件的分配和回收對記憶體堆產生的影響。設定上包括對堆中年輕代(年輕代中eden區和survivor區)、年老代大小的設定,以及設定閾值控制年輕代到年老代的晉升。

二、示例程式碼

下面是最簡單的程式碼,通過程式碼的每一步的執行來剖析其中的規則。

package com.idouba.jvm.demo;

/**
 * @author idouba
 * Use shortest code demo jvm allocation, gc, and someting in gc.
 *
 * In details
 * 1) sizing of young generation (eden space,survivor space),old generation.
 * 2) allocation in eden space, gc in young generation,
 * 3) working with survivor space and with old generation.
 *
 */
public class SimpleJVMArg {

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		demo();
	}

	/**
	 * VM arg:-verbose:gc -Xms200M -Xmx200M -Xmn100M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
	 *
	 */
	@SuppressWarnings("unused")
	public static void demo() {

		final int tenMB = 10* 1024 * 1024;

		byte[] alloc1, alloc2, alloc3;

		alloc1 = new byte[tenMB / 5];
		alloc2 = new byte[5 * tenMB];
		alloc3 = new byte[4 * tenMB];
		alloc3 = null;
		alloc3 = new byte[6 * tenMB];
	}
}

三、執行輸出

通過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。

執行上述程式碼結果如下:

[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   1:    2237152 bytes,    2237152 total
: 54886K->2184K(92160K), 0.0508477 secs] 54886K->53384K(194560K), 0.0508847 secs] [Times: user=0.03 sys=0.03, real=0.06 secs] 
[GC [DefNew
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] 
Heap
 def new generation   total 92160K, used 65263K [0x1a1d0000, 0x205d0000, 0x205d0000)
  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)
  from space 10240K,  21% used [0x1f1d0000, 0x1f3f2250, 0x1fbd0000)
  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)
 tenured generation   total 102400K, used 51200K [0x205d0000, 0x269d0000, 0x269d0000)
   the space 102400K,  50% used [0x205d0000, 0x237d0010, 0x237d0200, 0x269d0000)
 compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)
   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)
    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)
    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

從中可以看到eden 大小為81920K, Survivor中from區域和to區域大小都是10240k。新生代總的92160K指的是eden和一個Survivor區域的和。

即原始的記憶體如圖:

01-empty-heap

為了演示年輕代物件晉級到年老代的過程。需要設定一個VM引數, 這裡設定MaxTenuringThreshold=1。前面不設定的時候,預設MaxTenuringThreshold取值15。當設定不同的閾值,jvm在記憶體處理會有不同。我們重點觀察觀察alloc1 這麼小塊區域在不同的MaxTenuringThreshold引數設定下的遭遇。

這時候JVM的引數中加上MaxTenuringThreshold=1如下:

-verbose:gc  -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

可以看到輸出結果是:

[GC [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
- age   1:    2237152 bytes,    2237152 total
: 54886K->2184K(92160K), 0.0641037 secs] 54886K->53384K(194560K), 0.0641390 secs] [Times: user=0.03 sys=0.03, real=0.06 secs] 
[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] 
Heap
 def new generation   total 92160K, used 63078K [0x1a1d0000, 0x205d0000, 0x205d0000)
  eden space 81920K,  77% used [0x1a1d0000, 0x1df69a10, 0x1f1d0000)
  from space 10240K,   0% used [0x1f1d0000, 0x1f1d0000, 0x1fbd0000)
  to   space 10240K,   0% used [0x1fbd0000, 0x1fbd0000, 0x205d0000)
 tenured generation   total 102400K, used 53384K [0x205d0000, 0x269d0000, 0x269d0000)
   the space 102400K,  52% used [0x205d0000, 0x239f2260, 0x239f2400, 0x269d0000)
 compacting perm gen  total 12288K, used 360K [0x269d0000, 0x275d0000, 0x2a9d0000)
   the space 12288K,   2% used [0x269d0000, 0x26a2a3c0, 0x26a2a400, 0x275d0000)
    ro space 8192K,  66% used [0x2a9d0000, 0x2af20f10, 0x2af21000, 0x2b1d0000)
    rw space 12288K,  52% used [0x2b1d0000, 0x2b8206d0, 0x2b820800, 0x2bdd0000)

四、過程解析

下面觀察每一步語句執行後,jvm記憶體的變化情況,並給出解析。

1)在執行第一個語句,alloc1分配2M空間

alloc1 = new byte[tenMB / 5];

後,根據分代策略,在新生代的eden區分配2M的空間儲存物件。

02-alloc1-allocation

2)在執行第二語句,alloc2分配50M

alloc2 = new byte[5 * tenMB];

前面alloc1分配2M後,因為eden的80M空間還有80-2=78M還可以容納下allocation2要求的50M空間,因此接著在eden區域分配。

03-alloc2-allocation

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後期物件年齡會被加一。

04-gc1

在eden區gc後騰出空間可以存放allocation3的40M物件,則alloc3分配40M物件如圖:

05-alloc3-allocation

4)執行第四句,將alloc3置空

alloc3 = null;

這是eden上alloc3分配的的40M物件則變成可被回收狀態。

06-alloc3-null

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.如圖:

07-gc2(no-threshold)

通過輸出日誌也顯示:經過這次回收年輕代大小,由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 區域上。分配後堆如下:

09-alloc3-reallocation(no threshold)

以上是不設定晉升閾值MaxTenuringThreshold情況下進行的gc,以及gc後alloc3的分配。

再看看當MaxTenuringThreshold設定為1的情況。同樣eden區上無用的40M回收後,再考察Survivor區域的物件是否滿足物件晉升老年代的年齡閾值,發現from中的2M物件,年齡是1,滿足晉升條件,則Survivor區域滿足年齡的物件被拷貝到年老區。

08-gc2(threshold=1)

通過日誌顯示年輕代的大小被清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物件,這種情況下堆結構如圖:

10-alloc3-reallocation(threshold=1)

比較上面兩個圖,發現差別就僅僅在於survivor中的2M物件是否被認為生存時間足夠長科院被移到年老代中去。從上面日誌高亮部分from區域的最終儲存也可反映出了這個差別。

比較前面兩個日誌可以看到:總的大小和上面設定和不設定MaxTenuringThreshold(其實是MaxTenuringThreshold設定1還是15)沒有關係,都是由94344k變為53384k,即都是回收了40M eden區域無用物件。第N次gc時存活的滿足晉升條件則由survivor移到年老代,不滿足的還留在survivor區域,堆的總的大小沒有變。

五、最後

上面通過最簡單的例子示意了下在jvm堆上物件是如果分配的,當空間不足時,是如何調整回收的。希望可以對jvm的堆上結構和gc思路有個基本的瞭解。當然相關引數(其實反映的是機制)遠比這個複雜,有挺多細節,更多的是在實踐中來體會。