我們都知道Java對象分配在堆中,但是堆分新生代、老年代,新生代又分eden、from Survivor、to Survivor。今天通過簡單的示例來驗證下!
一、對象優先分配在Eden區
對象創建一般都優先放到eden區,jvm參數配置:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8。
示例運行結果如下圖:
很簡單的程序,兩個字節數組大小一共512k,加上jvm運行需要一些對象一共使用了新生代內存3243K,3242/2182約等於39%,survivor和老年代使用率0%。
當如果Eden區沒有足夠的空間時,虛擬機執行一次Minor GC,運行如下圖:
上一步我們知道eden總共9M用了3243K,然後我調用看一個方法產生了4M的數組,接著又準備分配一個3M的內存,很明顯新生代內存肯定不夠了,所以觸發了GC。
GC日誌簡單學習:
1、圖中第一個框GC表示新生代回收,如果是Full GC這裡就會是Full GC;
2、第二個框表示觸發回收的原因,這裡的原因是分配失敗;
3、第三個框表示新生代回收內存使用變化,回收前7175K(與3243k+4*1024k接近),回收後剩餘992K
4、第四個框是總的堆內存回收前後變化,7175K減少到1120K,從後面堆內存信息可以看到,老年代有228K,992+228=1120。
看最終堆內存信息,from使用了1024*96%=992,剛好就是回收後的內存,說明上一次回收後對象都放到了這裡,並且把他作為了from survivor。
eden使用了4468-992=3476,3476/8196約等於42%,其中包含新建的3M數組。
二、大對象直接進入老年代
這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代採用複製算法收集內存)。
這裡要設置兩個參數:1、-XX:PretenureSizeThreshold=3145728表示超過指定大小(這裡是3M)就直接分配到老年代。2、-XX:+UseSerialGC表示使用Serial收集器,我按默認的收集器(PS收集器)不成功。
直接看代碼運行如下圖:
可以看到在老年代分配了4M內存,說明分配成功。
三、長期存活的對象進入老年代
虛擬機為每個對象定義了一個年齡計數器,如果對象經過了1次Minor GC那麼對象會進入Survivor區,之後每經過一次Minor GC那麼對象的年齡加1,直到達到閥值對象進入老年區。
實例代碼jvm參數配置:-verbose:gc、-Xms40M、-Xmx40M、-Xmn20M、-XX:+PrintGCDetails、-XX:SurvivorRatio=8、-XX:MaxTenuringThreshold=3、-XX:+PrintTenuringDistribution、-XX:+UseSerialGC
-XX:MaxTenuringThreshold=3參數表示對象的年齡超過3就會被放進老年代;
-XX:+PrintTenuringDistribution打印的是survivor裡面的對象年齡信息;
代碼如下圖:
每個打印的後面都會調用一個方法創建8M的數組,所以都會觸發一次回收,在主方法裡交替創建256k和523k的數組(不會被回收)。
運行結果如下圖:
這裡只截取了從第四次回收開始的信息,簡單按順序介紹下框中的數據。
1、survivor的一半,引用新生代是20 M、-XX:SurvivorRatio=8,所以每一個survivor是2M,半個就是1M;
2、new threshold 3表示jvm計算出來的回收閥值,max 3就是-XX:MaxTenuringThreshold=3設置的最大晉升老年代年齡;
3、分別表示survivor各個年齡的對象大小,可以看到是512k和256k交替出現,等到下次回收的時候年齡3的進入了老年代,各個年齡段又交替變化了;
4、是各年齡段的累加,年齡2的值是年齡1加年齡2,年齡3是年齡1、2、3累計;
5、新生代被使用的總內存;
6、看最後一次回收survivor內存1311024/(2048*1024)剛好約等於62%,說明這裡面全是那幾個數組;
7、老年代使用的內存只有1642K,包括最開始建的三個數組和jvm本來的。
如果是一步一步的看堆內存的話就更能看到數組被一個一個的放到老年代了。
四、動態判斷對象年齡
動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代。
jvm參數與上一步一樣,不用修改。
代碼如下:
觸發了第一次回收後,新建多5個256K的數組,超過了Survivor的一般1024,所以下次觸發就會把數組1~6全部進入老年代。
運行結果如下圖:
可以看到,第二次回收的時候"new threshold 1"表示下次回收的時候最大年齡是1,可以看到第三次回收survivor中就只有年齡1的256K。上一步的兩個已經進入老年代了。
五、老年代空間分配擔保
每次進行Minor GC時,JVM會計算Survivor區移至老年區的對象的平均大小,如果這個值大於老年區的剩餘值大小則進行一次Full GC。
先利用-XX:PretenureSizeThreshold=9437184參數保存兩個9M數組數組把老年代基本填滿,然後新生代多次觸發回收;
代碼如下圖:
運行結果如下圖:
最後兩次survivor中對象大小達到他的一半,所以會進入老年代,回收幾次後老年代內存不足,觸發Full GC.
六、總結
對象在堆中的流轉就大概分為以上5種,已經用例子一一證明了,現在來總結下。分兩種情況:
1、正常情況:進入eden->to survivor(下一次的from survivor)->to survivor->.......年齡上限->老年代,每個->代表一次GC。
2、特殊情況:a、大對象直接進入老年代;b、survivor中相同年齡的對象佔的內存達到survivor的一半(一個survivor的一半),那麼這個年齡以及年齡比它大的全部直接晉升老年代;
對象在堆中的流轉情況比較簡單,不過可以通過上面的例子並且去閱讀GC日誌能夠理解更加的深刻。
Java程序員日常學習筆記,如理解有誤歡迎各位交流討論!