溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java垃圾收集器與內(nèi)存分配的方法是什么

發(fā)布時間:2022-03-17 15:52:21 來源:億速云 閱讀:140 作者:iii 欄目:大數(shù)據(jù)

今天小編給大家分享一下Java垃圾收集器與內(nèi)存分配的方法是什么的相關(guān)知識點,內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

Java內(nèi)存運行時區(qū)域的各個部分,其中程序計數(shù)器、虛擬機棧、本地方法棧3個區(qū)域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的(盡管在運行期會由即時編譯器進行一些優(yōu)化,但在基于概念模型的討論里,大體上可以認(rèn)為是編譯期可知的),因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個區(qū)域內(nèi)就不需要過多考慮如何回收的問題,當(dāng)方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著

回收了。

引用計數(shù)算法

在對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加一;當(dāng)引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為零的對象就是不可能再被使用的。

單純的引用計數(shù)就很難解決對象之間相互循環(huán)引用的問題。

可達(dá)性分析算法

這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達(dá)時,則證明此對象是不可能再被使用的。

為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2版之后提供了PhantomReference類來實現(xiàn)虛引用。

虛引用場景?

即使在可達(dá)性分析算法中判定為不可達(dá)的對象,也不是“非死不可”的,這時候它們暫時還處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標(biāo)記,隨后進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,那么虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。

如果這個對象被判定為確有必要執(zhí)行finalize()方法,那么該對象將會被放置在一個名為F-Queue的隊列之中,并在稍后由一條由虛擬機自動建立的、低調(diào)度優(yōu)先級的Finalizer線程去執(zhí)行它們的finalize()方法。

這樣做的原因是,如果某個對象的finalize()方法執(zhí)行緩慢,或者更極端地發(fā)生了死循環(huán),將很可能導(dǎo)致F-Queue隊列中的其他對象永久處于等待,甚至導(dǎo)致整個內(nèi)存回收子系統(tǒng)的崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后收集器將對F-Queue中的對象進行第二次小規(guī)模的標(biāo)記,如果對

象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標(biāo)記時它將被移出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。

這是因為任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執(zhí)行,因此第二段代碼的自救行動失敗了。

finalize()它的運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序,如今已被官方明確聲明為不推薦使用的語法。有些教材中描述它適合做“關(guān)閉外部資源”之類的清理性工作,這完全是對finalize()方法用途的一種自我安慰。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆者建議大家完全可以忘掉Java語言里面的這個方法。

回收方法區(qū)

《Java虛擬機規(guī)范》中提到過可以不要求虛擬機在方法區(qū)中實現(xiàn)垃圾收集,事實上也確實有未實現(xiàn)或未能完整實現(xiàn)方法區(qū)類型卸載的收集器存在(如JDK 11時期的ZGC收集器就不支持類卸載)

方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型?;厥諒U棄常量與回收J(rèn)ava堆中的對象非常類似。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經(jīng)進入常量池中,但是當(dāng)前系統(tǒng)又沒有任何一個字符串對象的值是“java”,換句話說,已經(jīng)沒有任何字符串對象引用常量池中的“java”常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統(tǒng)清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。

判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:

·該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。

·加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設(shè)計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達(dá)成的。

·該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和對象一樣,沒有引用了就必然會回收。關(guān)于是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版 [1] 的虛擬機支持。

在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架,動態(tài)生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區(qū)造成過大的內(nèi)存壓力。

垃圾收集算法

從如何判定對象消亡的角度出發(fā),垃圾收集算法可以劃分為“引用計數(shù)式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”。

分代收集理論

1)弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的。

2)強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。

3)跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對于同代引用來說僅占極少數(shù)。

這其實是可根據(jù)前兩條假說邏輯推理得出的隱含推論:存在互相引用關(guān)系的兩個對象,是應(yīng)該傾向于同時生存或者同時消亡的。舉個例子,如果某個新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會使得新生代對象在收集時同樣得以存活,進而在年齡增長之后晉升到老年代中,這時跨代引用也隨即被消除了。

分代收集并非只是簡單劃分一下內(nèi)存區(qū)域那么容易,它至少存在一個明顯的困難:對象不是孤立的,對象之間會存在跨代引用。

假如要現(xiàn)在進行一次只局限于新生代區(qū)域內(nèi)的收集(Minor GC),但新生代中的對象是完全有可能被老年代所引用的,為了找出該區(qū)域中的存活對象,不得不在固定的GC Roots之外,再額外遍歷整個老年代中所有對象來確??蛇_(dá)性分析結(jié)果的正確性,反過來也是一樣 [3] 。遍歷整個老年代所有對象的方案雖然理論上可行,但無疑會為內(nèi)存回收帶來很大的性能負(fù)擔(dān)。為了解決這個問題,就需要對分代收集理論添加第三條經(jīng)驗法則: 跨代引用假說。

依據(jù)這條假說,我們就不應(yīng)再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱為“記憶集”,Remembered Set),這個結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識出老年代的哪一塊內(nèi)存會存在跨代引用。此后當(dāng)發(fā)生Minor GC時,只有包含了跨代引用的小塊內(nèi)存里的對象才會被加入到GCRoots進行掃描。雖然這種方法需要在對象改變引用關(guān)系(如將自己或者某個屬性賦值)時維護記錄數(shù)據(jù)的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。

部分收集(Partial GC  怕羞):指目標(biāo)不是完整收集整個Java堆的垃圾收集,其中又分為:

■新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。

■老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。實際上除了CMS收集器,其他都不存在只針對老年代的收集。

■混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。

■整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集。

標(biāo)記-清除算法

它的主要缺點有兩個:第一個是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標(biāo)記和清除的動作,導(dǎo)致標(biāo)記和清除兩個過程的執(zhí)行效率都隨對象數(shù)量增長而降低;第二個是內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致當(dāng)以后在程序運行過程中需要分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

標(biāo)記-復(fù)制算法

為了解決標(biāo)記-清除算法面對大量可回收對象時執(zhí)行效率低的問題。現(xiàn)在的商用Java虛擬機大多都優(yōu)先采用了這種收集算法去回收新生代。

它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對象都是存活的,這種算法將會產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對于多數(shù)對象都是可回收的情況,算法需要復(fù)制的就是占少數(shù)的存活對象,而且每次都是針對整個半?yún)^(qū)進行內(nèi)存回收,分配內(nèi)存時也就不用考慮有空間碎片的復(fù)雜情況,只要移動堆頂指針,按順序分配即可。

這種復(fù)制回收算法的代價是將可用內(nèi)存縮小為了原來的一半,空間浪費未免太多了一點。

Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對象時,就需要依賴其他內(nèi)存區(qū)域(實際上大多就是老年代)進行分配擔(dān)保(Handle Promotion)。

標(biāo)記-整理算法

針對老年代對象的存亡特征,1974年Edward Lueders提出了另外一種有針對性的“標(biāo)記-整理”(Mark-Compact)算法,其中的標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi)存。

標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動式的回收算法,而后者是移動式的。是否移動回收后的存活對象是一項優(yōu)缺點并存的風(fēng)險決策:

如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負(fù)重的操作,而且這種對象移動操作必須全程暫停用戶應(yīng)用程序才能進行 ,這就更加讓使用者不得不小心翼翼地權(quán)衡其弊端了,像這樣的停頓被最初的虛擬機設(shè)計者形象地描述為“Stop The World”。

[1] 最新的ZGC和Shenandoah收集器使用讀屏障(Read Barrier)技術(shù)實現(xiàn)了整理過程與用戶線程的并發(fā)執(zhí)行,稍后將會介紹這種收集器的工作原理。

[2] 通常標(biāo)記-清除算法也是需要停頓用戶線程來標(biāo)記、清理可回收對象的,只是停頓時間相對而言要來的短而已。

但如果跟標(biāo)記-清除算法那樣完全不考慮移動和整理存活對象的話,彌散于堆中的存活對象導(dǎo)致的空間碎片化問題就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來解決。譬如通過“分區(qū)空閑分配鏈表”來解決內(nèi)存分配問題(計算機硬盤存儲大文件就不要求物理連續(xù)的磁盤空間,能夠在碎片化的硬盤上存儲和訪問就是通過硬盤分區(qū)表實現(xiàn)的)。內(nèi)存的訪問是用戶程序最頻繁的操作,甚至都沒有之一,假如在這個環(huán)節(jié)上增加了額外的負(fù)擔(dān),勢必會直接影響應(yīng)用程序的吞吐量。

基于以上兩點,是否移動對象都存在弊端,移動則內(nèi)存回收時會更復(fù)雜,不移動則內(nèi)存分配時會更復(fù)雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量來看,移動對象會更劃算。此語境中,吞吐量的實質(zhì)是賦值器(Mutator,可以理解為使用垃圾收集的用戶程序,本書為便于理解,多數(shù)地方用“用戶程序”或“用戶線程”代替)與收集器的效率總和。

HotSpot虛擬機里面關(guān)注吞吐量的Parallel  Scavenge收集器是基于標(biāo)記-整理算法的,而關(guān)注延遲的CMS收集器則是基于標(biāo)記-清除算法的,這也從側(cè)面印證這點。

另外,還有一種“和稀泥式”解決方案可以不在內(nèi)存分配和訪問上增加太大額外負(fù)擔(dān),做法是讓虛擬機平時多數(shù)時間都采用標(biāo)記-清除算法,暫時容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng)大到影響對象分配時,再采用標(biāo)記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。前面提到的基于標(biāo)記-清除算法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法。

以上就是“Java垃圾收集器與內(nèi)存分配的方法是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI