此文閱讀大約10分鐘茬腿,
目錄如下:
什么是JVM的堆
是不是所有的Java對象都放在堆上?
線程和堆的關系
堆的內(nèi)部結構
面試題
新生代與老年代
如何設置堆的大小?
新生代與老年代的比例
設置Eden呼奢、幸存者的比例
常用參數(shù)
對象分配
金句:
分配過程
內(nèi)存分配策略(或對象提升(promotion)規(guī)則):
對象分配原則
Minor GC 、Major GC切平、Full GC
Minor GC觸發(fā)機制
老年代GC(Major GC/Full GC)觸發(fā)機制:
Full GC觸發(fā)機制:
OOM如何解決
為什么需要把Java堆分代握础?不分代就不能正常工作了嗎?
什么是TLAB(快速分配策略)悴品?
為什么有TLAB(Thread Local Allocation Buffer)快速分配策略禀综?
什么是TLAB?
TLAB的說明:
什么是JVM的堆
- 一個JVM實例只存在一個堆內(nèi)存他匪,堆也是Java內(nèi)存管理的核心區(qū)域菇存。
- Java 堆區(qū)在JVM啟動的時候即被創(chuàng)建,其空間大小也就確定了邦蜜。是JVM管理的最大一塊內(nèi)存空間依鸥。
- 堆內(nèi)存的大小是可以調(diào)節(jié)的。
- 《Java虛擬機規(guī)范》規(guī)定悼沈,堆可以處于物理上不連續(xù)的內(nèi)存空間中贱迟,但在邏輯上它應該被視為連續(xù)的。
- 堆絮供,是GC ( Garbage Collection衣吠,垃圾收集器)執(zhí)行垃圾回收的重點區(qū)域。
- 在方法結束后壤靶,堆中的對象不會馬上被移除缚俏,僅僅在垃圾收集的時候才會被移除。
是不是所有的Java對象都放在堆上?
《Java虛擬機規(guī)范》中對Java堆的描述是:所有的對象實例以及數(shù)組都應當在運行時分配在堆上贮乳。(The heap is the run-time data area from which memory for all class instances and arrays is allocated ) 數(shù)組和對象可能永遠不會存儲在棧上忧换,因為棧幀中保存引用,這個引用指向對象或者數(shù)組在堆中的位置向拆。
我要說的是:“幾乎”所有的對象實例都在這里分配內(nèi)存亚茬。——從實際使用角度看的浓恳。
舉例:
public class SimpleHeap {
private int id;
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimpleHeap sl = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
}
}
線程和堆的關系
所有的線程共享Java堆刹缝,在這里還可以劃分線程私有的緩沖區(qū)(Thread Local Allocation Buffer, TLAB)碗暗。
堆的內(nèi)部結構
現(xiàn)代垃圾收集器大部分都基于分代收集理論設計,堆空間細分為:
- Java 7及之前堆內(nèi)存邏輯上分為三部分:新生區(qū)+養(yǎng)老區(qū)+永久區(qū)
- Young Generation Space 新生區(qū) Young/New
- 又被劃分為Eden區(qū)和Survivor區(qū)
- Young Generation Space 新生區(qū) Young/New
- Tenure generation space 養(yǎng)老區(qū) Old/Tenure
- Permanent Space 永久區(qū) Perm
- Java 8及之后堆內(nèi)存邏輯上分為三部分:新生區(qū)+養(yǎng)老區(qū)+元空間
- Young Generation Space 新生區(qū) Young/New
- 又被劃分為Eden區(qū)和Survivor區(qū)
- Tenure generation space 養(yǎng)老區(qū) Old/Tenure
- Meta Space 元空間 Meta
- Young Generation Space 新生區(qū) Young/New
約定:
新生區(qū)<=>新生代<=>年輕代
養(yǎng)老區(qū)<=>老年區(qū)<=>老年代
永久區(qū)<=>永久代
面試題
- Java 堆的結構是什么樣子的梢夯?(獵聘)
- JVM內(nèi)存為什么要分成新生代言疗,老年代,持久代厨疙。新生代中為什么要分為Eden和Survivor(字節(jié)跳動)
- 堆里面的分區(qū):Eden洲守,survival (from+ to)疑务,老年代沾凄,各自的特點。(京東-物流)
- 堆的結構知允?為什么兩個survivor區(qū)撒蟀? (螞蟻金服)
- Eden和Survior的比例分配 (螞蟻金服)
- JVM內(nèi)存分區(qū),為什么要有新生代和老年代 (小米)
- JVM的內(nèi)存結構温鸽,Eden和Survivor比例保屯。 (京東)
- JVM內(nèi)存為什么要分成新生代,老年代涤垫,持久代姑尺。新生代中為什么要分為Eden和Survivor。 (京東)
- JVM內(nèi)存分區(qū)蝠猬,為什么要有新生代和老年代切蟋? (美團)
- JVM的內(nèi)存結構,Eden和Survivor比例榆芦。 (京東)
新生代與老年代
- 存儲在JVM中的Java對象可以被劃分為兩類:
- 一類是生命周期較短的瞬時對象柄粹,這類對象的創(chuàng)建和消亡都非常迅速
- 另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致匆绣。
- Java堆區(qū)進一步細分的話驻右,可以劃分為年輕代(YoungGen)和老年代(OldGen)
- 其中年輕代又可以劃分為Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區(qū)崎淳、to區(qū))堪夭。
幾乎所有的Java對象都是在Eden區(qū)被new出來的。
絕大部分的Java對象的銷毀都在新生代進行了拣凹。
IBM 公司的專門研究表明森爽,新生代中 80% 的對象都是“朝生夕死”的。
如何設置堆的大小?
新生代與老年代的比例
- 配置新生代與老年代在堆結構的占比咐鹤。
- 默認-XX:NewRatio=2拗秘,表示新生代占1,老年代占2祈惶,新生代占整個堆的1/3
- 可以修改-XX:NewRatio=4雕旨,表示新生代占1扮匠,老年代占4,新生代占整個堆的1/5
- 可以使用選項”-Xmn”設置新生代最大內(nèi)存大小
這個參數(shù)一般使用默認值就可以了凡涩。
設置Eden棒搜、幸存者的比例
- 在HotSpot中,Eden空間和另外兩個Survivor空間缺省所占的比例是8:1:1
- 當然開發(fā)人員可以通過選項“-XX:SurvivorRatio”調(diào)整這個空間比例活箕。比如-XX:SurvivorRatio=8
常用參數(shù)
堆空間大小的設置:
-Xms:初始內(nèi)存 (默認為物理內(nèi)存的1/64)力麸;
-Xmx:最大內(nèi)存(默認為物理內(nèi)存的1/4);
設置新生代的大小育韩。(初始值及最大值)
-Xmn
通常默認即可克蚂。
配置新生代與老年代在堆結構的占比。賦的值即為老年代的占比筋讨,剩下的1給新生代
默認-XX:NewRatio=2埃叭,表示新生代占1,老年代占2悉罕,新生代占整個堆的1/3
-XX:NewRatio=4赤屋,表示新生代占1,老年代占4壁袄,新生代占整個堆的1/5
在HotSpot中类早,Eden空間和另外兩個Survivor空間缺省所占的比例是8:1
開發(fā)人員可以通過選項“-XX:SurvivorRatio”調(diào)整這個空間比例。比如-XX:SurvivorRatio=8
設置新生代垃圾的最大年齡嗜逻。超過此值涩僻,仍未被回收的話,則進入老年代变泄。
默認值為15
-XX:MaxTenuringThreshold=0:表示年輕代對象不經(jīng)過Survivor區(qū)令哟,直接進入老年代。對于老年代比較多的應用妨蛹,可以提高效率屏富。
如果將此值設置為一個較大值,則年輕代對象會在Survivor區(qū)進行多次復制蛙卤,這樣可以增加對象在年輕代的存活時間狠半,增加在年輕代即被回收的概率。
輸出詳細的GC處理日志
-XX:+PrintGcDetail
-XX:HandlePromotionFailure
在發(fā)生Minor GC之前颤难,虛擬機會檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間神年,
如果大于,則此次Minor GC是安全的
如果小于行嗤,則虛擬機會查看-XX:HandlePromotionFailure設置值是否允許擔保失敗已日。
如果HandlePromotionFailure=true,那么會繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升到老年代的對象的平均大小栅屏,如果大于飘千,則嘗試進行一次Minor GC堂鲜,但這次Minor GC依然是有風險的;如果小于或者HandlePromotionFailure=false护奈,則改為進行一次Full GC缔莲。
----------------------------
在JDK 6 Update 24之后,HandlePromotionFailure參數(shù)不會再影響到虛擬機的空間分配擔保策略霉旗,觀察OpenJDK中的源碼變化痴奏,雖然源碼中還定義了HandlePromotionFailure參數(shù),但是在代碼中已經(jīng)不會再使用它厌秒。JDK 6 Update 24之后的規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC读拆,否則將進行Full GC。
-XX:+PrintFlagsFinal :查看所有的參數(shù)的最終值(可能會存在修改简僧,不再是初始值)
具體查看某個參數(shù)的指令: jps:查看當前運行中的進程
jinfo -flag SurvivorRatio 進程id
對象分配
為新對象分配內(nèi)存是一件非常嚴謹和復雜的任務建椰,JVM的設計者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題岛马,并且由于內(nèi)存分配算法與內(nèi)存回收算法密切相關,所以還需要考慮GC執(zhí)行完內(nèi)存回收后是否會在內(nèi)存空間中產(chǎn)生內(nèi)存碎片屠列。
金句:
針對幸存者s0,s1區(qū)的總結:復制之后有交換啦逆,誰空誰是to.
關于垃圾回收:
頻繁在新生區(qū)收集
很少在養(yǎng)老區(qū)收集
幾乎不在永久區(qū)/元空間收集
分配過程
1.new的對象先放伊甸園區(qū)。此區(qū)有大小限制笛洛。
2.當伊甸園的空間填滿時夏志,程序又需要創(chuàng)建對象,JVM的垃圾回收器將對伊甸園區(qū)進行垃圾回收(Minor GC/YGC)苛让,將伊甸園區(qū)中的不再被其他對象所引用的對象進行銷毀沟蔑。再加載新的對象放到伊甸園區(qū)
3.然后將伊甸園中的剩余對象移動到幸存者0區(qū)。
4.如果再次觸發(fā)垃圾回收狱杰,此時上次幸存下來的放到幸存者0區(qū)的瘦材,如果沒有回收,就會放到幸存者1區(qū)仿畸。
5.如果再次經(jīng)歷垃圾回收食棕,此時會重新放回幸存者0區(qū),接著再去幸存者1區(qū)错沽。
6.啥時候能去養(yǎng)老區(qū)呢簿晓?可以設置次數(shù)。默認是15次千埃。
可以設置參數(shù):-XX:MaxTenuringThreshold=<N> 設置對象晉升老年代的年齡閾值憔儿。
7.在養(yǎng)老區(qū),相對悠閑放可。當養(yǎng)老區(qū)內(nèi)存不足時谒臼,再次觸發(fā)GC:Major GC唱逢,進行養(yǎng)老區(qū)的內(nèi)存清理。
8.若養(yǎng)老區(qū)執(zhí)行了Major GC之后發(fā)現(xiàn)依然無法進行對象的保存屋休,就會產(chǎn)生OOM異常
java.lang.OutOfMemoryError: Java heap space
內(nèi)存分配策略(或對象提升(promotion)規(guī)則):
如果對象在Eden 出生并經(jīng)過第一次MinorGC 后仍然存活坞古,并且能被Survivor 容納的話,將被移動到Survivor 空間中劫樟,并將對象年齡設為1 痪枫。對象在Survivor 區(qū)中每熬過一次MinorGC , 年齡就增加1歲叠艳,當它的年齡增加到一定程度(默認為15 歲奶陈,其實每個JVM、每個GC都有所不同)時附较,就會被晉升到老年代中吃粒。
對象分配原則
針對不同年齡段的對象分配原則如下所示:
- 優(yōu)先分配到Eden
- 大對象直接分配到老年代
- 盡量避免程序中出現(xiàn)過多的大對象
- 長期存活的對象分配到老年代
- 動態(tài)對象年齡判斷
- 如果Survivor 區(qū)中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代拒课,無須等到 MaxTenuringThreshold 中要求的年齡徐勃。
- 空間分配擔保
- -XX:HandlePromotionFailure
/** 測試:大對象直接進入老年代
* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
byte[] buffer = new byte[1024 * 1024 * 20];//20m
}
}
Minor GC 、Major GC早像、Full GC
JVM在進行GC時僻肖,并非每次都對上面三個內(nèi)存(新生代、老年代卢鹦;方法區(qū))區(qū)域一起回收的臀脏,大部分時候回收的都是指新生代。
針對HotSpot VM的實現(xiàn)冀自,它里面的GC按照回收區(qū)域又分為兩大種類型:
- 一種是部分收集(Partial GC)
- 一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集揉稚。
其中又分為:
- 新生代收集(Minor GC / Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
目前熬粗,只有CMS GC會有單獨收集老年代的行為搀玖。
注意,很多時候Major GC會和Full GC混淆使用荐糜,需要具體分辨是老年代回收還是整堆回收巷怜。
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
目前暴氏,只有G1 GC會有這種行為
整堆收集(Full GC):收集整個java堆和方法區(qū)的垃圾收集延塑。
Minor GC觸發(fā)機制
- 當年輕代空間不足時,就會觸發(fā)Minor GC答渔。這里的年輕代滿指的是Eden區(qū)滿关带,Survivor滿不會引發(fā)GC。(每次 Minor GC 會清理年輕代的內(nèi)存。)
- 因為 Java 對象大多都具備朝生夕滅的特性宋雏,所以 Minor GC 非常頻繁芜飘,一般回收速度也比較快抖棘。這一定義既清晰又易于理解驻售。
- Minor GC會引發(fā)STW,暫停其它用戶的線程凯楔,等垃圾回收結束蚪燕,用戶線程才恢復運行娶牌。
老年代GC(Major GC/Full GC)觸發(fā)機制:
- 指發(fā)生在老年代的GC,對象從老年代消失時馆纳,我們說“Major GC”或“Full GC”發(fā)生了诗良。
- 出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC(但非絕對的鲁驶,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)鉴裹。
- 也就是在老年代空間不足時,會先嘗試觸發(fā)Minor GC钥弯。如果之后空間還不足径荔,則觸發(fā)Major GC
- Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長寿羞。
- 如果Major GC 后猖凛,內(nèi)存還不足,就報OOM了绪穆。
Full GC觸發(fā)機制:
觸發(fā)Full GC 執(zhí)行的情況有如下五種:
(1)調(diào)用System.gc()時,系統(tǒng)建議執(zhí)行Full GC虱岂,但是不必然執(zhí)行
(2)老年代空間不足
(3)方法區(qū)空間不足
(4)通過Minor GC后進入老年代的平均大小大于老年代的可用內(nèi)存
(5)由Eden區(qū)玖院、survivor space0(From Space)區(qū)向survivor space1(To Space)區(qū)復制時,對象大小大于To Space可用內(nèi)存第岖,則把該對象轉存到老年代难菌,且老年代的可用內(nèi)存小于該對象大小
說明:full gc是開發(fā)或調(diào)優(yōu)中盡量要避免的。這樣暫時時間會短一些蔑滓。
public class OOMTest {
public static void main(String[] args) {
String str = "www.atguigu.com";
//將參數(shù)調(diào)整的小一些郊酒,這樣問題會出現(xiàn)的比較早。
// -Xms8m -Xmx8m -XX:+PrintGCDetails
while(true){
str += str + new Random().nextInt(88888888) +
new Random().nextInt(999999999);
}
}
}
/**
* 測試MinorGC 键袱、 MajorGC燎窘、FullGC
* -Xms10m -Xmx10m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "atguigu.com";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍歷次數(shù)為:" + i);
}
}
}
OOM如何解決
1、要解決OOM異程憧В或heap space的異常褐健,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內(nèi)存中的對象是否是必要的澜汤,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)蚜迅。
2舵匾、如果是內(nèi)存泄漏,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈谁不。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關聯(lián)并導致垃圾收集器無法自動回收它們的坐梯。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息刹帕,就可以比較準確地定位出泄漏代碼的位置吵血。
3、如果不存在內(nèi)存泄漏轩拨,換句話說就是內(nèi)存中的對象確實都還必須存活著践瓷,那就應當檢查虛擬機的堆參數(shù)(-Xmx 與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大亡蓉,從代碼上檢查是否存在某些對象生命周期過長晕翠、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗砍濒。
為什么需要把Java堆分代淋肾?不分代就不能正常工作了嗎?
其實不分代完全可以爸邢,分代的唯一理由就是優(yōu)化GC性能樊卓。如果沒有分代,那所有的對象都在一塊杠河,就如同把一個學校的人都關在一個教室碌尔。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區(qū)域進行掃描券敌。而很多對象都是朝生夕死的唾戚,如果分代的話,把新創(chuàng)建的對象放到某一地方待诅,當GC 的時候先把這塊存儲“朝生夕死”對象的區(qū)域進行回收叹坦,這樣就會騰出很大的空間出來。
什么是TLAB(快速分配策略)卑雁?
為什么有TLAB(Thread Local Allocation Buffer)快速分配策略募书?
- 堆區(qū)是線程共享區(qū)域,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
- 由于對象實例的創(chuàng)建在JVM中非常頻繁测蹲,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
- 為避免多個線程操作同一地址莹捡,需要使用加鎖等機制,進而影響分配速度弛房。
所以道盏,多線程同時分配內(nèi)存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內(nèi)存分配的吞吐量荷逞,因此我們可以將這種內(nèi)存分配方式稱之為快速分配策略媒咳。
什么是TLAB?
從內(nèi)存模型而不是垃圾收集的角度种远,對Eden區(qū)域繼續(xù)進行劃分涩澡,JVM為每個線程分配了一個私有緩存區(qū)域,它包含在Eden空間內(nèi)坠敷。
據(jù)我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計妙同。
TLAB的說明:
- 盡管不是所有的對象實例都能夠在TLAB中成功分配內(nèi)存,但JVM確實是將TLAB作為內(nèi)存分配的首選膝迎。
- 在程序中粥帚,開發(fā)人員可以通過選項“-XX:+/-UseTLAB”設置是否開啟TLAB空間。
- 默認情況下限次,TLAB空間的內(nèi)存非常小芒涡,僅占有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent”設置TLAB空間所占用Eden空間的百分比大小卖漫。
- 一旦對象在TLAB空間分配內(nèi)存失敗時费尽,JVM就會嘗試著通過使用加鎖機制確保數(shù)據(jù)操作的原子性,從而直接在Eden空間中分配內(nèi)存羊始。
跪求三連
碼字不易旱幼,還請點個贊和收藏~