端午佳節一下子就過完了,大家是不是還沉迷在假期的歡樂氣氛中無法自拔?今天阿Q爲大家準備了上好的“醒酒菜”——JVM
運行時數據區的核心內存區——堆。
堆的概述
一般來說:
- 一個
Java
程序的運行對應一個進程; - 一個進程對應着一個
JVM
實例(JVM
的啓動由引導類加載器加載啓動),同時也對應着多個線程; - 一個
JVM
實例擁有一個運行時數據區(Runtime
類,爲餓漢式單例類); - 一個運行時數據區中的堆和方法區是多線程共享的,而本地方法棧、虛擬機棧、程序計數器是線程私有的。
堆空間差不多是最大的內存空間,也是運行時數據區最重要的內存空間。堆可以處於物理上不連續的內存空間,但在邏輯上它應該被視爲連續的。
在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候纔會被移除。堆,是GC
(Garbage Collection
,垃圾收集器)執行垃圾回收的重點區域。
堆內存大小設置
堆一旦被創建,它的大小也就確定了,初始內存默認爲電腦物理內存大小的1/64
,最大內存默認爲電腦物理內存的1/4
,但是堆空間的大小是可以調節,接下來我們來演示一下。
準備工具
JDK
自帶內存分析的工具:在已安裝JDK
的bin
目錄下找到jvisualvm.exe
。打開該軟件,下載插件Visual GC
,一定要點擊檢查最新版本,否則會導致安裝失敗。
安裝完重啓jvisualvm
代碼樣例
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
IDEA設置
-
-Xms10m
用於表示堆區的起始內存爲10m,等價於-XX:InitialHeapSize
; -
-Xmx10m
用於表示堆區的最大內存爲10m,等價於-XX:MaxHeapSize
; - 其中
-X
是JVM
的運行參數,ms
是memory start
❝
通常會將
-Xms
和-Xmx
兩個參數配置相同的值,其目的就是爲了能夠在java
垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,從而提高性能。❞
啓動程序
啓動程序之後去jvisualvm
查看
一旦堆區中的內存大小超過-Xmx
所指定的最大內存時,將會拋出OOM
(Out Of MemoryError
)異常。
堆的分代
存儲在JVM
中的java
對象可以被劃分爲兩類:
- 一類是生命週期較短的瞬時對象,這類對象的創建和消亡都非常迅速;
- 另一類是生命週期非常長,在某些情況下還能與
JVM
的生命週期保持一致;
堆區分代
經研究表明70%-99%
的對象屬於臨時對象,爲了提高GC
的性能,Hotspot
虛擬機又將堆區進行了進一步劃分。
如圖所示,堆區又分爲年輕代(YoungGen
)和老年代(OldGen
);其中年輕代又分爲伊甸園區(Eden
)和倖存者區(Survivor
);倖存者區分爲倖存者0區(Survivor0,S0
)和倖存者1區(Survivor1,S1
),有時也叫from
區和to
區。
❝
分代完成之後,GC時主要檢測新生代
Eden
區。❞
「統一概念:」
新生區<=>新生代<=>年輕代
養老區<=>老年區<=>老年代
幾乎所有的Java
對象都是在Eden
區被new
出來的,有的大對象在該區存不下可直接進入老年代。絕大部分的Java
對象都銷燬在新生代了(IBM
公司的專門研究表明,新生代80%的對象都是“朝生夕死”的)。
新生代與老年代在堆結構的佔比
- 默認參數
-XX:NewRatio=2
,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3; - 可以修改
-XX:NewRatio=4
,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5;
❝
該參數在開發中一般不會調整,如果生命週期長的對象偏多時可以選擇調整。
❞
Eden與Survivor在堆結構的佔比
在HotSpot
中,Eden
空間和另外兩個Survivor
空間所佔的比例是8:1:1(測試的時候是6:1:1),開發人員可以通過選項-XX:SurvivorRatio
調整空間比例,如-XX:SurvivorRatio=8
❝
可以在
cmd
中通過jps 查詢進程號-> jinfo -flag NewRatio(SurvivorRatio) + 進程號
查詢配置信息❞
-Xmn
設置新生代最大內存大小(默認就好),如果既設置了該參數,又設置了NewRatio
的值,則以該參數設置爲準。
查看設置的參數
以上邊的代碼爲例:設置啓動參數-XX:+PrintGCDetails
;可在cmd窗口中輸入jps
查詢進程號,然後通過jstat -gc 進程id
指令查看進程的內存使用情況。
圖解對象分配過程
對象分配過程
- new的對象先放伊甸園區,此區有大小限制;
- 當伊甸園的空間填滿時,程序繼續創建對象,
JVM
的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC
,也叫YGC
):將伊甸園區中的不再被其他對象所引用的對象進行銷燬,將未被銷燬的對象移動到倖存者0區並分配age
; - 然後再加載新的對象放到伊甸園區;
- 如果再次觸發垃圾回收,將此次未被銷燬的對象和上一次放在倖存者0區且此次也未被銷燬的對象一齊移動到倖存者一區,此時新對象的
age
爲1,上次的對象的age
加1變爲2; - 如果再次經歷垃圾回收,此時會重新放回倖存者0區,接着再去倖存者1區,
age
也隨之增加; - 默認當
age
爲15時,未被回收的對象將移動到老年區。可以通過設置參數來更改默認配置:-XX:MaxTenuringThreshold=<N>
;該過程稱爲晉升(promotion
); - 在養老區,相對悠閒,當老年區內存不足時,再次觸發GC(
Major GC
),進行養老區的內存清理; - 若養老區執行了
Major GC
之後發現依然無法進行對象的保存,就會產生OOM
異常。
❝
S0,S1滿時不會觸發
YGC
,但是YGC
會回收S0,S1的對象。❞
「總結」
- 針對倖存者s0,s1區:複製之後有交換,誰空誰是to;
- 關於垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不再永久區/元空間收集。
對象特殊情況分配過程
- 新對象申請內存,如果
Eden
放的下,則直接存入Eden
;如果存不下則進行YGC
; -
YGC
之後如果能存下則放入Eden
,如果還存不下(爲超大對象),則嘗試存入Old
區; - 如果
Old
區可以存放,則存入;如果不能存入,則進行Full GC
; -
Full GC
之後如果可以存入Old
區,則存入;如果內存空間還不夠,則OOM
; - 圖右側爲
YGC
的流程圖:當YGC
之後未銷燬的對象放入倖存者區,此時如果倖存者區的空間可以裝下該對象,則存入倖存者區,否則,直接存入老年代; - 當在倖存者區的對象超過閾值時,可以晉升爲老年代,未達到閾值的依舊在倖存者區複製交換。
內存分配策略
針對不同年齡段的對象分配原則如下:
- 優先分配到
Eden
; - 大對象直接分配到老年代:儘量避免程序中出現過多的大對象;
- 長期存活的對象分配到老年代;
- 動態對象年齡判斷:如果
Survivor
區中相同年齡的所有對象大小的總和大於Survivor
空間的一半,年齡大於或等於該年齡的對象可以直接進入到老年代。無需等到MaxTenuringThreshold
中要求的年齡;
數值變小原理
代碼樣例,設置參數:-Xms600m,-Xmx600m
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虛擬機中的堆內存總量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虛擬機試圖使用的最大堆內存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//執行結果
-Xms : 575M
-Xmx : 575M
明明設置的600M,怎麼變成575M了呢?這是因爲在堆內存存取數據時,新生代裏邊只有伊甸園和倖存者1區或者是倖存者2區存儲對象,所以會少一個倖存者區的內存空間。
GC
JVM
進行GC
時,並非每次都對新生代、老年代、方法區(永久代、元空間)這三個區域一起回收,大部分回收是指新生代。
針對HotSpot VM
的實現,它裏面的GC
按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC
),一種是整堆收集(Full GC
)
Partial GC
部分收集:不是完整收集整個Java
堆的垃圾收集。其中又分爲:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集;
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集;
- 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集,只有
G1 GC
(按照region
劃分新生代和老年代的數據)會有這種行爲。
目前,只有CMS GC
會有單獨收集老年代的行爲;很多時候Major GC
會和Full GC
混淆使用,需要具體分辨是老年代回收還是整堆回收。
Full GC
整堆收集(Full GC
):整個java
堆和方法區的垃圾收集。
觸發機制
年輕代GC(Minor GC)觸發機制
- 當年輕代空間不足時,就會觸發
Minor GC
,這裏的年輕代滿指的是Eden
代滿,Survivor
滿不會引發GC
。(每次Minor GC
會清理年輕代的內存,Survivor
是被動GC
,不會主動GC
) - 因爲
Java
對象大多都具備“朝生夕滅”的特性,所以Minor GC
非常頻繁,一般回收速度也比較快。 -
Minor GC
會引發STW
(Stop The World
),暫停其他用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
老年代GC(Major GC/Full GC)觸發機制
- 指發生在老年代的
GC
,對象從老年代消失時,Major GC
或者Full GC
發生了; - 出現了
Major GC
,經常會伴隨至少一次的Minor GC
(不是絕對的,在Parallel Scavenge
收集器的收集策略裏就有直接進行Major GC
的策略選擇過程),也就是老年代空間不足時,會先嚐試觸發Minor GC
。如果之後空間還不足,則觸發Major GC
; -
Major GC
速度一般會比Minor GC
慢10倍以上,STW
時間更長; - 如果
Major GC
後,內存還不足,就報OOM
了。
Full GC觸發機制
觸發Full GC執行的情況有以下五種:
- 調用
System.gc()
時,系統建議執行Full GC
,但是不必然執行; - 老年代空間不足;
- 方法區空間不足;
- 通過
Minor GC
後進入老年代的平均大小小於老年代的可用內存; - 由
Eden
區,Survivor S0
(from
)區向S1
(to
)區複製時,對象大小大於To Space
可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。
❝
Full GC
是開發或調優中儘量要避免的,這樣暫停時間會短一些。❞
以上就是今天的所有內容了,如果你有不同的意見或者更好的idea
,歡迎聯繫阿Q:qingqing-4132
,阿Q期待你的到來!
後臺留言領取java乾貨資料:學習筆記與大廠面試題