Java虛擬機內存分配與回收策略問題精細解讀
本文參考于《深入理解Java虛擬機》
內存分配與回收策略
Java技術體系的自動內存管理,最根本的目標是自動化地解決兩個問題:自動給對象分配內存以及自動回收分配給對象的內存。
1. 綜述
對象的內存分配,從概念上講,應該都是在堆上分配(而實際上也有可能經過即時編譯后被拆散為標量類型并間接地在棧上分配)。在經典分代的設計下,新生對象通常會分配在新生代中,少數(shù)情況下(例如對象大小超過一定閾值)也可能會直接分配在老年代。對象分配的規(guī)則并不是固定的,《Java虛擬機規(guī)范》并未規(guī)定新對象的創(chuàng)建和存儲細節(jié),這取決于虛擬機當前使用的是哪一種垃圾收集器,以及虛擬機中與內存相關的參數(shù)的設定。
(1)、對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配。當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次Minor GC。
1. Eden區(qū)有足夠空間的情形
虛擬機參數(shù)
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
參數(shù)說明
嘗試分配三個2MB大小和一個4MB大小的對象,在運行時通過-Xms20M、-Xmx20M、-Xmn10M這三個參數(shù)限制了Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:Survivor-Ratio=8決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例是8∶1。
package com.xiao.test.Test; public class test { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] byte1,byte2,byte3; byte1 = new byte[2 * _1MB]; byte2 = new byte[2 * _1MB]; byte3 = new byte[2 * _1MB]; } }
2. Eden區(qū)沒有足夠空間的情形
虛擬機參數(shù)相同
package com.xiao.test.Test; public class test { private static final int _1MB = 1024 * 1024; public static void main(String[] args) { byte[] byte1,byte2,byte3,byte4; byte1 = new byte[2 * _1MB]; byte2 = new byte[2 * _1MB]; byte3 = new byte[2 * _1MB]; byte4 = new byte[3 * _1MB]; } }
顯然進行了Minor GC
(2)、大對象直接進入老年代
1. 什么是大對象?
大對象就是指需要大量連續(xù)內存空間的Java對象,最典型的大對象便是那種很長的字符串,或者元素數(shù)量很龐大的數(shù)組。
2. Java虛擬機中要避免大對象的原因
在分配空間時,它容易導致內存明明還有不少空間時就提前觸發(fā)垃圾收集,以獲取足夠的連續(xù)空間才能安置好它們。而當復制對象時,大對象就意味著高額的內存復制開銷。
3. 大對象直接進入老年代的好處
避免在Eden區(qū)及兩個Survivor區(qū)之間來回復制,產生大量的內存復制操作(HotSpot虛擬機提供了-XX:PretenureSizeThreshold參數(shù),指定大于該設置值的對象直接在老年代分配)。
(3)、長期存活的對象將進入老年代
1. 虛擬機是怎樣判斷對象是否是長期存活?
內存回收時就必須能決策哪些存活對象應當放在新生代,哪些存活對象放在老年代中。為做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數(shù)器,存儲在對象頭中。
2. 對象年齡增加及晉升至老年代過程
對象通常在Eden區(qū)里誕生,如果經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,該對象會被移動到Survivor空間中,并且將其對象年齡設為1歲。對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15),就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold設置。
3. 長期存活的對象將進入老年代的原因
我們都知道新生代的垃圾收集算法算法是標記-復制算法,如果長期存活的對象仍然存放在新生代的話,那么就會帶來復制的開銷增大的問題。所以我們將大于某一年齡閾值的對象放入老年代,這樣可以減輕新生代垃圾回收時的壓力。
(4)、動態(tài)對象年齡判定
為了能更好地適應不同程序的內存狀況,HotSpot虛擬機并不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。
(5)、空間分配擔保
1. 空間分配擔保的內容
在發(fā)生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機會先查看-XX:HandlePromotionFailure參數(shù)的設置值是否允許擔保失??;如果允許,那會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者 -XX:HandlePromotionFailure設置不允許冒險,那這時就要改為進行一次Full GC。
2. “冒險”是冒了什么風險
前面提到過,新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現(xiàn)大量對象在Minor GC后仍然存活的情況——最極端的情況就是內存回收后新生代中所有對象都存活,需要老年代進行分配擔保,把Survivor無法容納的對象直接送入老年代,這與生活中貸款擔保類似。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,但一共有多少對象會在這次回收中活下來在實際完成內存回收之前是無法明確知道的,所以只能取之前每一次回收晉升到老年代對象容量的平均大小作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
3. 那我們需要把擔保打開嗎?
取歷史平均值來比較其實仍然是一種賭概率的解決辦法,也就是說假如某次Minor GC存活后的對象突增,遠遠高于歷史平均值的話,依然會導致?lián)J?。如果出現(xiàn)了擔保失敗,那就只好老老實實地重新發(fā)起一次Full GC,這樣停頓時間就很長了。雖然擔保失敗時繞的圈子是最大的,但通常情況下都還是會將-XX:HandlePromotionFailure開關打開,避免Full GC過于頻繁。
到此這篇關于Java虛擬機內存分配與回收策略問題精細解讀的文章就介紹到這了,更多相關Java 虛擬機內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java實現(xiàn)常用加密算法——單向加密算法MD5和SHA
本篇文章主要介紹了Java實現(xiàn)常用加密算法——單向加密算法MD5和SHA,信息加密后數(shù)據更安全,需要的朋友可以參考下。2016-10-10SpringBoot配置SwaggerUI訪問404錯誤的解決方法
這篇文章主要為大家詳細介紹了SpringBoot配置SwaggerUI訪問404錯誤的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12Java對象級別與類級別的同步鎖synchronized語法示例
這篇文章主要為大家介紹了Java對象級別與類級別的同步鎖synchronized語法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2022-03-03