5.Java虛擬機內存分配

一、堆內存分配
Java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決了兩個問題:給對象分配內存以及回收分配給對象的內存。
1.概述
內存分配策略:
對象優先在Eden分配
大對象直接進入老年代
長期存活的對象將進入老年代
動態對象年齡判定
空間分配擔保

2.對象優先在Eden分配
概述:大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,並且在進程退出的時候輸出當前的內存各區域分配情況。

測試代碼

public class test{
	public static void main(String[] args) {
		byte[] a1 = new byte[2*1024*1024];
		byte[] a2 = new byte[2*1024*1024];
		byte[] a3 = new byte[2*1024*1024];
		byte[] a4 = new byte[4*1024*1024];
		System.gc();   //手動垃圾回收
	}
}

對於一般的CPU內存超過2G,HotSpot一般認爲其爲Server,所以,默認使用ParNew收集器,設置虛擬機
變量:-verbose:gc -XX:+PrintGCDetails,得到結果如下:
在這裏插入圖片描述

將垃圾收集器手動設置爲Serial收集器,並通過使用參數(-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8),設置Java堆大小爲20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代,新生代中Eden區與一個Survivor區的空間比例是8:1,從輸出的結果也可以清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代總可用空間爲9216KB(Eden區+1個Survivor區的總容量)。得到結果如下:
在這裏插入圖片描述
分析:執行語句byte[] a4 = new byte[4* 1024* 1024];時會觸發一次Minor GC,GC,這次GC的結果是新生代7130KB變爲547KB,而總內存佔用量則幾乎沒有減少(因爲a1、a2、a3三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象,從Java堆7130K ->6691K(19456K)可見,Java堆內存的使用量並沒有改變,變的是對象從新生代移動到老年代)。
這次Minor GC發生的原因:是給a4分配內存的時候,發現Eden已經被佔用了6MB,剩餘空間已不足以分配allocation4所需的4MB內存,因此發生Minor GC。GC期間虛擬機又發現已有的3個2MB大小的對象全部無法放入Survivor空間(Survivor空間只有1MB大小),所以只好通過分配擔保機制提前轉移到老年代去。
這次Minor GC結束後:4MB的allocation4對象順利分配在Eden中,因此程序執行完的結果是Eden佔用4MB(被a4佔用),Survivor空閒,老年代被佔用6MB(被a1、a2、a3佔用)。

注:Minor 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倍以上。

3.大對象直接進入老年代
概述:所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(筆者列出的例子中的byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來說就是一個壞消息(替Java虛擬機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一羣“朝生夕滅”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

虛擬機參數設置:虛擬機提供了一個-XX:PretenureSizeThreshold = 參數的設置,令大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製(複習一下:新生代採用複製算法收集內存)。這裏的虛擬機參數設置爲:-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728

代碼

public class test{
	public static void main(String[] args) {
		byte[] a1 = new byte[4*1024*1024];
	}
}

運行結果:由於a1對象超過3M,所以直接分配的老年代中
在這裏插入圖片描述

分析:我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的a1對象直接就分配在老年代中,這是因爲PretenureSizeThreshold被設置爲3MB(就是3145728,有的JDK版本可以直接寫3M),因此超過3MB的對象都會直接在老年代進行分配。注意:PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器一般並不需要設置。如果遇到必須使用此參數的場合,可以考慮ParNew加CMS的收集器組合。

4.長期存活的對象將進入老年代
概述:虛擬機採用了分代收集的思想來管理內存,虛擬機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認爲15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以設置參數:-XX:MaxTenuringThreshold=15。(設置完整參數爲:-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution)

注意:自從JDK6後,這個標準就不是嚴格地執行,可能設置-XX:MaxTenuringThreshold=15,但是在第一次或者第二次垃圾回收時就將對象放入老年代中。如下:(設置參數1和15得到的結果一樣)
在這裏插入圖片描述
在這裏插入圖片描述

5.動態對象年齡判定
概述:爲了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

6.空間分配擔保
概述:在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗(HandlePromotionFailure前面是“+”號則允許,是“-”則不允許)。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險(即前面設置爲“-”號),那這時也要改爲進行一次Full GC。將HandlePromotionFailure設置爲允許,可以減少頻繁地減少Full GC,因爲若設置爲不允許,那麼只能進行頻繁進行Full GC,從而使得老年代內存變大從而實現無風險擔保。(注:以上規則適用於JDK 6之前的版本)

“風險”解釋:新生代使用複製收集算法,但爲了內存利用率,只使用其中一個Survivor空間來作爲輪換備份,因此當出現大量對象在Minor GC後仍然存活的情況(最極端的情況就是內存回收後新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作爲經驗值(在開啓允許擔保失敗的情況下),與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

二、棧內存分配
概述:隨着程序的運行,會有棧幀入棧和出棧,入棧分配給對象分配內存,出棧對象自動銷燬,內存自動回收,所以在棧上分配內存不需要垃圾回收,會提高性能。

逃逸分析:即分析對象的作用域,若創建的對象作用域只在某個方法中,則說明未發生逃逸,會隨着棧幀出棧而回收對象內存,若創建的對象在方法外也可以使用,則說明發生逃逸,隨着棧幀出棧,也不會被回收,需要垃圾收集器清理。

代碼舉例

public class StackAllocation {
    public StackAllocation obj;
    
    /***方法返回StackAllocation對象,obj的作用域不僅僅在方法內,發生逃逸***/
    public StackAllocation getInstance() {
    	return obj == null ? new StackAllocation() : obj;
    }
    
    /***爲成員屬性賦值,發生逃逸***/
    public void setObj() {
    	this.obj = new StackAllocation();
    }
    
    /***當前對象的作用域僅在方法內有效,未發生逃逸***/
    public void useStackAllocation() {
    	StackAllocation s = new StackAllocation();
    }
    
    /***引用成員變量的值,發生逃逸***/
    public void useStackAllocation1() {
    	StackAllocation s = getInstance();
    }
}

注意:能在佔內存中創建對象,儘量在棧內存中創建,隨着方法出棧,佔內存中的對象會自動釋放,會提高系統性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章