淺談JVM基本結(jié)構(gòu),內(nèi)存分配與垃圾回收問題

淺談JVM基本結(jié)構(gòu),內(nèi)存分配與垃圾回收問題

JVM之于Java的重要性不再贅述,言歸正傳,接下來談?wù)勱P(guān)于JVM內(nèi)存的事.

在Oracle官方發(fā)布的<<Java虛擬機(jī)規(guī)范>>中,關(guān)于JVM的基本結(jié)構(gòu)一般如下圖所示,此圖關(guān)于JVM的基本結(jié)構(gòu)還是比較清晰的.

img

從圖中可以看出,JVM的基本結(jié)構(gòu)可以分為4大部分.

一.類加載器(ClassLoader):其作用是在程序運(yùn)行時(shí),將編譯好的.class字節(jié)碼文件裝載到JVM的內(nèi)存區(qū)域中.如下圖所示流程,Java源碼被編譯器編譯為字節(jié)碼文件,字節(jié)碼文件被類加載器加載到數(shù)據(jù)運(yùn)行時(shí)區(qū)域(其實(shí)就是內(nèi)存空間當(dāng)中),然后再由執(zhí)行引擎執(zhí)行.class文件中的字節(jié)碼指令.

img

二.執(zhí)行引擎:執(zhí)行.class字節(jié)碼文件中的指令集,如果想了解class中的字節(jié)碼指令,可以參考<<深入分析Java Web技術(shù)內(nèi)幕>>的第5章深入class文件結(jié)構(gòu).

三.本地庫接口(本地方法庫):我的理解這是JVM與本地操作系統(tǒng)交互的接口,調(diào)用一些由C語言等編寫的本地方法,一般的開發(fā)者并不用細(xì)糾.

四.JVM內(nèi)存區(qū)(運(yùn)行時(shí)數(shù)據(jù)區(qū)):這是JVM中非常重要的一部分,是Java程序運(yùn)行時(shí)JVM所分配的內(nèi)存區(qū)域,絕大部分開發(fā)者關(guān)注的重點(diǎn)都在此.

JVM的內(nèi)存區(qū)域分為5大塊,如下圖所示.

1.虛擬機(jī)棧(Stack):一般俗稱棧區(qū),是線程私有的.棧區(qū)一般與線程緊密相聯(lián),一旦有新的線程被創(chuàng)建,JVM就會為該線程分配一個(gè)對應(yīng)的java棧區(qū),在這個(gè)棧區(qū)中會有許多棧幀,每運(yùn)行一個(gè)方法就創(chuàng)建一個(gè)棧幀,用于存儲局部變量,方法返回值等.棧幀中存儲的局部變量隨著線程的結(jié)束而結(jié)束,其生命周期取決于線程的生命周期,所以講java棧中的變量都是線程私有的.

2.堆(Heap):真正存儲對象的區(qū)域,當(dāng)進(jìn)行Object obj = new Object()這樣一個(gè)操作時(shí),真正的obj對象實(shí)例就會在heap中.

3.方法區(qū)(Method Area):包含常量池,靜態(tài)變量等,有人說常量池也屬于heap的一部分,但是嚴(yán)格上講方法區(qū)只是堆的邏輯部分,方法區(qū)還有個(gè)別名叫做非堆(non-heap),所以方法區(qū)和堆還是有不同的.

4.程序計(jì)數(shù)器(Program Couter Register):用于保存當(dāng)前線程的執(zhí)行的內(nèi)存地址.因?yàn)镴VM是支持多線程的,多線程同時(shí)執(zhí)行的時(shí)候可能會輪流切換,為了保證線程切換回來后還能恢復(fù)到原先狀態(tài),就需要一個(gè)獨(dú)立的計(jì)數(shù)器,記錄之前中斷的位置,由此可以看出程序計(jì)數(shù)器也是線程私有的.

5.本地方法棧(Native Method Stack):性質(zhì)與虛擬機(jī)棧類似,是為了方便JVM去調(diào)用本地方法接口的棧區(qū),此處開發(fā)者很少去關(guān)注,我也是了解有限,因此不深入探究其作用.

img

說完JVM的基本結(jié)構(gòu),接下來談?wù)凧VM的內(nèi)存分配與垃圾回收問題,這是一個(gè)對于Java開發(fā)者而言老生常談且面試經(jīng)常都會被問到的點(diǎn).

JVM的內(nèi)存分配一般是先一次性分配出一個(gè)較大的空間,當(dāng)有新對象被創(chuàng)建時(shí)都在該空間上進(jìn)行資源分配,這種分配方式有利于節(jié)省開銷( 相比于C來講),但這塊被一次性開辟出來的內(nèi)存空間也是有限的,如何清理這塊空間上的無用(垃圾)對象,這就與垃圾回收(GC)機(jī)制密切相關(guān).內(nèi)存申請一般分為靜態(tài)內(nèi)存和動態(tài)內(nèi)存.靜態(tài)內(nèi)存比較容易理解,編譯時(shí)就能確定的內(nèi)存就是靜態(tài)內(nèi)存,即內(nèi)存是固定的,系統(tǒng)可以直接進(jìn)行分配,比如short,int類型的變量,其占用的內(nèi)存大小是固定的.而動態(tài)內(nèi)存分配是只有當(dāng)程序運(yùn)行時(shí),才能知道所要分配的內(nèi)存空間大小,在運(yùn)行之前是不確定的,因此屬于動態(tài)內(nèi)存.

如前文所述,無論是虛擬機(jī)棧,程序計(jì)數(shù)器以及本地方法棧均屬于線程私有,其生命周期與線程的生命周期一致,當(dāng)線程執(zhí)行完畢,其所占用的內(nèi)存空間也就隨之釋放,因此這三部分是不存在垃圾回收問題的.Java開發(fā)者平常所說的垃圾回收,主要針對的是堆區(qū)和方法區(qū)而言的,對象實(shí)例在程序運(yùn)行時(shí)被在存放在堆區(qū).在堆區(qū)中,JVM為每個(gè)對象分配的內(nèi)存空間大小并不確定,所以這部分存在垃圾回收問題.

現(xiàn)在我們得知堆區(qū)中存在垃圾回收問題,可是如何確定堆區(qū)中哪些對象是有用的,哪些對象是垃圾,這就又涉及到了垃圾檢測問題.一般垃圾檢測有以下方法:

1.引用計(jì)數(shù)器:為每一個(gè)對象添加一個(gè)引用計(jì)數(shù)器,當(dāng)有地方引用該對象時(shí),這個(gè)計(jì)數(shù)器+1,當(dāng)引用失效是則-1,當(dāng)計(jì)數(shù)器為0時(shí)則視該對象為垃圾對象.但這種檢測方式存在問題,那就是兩個(gè)對象互相訪問,計(jì)數(shù)器不會為0,但實(shí)際上這兩個(gè)對象已經(jīng)無法訪問,導(dǎo)致垃圾對象無法正常回收.

2可達(dá)性分析算法:針對于上述垃圾檢測機(jī)制的問題,所以還有另一種檢測方式.以根集對象(這里講的根集對象,一般指的是虛擬機(jī)棧中引用的對象,方法區(qū)常量池引用的對象,本地方法中引用的對象)為起始點(diǎn)進(jìn)行搜索,如果發(fā)現(xiàn)有對象不可達(dá)的話,即視為垃圾對象.一般在JVM進(jìn)行垃圾回收的時(shí)候,會檢索堆中的所有對象是否會被這些根集對象引用,如果發(fā)現(xiàn)不能被引用的對象,則該對象就會被垃圾回收器進(jìn)行回收.

那么垃圾對象被檢測到了,如何處理這些垃圾對象也是一門學(xué)問,這里就涉及到了一些回收算法.

1.標(biāo)記-清除算法(Mark-sweep):先標(biāo)記,后清除,標(biāo)記所有需要進(jìn)行回收的垃圾對象,然后進(jìn)行統(tǒng)一回收,這是最基礎(chǔ)的一種回收算法,后續(xù)的復(fù)制算法和標(biāo)記整理算法都基于標(biāo)記清除算法.但是該算法的缺點(diǎn)非常明顯,就是效率低,且容易造成大量內(nèi)存碎片.

img

2.復(fù)制算法(Copying):此算法將內(nèi)存空間劃分為兩個(gè)相等的區(qū)域,當(dāng)進(jìn)行垃圾回收時(shí),通過遍歷將正在使用的對象復(fù)制到另一個(gè)區(qū)域,然后回收舊區(qū)域的垃圾對象.該算法的優(yōu)點(diǎn)在于復(fù)制的成本降低,且復(fù)制過去之后還可以進(jìn)行相應(yīng)的內(nèi)存整理,不會出現(xiàn)內(nèi)存碎片問題,但缺點(diǎn)也是非常明顯的,就是需要兩倍的內(nèi)存空間做支撐.

img

3.標(biāo)記-整理算法(Mark-Compact):此算法結(jié)合了標(biāo)記-清除算法和復(fù)制算法的優(yōu)點(diǎn),從根節(jié)點(diǎn)開始標(biāo)記所有被引用的的對象,然后遍歷整個(gè)堆,把存活的對象壓縮到一塊,按順序排放.這樣就避免了內(nèi)存碎片問題和空間浪費(fèi)問題.

img

以上介紹了3種垃圾回收算法,但是JVM在進(jìn)行垃圾回收的時(shí)候必然不能只使用其中一種或幾種算法,而是采用了分代的垃圾回收策略.

為什么要采用分代的垃圾回收策略呢?顯而易見,java中不同的對象具有不同的生命周期,不能簡單的一刀切,根據(jù)對象的生命周期不同采用不同的垃圾回收方式,可以提高回收效率,降低開銷.舉個(gè)例子,在一個(gè)java程序運(yùn)行過程中,一定很產(chǎn)生很多的對象,每個(gè)對象的作用不同,如生命周期較長的HttpSession(一次會話)對象,又比如生命周期較短的StringBuffer(字符串)對象,如果每次進(jìn)行垃圾回收都對整個(gè)堆空間進(jìn)行掃描檢測,可以想象系統(tǒng)會承擔(dān)非常大的開銷,因此將java對象進(jìn)行分代垃圾回收,是非常必要的策略.

那么問題來了,究竟是如何分代的呢?按照生命周期不同,將對象劃分為如下三代:

年輕代(Young):

在年輕代中,又劃分為伊甸園區(qū)(Eden),幸存0區(qū)(Survivor0)幸存1區(qū)(Survivor1).所有對象最初被創(chuàng)建的時(shí)候都會在Eden區(qū),當(dāng)Eden區(qū)被填滿時(shí)會進(jìn)行Minor GC,如果還有對象存活,那么就會被JVM轉(zhuǎn)移到幸存區(qū)0區(qū)或幸存1區(qū).一般地,幸存0區(qū)和幸存1區(qū)中總有一個(gè)是空的,當(dāng)其中一個(gè)區(qū)被填滿時(shí)就會再進(jìn)行Minor GC,就這樣還能存活的對象就會在幸存0區(qū)和幸存1區(qū)之間來回切換.在幸存區(qū)經(jīng)過很多次GC后依然還能存活的對象會被轉(zhuǎn)移到年老代(一般需要設(shè)定一個(gè)存活閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 來設(shè)置),當(dāng)超過這個(gè)臨界值時(shí),就將還依舊存活的對象轉(zhuǎn)移到年老代當(dāng)中.

年老代(Old):

處于該代的java對象都是在年輕代久經(jīng)考驗(yàn)而存活的對象,一般都是生命周期較長的對象.當(dāng)年老代的空間被占滿時(shí),則不會進(jìn)行Minor GC,而會觸發(fā)Major GC/Full GC,回收整個(gè)堆內(nèi)存.

注意:在年輕代進(jìn)行的都是Minor GC

幸存0區(qū)和幸存1區(qū)是對稱的,不存在先后關(guān)系

關(guān)于Minor GC,Major GC,Full GC的不同:

新生代 GC(Minor GC):指發(fā)生在新生代的垃圾收集動作,因?yàn)?Java 對象大多都具
   備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,所有的Minor GC都會觸發(fā)全世界的暫停(stop-the-world),停止應(yīng)用程序的線程,不過這個(gè)過程非常短暫。
   老年代 GC(Major GC ):指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC,經(jīng)常會伴隨至少一次的 Minor GC.MajorGC 的速度一般會比 Minor GC慢10倍以上。FULL GC:對整個(gè)堆空間進(jìn)行一次GC,系統(tǒng)開銷非常大,很有可能影響程序正常運(yùn)行.

持久代(Perm):

一般存放Class、Method等元信息,與java對象的垃圾回收關(guān)系不大,一般情況下128M就夠了.

值得一提的是剛才提到了垃圾回收算法在分代垃圾回收策略中也被采用:

年輕代:復(fù)制算法

年老代:標(biāo)記-整理算法

關(guān)于JVM內(nèi)存參數(shù)設(shè)置:

img

如上圖所示是關(guān)于JVM內(nèi)存參數(shù)調(diào)優(yōu)的設(shè)置區(qū)間,這種參數(shù)設(shè)置一般不是固定的,需要根據(jù)生產(chǎn)環(huán)境的情況設(shè)定.如下為相關(guān)參數(shù)解釋:

img
img
img

如下圖是某個(gè)測試環(huán)境下某應(yīng)用服務(wù)器的JVM參數(shù)配置(僅舉例,不可用于真實(shí)生產(chǎn)環(huán)境.)

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

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

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