前言
? ? 由于先前也遇到過一些性能問題,OOM算是其中的一大類了模她。因此也對(duì)jvm產(chǎn)生了一些興趣努潘。自己對(duì)jvm略做了些研究。后續(xù)繼續(xù)補(bǔ)充客给。
從oom引申出去
? ? 既然說到oom用押,首先需要知道oom的原因是什么。為啥會(huì)oom嘞靶剑?
? ? oom的定義是outofmemory,當(dāng)內(nèi)存想為對(duì)象分配內(nèi)存的時(shí)候只恨,發(fā)現(xiàn)內(nèi)存不足以去分配內(nèi)存译仗。或者gc的時(shí)候發(fā)現(xiàn)沒有可以被回收的對(duì)象或回收后的內(nèi)存也不足以為對(duì)象分配內(nèi)存官觅。因此拋出這個(gè)java異常纵菌。
oom
可以分為以下四類
堆溢出:java堆
棧溢出:虛擬機(jī)棧和本地方法棧
方法區(qū)內(nèi)存溢出:方法區(qū)和內(nèi)存時(shí)常量池
本機(jī)直接內(nèi)存溢出
因此,需要先了解堆休涤,棧咱圆,方法區(qū)都是些啥
運(yùn)行時(shí)數(shù)據(jù)區(qū)
先上圖
程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
? ? java虛擬機(jī)的多線程是通過輪流切換線程功氨,并為線程分配執(zhí)行時(shí)間片去運(yùn)行來執(zhí)行的序苏。每個(gè)線程都有一個(gè)自己的程序計(jì)數(shù)器。我覺得這個(gè)可以這么理解:當(dāng)一個(gè)線程在運(yùn)行的時(shí)候捷凄,每執(zhí)行一步程序計(jì)數(shù)器都會(huì)有個(gè)記錄忱详,記錄當(dāng)前執(zhí)行到哪一步了。如果線程被切換后又切換回來跺涤,那么通過程序計(jì)數(shù)器就能知道執(zhí)行到哪一步了匈睁,然后繼續(xù)向下執(zhí)行。
虛擬機(jī)棧:每個(gè)線程都會(huì)有一個(gè)虛擬機(jī)棧桶错。虛擬機(jī)棧描述的是java方法執(zhí)行的內(nèi)存模型航唆。因?yàn)榫€程執(zhí)行的過程就是執(zhí)行線程里的一個(gè)個(gè)方法,而每個(gè)方法都會(huì)創(chuàng)建對(duì)應(yīng)自己的棧幀院刁。
棧幀里存的內(nèi)容如下:
- 局部變量表:存放了各種編譯期可知基本數(shù)據(jù)類型糯钙,對(duì)象引用(引用指針或句柄)
- 操作數(shù)棧:大多數(shù)指令都要從這里彈出數(shù)據(jù),執(zhí)行運(yùn)算退腥,然后把結(jié)果壓回操作數(shù)棧
- 動(dòng)態(tài)鏈接
- 方法出口
? ? 64位的long和都double類型數(shù)據(jù)占用2個(gè)局部變量空間任岸,其他數(shù)據(jù)類型占用一個(gè),也就是每個(gè)局部變量空間為32位狡刘。
? ? 在這個(gè)地方演闭,如果線程請(qǐng)求的深度大于虛擬機(jī)允許的深度,會(huì)拋出StackOverflowError.因?yàn)閖vm分配給虛擬機(jī)棧的內(nèi)存是有限的颓帝,而每個(gè)方法都會(huì)有對(duì)應(yīng)的棧幀壓入到棧中米碰,如果調(diào)用方法過多,那么棧滿了自然也就溢出了购城。(可能的場景:死循環(huán)代碼吕座,大量遞歸調(diào)用,那排查問題的時(shí)候也可以由此有一個(gè)思路)瘪板∥馀浚可以通過調(diào)整-Xss去調(diào)整棧大小。
大部分java虛擬機(jī)允許動(dòng)態(tài)擴(kuò)展侮攀,但如果擴(kuò)展的時(shí)候也申請(qǐng)不到足夠內(nèi)存時(shí)锣枝,就會(huì)報(bào)OOM了厢拭。
本地方法棧:和虛擬機(jī)發(fā)揮作用相似。區(qū)別:虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法服務(wù)撇叁,本地方法棧為虛擬機(jī)使用的Native方法服務(wù)供鸠。Native Method就是一個(gè)java調(diào)用非java代碼的接口,Native方法的實(shí)現(xiàn)由非java語言實(shí)現(xiàn)陨闹。讀者不用糾結(jié)楞捂,略作了解即可。
堆:堆是所有線程共享的一塊內(nèi)存趋厉,作用是存放對(duì)象實(shí)例寨闹。堆可以分為新生代和老年代。新生代里還可細(xì)分為Eden,From survivor,To survivor等空間君账。后面講述GC過程時(shí)會(huì)說到繁堡。
方法區(qū):也是所有線程共享的一塊內(nèi)存,存放被虛擬機(jī)加載的類信息乡数,常量椭蹄,靜態(tài)變量,編譯器編譯后的代碼瞳脓。也就是常說的永久代。
永久代的大小可以用-XX:MaxPermSize去設(shè)置澈侠。
運(yùn)行時(shí)常量池:方法區(qū)的一部分劫侧。存放編譯期生成的各種字面量和符號(hào)引用。字面量就是指這個(gè)量本身哨啃。比如字面量2烧栋,就是指2.
運(yùn)行時(shí)常量池有一個(gè)重要特性就是動(dòng)態(tài)性。常量不一定只有編譯期才能產(chǎn)生拳球,運(yùn)行期間也可能將新的常量放入常量池审姓。詳情可見String類的
intern()方法。
此處推薦這篇博客祝峻,對(duì)intern()方法介紹的挺清楚的魔吐。
幾張圖輕松理解String.intern() - 程序老兵的博客 - CSDN博客
直接內(nèi)存:它不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,但也頻繁的被使用莱找。直接內(nèi)存不會(huì)受到j(luò)ava堆大小的限制酬姆,但是會(huì)受到本機(jī)總內(nèi)存的限制。
GC過程
GC分為新生代GC(minor gc)和老年代GC(full gc)奥溺。新生代GC的頻率遠(yuǎn)遠(yuǎn)高于老年代辞色。而且
新生代GC的速度會(huì)比老年代的GC速度快10倍以上。根源在于新生代和老年代使用的GC算法不同浮定。讀者們可以去仔細(xì)思考下相满。新生代/老年代大小默認(rèn)為1:2层亿。
新生代GC過程:
? ? 新生代里可細(xì)分為Eden,From survivor,To survivor等空間。當(dāng)我們需要給對(duì)象分配內(nèi)存的時(shí)候立美,首先我們會(huì)在Eden區(qū)為對(duì)象分配內(nèi)存匿又,當(dāng)Eden區(qū)內(nèi)存不足時(shí),會(huì)發(fā)生minor gc悯辙,此時(shí)會(huì)把仍然存活的對(duì)象放到From survivor琳省,并給對(duì)象標(biāo)記存活次數(shù)1;然后當(dāng)Eden區(qū)再次被用完后躲撰,對(duì)Eden區(qū)和From survivor區(qū)篩選出存活的對(duì)象针贬,放到To survivor區(qū),清空Eden區(qū)和From survivor區(qū)拢蛋,存活次數(shù)加1桦他,之前存活的就是2了。
? ? 以此類推谆棱,默認(rèn)是當(dāng)存活次數(shù)到達(dá)15次(可配置)的時(shí)候快压,把這個(gè)對(duì)象存入老年代中。同時(shí)也可以看到垃瞧,F(xiàn)rom survivor,To survivor區(qū)始終有一個(gè)是空置的蔫劣。所以新生代使用的只有9/10的空間。
然而大家可以思考一下个从。Eden區(qū)和survivor區(qū)的大小為8:1脉幢,那么發(fā)生minor gc后如果存活的對(duì)象
的大小比survivor區(qū)還要大。這個(gè)時(shí)候會(huì)怎么處理嗦锐?
? ? 這里需要引入一個(gè)叫“內(nèi)存分配擔(dān)保機(jī)制”的概念嫌松。就是當(dāng)存活的對(duì)象連survivor區(qū)都放不下的時(shí)候,這部分放不下的對(duì)象會(huì)直接進(jìn)入老年代奕污。老年代是擔(dān)保人萎羔。老年代進(jìn)行擔(dān)保,前提是老年代還有剩余空間碳默。但是每次存活下來的對(duì)象大小是不確定的贾陷。所以只好取之前每次存儲(chǔ)到老年代的對(duì)象大小的平均值。如果大于平均值嘱根,那么直接full gc昵宇。但是為了避免頻繁full gc,仍然會(huì)開啟handlepromotionfailure配置儿子。如下圖
老年代GC過程:
老年代采用了標(biāo)記整理瓦哎,標(biāo)記清楚的算法。老年代會(huì)把仍然存活的對(duì)象都整理統(tǒng)一放到一邊。整理完成后就會(huì)清楚掉邊界外的對(duì)象蒋譬。這樣就避免了產(chǎn)生大量的內(nèi)存碎片的問題割岛。但是整理算法相較于新生代采用的復(fù)制算法,復(fù)雜程度肯定更高犯助。這也導(dǎo)致了full gc的速度要遠(yuǎn)遠(yuǎn)慢于minor gc癣漆。