為什么需要垃圾收集
在回答這個(gè)問題之前,可以先比較目前最流行的兩款面向?qū)ο蟮恼Z言 JAVA 和 C++。JAVA 是帶垃圾收集功能的,而 C++ 是沒有垃圾收集功能的。其本質(zhì)原因就是在于 JAVA 是一門使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語言,因此對(duì)象的創(chuàng)建和回收都不需要程序員手動(dòng)操作,只需要交給虛擬機(jī)即可,因此由于語言本身的特點(diǎn)所以需要專門的垃圾收集器來回收“已死”的對(duì)象。而 C++ 的內(nèi)存是需要程序員手動(dòng)分配和釋放的,因此不需要進(jìn)行專門的垃圾收集操作。由于 C++ 需要手動(dòng)分配和釋放內(nèi)存因此若是釋放不及時(shí)很容易出現(xiàn)內(nèi)存泄露的問題,JAVA 是交給專門的收集器因此極少出現(xiàn)內(nèi)存泄露問題。由于有了垃圾收集器的參與 JAVA 在效率上就會(huì)比 C++ 差,但是就避免了區(qū)分配和釋放內(nèi)存的麻煩,有得就必有失。
垃圾收集需要關(guān)注的三個(gè)問題
哪些內(nèi)存需要回收
籠統(tǒng)的來講 JAVA 里面的內(nèi)存分為堆內(nèi)存和棧內(nèi)存,由于棧是伴隨著線程而存在的,線程執(zhí)行結(jié)束棧內(nèi)存就應(yīng)該被回收,因此相對(duì)簡(jiǎn)單不需要太關(guān)心。堆內(nèi)存才是需要考慮的重點(diǎn),主要是堆內(nèi)存非線程私有,它是被共用的,因此它的回收會(huì)比較復(fù)雜,才是垃圾收集器需要關(guān)注的重點(diǎn)區(qū)域,這就回答了哪些內(nèi)存需要回收的問題了。
什么時(shí)候回收
現(xiàn)在知道了需要關(guān)注的回收區(qū)域是堆內(nèi)存的回收,而且也知道堆內(nèi)存上分配的都是引用類型的數(shù)據(jù),即對(duì)象和數(shù)組。也就是需要關(guān)心什么時(shí)候回收堆中的對(duì)象,于是問題就轉(zhuǎn)化成了確定堆中哪些對(duì)象是無用的“已死”對(duì)象。于是引出了兩種確定“已死”對(duì)象的方法。
引用計(jì)數(shù)法
引用計(jì)數(shù)法說白了就是把堆中對(duì)象被其他對(duì)象引用總次數(shù)記錄下來,每次被別的對(duì)象引用時(shí)計(jì)數(shù)器的數(shù)量就加 1 引用失效后數(shù)量就減 1,當(dāng)對(duì)象的引用計(jì)數(shù)為 0 時(shí)就回收該對(duì)象。從算法上來看是一個(gè)非常簡(jiǎn)單的算法,但是其有一個(gè)問題就是循環(huán)引用,當(dāng)兩個(gè)已失效的對(duì)象互相引用彼此時(shí),通過這個(gè)方法是無法把計(jì)數(shù)減到 0 的。JAVA 虛擬機(jī)也不是采用這種方式確定對(duì)象“已死”的。
可達(dá)性分析法
這種方法有一個(gè)叫 GCRoots 的概念,從名字可知就是垃圾收集的根節(jié)點(diǎn)集合。以這一系列的根節(jié)點(diǎn)為開始去遍歷其引用鏈,在引用鏈中可達(dá)的對(duì)象就當(dāng)作“存活”的對(duì)象,不可達(dá)的則為“已死”對(duì)象。從而可達(dá)性分析法就避免了計(jì)數(shù)法的問題,能正確找到“存活”的對(duì)象。這里需要關(guān)注的是哪些對(duì)象可以作為 GCRoots:
1.虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象;
2.方法區(qū)中的類靜態(tài)屬性引用的對(duì)象;
3.方法區(qū)中常量引用的對(duì)象;
4.本地方法棧中JNI(即一般說的Native方法)中引用的對(duì)象;
5.JAVA 虛擬機(jī)內(nèi)部的引用;
6.所有被同步鎖持有的對(duì)象;
7.反映 Java 虛擬機(jī)內(nèi)部情況的 JMZBean、JVMTI 中注冊(cè)的回調(diào)、本地代碼緩存等;
8.除了這些外還有跨代引用的記憶集等;
如何回收
對(duì)于“已死”對(duì)象的回收問題,這就涉及到具體的垃圾收集算法了。在討論垃圾收集算法前,有一個(gè)重要的特征需要說明。根據(jù)對(duì)象“朝生夕死”的特點(diǎn),在 G1 前的垃圾收集器都有分代的思想,也就是會(huì)把整個(gè)堆內(nèi)存分為新生代和老年代,而不同的分代會(huì)應(yīng)用不同的的垃圾收集算法進(jìn)而有不同的垃圾收集器。本次系列文章只討論 CMS、G1、shenandoah、ZGC,這幾種關(guān)注低延遲的垃圾收集器。由于分代的引入,于是出現(xiàn)了跨代引用問題,不同收集器處理跨代引用方法略有不同,都會(huì)使用記憶集這種數(shù)據(jù)結(jié)構(gòu)去解決跨代引用問題。同時(shí)也出現(xiàn)了幾種專有名詞:
MinorGC、YoungGC:指的是新生代的垃圾回收;
MajorGC、OldGC:指的是老年代的 GC,目前只有 CMS 有老年代 GC 的說法。
MixedGC:會(huì)收集整個(gè)新生代以及部分老年代,目前只有 G1 會(huì)有這種 GC。
FullGC:會(huì)收集整個(gè) Java 堆和方法區(qū);
有時(shí)候也會(huì)把 FullGC 和 MajorGC 混用,某些場(chǎng)景需要注意區(qū)分。
標(biāo)記-清除算法
它是最基礎(chǔ)的垃圾收集算法,其他的收集算法都是在它的基礎(chǔ)上改進(jìn)而來的。
根據(jù)名字可知該算法有兩個(gè)過程,即標(biāo)記和清除。根據(jù)上文的可達(dá)性分析法即可標(biāo)記出存活的對(duì)象,然后清除調(diào)未被標(biāo)識(shí)的對(duì)象即可。該算法的缺點(diǎn):
1、執(zhí)行效率不穩(wěn)定當(dāng)對(duì)象太多時(shí),標(biāo)記和清除兩個(gè)過程效率會(huì)降低;
2、會(huì)導(dǎo)致很多內(nèi)存碎片,從而會(huì)影響大對(duì)象的分配;
標(biāo)記-復(fù)制算法
如算法名所示它也包含兩個(gè)過程,即標(biāo)記和復(fù)制。該算法在分配對(duì)象內(nèi)存時(shí),會(huì)留出一塊區(qū)域,此區(qū)域不分配新對(duì)象,也就是會(huì)有一塊空閑的內(nèi)存區(qū)域,用以復(fù)制對(duì)象。其標(biāo)記過程和標(biāo)記-整理算法一樣,而清除時(shí),它是將標(biāo)記后存活的對(duì)象復(fù)制到空閑的的那塊內(nèi)存區(qū)域中,然后將另一塊作為垃圾整個(gè)回收掉。由于新生代對(duì)象“朝生夕死”的特點(diǎn),Java 虛擬機(jī)在新生代使用的就是復(fù)制算法,將新生代內(nèi)存分成三塊,Eden、From Survivor 、To Survivor,其默認(rèn)比例為 8:1:1。新創(chuàng)建的對(duì)象在 Eden 區(qū)分配,Survivor 區(qū)用來保存從 Eden 區(qū)存活下來的對(duì)象,當(dāng)對(duì)象的可達(dá)性分析標(biāo)記完成后,將 Eden 區(qū)中存活的對(duì)象復(fù)制到 To Suirvivor 區(qū),F(xiàn)rom Survivor 區(qū)的對(duì)象根據(jù)對(duì)象的分代年齡,大于設(shè)置的分代年齡的對(duì)象復(fù)制到老年代,小于的也復(fù)制到 To Survivor 區(qū),然后清空 Eden 和 From Survivor 區(qū),此時(shí)的 To Survivor 區(qū)變成 From Survivor 區(qū),被清空的 From Survivor 區(qū)變成 To Survivor 區(qū)。如此便完成了標(biāo)記-復(fù)制算法在新生代的流程。由于新生代對(duì)象“朝生夕死”的特點(diǎn),所以每次存活下來的對(duì)象比較少,因此復(fù)制的開銷也比較小,并且也完全避免了空間碎片的產(chǎn)生,也有利于垃圾的整塊回收;但是也有兩個(gè)缺點(diǎn):
1、由于需要專門留一塊做為空閑的區(qū)域,故浪費(fèi)內(nèi)存空間,但是由于對(duì)象朝生夕死的特點(diǎn),浪費(fèi)的空間有限;
2、由于預(yù)留的的空間比較小,就有可能存在存活的對(duì)象內(nèi)存超過預(yù)留的內(nèi)存的可能性,故需要做空間的擔(dān)保,當(dāng)預(yù)留空間不夠用時(shí)就直接在老年代分配新對(duì)象。
標(biāo)記-整理算法
這個(gè)算法的標(biāo)記過程和標(biāo)記-清除算法一樣,區(qū)別就是標(biāo)記完成后不是直接清除“已死”對(duì)象,而是將存活的對(duì)象進(jìn)行整理,即移動(dòng)到一起保證中間不存在碎片。這個(gè)算法適用于老年代,但是老年代的存活的對(duì)象比較多,每次移動(dòng)對(duì)象效率不高,更關(guān)鍵的是需要暫停用戶線程 stop the world 因此老年代也不會(huì)直接使用該算法,一般都是先進(jìn)行幾輪標(biāo)記-清除算法收集后,再進(jìn)行一次壓縮的工作,從而減少空間碎片的產(chǎn)生。他的缺點(diǎn)就是:
1、老年代存活對(duì)象多,整理需要移動(dòng)對(duì)象,需要開銷會(huì)很大;
2、由于需要移動(dòng)對(duì)象就需要 stop the world 因此會(huì)影響用戶線程的執(zhí)行;