前言
從上面這個(gè)圖我們總體上對(duì)JVM的結(jié)構(gòu)特別是內(nèi)存結(jié)構(gòu)有了比較清晰的認(rèn)識(shí)瓜浸,雖然在JDK1.8+的版本中拂酣,JVM內(nèi)存管理結(jié)構(gòu)有了一定的優(yōu)化調(diào)整秋冰。主要是方法區(qū)(持久代)取消變成了直接使用元數(shù)據(jù)區(qū)(直接內(nèi)存)的方式,但是整體上JVM的結(jié)構(gòu)并沒(méi)有大改婶熬,特別是我們最為關(guān)心的堆內(nèi)存管理方式并沒(méi)有在JDK1.8+的版本中有什么變化剑勾,所以圖中的結(jié)構(gòu)整體上是沒(méi)有什么不準(zhǔn)確的,之所以將方法區(qū)以及持久代標(biāo)注出來(lái)赵颅,主要還是為了起到對(duì)比認(rèn)識(shí)的作用虽另,大家知道就可以了。
關(guān)于持久代元數(shù)據(jù)區(qū)的使用問(wèn)題饺谬,目前可以理解就是使用的物理內(nèi)存捂刺,理論上是不受JVM自動(dòng)內(nèi)存回收機(jī)制管理的,如果不設(shè)置參數(shù)大小默認(rèn)最大使用限制就是操作系統(tǒng)可用物理內(nèi)存的大小募寨,設(shè)置了-XX:MetaspaceSize參數(shù)的話族展,JVM就會(huì)在使用物理內(nèi)存空間時(shí)自己進(jìn)行限制。
至于直接內(nèi)存與物理內(nèi)存到底是不是一回事拔鹰,我認(rèn)為對(duì)于我們理解上沒(méi)有區(qū)別仪缸,只是概念的區(qū)別,另外就是對(duì)這塊內(nèi)存使用細(xì)節(jié)上的區(qū)別列肢,如果不受JVM的自動(dòng)回收管理恰画,那么怎么管理呢?說(shuō)到底還是JVM本身在直接使用物理內(nèi)存或者說(shuō)是直接內(nèi)存(用時(shí)直接“malloc”物理內(nèi)存區(qū)域瓷马,而不再是JVM進(jìn)程啟動(dòng)時(shí)初始化的內(nèi)存區(qū)域)拴还,還有一種概念叫native memory,說(shuō)實(shí)話我暫時(shí)還不理解他們到底有啥區(qū)別决采,如果大家對(duì)這些概念有更好的認(rèn)識(shí)自沧,也可以給我留言哦!之所以對(duì)這幾個(gè)問(wèn)題做一些筆墨的說(shuō)明树瞭,主要是在之前的文章中大家對(duì)此提出了疑問(wèn)拇厢,所以正好在這節(jié)的內(nèi)容中進(jìn)行下闡述。
回到今天的主題晒喷,我們知道JAVA最大的優(yōu)點(diǎn)就是可以實(shí)現(xiàn)自動(dòng)內(nèi)存管理孝偎,這極大的便利了JAVA程序員,降低了使用成本凉敲。但這也使得平時(shí)我們?cè)谑褂肑AVA編程時(shí)不太關(guān)注JVM到底是怎樣進(jìn)行內(nèi)存回收的衣盾,只有在需要實(shí)際對(duì)JVM進(jìn)行系統(tǒng)性能調(diào)優(yōu)寺旺,這里的場(chǎng)景可能是在系統(tǒng)面臨極致性能優(yōu)化要求時(shí),我們才發(fā)現(xiàn)需要對(duì)JAVA的整體內(nèi)存結(jié)構(gòu)以及內(nèi)存回收機(jī)制要有一定的認(rèn)識(shí)和了解才行势决。
從上面的圖中阻塑,我們也大致對(duì)整個(gè)垃圾回收系統(tǒng)進(jìn)行了標(biāo)注,這里主要涉及回收策略果复、回收算法陈莽、垃圾回收器這幾個(gè)部分。形象一點(diǎn)表述虽抄,就是JVM需要知道那些內(nèi)存可以被回收走搁,要有一套識(shí)別機(jī)制,在知道那些內(nèi)存可以回收以后具體采用什么樣的回收方式迈窟,這就需要設(shè)計(jì)一些回收算法私植,而具體的垃圾回收器就是根據(jù)不同內(nèi)存區(qū)域的使用特點(diǎn),采用相應(yīng)地回收策略和算法的具體實(shí)現(xiàn)了车酣。
在??圖中曲稼,我們也標(biāo)注了不同垃圾回收器所適用的特定內(nèi)存區(qū)域,對(duì)于JVM垃圾回收這塊的優(yōu)化湖员,就是我們需要在了解這些垃圾回收算法躯肌、垃圾回收器特點(diǎn)后能夠根據(jù)自己應(yīng)用的場(chǎng)景選擇合適的垃圾收集器,以及各區(qū)域垃圾收集器的搭配關(guān)系破衔。下面我們就從這幾個(gè)方面給大家介紹,JVM的垃圾回收相關(guān)的知識(shí)點(diǎn)钱烟。
回收策略
我們知道晰筛,JVM進(jìn)行內(nèi)存回收的主要目的是為了回收不再使用的內(nèi)存,因?yàn)樵谶M(jìn)行JAVA程序編寫時(shí)拴袭,我們只有new的操作,而不需要收工釋放不再使用的空間读第,如果這些空閑內(nèi)存不能及時(shí)被回收,很快我們的JVM內(nèi)存空間就會(huì)泄露(新申請(qǐng)內(nèi)存空間的操作失敗拥刻,導(dǎo)致程序報(bào)錯(cuò))怜瞒,所以回收不再使用的內(nèi)存的目的則是為了及時(shí)釋放空間,騰籠換鳥般哼,以防止內(nèi)存泄漏吴汪。
那么問(wèn)題來(lái)了,JAVA程序申請(qǐng)了那么多的內(nèi)存空間蒸眠,那些內(nèi)存才能被認(rèn)定是不再使用的內(nèi)存呢漾橙?搞錯(cuò)了,如果把正在被程序使用的內(nèi)存給釋放了楞卡,程序邏輯就空指針異常了霜运!
我們知道在JVM中內(nèi)存分配的基本粒度主要是對(duì)象脾歇、基本類型。而基本類型的使用主要是包括在對(duì)象中的局部變量淘捡,所以回收對(duì)象所占用的內(nèi)存是JAVA垃圾回收的主要目標(biāo)藕各。
那么如何判斷對(duì)象是處于可回收狀態(tài)的呢?在主流的JVM中是采用“可達(dá)性分析算法”來(lái)進(jìn)行判斷的焦除。
這個(gè)算法的基本思路就是通過(guò)一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn)激况,并從這些節(jié)點(diǎn)開始往下進(jìn)行搜索,搜索走過(guò)的路徑我們稱之為引用鏈(Reference Chain)踢京,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)誉碴,我們就稱之為對(duì)象引用不可達(dá),則證明這個(gè)對(duì)象是不可用的瓣距,就可以暫時(shí)判定這個(gè)對(duì)象為可回收對(duì)象黔帕。示意圖如下:
在圖中雖然Obj F與Obj J之間互相有關(guān)聯(lián)但是它們到GC Roots是不可達(dá)的,所以將會(huì)被判定為可回收對(duì)象蹈丸。既然如此成黄,什么樣的對(duì)象可以作為GC Roots對(duì)象呢?
在JAVA中可以被作為GC Roots的對(duì)象主要是:虛擬機(jī)棧-棧幀中的本地變量表所引用的對(duì)象逻杖、方法區(qū)(<JDK1.8)中類靜態(tài)屬性所引用的對(duì)象/常量屬性所引用的對(duì)象奋岁、本地方法棧中引用的對(duì)象。
這里還需要注意一個(gè)小的細(xì)節(jié)荸百,就是被判定為對(duì)象不可達(dá)的對(duì)象也并非會(huì)被立刻回收闻伶,在學(xué)習(xí)JAVA語(yǔ)法是我們應(yīng)該學(xué)習(xí)過(guò)finalize()方法,如果對(duì)象重寫了finalize方法够话,并重新把this關(guān)鍵字賦值給了某個(gè)類變量或?qū)ο蟮某蓡T變量的話蓝翰,該對(duì)象就會(huì)被"救活",具體過(guò)程可參考上圖所示女嘲,只是這種方式并不鼓勵(lì)大家使用畜份,了解下就行。
在關(guān)于如何判定對(duì)象是否屬于不再使用的內(nèi)存時(shí)欣尼,還有個(gè)通常會(huì)被大家錯(cuò)誤認(rèn)為是JVM使用的方式-“引用計(jì)數(shù)法”爆雹,事實(shí)上引用計(jì)數(shù)法的實(shí)現(xiàn)比較簡(jiǎn)單,判定效率也比較高愕鼓,在Python語(yǔ)言中就使用了這種算法進(jìn)行內(nèi)存管理钙态,但是它有一個(gè)比較難解決的對(duì)象之間循環(huán)引用的問(wèn)題,所以在JAVA虛擬機(jī)里并沒(méi)有選用“引用計(jì)數(shù)法”來(lái)管理內(nèi)存菇晃。這個(gè)問(wèn)題很多人都會(huì)搞錯(cuò)驯绎,包括有很多年開發(fā)經(jīng)驗(yàn)的程序員,需要大家注意下谋旦!
回收算法
在JVM中主要的垃圾收集算法有:標(biāo)記-清除剩失、標(biāo)記-清除-壓縮(簡(jiǎn)稱“標(biāo)記-整理”)屈尼、標(biāo)記-復(fù)制-清除(簡(jiǎn)稱“復(fù)制”)、分代收集算法拴孤。這幾種收集算法互相配合脾歧,針對(duì)不同的內(nèi)存區(qū)域采取對(duì)應(yīng)的收集算法實(shí)現(xiàn)(這里具體是由相應(yīng)的垃圾收集器實(shí)現(xiàn))。
下面我們就分別來(lái)看下這幾種收集算法的特點(diǎn):
1)演熟、標(biāo)記-清除
標(biāo)記-清除算法是最為基礎(chǔ)的一種收集算法鞭执,算法分為:“標(biāo)記”和“清除”兩個(gè)階段。首先標(biāo)記出所有需要回收的對(duì)象(標(biāo)記的過(guò)程就是上面介紹過(guò)的根節(jié)點(diǎn)可達(dá)算法)芒粹,在標(biāo)記完后統(tǒng)一回收所有被標(biāo)記對(duì)象占用的內(nèi)存空間兄纺。
示意圖如下:
這種收集算法的優(yōu)點(diǎn)是簡(jiǎn)單直接,不會(huì)影響JVM進(jìn)程的正常運(yùn)行化漆。而其缺點(diǎn)也是非常明顯估脆,首先,這樣的回收方式會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片座云,不利于后續(xù)連續(xù)內(nèi)存的分配疙赠;其次,這種方式的效率也不高朦拖。
2)圃阳、標(biāo)記-復(fù)制-清除
這種算法的思路是將可用的內(nèi)存空間按容量劃分為大小相等的兩塊,每次只使用其中一塊璧帝。當(dāng)這一塊使用完了捍岳,就將還存活著的對(duì)象復(fù)制到另外一塊上面(移動(dòng)堆頂指針,按順序分配內(nèi)存)睬隶,然后再把已使用過(guò)的內(nèi)存空間一次清理掉祟同。
示意圖如下:
這種收集方式比較好的解決了效率和內(nèi)存碎片的問(wèn)題,但是會(huì)浪費(fèi)掉一般的內(nèi)存空間理疙。目前此種算法主要用于新生代回收(文頂?shù)膱D中有標(biāo)注)。
因?yàn)樾律闹?8%的對(duì)象都是很快就需要被回收的對(duì)象泞坦,這一點(diǎn)大家在編程時(shí)可以體會(huì)到窖贤,所以并不需要1:1的比例來(lái)劃分內(nèi)存空間,在新生代中JVM是按照“8:1:1”的比例(文頂圖中有標(biāo)注)來(lái)將整個(gè)新生代內(nèi)存劃分為一塊較大的Eden區(qū)和兩塊較小的Survivor區(qū)(S0贰锁、S1)赃梧。
每次使用Eden區(qū)和其中一個(gè)Survivor區(qū),當(dāng)發(fā)生回收時(shí)將Eden區(qū)和Survivor區(qū)中還存活的對(duì)象一次性復(fù)制到另一塊Survivor區(qū)上豌熄,最后清理掉Eden區(qū)和剛才使用過(guò)的Survivor區(qū)授嘀。理想情況下,每次新生代中的可用空間是整個(gè)新生代容量的90%(80%+10%)锣险,只會(huì)有10%的內(nèi)存會(huì)被浪費(fèi)蹄皱。實(shí)際情況中览闰,如果另外一個(gè)10%的Survivor區(qū)無(wú)法裝下所有還存活的對(duì)象時(shí),就會(huì)將這些對(duì)象直接放入老年代空間中(這塊在后面的分代回收算法會(huì)說(shuō)到巷折,這里先了解下)压鉴。
3)、標(biāo)記-清除-壓縮
如果在對(duì)象存活率較高的情況下锻拘,仍然采用復(fù)制算法的話油吭,因?yàn)橐M(jìn)行較多的復(fù)制操作,效率就會(huì)變得很低署拟,而且如果不想浪費(fèi)50%的內(nèi)存空間的話婉宰,就還需要額外的空間進(jìn)行分配擔(dān)保****,以應(yīng)對(duì)存活對(duì)象超額的情況推穷。顯然老年代不能采用2)中的復(fù)制算法心包。
根據(jù)老年代的特點(diǎn),標(biāo)記-清除-壓縮(簡(jiǎn)稱標(biāo)記-整理)算法應(yīng)運(yùn)而生缨恒,這種算法的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣谴咸,只是后續(xù)的步驟不再是直接清除可以回收的對(duì)象,而是將所有存活的對(duì)象都向一端移動(dòng)后骗露,再直接清理掉端邊界以外的內(nèi)存岭佳。
示意圖如下:
4)、分代回收算法
實(shí)際上在講解復(fù)制算法時(shí)已經(jīng)涉及到了分代回收的內(nèi)容萧锉,這種算法根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊珊随,Java中主要是新生代、年老代柿隙。這樣就可以根據(jù)各個(gè)年代的特點(diǎn)叶洞,采用合適的收集算法了,在文頂?shù)膱D中已經(jīng)標(biāo)示禀崖,新生代采用了復(fù)制算法衩辟,而老年代采用了整理算法,這里就不再贅述波附。
垃圾回收器
關(guān)于垃圾回收器部分的內(nèi)容艺晴,由于篇幅的關(guān)系會(huì)在后續(xù)的《一張圖讓你看懂JVM之垃圾回收器詳解》一文中進(jìn)行講解,敬請(qǐng)關(guān)注掸屡!