前言
由于先前也遇到過一些性能問題,OOM算是其中的一大類了。因此也對jvm產(chǎn)生了一些興趣。自己對jvm略做了些研究。后續(xù)繼續(xù)補充。
從oom引申出去
既然說到oom,首先需要知道oom的原因是什么。為啥會oom嘞?
oom的定義是outofmemory。當內(nèi)存想為對象分配內(nèi)存的時候,發(fā)現(xiàn)內(nèi)存不足以去分配內(nèi)存,或者gc的時候發(fā)現(xiàn)沒有可以被回收的對象或回收后的內(nèi)存也不足以為對象分配內(nèi)存。
因此拋出這個java異常。
oom
可以分為以下四類
1.堆溢出:java堆
2.棧溢出:虛擬機棧和本地方法棧
3.方法區(qū)內(nèi)存溢出:方法區(qū)和內(nèi)存時常量池
4.本機直接內(nèi)存溢出
因此,需要先了解堆,棧,方法區(qū)都是些啥
運行時數(shù)據(jù)區(qū)
先上圖
程序計數(shù)器:當前線程所執(zhí)行的字節(jié)碼的行號指示器。
java虛擬機的多線程是通過輪流切換線程,并為線程分配執(zhí)行時間片去運行來執(zhí)行的。每個線程都有一個自己的程序計數(shù)器。我覺得這個可以這么理解:當一個線程在運行的時候,每執(zhí)行一步程序計數(shù)器都會有個記錄,記錄當前執(zhí)行到哪一步了。如果線程被切換后又切換回來,那么通過程序計數(shù)器就能知道執(zhí)行到哪一步了,然后繼續(xù)向下執(zhí)行。
虛擬機棧:每個線程都會有一個虛擬機棧。虛擬機棧描述的是java方法執(zhí)行的內(nèi)存模型。因為線程執(zhí)行的過程就是執(zhí)行線程里的一個個方法,而每個方法都會創(chuàng)建對應自己的棧幀。
棧幀里存的內(nèi)容如下:
局部變量表:存放了各種編譯期可知基本數(shù)據(jù)類型,對象引用(引用指針或句柄)
操作數(shù)棧:大多數(shù)指令都要從這里彈出數(shù)據(jù),執(zhí)行運算,然后把結果壓回操作數(shù)棧
動態(tài)鏈接
方法出口
64位的long和都double類型數(shù)據(jù)占用2個局部變量空間,其他數(shù)據(jù)類型占用一個,也就是每個局部變量空間為32位。
在這個地方,如果線程請求的深度大于虛擬機允許的深度,會拋出StackOverflowError.因為jvm分配給虛擬機棧的內(nèi)存是有限的,而每個方法都會有對應的棧幀壓入到棧中,如果調(diào)用方法過多,那么棧滿了自然也就溢出了。(可能的場景:死循環(huán)代碼,大量遞歸調(diào)用,那排查問題的時候也可以由此有一個思路)。可以通過調(diào)整-Xss去調(diào)整棧大小。
大部分java虛擬機允許動態(tài)擴展,但如果擴展的時候也申請不到足夠內(nèi)存時,就會報OOM了。
本地方法棧:和虛擬機發(fā)揮作用相似。區(qū)別:虛擬機棧為虛擬機執(zhí)行java方法服務,本地方法棧為虛擬機使用的Native方法服務。Native Method就是一個java調(diào)用非java代碼的接口,Native方法的實現(xiàn)由非java語言實現(xiàn)。讀者不用糾結,略作了解即可。
堆:堆是所有線程共享的一塊內(nèi)存,作用是存放對象實例。堆可以分為新生代和老年代。新生代里還可細分為Eden,From survivor,To survivor等空間。后面講述GC過程時會說到。
方法區(qū):也是所有線程共享的一塊內(nèi)存,存放被虛擬機加載的類信息,常量,靜態(tài)變量,編譯器編譯后的代碼。也就是常說的永久代。
永久代的大小可以用-XX:MaxPermSize去設置。
運行時常量池:方法區(qū)的一部分。存放編譯期生成的各種字面量和符號引用。字面量就是指這個量本身。比如字面量2,就是指2.
運行時常量池有一個重要特性就是動態(tài)性。常量不一定只有編譯期才能產(chǎn)生,運行期間也可能將新的常量放入常量池。詳情可見String類的
intern()方法。
直接內(nèi)存:它不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,但也頻繁的被使用。直接內(nèi)存不會受到java堆大小的限制,但是會受到本機總內(nèi)存的限制。
GC過程
GC分為新生代GC(minor gc)和老年代GC(full gc)。新生代GC的頻率遠遠高于老年代。而且
新生代GC的速度會比老年代的GC速度快10倍以上。根源在于新生代和老年代使用的GC算法不同。讀者們可以去仔細思考下(答案文中有,哈哈)。新生代/老年代大小默認為1:2。
新生代GC過程:
新生代里可細分為Eden,From survivor,To survivor等空間。當我們需要給對象分配內(nèi)存的時候,首先我們會在Eden區(qū)為對象分配內(nèi)存,當Eden區(qū)內(nèi)存不足時,會發(fā)生minor gc,此時會把仍然存活的對象放到From survivor,并給對象標記存活次數(shù)1;然后當Eden區(qū)再次被用完后,對Eden區(qū)和From survivor區(qū)篩選出存活的對象,放到To survivor區(qū),清空Eden區(qū)和From survivor區(qū),存活次數(shù)加1,之前存活的就是2了。
以此類推,默認是當存活次數(shù)到達15次(可配置)的時候,把這個對象存入老年代中。同時也可以看到,F(xiàn)rom survivor,To survivor區(qū)始終有一個是空置的。所以新生代使用的只有9/10的空間。
然而大家可以思考一下。Eden區(qū)和survivor區(qū)的大小為8:1,那么發(fā)生minor gc后如果存活的對象
的大小比survivor區(qū)還要大。這個時候會怎么處理?
這里需要引入一個叫“內(nèi)存分配擔保機制”的概念。就是當存活的對象連survivor區(qū)都放不下的時候,這部分放不下的對象會直接進入老年代。老年代是擔保人。老年代進行擔保,前提是老年代還有剩余空間。但是每次存活下來的對象大小是不確定的。所以只好取之前每次存儲到老年代的對象大小的平均值。如果大于平均值,那么直接full gc。但是為了避免頻繁full gc,仍然會開啟handlepromotionfailure配置。如下圖
老年代GC過程:
老年代采用了標記整理,標記清楚的算法。老年代會把仍然存活的對象都整理統(tǒng)一放到一邊。整理完成后就會清楚掉邊界外的對象。這樣就避免了產(chǎn)生大量的內(nèi)存碎片的問題。但是整理算法相較于新生代采用的復制算法,復雜程度肯定更高。這也導致了full gc的速度要遠遠慢于minor gc。
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2970瀏覽量
104814 -
計數(shù)器
+關注
關注
32文章
2256瀏覽量
94646 -
虛擬機
+關注
關注
1文章
918瀏覽量
28232 -
內(nèi)存溢出
+關注
關注
0文章
10瀏覽量
1229
發(fā)布評論請先 登錄
相關推薦
評論