JVM之內(nèi)存分配和回收機(jī)制
前言
本篇主要介紹JVM內(nèi)存分配和回收策略,內(nèi)容主要節(jié)選自《深入理解java虛擬機(jī)》。
一、內(nèi)存分配策略
1. 堆內(nèi)存模型
組成:
- 新生代 默認(rèn)占堆空間的三分之一,由于在新生代對象大多都是朝生夕死,則采用的是復(fù)制算法,在復(fù)制的期間會有頻繁的Minor GC。
- 老年代 默認(rèn)占堆空間的三分之二,老年代對象大多都是長期存活的,則采用的是標(biāo)記算法,老年代的Major GC開銷非常大。
- Eden 新生代分為2個(gè)區(qū) Eden和Survivor區(qū) 其中Eden區(qū)默認(rèn)占新生代十分之八的空間,對象優(yōu)先進(jìn)入Eden區(qū)。
- Survivor 當(dāng)Eden區(qū)內(nèi)存滿了之后會進(jìn)行一次Minor GC存活對象會進(jìn)入Survivor區(qū),默認(rèn)占新生代十分之二空間, 其中它又均分為From區(qū)和To區(qū),在Survivor區(qū)的對象每熬過一次從From區(qū)到TO區(qū)則年齡+1。
大多情況下,對象在新生代Eden區(qū)中分配。當(dāng)Eden沒有足夠的空間分配對象時(shí)虛擬機(jī)會發(fā)起一次Minor GC。
2.2 大對象直接到老年代
大對象即需要大量連續(xù)內(nèi)存空間的對象(例如很長的字符串及數(shù)組)。虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshoId參數(shù),令大于這個(gè)設(shè)置值的對象直接在老年代分配,這樣做的目的是避免在Eden區(qū)及兩個(gè)區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。注意PretenureSizeThreshoId參數(shù)只對Serial和ParNew兩款收集器有效。
2.3 動態(tài)年齡判斷
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)人老年代,無須等到MaxTenuringThreshoId中要求的年齡。
2.4 內(nèi)存擔(dān)保機(jī)制
在發(fā)生Minor GC之前,虛擬機(jī)會先檢查Survivor空間是否夠用,如果夠用則直接進(jìn)行Minor GC。否則進(jìn)行檢查老年代最大連續(xù)可用空間是否大于新生代的總和,假如大于,那么這個(gè)時(shí)候發(fā)生Minor GC是安全的。假如不大于,那么需要判斷HandlePromotionFailure設(shè)置是否允許擔(dān)保失敗。假如允許,則繼續(xù)判定老年代最大可用的連續(xù)空間是否大于平均晉升到老年代對象的平均值,如果大于,這個(gè)時(shí)候可以發(fā)生Minor GC ,如果小于或者設(shè)置HandlePromotionFailure不允許擔(dān)保失敗,則需要做一次Full GC。通常會把HandlePromotionFailure開關(guān)打開,以減少Full GC。
2.5 長期存活對象
虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡(Age)計(jì)數(shù)器(存在于對象頭中)。如果對象在Eden出生并經(jīng)過第一次MinorGC后仍然存活,并且能被Survivor容納的話,將被移動到Survwor空間中,并且對象年齡設(shè)為1。對象在Survivor區(qū)中每“熬過”一次MinorGC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshoId設(shè)置。
二、對象存活
判斷對象存活一般有兩種方式: 引用計(jì)數(shù)算法和可達(dá)性分析算法。
1.引用計(jì)數(shù)算法
原理:
- 引用計(jì)數(shù)算法(Reference Counting)比較簡單,對每個(gè)對象保存一個(gè)整型的引用計(jì)數(shù)器屬性。用于記錄對象被引用的情況。
- 對于一個(gè)對象A,只要有任何一個(gè)對象引用了A,則A的引用計(jì)數(shù)器就加1;當(dāng)引用失效時(shí),引用計(jì)數(shù)器就減1。只要對象A的引用計(jì)數(shù)器的值為0,即表示對象A不可能再被使用,可進(jìn)行回收。
優(yōu)點(diǎn):
- 實(shí)現(xiàn)簡單,垃圾對象便于辨識;
- 判定效率高,回收沒有延遲性。
缺點(diǎn):
- 它需要單獨(dú)的字段存儲計(jì)數(shù)器,這樣的做法增加了存儲空間的開銷。
- 每次復(fù)制都需要更新計(jì)數(shù)器,伴隨著加法和減法操作,這增加了時(shí)間開銷。
- 引用計(jì)數(shù)器有一個(gè)嚴(yán)重的問題,即無法處理循環(huán)引用的情況。這是一條致命缺陷,導(dǎo)致在Java的垃圾回收器中沒有使用這類算法。
2.可達(dá)性分析算法
定義:相對于引用計(jì)數(shù)算法而言,可達(dá)性分析算法不僅同樣具備實(shí)現(xiàn)簡單和執(zhí)行高效等特點(diǎn),更重要的是該算法可以有效地解決在引用計(jì)數(shù)算法中循環(huán)引用的問題,防止內(nèi)存泄漏的發(fā)生。
原理:
- 可達(dá)性分析算法是以根對象集合(GCRoots)為起始點(diǎn),按照從上至下的方式搜索被根對象集合所連接的目標(biāo)對象是否可達(dá)。
- 使用可達(dá)性分析算法后,內(nèi)存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)。
- 如果目標(biāo)對象沒有任何引用鏈相連,則是不可達(dá)的,就意味著該對象己經(jīng)死亡,可以標(biāo)記為垃圾對象。
- 在可達(dá)性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
GC ROOT 對象:
- 虛擬機(jī)棧中引用的對象;
比如:各個(gè)線程被調(diào)用的方法中使用到的參數(shù)、局部變量等。 - 本地方法棧內(nèi) JNI(通常說的本地方法)引用的對象;
- 方法區(qū)中類靜態(tài)屬性引用的對象;
比如:Java類的引用類型靜態(tài)變量 - 方法區(qū)中常量引用的對象;
比如:字符串常量池(string Table)里的引用 - 所有被同步鎖 synchronized 持有的對象;
- Java虛擬機(jī)內(nèi)部的引用。
基本數(shù)據(jù)類型對應(yīng)的 Class 對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統(tǒng)類加載器。
3.再談引用
- 強(qiáng)引用:強(qiáng)引用在代碼中普遍存在,例如Object obj=new Object() 這類的引用。只要強(qiáng)引用存在,垃圾回收器永遠(yuǎn)不會回收掉被引用的對象。
- 軟引用:軟引用來描述一些還有用但并非必須的對象,在系統(tǒng)要發(fā)生內(nèi)存溢出之前,將會把這些對象列入回收范圍之中進(jìn)行第二次回收,如果第二次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
- 弱引用:弱引用也是用來描述必須對象的,但是它的強(qiáng)度比軟引用更弱,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾回收發(fā)生之前。當(dāng)垃圾回收器工作時(shí),無論內(nèi)存是否足夠,都回收掉只被弱引用關(guān)聯(lián)的對象。
- 虛引用:它是最弱的一種引用關(guān)系。它無法通過虛引用來取得一個(gè)對象實(shí)例。唯一目的就是能在這個(gè)對象被回收之前會收到一個(gè)系統(tǒng)通知。
三、內(nèi)存回收
1.堆內(nèi)存回收
是JVM所管理內(nèi)存最大的一塊,也是gc回收的主要區(qū)域。
1.1 哪些對象能回收?
堆內(nèi)存中對象存活是使用可達(dá)性分析算法來判斷,其中非存活對象由GC回收掉。這個(gè)就是虛擬機(jī)需要回收堆的對象。
1.2 如何回收?
- Minor GC:新生代收集,目標(biāo)只是新生代的垃圾收集;
- Major GC:老年代收集,目標(biāo)是老年代的垃圾收集(具體說只有CMS會有單獨(dú)收集老年代的行為);
- Full GC:收集整個(gè)java堆和方法區(qū)的垃圾收集。這里補(bǔ)充說明一下雖然網(wǎng)上很多說什么Full GC就是Major GC,在這里我要重申一下并不是,具體看書上描述如下:
Mixed GC:收集整個(gè)新生代以及部分老年代的垃圾收集,僅G1支持。(類似于Full GC)
1.3 什么時(shí)候回收?
Minor GC觸發(fā)條件:
- Eden區(qū)域滿了,會觸發(fā)Minor GC;
- 新生對象需要分配到新生代的Eden,當(dāng)Eden區(qū)的內(nèi)存不夠時(shí)需要進(jìn)行MinorGC。
Major GC觸發(fā)條件:
- 老年代區(qū)域設(shè)置的閾值空間滿了,會觸發(fā)Major GC;
- 新對象需要分配到老年代,此時(shí)老年代設(shè)置閾值可用空間不足時(shí)觸發(fā)Major GC。
Full GC觸發(fā)條件:
- 內(nèi)存擔(dān)保機(jī)制 ,Survivor空間不足時(shí),判斷是否允許擔(dān)保失敗,如果不允許則進(jìn)行Full GC。如果允許,并且每次晉升到老年代的對象平均大小>老年代最大可用連續(xù)內(nèi)存空間,也會進(jìn)行Full GC;
- MinorGC后存活的對象超過了老年代剩余空間;
- 方法區(qū)內(nèi)存不足時(shí);
- 程序中調(diào)用了System.gc()方法,可用通過-XX:+ DisableExplicitGC來禁止調(diào)用System.gc;
- CMS GC異常,CMS運(yùn)行期間預(yù)留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次“Concurrent Mode Failure”失敗,會觸發(fā)Full GC。
2.方法區(qū)回收
方法區(qū)主要回收廢棄的常量池和不再使用類型,但這個(gè)2類對象存活的判斷還不一樣。
2.1 常量池
同堆的對象存活類似-可達(dá)性分析法,具體請參考之前的可達(dá)性分析法。
2.2 類型數(shù)據(jù)
- 該類索引的實(shí)例都已經(jīng)被回收;
- 加載該類的類加載器已經(jīng)被回收;
- 該類對應(yīng)的java.lang.class對象沒有任何地方被引用。
以上都是我簡單總結(jié),以下是書上關(guān)于方法區(qū)回收描述的內(nèi)容:
其實(shí)從書上就可以看出來,關(guān)于方法區(qū)OOM問題大都是在程序中是有大量使用反射、動態(tài)代理、CGLIB等框架,如果在實(shí)際開發(fā)中遇到關(guān)于可以從以上幾個(gè)維度來定位問題。
總結(jié)
本篇所有理論知識都是摘抄于《深入理解java虛擬機(jī)》,有部分是自己簡單總結(jié),JVM內(nèi)存分配和回收是我們在分析JVM調(diào)優(yōu)和相關(guān)問題的基石,建議看完我本篇的去多看幾遍《深入理解java虛擬機(jī)》。
相關(guān)文章
Java 獲取兩個(gè)List的交集和差集,以及應(yīng)用場景操作
這篇文章主要介紹了Java 獲取兩個(gè)List的交集和差集,以及應(yīng)用場景操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09淺談Storm在zookeeper上的目錄結(jié)構(gòu)
這篇文章主要介紹了淺談Storm在zookeeper上的目錄結(jié)構(gòu)的相關(guān)內(nèi)容,涉及storm使用zookeeper的操作以及詳細(xì)結(jié)構(gòu)圖,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10Java Date時(shí)間類型的操作實(shí)現(xiàn)
本文主要介紹Java Date 日期類型,以及Calendar的怎么獲取時(shí)間,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03Spring 整合 MyBatis的實(shí)現(xiàn)步驟
SpringMVC 本來就是 Spring 框架的一部分,這兩者無須再做整合,所以 SSM 整合的關(guān)鍵就是Spring對MyBatis的整合,三大框架整合完成后,將以 Spring 為核心,調(diào)用有關(guān)資源,高效運(yùn)作,這篇文章主要介紹了 Spring 整合 MyBatis的實(shí)現(xiàn)步驟,需要的朋友可以參考下2023-02-02