JVM內(nèi)存模型和GC

JVM內(nèi)存模型

Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺(tái)下對內(nèi)存的訪問都能保證效果一致的機(jī)制及規(guī)范。


JVM內(nèi)存模型.png

從圖中可以看出來,Java數(shù)據(jù)區(qū)域分為五大數(shù)據(jù)區(qū)域。這些區(qū)域各有各的用途,創(chuàng)建及銷毀時(shí)間。


1、程序計(jì)數(shù)器

程序計(jì)數(shù)器是一塊很小的內(nèi)存空間,它是線程私有的,可以認(rèn)作為當(dāng)前線程的行號指示器。為了線程切換可以恢復(fù)到正確執(zhí)行位置,每個(gè)線程都需有獨(dú)立的一個(gè)程序計(jì)數(shù)器。
注意:如果線程執(zhí)行的是個(gè)java方法,那么計(jì)數(shù)器記錄虛擬機(jī)字節(jié)碼指令的地址。如果為native【底層方法】,那么計(jì)數(shù)器為空。這塊內(nèi)存區(qū)域是虛擬機(jī)規(guī)范中唯一沒有OutOfMemoryError的區(qū)域。


2、虛擬機(jī)棧(Java棧)

也為線程私有,生命周期與線程相同。
每個(gè)方法被執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作棧,動(dòng)態(tài)鏈接,方法出口等信息。每一個(gè)方法被調(diào)用的過程就對應(yīng)一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。【棧先進(jìn)后出,下圖棧1先進(jìn)最后出來】

局部變量表:一片連續(xù)的內(nèi)存空間,用來存放方法參數(shù),以及方法內(nèi)定義的局部變量,存放著編譯期間已知的數(shù)據(jù)類型(八大基本類型和對象引用(reference類型),returnAddress類型。它的最小的局部變量表空間單位為Slot,虛擬機(jī)沒有指明Slot的大小,但在jvm中,long和double類型數(shù)據(jù)明確規(guī)定為64位,這兩個(gè)類型占2個(gè)Slot,其它基本類型固定占用1個(gè)Slot。

reference類型:與基本類型不同的是它不等同本身,即使是String,內(nèi)部也是char數(shù)組組成,它可能是指向一個(gè)對象起始位置指針,也可能指向一個(gè)代表對象的句柄或其他與該對象有關(guān)的位置。

returnAddress類型:指向一條字節(jié)碼指令的地址

操作數(shù)棧
操作數(shù)棧(Operand Stack)也常被稱為操作棧,它是一個(gè)后入先出(Last In First Out,LIFO)棧。操作數(shù)棧的每一個(gè)元素都可以是包括long和double在內(nèi)的任意Java數(shù)據(jù)類型。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。

動(dòng)態(tài)鏈接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。Class文件的常量池中存有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池里指向方法的符號引用作為參數(shù)。這些符號引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就被轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化被稱為靜態(tài)解析。另外一部分將在每一次運(yùn)行期間都轉(zhuǎn)化為直接引用,這部分就稱為動(dòng)態(tài)連接。

方法的返回地址
當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式退出這個(gè)方法:
正常調(diào)用完成:執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者或者主調(diào)方法),方法是否有返回值以及返回值的類型將根據(jù)遇到何種方法返回指令來決定
異常調(diào)用完成:在方法執(zhí)行的過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到妥善處理。無論是Java虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出。
在方法退出之后,都必須返回到最初方法被調(diào)用時(shí)的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層主調(diào)方法的執(zhí)行狀態(tài)。


3、本地方法棧

本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。

本地方法棧也會(huì)在棧深度溢出或者棧擴(kuò)展失敗時(shí)分別拋出StackOverflowError和OutOfMemoryError異常


4、堆

堆是java虛擬機(jī)管理內(nèi)存最大的一塊內(nèi)存區(qū)域,因?yàn)槎汛娣诺膶ο笫蔷€程共享的,所以多線程的時(shí)候也需要同步機(jī)制。因此需要重點(diǎn)了解下。

java虛擬機(jī)規(guī)范對這塊的描述是:所有對象實(shí)例及數(shù)組都要在堆上分配內(nèi)存,但隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)的成熟,這個(gè)說法也不是那么絕對,但是大多數(shù)情況都是這樣的。

即時(shí)編譯器:可以把把Java的字節(jié)碼,包括需要被解釋的指令的程序)轉(zhuǎn)換成可以直接發(fā)送給處理器的指令的程序)

逃逸分析:通過逃逸分析來決定某些實(shí)例或者變量是否要在堆中進(jìn)行分配,如果開啟了逃逸分析,即可將這些變量直接在棧上進(jìn)行分配,而非堆上進(jìn)行分配。這些變量的指針可以被全局所引用,或者其其它線程所引用。

注意:它是所有線程共享的,它的目的是存放對象實(shí)例。同時(shí)它也是GC所管理的主要區(qū)域,因此常被稱為GC堆,又由于現(xiàn)在收集器常使用分代算法,Java堆中還可以細(xì)分為新生代和老年代,再細(xì)致點(diǎn)還有Eden(伊甸園)空間之類的不做深究。

根據(jù)虛擬機(jī)規(guī)范,Java堆可以存在物理上不連續(xù)的內(nèi)存空間,就像磁盤空間只要邏輯是連續(xù)的即可。它的內(nèi)存大小可以設(shè)為固定大小,也可以擴(kuò)展。

當(dāng)前主流的虛擬機(jī)如HotPot都能按擴(kuò)展實(shí)現(xiàn)(通過設(shè)置 -Xmx和-Xms),如果堆中沒有內(nèi)存內(nèi)存完成實(shí)例分配,而且堆無法擴(kuò)展將報(bào)OOM錯(cuò)誤(OutOfMemoryError)

5、方法區(qū)

各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。

如果方法區(qū)無法滿足新的內(nèi)存分配需求時(shí),將拋出OutOfMemoryError異常

運(yùn)行時(shí)常量池

是方法區(qū)的一部分,class文件除了有類的字段、接口、方法等描述信息之外,還有常量池用于存放編譯期間生成的各種字面量和符號引用。

GC

GC簡介

Java GC泛指java的垃圾回收機(jī)制,該機(jī)制是java與C/C++的主要區(qū)別之一,我們在日常寫java代碼的時(shí)候,一般都不需要編寫內(nèi)存回收或者垃圾清理的代碼,也不需要像C/C++那樣做類似delete/free的操作。

java內(nèi)存模型中分為五大區(qū)域已經(jīng)有所了解。我們知道程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧,由線程而生,隨線程而滅,其中棧中的棧幀隨著方法的進(jìn)入順序的執(zhí)行的入棧和出棧的操作,一個(gè)棧幀需要分配多少內(nèi)存取決于具體的虛擬機(jī)實(shí)現(xiàn)并且在編譯期間即確定下來【忽略JIT編譯器做的優(yōu)化,基本當(dāng)成編譯期間可知】,當(dāng)方法或線程執(zhí)行完畢后,內(nèi)存就隨著回收,因此無需關(guān)心。

而Java堆、方法區(qū)則不一樣。方法區(qū)存放著類加載信息,但是一個(gè)接口中多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能不太一樣,一個(gè)方法中多個(gè)分支需要的內(nèi)存也可能不一樣【只有在運(yùn)行期間才可知道這個(gè)方法創(chuàng)建了哪些對象沒需要多少內(nèi)存】,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,gc關(guān)注的也正是這部分的內(nèi)存。

堆的回收區(qū)域
為了高效的回收,jvm將堆分為三個(gè)區(qū)域
1、新生區(qū)(伊甸園區(qū)(對象都是在這個(gè)區(qū)new出來的)、幸存區(qū)to、幸存區(qū)from:幸存區(qū)位置會(huì)互相交換,誰空誰是to)
2、老年區(qū)
3、永久區(qū):存儲(chǔ)的是java的運(yùn)行環(huán)境或類信息,這個(gè)區(qū)域不存在垃圾回收,關(guān)閉jvm就會(huì)釋放內(nèi)存
一個(gè)啟動(dòng)類加載大量的jar包。tomcat部署太多應(yīng)用。內(nèi)存滿了就oom
jdk1.6之前:永久代,常量池是在方法區(qū)
jdk1.7去永久代,常量池在堆中
jdk1.8之后:無永久代,常量池在元空間中

堆新生代老年代分區(qū)示意圖.png

判斷對象是否存活算法

1.引用計(jì)數(shù)算法
早期判斷對象是否存活大多都是以這種算法,這種算法判斷很簡單,簡單來說就是給對象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)對象被引用一次就加1,引用失效時(shí)就減1。當(dāng)為0的時(shí)候就判斷對象不會(huì)再被引用。
優(yōu)點(diǎn):實(shí)現(xiàn)簡單效率高,被廣泛使用與如python何游戲腳本語言上。
缺點(diǎn):難以解決循環(huán)引用的問題,就是假如兩個(gè)對象互相引用已經(jīng)不會(huì)再被其它其它引用,導(dǎo)致一直不會(huì)為0就無法進(jìn)行回收。

2.可達(dá)性分析算法
目前主流的商用語言[如java、c#]采用的是可達(dá)性分析算法判斷對象是否存活。這個(gè)算法有效解決了循環(huán)利用的弊端。
它的基本思路是通過一個(gè)稱為“GC Roots”的對象為起始點(diǎn),搜索所經(jīng)過的路徑稱為引用鏈,當(dāng)一個(gè)對象到GC Roots沒有任何引用跟它連接則證明對象是不可用的。

可作為GC Roots的對象有四種
①虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對象。
②方法區(qū)中的類靜態(tài)屬性引用的對象,一般指被static修飾引用的對象,加載類的時(shí)候就加載到內(nèi)存中。
③方法區(qū)中的常量引用的對象,
④本地方法棧中JNI(native方法)引用的對象

要真正宣告對象死亡需經(jīng)過兩個(gè)過程。
1.可達(dá)性分析后沒有發(fā)現(xiàn)引用鏈
2.查看對象是否有finalize方法,如果有重寫且在方法內(nèi)完成自救[比如再建立引用],還是可以搶救一下,注意這邊一個(gè)類的finalize只執(zhí)行一次,這就會(huì)出現(xiàn)一樣的代碼第一次自救成功第二次失敗的情況。[如果類重寫finalize且還沒調(diào)用過,會(huì)將這個(gè)對象放到一個(gè)叫做F-Queue的序列里,這邊f(xié)inalize不承諾一定會(huì)執(zhí)行,這么做是因?yàn)槿绻锩嫠姥h(huán)的話可能會(huì)時(shí)F-Queue隊(duì)列處于等待,嚴(yán)重會(huì)導(dǎo)致內(nèi)存崩潰,這是我們不希望看到的。]

枚舉根節(jié)點(diǎn)算法
GC Roots 被虛擬機(jī)用來判斷對象是否存活

可作為GC Roos的節(jié)點(diǎn)主要是在一些全局引用【如常量或靜態(tài)屬性】、執(zhí)行上下文【如棧幀中本地變量表】中。那么如何在這么多全局變量和本地變量表找到【枚舉】根節(jié)點(diǎn)將是個(gè)問題。

可達(dá)性分析算法需考慮

1.如果方法區(qū)幾百兆,一個(gè)個(gè)檢查里面的引用,將耗費(fèi)大量資源。

2.在分析時(shí),需保證這個(gè)對象引用關(guān)系不再變化,否則結(jié)果將不準(zhǔn)確?!疽虼薌C進(jìn)行時(shí)需停掉其它所有java執(zhí)行線程(Sun把這種行為稱為‘Stop the World’),即使是號稱幾乎不會(huì)停頓的CMS收集器,枚舉根節(jié)點(diǎn)時(shí)也需停掉線程】

解決辦法:實(shí)際上當(dāng)系統(tǒng)停下來后JVM不需要一個(gè)個(gè)檢查引用,而是通過OopMap數(shù)據(jù)結(jié)構(gòu)【HotSpot的叫法】來標(biāo)記對象引用。

虛擬機(jī)先得知哪些地方存放對象的引用,在類加載完時(shí)。HotSpot把對象內(nèi)什么偏移量什么類型的數(shù)據(jù)算出來,在jit編譯過程中,也會(huì)在特定位置記錄下棧和寄存器哪些位置是引用,這樣GC在掃描時(shí)就可以知道這些信息?!灸壳爸髁鱆VM使用準(zhǔn)確式GC】

OopMap可以幫助HotSpot快速且準(zhǔn)確完成GC Roots枚舉以及確定相關(guān)信息。但是也存在一個(gè)問題,可能導(dǎo)致引用關(guān)系變化。

這個(gè)時(shí)候有個(gè)safepoint(安全點(diǎn))的概念。

HotSpot中GC不是在任意位置都可以進(jìn)入,而只能在safepoint處進(jìn)入。 GC時(shí)對一個(gè)Java線程來說,它要么處在safepoint,要么不在safepoint。

safepoint不能太少,否則GC等待的時(shí)間會(huì)很久

safepoint不能太多,否則將增加運(yùn)行GC的負(fù)擔(dān)

安全點(diǎn)主要存放的位置

1:循環(huán)的末尾
2:方法臨返回前/調(diào)用方法的call指令后
3:可能拋異常的位置

垃圾收集算法

JVM中,可達(dá)性分析算法幫我們解決了哪些對象可以回收的問題,垃圾收集算法則關(guān)心怎么回收。

三大垃圾收集算法

1.標(biāo)記/清除算法【最基礎(chǔ)】
2.復(fù)制算法
3.標(biāo)記/整理算法
jvm采用分代收集算法對不同區(qū)域采用不同的回收算法。

新生代采用復(fù)制算法
新生代中因?yàn)閷ο蠖际?朝生夕死的",【深入理解JVM虛擬機(jī)上說98%的對象,不知道是不是這么多,總之就是存活率很低】,適用于復(fù)制算法【復(fù)制算法比較適合用于存活率低的內(nèi)存區(qū)域】。它優(yōu)化了標(biāo)記/清除算法的效率和內(nèi)存碎片問題,且JVM不以5:5分配內(nèi)存【由于存活率低,不需要復(fù)制保留那么大的區(qū)域造成空間上的浪費(fèi),因此不需要按1:1【原有區(qū)域:保留空間】劃分內(nèi)存區(qū)域,而是將內(nèi)存分為一塊Eden空間和From Survivor、To Survivor【保留空間】,三者默認(rèn)比例為8:1:1,優(yōu)先使用Eden區(qū),若Eden區(qū)滿,則將對象復(fù)制到第二塊內(nèi)存區(qū)上。但是不能保證每次回收都只有不多于10%的對象存貨,所以Survivor區(qū)不夠的話,則會(huì)依賴?yán)夏甏甏孢M(jìn)行分配】。
GC開始時(shí),對象只會(huì)存于Eden和From Survivor區(qū)域,To Survivor【保留空間】為空。

GC進(jìn)行時(shí),Eden區(qū)所有存活的對象都被復(fù)制到To Survivor區(qū),而From Survivor區(qū)中,仍存活的對象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閾值(默認(rèn)15是因?yàn)閷ο箢^中年齡戰(zhàn)4bit,新生代每熬過一次垃圾回收,年齡+1),則移到老年代,沒有達(dá)到則復(fù)制到To Survivor。

老年代采用標(biāo)記/清除算法或標(biāo)記/整理算法

由于老年代存活率高,沒有額外空間給他做擔(dān)保,必須使用這兩種算法。

1、標(biāo)記/清除算法

標(biāo)記/清除算法的基本思想就跟它的名字一樣,分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。

標(biāo)記階段:標(biāo)記的過程其實(shí)就是前面介紹的可達(dá)性分析算法的過程,遍歷所有的GC Roots對象,對從GC Roots對象可達(dá)的對象都打上一個(gè)標(biāo)識(shí),一般是在對象的header中,將其記錄為可達(dá)對象;

清除階段:清除的過程是對堆內(nèi)存進(jìn)行遍歷,如果發(fā)現(xiàn)某個(gè)對象沒有被標(biāo)記為可達(dá)對象(通過讀取對象header信息),則將其回收。


標(biāo)記清除算法.jpg

上圖是標(biāo)記/清除算法的示意圖,在標(biāo)記階段,從對象GC Root 1可以訪問到B對象,從B對象又可以訪問到E對象,因此從GC Root 1到B、E都是可達(dá)的,同理,對象F、G、J、K都是可達(dá)對象;到了清除階段,所有不可達(dá)對象都會(huì)被回收。

在垃圾收集器進(jìn)行GC時(shí),必須停止所有Java執(zhí)行線程(也稱"Stop The World"),原因是在標(biāo)記階段進(jìn)行可達(dá)性分析時(shí),不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,否則的話可達(dá)性分析結(jié)果的準(zhǔn)確性就無法得到保證。在等待標(biāo)記清除結(jié)束后,應(yīng)用線程才會(huì)恢復(fù)運(yùn)行。

前面剛提過,后續(xù)的收集算法是在標(biāo)記/清除算法的基礎(chǔ)上進(jìn)行改進(jìn)而來的,那也就是說標(biāo)記/清除算法有它的不足。其實(shí)了解了它的原理,其缺點(diǎn)也就不難看出了。

1、效率問題。標(biāo)記和清除兩個(gè)階段的效率都不高,因?yàn)檫@兩個(gè)階段都需要遍歷內(nèi)存中的對象,很多時(shí)候內(nèi)存中的對象實(shí)例數(shù)量是非常龐大的,這無疑很耗費(fèi)時(shí)間,而且GC時(shí)需要停止應(yīng)用程序,這會(huì)導(dǎo)致非常差的用戶體驗(yàn)。

2、空間問題。標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片(從上圖可以看出),內(nèi)存空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動(dòng)作。

既然標(biāo)記/清除算法有這么多的缺點(diǎn),那它還有存在的意義嗎?別急,一個(gè)算法有缺陷,人們肯定會(huì)想辦法去完善它,接下來的兩個(gè)算法就是在標(biāo)記/清除算法的基礎(chǔ)上完善而來的。

2、復(fù)制算法

為了解決效率問題,復(fù)制算法出現(xiàn)了。復(fù)制算法的原理是:將可用內(nèi)存按容量劃分為大小相等的兩塊,每次使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活的對象復(fù)制到另一塊內(nèi)存上,然后把這一塊內(nèi)存所有的對象一次性清理掉。用圖說明如下:
回收前


復(fù)制算法1.jpg

回收后


復(fù)制算法2.jpg

復(fù)制算法每次都是對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,這樣就減少了標(biāo)記對象遍歷的時(shí)間,在清除使用區(qū)域?qū)ο髸r(shí),不用進(jìn)行遍歷,直接清空整個(gè)區(qū)域內(nèi)存,而且在將存活對象復(fù)制到保留區(qū)域時(shí)也是按地址順序存儲(chǔ)的,這樣就解決了內(nèi)存碎片的問題,在分配對象內(nèi)存時(shí)不用考慮內(nèi)存碎片等復(fù)雜問題,只需要按順序分配內(nèi)存即可。

復(fù)制算法簡單高效,優(yōu)化了標(biāo)記/清除算法的效率低、內(nèi)存碎片多的問題。但是它的缺點(diǎn)也很明顯:

1、將內(nèi)存縮小為原來的一半,浪費(fèi)了一半的內(nèi)存空間,代價(jià)太高;

2、如果對象的存活率很高,極端一點(diǎn)的情況假設(shè)對象存活率為100%,那么我們需要將所有存活的對象復(fù)制一遍,耗費(fèi)的時(shí)間代價(jià)也是不可忽視的。

基于以上復(fù)制算法的缺點(diǎn),由于新生代中的對象幾乎都是“朝生夕死”的(達(dá)到98%),現(xiàn)在的商業(yè)虛擬機(jī)都采用復(fù)制算法來回收新生代。由于新生代的對象存活率低,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的From Survivor空間、To Survivor空間,三者的比例為8:1:1。每次使用Eden和From Survivor區(qū)域,To Survivor作為保留空間。GC開始時(shí),對象只會(huì)存在于Eden區(qū)和From Survivor區(qū),To Survivor區(qū)是空的。GC進(jìn)行時(shí),Eden區(qū)中所有存活的對象都會(huì)被復(fù)制到To Survivor區(qū),而在From Survivor區(qū)中,仍存活的對象會(huì)根據(jù)它們的年齡值決定去向,年齡值達(dá)到年齡閥值(默認(rèn)為15,新生代中的對象每熬過一輪垃圾回收,年齡值就加1)的對象會(huì)被移到老年代中,沒有達(dá)到閥值的對象會(huì)被復(fù)制到To Survivor區(qū)。接著清空Eden區(qū)和From Survivor區(qū),新生代中存活的對象都在To Survivor區(qū)。接著, From Survivor區(qū)和To Survivor區(qū)會(huì)交換它們的角色,也就是新的To Survivor區(qū)就是上次GC清空的From Survivor區(qū),新的From Survivor區(qū)就是上次GC的To Survivor區(qū),總之,不管怎樣都會(huì)保證To Survivor區(qū)在一輪GC后是空的。GC時(shí)當(dāng)To Survivor區(qū)沒有足夠的空間存放上一次新生代收集下來的存活對象時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保,將這些對象存放在老年代中。

3、標(biāo)記/整理算法

復(fù)制算法在對象存活率較高時(shí)要進(jìn)行較多的復(fù)制操作,效率會(huì)變得很低,更關(guān)鍵的是,如果不想浪費(fèi)50%的內(nèi)存空間,就需要有額外的內(nèi)存空間進(jìn)行分配擔(dān)保,以應(yīng)對內(nèi)存中對象100%存活的極端情況,因此,在老年代中由于對象的存活率非常高,復(fù)制算法就不合適了。根據(jù)老年代的特點(diǎn),高人們提出了另一種算法:標(biāo)記/整理算法。從名字上看,這種算法與標(biāo)記/清除算法很像,事實(shí)上,標(biāo)記/整理算法的標(biāo)記過程任然與標(biāo)記/清除算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行回收,而是讓所有存活的對象都向一端移動(dòng),然后直接清理掉端邊線以外的內(nèi)存。

回收前:


標(biāo)記整理算法1.jpg

回收后:


標(biāo)記整理算法2.jpg

可以看到,回收后可回收對象被清理掉了,存活的對象按規(guī)則排列存放在內(nèi)存中。這樣一來,當(dāng)我們給新對象分配內(nèi)存時(shí),jvm只需要持有內(nèi)存的起始地址即可。標(biāo)記/整理算法不僅彌補(bǔ)了標(biāo)記/清除算法存在內(nèi)存碎片的問題,也消除了復(fù)制算法內(nèi)存減半的高額代價(jià),可謂一舉兩得。但任何算法都有缺點(diǎn),就像人無完人,標(biāo)記/整理算法的缺點(diǎn)就是效率也不高,不僅要標(biāo)記存活對象,還要整理所有存活對象的引用地址,在效率上不如復(fù)制算法。

弄清了以上三種算法的原理,下面我們來從幾個(gè)方面對這幾種算法做一個(gè)簡單排行。

效率:復(fù)制算法 > 標(biāo)記/整理算法 > 標(biāo)記/清除算法(標(biāo)記/清除算法有內(nèi)存碎片問題,給大對象分配內(nèi)存時(shí)可能會(huì)觸發(fā)新一輪垃圾回收)

內(nèi)存整齊率:復(fù)制算法 = 標(biāo)記/整理算法 > 標(biāo)記/清除算法

內(nèi)存利用率:標(biāo)記/整理算法 = 標(biāo)記/清除算法 > 復(fù)制算法

從上面簡單的評估可以看出,標(biāo)記/清除算法已經(jīng)比較落后了,但是吃水不忘挖井人,它是后面幾種算法的前輩、是基礎(chǔ),在某些場景下它也有用武之地。

4、分代收集算法

當(dāng)前商業(yè)虛擬機(jī)都采用分代收集算法,說它是終極算法,是因?yàn)樗Y(jié)合了前幾種算法的優(yōu)點(diǎn),將算法組合使用進(jìn)行垃圾回收,與其說它是一種新的算法,不如說它是對前幾種算法的實(shí)際應(yīng)用。分代收集算法的思想是按對象的存活周期不同將內(nèi)存劃分為幾塊,一般是把Java堆分為新生代和老年代(還有一個(gè)永久代,是HotSpot特有的實(shí)現(xiàn),其他的虛擬機(jī)實(shí)現(xiàn)沒有這一概念,永久代的收集效果很差,一般很少對永久代進(jìn)行垃圾回收),這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最合適的收集算法。

新生代:朝生夕滅,存活時(shí)間很短。

老年代:經(jīng)過多次Minor GC而存活下來,存活周期長。

在新生代中每次垃圾回收都發(fā)現(xiàn)有大量的對象死去,只有少量存活,因此采用復(fù)制算法回收新生代,只需要付出少量對象的復(fù)制成本就可以完成收集;而老年代中對象的存活率高,不適合采用復(fù)制算法,而且如果老年代采用復(fù)制算法,它是沒有額外的空間進(jìn)行分配擔(dān)保的,因此必須使用標(biāo)記/清理算法或者標(biāo)記/整理算法來進(jìn)行回收。
總結(jié)一下就是,分代收集算法的原理是采用復(fù)制算法來收集新生代,采用標(biāo)記/清理算法或者標(biāo)記/整理算法收集老年代。

垃圾收集器

如果說垃圾回收算法是內(nèi)存回收的方法論,那么垃圾收集器就是具體實(shí)現(xiàn)。jvm會(huì)結(jié)合針對不同的場景及用戶的配置使用不同的收集器。

年輕代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年輕、老年代范疇內(nèi)]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JVM調(diào)優(yōu)是調(diào)整:方法區(qū)和堆(主要是堆) 棧管運(yùn)行,堆管存儲(chǔ) 堆和方法區(qū)是所有線程共享的內(nèi)存區(qū)域;棧和程序計(jì)數(shù)器是...
    江澈_SIMON閱讀 76評論 0 1
  • 最近學(xué)習(xí)Python的GC機(jī)制時(shí),想到了java的GC,忘得差不多了,(⊙﹏⊙)b??!這里便做一下回顧總結(jié)。推薦周...
    廿陸小生閱讀 1,285評論 0 0
  • 1 CPU和內(nèi)存的交互 了解jvm內(nèi)存模型前,了解下cpu和計(jì)算機(jī)內(nèi)存的交互情況?!疽?yàn)镴ava虛擬機(jī)內(nèi)存模型定義...
    Zal哥哥閱讀 279評論 0 2
  • JVM內(nèi)存模型 根據(jù)Java虛擬機(jī)規(guī)范,Java數(shù)據(jù)區(qū)域分為五大數(shù)據(jù)區(qū)域。 其中方法區(qū)和堆是所有線程共享的,虛擬機(jī)...
    全菜工程師小輝閱讀 1,496評論 2 9
  • 1 CPU和內(nèi)存的交互 了解jvm內(nèi)存模型前,了解下cpu和計(jì)算機(jī)內(nèi)存的交互情況。【因?yàn)镴ava虛擬機(jī)內(nèi)存模型定義...
    Garwer閱讀 374,387評論 54 551

友情鏈接更多精彩內(nèi)容