Java8的GC垃圾回收


Java垃圾回收概況

Java GC(Garbage Collection,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼,對內存泄露和溢出的問題,也不需要像C程序員那樣戰戰兢兢。這是因為在Java虛擬機中,存在自動內存管理和垃圾清掃機制。概括地說,該機制對JVM中的內存進行標記,並確定哪些內存需要回收,根據一定的回收策略,自動的回收內存,永不停息的保證JVM中的內存空間,防止出現內存泄露和溢出問題。

關於JVM,需要說明一下的是,目前使用最多的Sun公司的JDK中,自從1999年的JDK1.2開始直至現在仍在廣泛使用的JDK6,其中默認的虛擬機都是HotSpot。2009年,Oracle收購Sun,加上之前收購的EBA公司,Oracle擁有3大虛擬機中的兩個:JRockit和HotSpot,Oracle也表明了想要整合兩大虛擬機的意圖,但是目前在新發布的JDK8中,默認的虛擬機仍然是HotSpot,因此本文中默認介紹的虛擬機都是HotSpot,相關機制也主要是指HotSpot的GC機制。

Java GC機制主要完成3件事:

確定哪些內存需要回收
確定什么時候需要執行GC
如何執行GC

經過這么長時間的發展,Java GC機制已經日臻完善,幾乎可以自動的為我們做絕大多數的事情。然而,如果我們從事較大型的應用軟件開發,曾經出現過內存優化的需求,就必定要研究Java GC機制。

學習Java GC機制,可以幫助我們在日常工作中排查各種內存溢出或泄露問題,解決性能瓶頸,達到更高的並發量,寫出更高效的程序。

我們將從4個方面學習Java GC機制,1,內存是如何分配的;2,如何保證內存不被錯誤回收(即:哪些內存需要回收);3,在什么情況下執行GC以及執行GC的方式;4,如何監控和優化GC機制。

內存是如何分配的

這里所說的內存分配,主要指的是在堆上的分配,一般的,對象的內存分配都是在堆上進行,但現代技術也支持將對象拆成標量類型(標量類型即原子類型,表示單個值,可以是基本類型或String等),然后在棧上分配,在棧上分配的很少見,我們這里不考慮,接下來我們一起來了解下內存分區,對我們后面學習的有所幫助。

 
內存分區

 

1、一個人(對象)出來(new 出來)后會在Eden Space(新生區)無憂無慮的生活,直到GC到來打破了他們平靜的生活。GC會逐一問清楚每個對象的情況,有沒有錢(此對象的引用)啊,因為GC想賺錢呀,有錢的才可以敲詐嘛。然后富人就會進入Survivor Space(幸存者區),窮人的就直接kill掉。

2、並不是進入Survivor Space(幸存者區)后就保證人身是安全的,但至少可以活段時間。GC會定期(可以自定義)會對這些人進行敲詐,億萬富翁每次都給錢,GC很滿意,就讓其進入了Old Gen(老年區)。萬元戶經不住幾次敲詐就沒錢了,GC看沒有啥價值啦,就直接kill掉了。

3、進入到老年區的人基本就可以保證人身安全啦,但是億萬富豪有的也會揮霍成窮光蛋,只要錢沒了,GC還是kill掉。

分區的目的:新生區由於對象產生的比較多並且大都是朝生夕滅的,所以直接采用標記-清理算法。而養老區生命力很強,則采用復制算法,針對不同情況使用不同算法。

在前面的文章里介紹了內存分區情況,接下來我們就來說下垃圾回收的相關算法。對於GC的回收算法是門相當深的學問,我們在這里就先從最簡單的入手開始學習,慢慢的由淺入深。

最基礎的收集算法 — 標記/清除算法

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

標記階段:標記的過程其實就是檢查哪些對象能被外界訪問的可達性分析算法的過程,遍歷所有的GC Roots對象,對從GC Roots對象可達的對象都打上一個標識,一般是在對象的header中,將其記錄為可達對象。


 
標記階段

清除階段:清除的過程是對堆內存進行遍歷,如果發現某個對象沒有被標記為可達對象(通過讀取對象header信息),則將其回收。


 
清除階段

上面兩張圖是標記/清除算法的示意圖,在標記階段,從對象1可以訪問到B對象,從B對象又可以訪問到E對象,因此從GC 對象1到B、E都是可達的,同理,對象F、G、J、K都是可達對象;到了清除階段,所有不可達對象都會被回收。

在垃圾收集器進行GC時,必須停止所有Java執行線程(也稱"Stop The World"),原因是在標記階段進行可達性分析時,不可以出現分析過程中對象引用關系還在不斷變化的情況,否則的話可達性分析結果的准確性就無法得到保證。在等待標記清除結束后,應用線程才會恢復運行。

標記/清除算法存在問題:

1、效率問題。標記和清除兩個階段的效率都不高,因為這兩個階段都需要遍歷內存中的對象,很多時候內存中的對象實例數量是非常龐大的,這無疑很耗費時間,而且GC時需要停止應用程序,這會導致非常差的用戶體驗。

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

既然標記/清除算法有這么多的缺點,那它還有存在的意義嗎?別急,一個算法有缺陷,人們肯定會想辦法去完善它,接下來的兩個算法就是在標記/清除算法的基礎上完善而來的。

復制算法

為了解決效率問題,復制算法出現了。復制算法的原理是:將可用內存按容量划分為大小相等的兩塊,每次使用其中的一塊。當這一塊的內存用完了,就將還存活的對象復制到另一塊內存上,然后把這一塊內存所有的對象一次性清理掉。
回收前示意圖:


 
回收前

回收后示意圖:


 
回收后

復制算法存在問題:

復制算法每次都是對整個半區進行內存回收,這樣就減少了標記對象遍歷的時間,在清除使用區域對象時,不用進行遍歷,直接清空整個區域內存,而且在將存活對象復制到保留區域時也是按地址順序存儲的,這樣就解決了內存碎片的問題,在分配對象內存時不用考慮內存碎片等復雜問題,只需要按順序分配內存即可。

復制算法簡單高效,優化了標記/清除算法的效率低、內存碎片多的問題。但是它的缺點也很明顯:

1、將內存縮小為原來的一半,浪費了一半的內存空間,代價太高;

2、如果對象的存活率很高,極端一點的情況假設對象存活率為100%,那么我們需要將所有存活的對象復制一遍,耗費的時間代價也是不可忽視的。

標記/整理算法

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


 
回收前

回收后示意圖:


 
回收后

參考資料:
《深入理解Java虛擬機》



作者:叩丁狼教育
鏈接:https://www.jianshu.com/p/826c30b48c80

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM