面試準(zhǔn)備之JVM的組成、垃圾回收機(jī)制(轉(zhuǎn)載)

原博客鏈接:https://www.cnblogs.com/wabi87547568/p/5282892.html

1.JVM的組成

JVM定義了控制Java代碼解釋執(zhí)行和具體實(shí)現(xiàn)的五種規(guī)格,因此把JVM分成了6個(gè)部分:JVM解釋器、指令系統(tǒng)、寄存器、棧、存儲(chǔ)區(qū)和碎片回收區(qū)。

◆JVM解釋器:即這個(gè)虛擬機(jī)處理字段碼的CPU。

◆JVM指令系統(tǒng):該系統(tǒng)與計(jì)算機(jī)很相似,一條指令由操作碼和操作數(shù)兩部分組成。操作碼為8位二進(jìn)制數(shù),主要是為了說明一條指令的功能,操作數(shù)可以根據(jù)需要而定,JVM最多有256種不同的操作指令。目前已使用了160多種操作碼。

◆寄存器:JVM有自己的虛擬寄存器,這樣就可以快速地與JVM的解釋器進(jìn)行數(shù)據(jù)交換。為了功能的需要,JVM設(shè)置了4個(gè)常用的32位寄存器:pc(程序計(jì)數(shù)器)、optop(操作數(shù)棧頂指針)、frame(當(dāng)前執(zhí)行環(huán)境指針)和vars(指向當(dāng)前執(zhí)行環(huán)境中第一個(gè)局部變量的指針)。

◆JVM棧:指令執(zhí)行時(shí)數(shù)據(jù)和信息存儲(chǔ)的場(chǎng)所和控制中心,它提供給JVM解釋器運(yùn)算所需要的信息。當(dāng)JVM得到一個(gè)Java字節(jié)碼應(yīng)用程序后,便為該代碼中一個(gè)類的每一個(gè)方法創(chuàng)建一個(gè)??蚣?,以保存該方法的狀態(tài)信息。每個(gè)??蚣馨ㄒ韵氯愋畔ⅲ壕植孔兞俊?zhí)行環(huán)境、操作數(shù)棧。

局部變量用于存儲(chǔ)一個(gè)類的方法中所用到的局部變量。vars寄存器指向該變量表中的第一個(gè)局部變量。

執(zhí)行環(huán)境用于保存解釋器對(duì)Java字節(jié)碼進(jìn)行解釋過程中所需的信息。它們是:上次調(diào)用的方法、局部變量指針和操作數(shù)棧的棧頂和棧底指針。執(zhí)行環(huán)境是一個(gè)執(zhí)行一個(gè)方法的控制中心。例如:如果解釋器要執(zhí)行iadd(整數(shù)加法),首先要從frame寄存器中找到當(dāng)前執(zhí)行環(huán)境,而后便從執(zhí)行環(huán)境中找到操作數(shù)棧,從棧頂彈出兩個(gè)整數(shù)進(jìn)行加法運(yùn)算,最后將結(jié)果壓入棧頂。

操作數(shù)棧用于存儲(chǔ)運(yùn)算所需操作數(shù)及運(yùn)算的結(jié)果。

◆存儲(chǔ)區(qū):JVM有兩類存儲(chǔ)區(qū):常量緩沖池和方法區(qū)。常量緩沖池用于存儲(chǔ)類名稱、方法和字段名稱以及串常量。方法區(qū)則用于存儲(chǔ)Java方法的字節(jié)碼。

◆碎片回收區(qū):JVM碎片回收是指將使用過的Java類的具體實(shí)例從內(nèi)存進(jìn)行回收,這就使得開發(fā)人員免去了自己編程控制內(nèi)存的麻煩和危險(xiǎn)。隨著JVM的不斷升級(jí),其碎片回收的技術(shù)和算法也更加合理。JVM 1.4.1版后產(chǎn)生了一種叫分代收集技術(shù),簡(jiǎn)單來說就是利用對(duì)象在程序中生存的時(shí)間劃分成代,以此為標(biāo)準(zhǔn)進(jìn)行碎片回收。

2.JAVA的垃圾回收機(jī)制 GC通過確定對(duì)象是否被活動(dòng)對(duì)象引用來確定是否收集該對(duì)象。

2.1 觸發(fā)GC(Garbage Collector)的條件

1)GC在優(yōu)先級(jí)最低的線程中運(yùn)行,一般在應(yīng)用程序空閑即沒有應(yīng)用線程在運(yùn)行時(shí)被調(diào)用。但下面的條件例外。

2)Java堆內(nèi)存不足時(shí),GC會(huì)被調(diào)用。當(dāng)應(yīng)用線程在運(yùn)行,并在運(yùn)行過程中創(chuàng)建新對(duì)象,若這時(shí)內(nèi)存空間不足,JVM就會(huì)強(qiáng)制調(diào)用GC線程。若GC一次之后仍不能滿足內(nèi)存分配,JVM會(huì)再進(jìn)行兩次GC,若仍無法滿足要求,則JVM將報(bào)“out of memory”的錯(cuò)誤,Java應(yīng)用將停止。

2.2 兩個(gè)重要方法

2.2.1 System.gc()方法

使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法,都可以請(qǐng)求Java的垃圾回收。在命令行中有一個(gè)參數(shù)-verbosegc可以查看Java使用的堆內(nèi)存的情況,它的格式如下:java -verbosegc classfile 由于這種方法會(huì)影響系統(tǒng)性能,不推薦使用,所以不詳訴。

2.2.2 finalize()方法

在JVM垃圾回收器收集一個(gè)對(duì)象之前,一般要求程序調(diào)用適當(dāng)?shù)姆椒ㄡ尫刨Y源,但在沒有明確釋放資源的情況下,Java提供了缺省機(jī)制來終止該對(duì)象心釋放資源,這個(gè)方法就是finalize()。它的原型為:protected void finalize() throws Throwable 在finalize()方法返回之后,對(duì)象消失,垃圾收集開始執(zhí)行。原型中的throws Throwable表示它可以拋出任何類型的異常。

之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況。例如:1)由于在分配內(nèi)存的時(shí)候可能采用了類似 C語言的做法,而非JAVA的通常new做法。這種情況主要發(fā)生在native method中,比如native method調(diào)用了C/C++方法malloc()函數(shù)系列來分配存儲(chǔ)空間,但是除非調(diào)用free()函數(shù),否則這些內(nèi)存空間將不會(huì)得到釋放,那么這個(gè)時(shí)候就可能造成內(nèi)存泄漏。但是由于free()方法是在C/C++中的函數(shù),所以finalize()中可以用本地方法來調(diào)用它。以釋放這些“特殊”的內(nèi)存空間。2)又或者打開的文件資源,這些資源不屬于垃圾回收器的回收范圍。

2.3 減少GC開銷的措施

1)不要顯式調(diào)用System.gc()。此函數(shù)建議JVM進(jìn)行主GC,雖然只是建議而非一定,但很多情況下它會(huì)觸發(fā)主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數(shù)。大大的影響系統(tǒng)性能。

2)盡量減少臨時(shí)對(duì)象的使用。臨時(shí)對(duì)象在跳出函數(shù)調(diào)用后,會(huì)成為垃圾,少用臨時(shí)變量就相當(dāng)于減少了垃圾的產(chǎn)生,從而延長(zhǎng)了出現(xiàn)上述第二個(gè)觸發(fā)條件出現(xiàn)的時(shí)間,減少了主GC的機(jī)會(huì)。

3)對(duì)象不用時(shí)最好顯式置為Null。一般而言,為Null的對(duì)象都會(huì)被作為垃圾處理,所以將不用的對(duì)象顯式地設(shè)為Null,有利于GC收集器判定垃圾,從而提高了GC的效率。

4)盡量使用StringBuffer,而不用String來累加字符串。由于String是固定長(zhǎng)的字符串對(duì)象,累加String對(duì)象時(shí),并非在一個(gè)String對(duì)象中擴(kuò)增,而是重新創(chuàng)建新的String對(duì)象,如Str5=Str1+Str2+Str3+Str4,這條語句執(zhí)行過程中會(huì)產(chǎn)生多個(gè)垃圾對(duì)象,因?yàn)閷?duì)次作“+”操作時(shí)都必須創(chuàng)建新的String對(duì)象,但這些過渡對(duì)象對(duì)系統(tǒng)來說是沒有實(shí)際意義的,只會(huì)增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長(zhǎng)的,它在原有基礎(chǔ)上進(jìn)行擴(kuò)增,不會(huì)產(chǎn)生中間對(duì)象。

5)能用基本類型如Int,Long,就不用Integer,Long對(duì)象?;绢愋妥兞空加玫膬?nèi)存資源比相應(yīng)對(duì)象占用的少得多,如果沒有必要,最好使用基本變量。

6)盡量少用靜態(tài)對(duì)象變量。靜態(tài)變量屬于全局變量,不會(huì)被GC回收,它們會(huì)一直占用內(nèi)存。

7)分散對(duì)象創(chuàng)建或刪除的時(shí)間。集中在短時(shí)間內(nèi)大量創(chuàng)建新對(duì)象,特別是大對(duì)象,會(huì)導(dǎo)致突然需要大量?jī)?nèi)存,JVM在面臨這種情況時(shí),只能進(jìn)行主GC,以回收內(nèi)存或整合內(nèi)存碎片,從而增加主GC的頻率。集中刪除對(duì)象,道理也是一樣的。它使得突然出現(xiàn)了大量的垃圾對(duì)象,空閑空間必然減少,從而大大增加了下一次創(chuàng)建新對(duì)象時(shí)強(qiáng)制主GC的機(jī)會(huì)。

2.4 對(duì)象在JVM堆區(qū)的狀態(tài)

1)可觸及狀態(tài):程序中還有變量引用,那么此對(duì)象為可觸及狀態(tài)。

2)可復(fù)活狀態(tài):當(dāng)程序中已經(jīng)沒有變量引用這個(gè)對(duì)象,那么此對(duì)象由可觸及狀態(tài)轉(zhuǎn)為可復(fù)活狀態(tài)。CG線程將在一定的時(shí)間準(zhǔn)備調(diào)用此對(duì)象的finalize方法(finalize方法繼承或重寫子Object),finalize方法內(nèi)的代碼有可能將對(duì)象轉(zhuǎn)為可觸及狀態(tài),否則對(duì)象轉(zhuǎn)化為不可觸及狀態(tài)。

3)不可觸及狀態(tài):只有當(dāng)對(duì)象處于不可觸及狀態(tài)時(shí),GC線程才能回收此對(duì)象的內(nèi)存。

[圖片上傳失敗...(image-125b4-1539142222224)]

Jvm堆區(qū)對(duì)象狀態(tài)轉(zhuǎn)換圖

2.5 常用垃圾收集器

1) 標(biāo)記-清除收集器 Mark-Sweep

2) 復(fù)制收集器 Copying

3) 標(biāo)記-壓縮收集器 Mark-Compact

4) 分代收集器   Generational

2.6 垃圾收集算法介紹

2.6.1 tracing算法

基于tracing算法的垃圾收集也稱為標(biāo)記和清除(mark-and-sweep)垃圾收集器.

這是最基礎(chǔ)的垃圾回收算法,之所以說它是最基礎(chǔ)的是因?yàn)樗钊菀讓?shí)現(xiàn),思想也是最簡(jiǎn)單的。標(biāo)記-清除算法分為兩個(gè)階段:標(biāo)記階段和清除階段。標(biāo)記階段的任務(wù)是標(biāo)記出所有需要被回收的對(duì)象,清除階段就是回收被標(biāo)記的對(duì)象所占用的空間。具體過程如下圖所示:

image

從圖中可以很容易看出標(biāo)記-清除算法實(shí)現(xiàn)起來比較容易,但是有一個(gè)比較嚴(yán)重的問題就是容易產(chǎn)生內(nèi)存碎片,碎片太多可能會(huì)導(dǎo)致后續(xù)過程中需要為大對(duì)象分配空間時(shí)無法找到足夠的空間而提前觸發(fā)新的一次垃圾收集動(dòng)作。

2.6.2 Copying算法

為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。具體過程如下圖所示:

image

這種算法雖然實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效且不容易產(chǎn)生內(nèi)存碎片,但是卻對(duì)內(nèi)存空間的使用做出了高昂的代價(jià),因?yàn)槟軌蚴褂玫膬?nèi)存縮減到原來的一半。很顯然,Copying算法的效率跟存活對(duì)象的數(shù)目多少有很大的關(guān)系,如果存活對(duì)象很多,那么Copying算法的效率將會(huì)大大降低。

2.6.3 compacting算法

為了解決Copying算法的缺陷,充分利用內(nèi)存空間,提出了Mark-Compact算法。該算法標(biāo)記階段和Mark-Sweep一樣,但是在完成標(biāo)記之后,它不是直接清理可回收對(duì)象,而是將存活對(duì)象都向一端移動(dòng),然后清理掉端邊界以外的內(nèi)存。具體過程如下圖所示:

image

2.6.4 Generation算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據(jù)對(duì)象存活的生命周期將內(nèi)存劃分為若干個(gè)不同的區(qū)域。一般情況下將堆區(qū)劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點(diǎn)是每次垃圾收集時(shí)只有少量對(duì)象需要被回收,而新生代的特點(diǎn)是每次垃圾回收時(shí)都有大量的對(duì)象需要被回收,那么就可以根據(jù)不同代的特點(diǎn)采取最適合的收集算法。

目前大部分垃圾收集器對(duì)于新生代都采取Copying算法,因?yàn)樾律忻看卫厥斩家厥沾蟛糠謱?duì)象,也就是說需要復(fù)制的操作次數(shù)較少,但是實(shí)際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當(dāng)進(jìn)行回收時(shí),將Eden和Survivor中還存活的對(duì)象復(fù)制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。

而由于老年代的特點(diǎn)是每次回收都只回收少量對(duì)象,一般使用的是Mark-Compact算法。

image

新年代:新創(chuàng)建的對(duì)象都存放在這里。因?yàn)榇蠖鄶?shù)對(duì)象很快變得不可達(dá),所以大多數(shù)對(duì)象在年輕代中創(chuàng)建,然后消失。當(dāng)對(duì)象從這塊內(nèi)存區(qū)域消失時(shí),我們說發(fā)生了一次“minor GC”。

老年代:沒有變得不可達(dá),存活下來的年輕代對(duì)象被復(fù)制到這里。這塊內(nèi)存區(qū)域一般大于年輕代。因?yàn)樗蟮囊?guī)模,GC發(fā)生的次數(shù)比在年輕代的少。對(duì)象從老年代消失時(shí),我們說“major GC”(或“full GC”)發(fā)生了。

上圖中的永久代(permanent generation)也稱為“方法區(qū)(method area)”,他存儲(chǔ)class對(duì)象和字符串常量。所以這塊內(nèi)存區(qū)域絕對(duì)不是永久的存放從老年代存活下來的對(duì)象的。在這塊內(nèi)存中有可能發(fā)生垃圾回收。發(fā)生在這里垃圾回收也被稱為major GC。

對(duì)于分代算法,我推薦一篇博客給大家:http://blog.jobbole.com/80499/

推薦閱讀:LeakCanary 內(nèi)存泄露監(jiān)測(cè)原理研究 , 話說ReferenceQueue

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

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

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