JVM淺談

前言

由於先前也遇到過一些性能問題,OOM算是其中的一大類了。因此也對jvm產生了一些興趣。自己對jvm略做了些研究。後續繼續補充。

從oom引申出去

既然說到oom,首先需要知道oom的原因是什麼。爲啥會oom嘞?
oom的定義是outofmemory。當內存想爲對象分配內存的時候,發現內存不足以去分配內存,或者gc的時候發現沒有可以被回收的對象或回收後的內存也不足以爲對象分配內存。
因此拋出這個java異常。

oom
可以分爲以下四類

1.堆溢出:java堆

2.棧溢出:虛擬機棧和本地方法棧

3.方法區內存溢出:方法區和內存時常量池

4.本機直接內存溢出

因此,需要先了解堆,棧,方法區都是些啥

運行時數據區

先上圖
file

程序計數器:當前線程所執行的字節碼的行號指示器。

java虛擬機的多線程是通過輪流切換線程,併爲線程分配執行時間片去運行來執行的。每個線程都有一個自己的程序計數器。我覺得這個可以這麼理解:當一個線程在運行的時候,每執行一步程序計數器都會有個記錄,記錄當前執行到哪一步了。如果線程被切換後又切換回來,那麼通過程序計數器就能知道執行到哪一步了,然後繼續向下執行。

虛擬機棧:每個線程都會有一個虛擬機棧。虛擬機棧描述的是java方法執行的內存模型。因爲線程執行的過程就是執行線程裏的一個個方法,而每個方法都會創建對應自己的棧幀。

棧幀裏存的內容如下:

  • 局部變量表:存放了各種編譯期可知基本數據類型,對象引用(引用指針或句柄)

  • 操作數棧:大多數指令都要從這裏彈出數據,執行運算,然後把結果壓回操作數棧

  • 動態鏈接

  • 方法出口

64位的long和都double類型數據佔用2個局部變量空間,其他數據類型佔用一個,也就是每個局部變量空間爲32位。

在這個地方,如果線程請求的深度大於虛擬機允許的深度,會拋出StackOverflowError.因爲jvm分配給虛擬機棧的內存是有限的,而每個方法都會有對應的棧幀壓入到棧中,如果調用方法過多,那麼棧滿了自然也就溢出了。(可能的場景:死循環代碼,大量遞歸調用,那排查問題的時候也可以由此有一個思路)。可以通過調整**-Xss**去調整棧大小。

大部分java虛擬機允許動態擴展,但如果擴展的時候也申請不到足夠內存時,就會報OOM了。

本地方法棧:和虛擬機發揮作用相似。區別:虛擬機棧爲虛擬機執行java方法服務,本地方法棧爲虛擬機使用的Native方法服務。Native Method就是一個java調用非java代碼的接口,Native方法的實現由非java語言實現。讀者不用糾結,略作了解即可。

:堆是所有線程共享的一塊內存,作用是存放對象實例。堆可以分爲新生代和老年代。新生代裏還可細分爲Eden,From survivor,To survivor等空間。後面講述GC過程時會說到。

方法區:也是所有線程共享的一塊內存,存放被虛擬機加載的類信息,常量,靜態變量,編譯器編譯後的代碼。也就是常說的永久代。

永久代的大小可以用**-XX:MaxPermSize**去設置。

運行時常量池:方法區的一部分。存放編譯期生成的各種字面量和符號引用。字面量就是指這個量本身。比如字面量2,就是指2.

運行時常量池有一個重要特性就是動態性。常量不一定只有編譯期才能產生,運行期間也可能將新的常量放入常量池。詳情可見String類的

intern()方法。

此處推薦這篇博客,對intern()方法介紹的挺清楚的。

https://blog.csdn.net/soonfly/article/details/70147205

直接內存:它不是虛擬機運行時數據區的一部分,但也頻繁的被使用。直接內存不會受到java堆大小的限制,但是會受到本機總內存的限制。

GC過程

GC分爲新生代GC(minor gc)和老年代GC(full gc)。新生代GC的頻率遠遠高於老年代。而且

新生代GC的速度會比老年代的GC速度快10倍以上。根源在於新生代和老年代使用的GC算法不同。讀者們可以去仔細思考下(答案文中有,哈哈)。新生代/老年代大小默認爲1:2。

新生代GC過程

新生代裏可細分爲Eden,From survivor,To survivor等空間。當我們需要給對象分配內存的時候,首先我們會在Eden區爲對象分配內存,當Eden區內存不足時,會發生minor gc,此時會把仍然存活的對象放到From survivor,並給對象標記存活次數1;然後當Eden區再次被用完後,對Eden區和From survivor區篩選出存活的對象,放到To survivor區,清空Eden區和From survivor區,存活次數加1,之前存活的就是2了。

以此類推,默認是當存活次數到達15次(可配置)的時候,把這個對象存入老年代中。同時也可以看到,From survivor,To survivor區始終有一個是空置的。所以新生代使用的只有9/10的空間。
然而大家可以思考一下。Eden區和survivor區的大小爲8:1,那麼發生minor gc後如果存活的對象
的大小比survivor區還要大。這個時候會怎麼處理?

這裏需要引入一個叫“內存分配擔保機制”的概念。就是當存活的對象連survivor區都放不下的時候,這部分放不下的對象會直接進入老年代。老年代是擔保人。老年代進行擔保,前提是老年代還有剩餘空間。但是每次存活下來的對象大小是不確定的。所以只好取之前每次存儲到老年代的對象大小的平均值。如果大於平均值,那麼直接full gc。但是爲了避免頻繁full gc,仍然會開啓handlepromotionfailure配置。如下圖

file

老年代GC過程

老年代採用了標記整理,標記清楚的算法。老年代會把仍然存活的對象都整理統一放到一邊。整理完成後就會清楚掉邊界外的對象。這樣就避免了產生大量的內存碎片的問題。但是整理算法相較於新生代採用的複製算法,複雜程度肯定更高。這也導致了full gc的速度要遠遠慢於minor gc。
文中若有考慮不周或者錯誤,歡迎大家支招指正。一起學習交流。betterFighter!

發佈了4 篇原創文章 · 獲贊 0 · 訪問量 1003
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章