天天看點

java面試-深入了解JVM(三)——垃圾收集政策詳解

Java虛拟機的記憶體模型分為五個部分,分别是:程式計數器、Java虛拟機棧、本地方法棧、堆、方法區。

這五個區域既然是存儲空間,那麼為了避免Java虛拟機在運作期間記憶體存滿的情況,就必須得有一個垃圾收集者的角色,不定期地回收一些無效記憶體,以保障Java虛拟機能夠健康地持續運作。

這個垃圾收集者就是平常我們所說的“垃圾收集器”,那麼垃圾收集器在何時清掃記憶體?清掃哪些資料?這就是接下來我們要解決的問題。 

程式計數器、Java虛拟機棧、本地方法棧都是線程私有的,也就是每條線程都擁有這三塊區域,而且會随着線程的建立而建立,線程的結束而銷毀。那麼,垃圾收集器在何時清掃這三塊區域的問題就解決了。

此外,Java虛拟機棧、本地方法棧中的棧幀會随着方法的開始而入棧,方法的結束而出棧,并且每個棧幀中的本地變量表都是在類被加載的時候就确定的。是以以上三個區域的垃圾收集工作具有确定性,垃圾收集器能夠清楚地知道何時清掃這三塊區域中的哪些資料。

然而,堆和方法區中的記憶體清理工作就沒那麼容易了。 

堆和方法區所有線程共享,并且都在JVM啟動時建立,一直得運作到JVM停止時。是以它們沒辦法根據線程的建立而建立、線程的結束而釋放。

堆中存放JVM運作期間的所有對象,雖然每個對象的記憶體大小在加載該對象所屬類的時候就确定了,但究竟建立多少個對象隻有在程式運作期間才能确定。 

方法區中存放類資訊、靜态成員變量、常量。類的加載是在程式運作過程中,當需要建立這個類的對象時才會加載這個類。是以,JVM究竟要加載多少個類也需要在程式運作期間确定。 

是以,堆和方法區的記憶體回收具有不确定性,是以垃圾收集器在回收堆和方法區記憶體的時候花了一些心思。 

在對堆進行對象回收之前,首先要判斷哪些是無效對象。我們知道,一個對象不被任何對象或變量引用,那麼就是無效對象,需要被回收。一般有兩種判别方式:

引用計數法 

每個對象都有一個計數器,當這個對象被一個變量或另一個對象引用一次,該計數器加一;若該引用失效則計數器減一。當計數器為0時,就認為該對象是無效對象。

可達性分析法 

所有和GC Roots直接或間接關聯的對象都是有效對象,和GC Roots沒有關聯的對象就是無效對象。 

GC Roots是指:

Java虛拟機棧所引用的對象(棧幀中局部變量表中引用類型的變量所引用的對象)

方法區中靜态屬性引用的對象

方法區中常量所引用的對象

本地方法棧所引用的對象 

PS:注意!GC Roots并不包括堆中對象所引用的對象!這樣就不會出現循環引用。

兩者對比: 

引用計數法雖然簡單,但存在一個嚴重的問題,它無法解決循環引用的問題。 

是以,目前主流語言均使用可達性分析方法來判斷對象是否有效。

當JVM篩選出失效的對象之後,并不是立即清除,而是再給對象一次重生的機會,具體過程如下:

判斷該對象是否覆寫了finalize()方法

若已覆寫該方法,并該對象的finalize()方法還沒有被執行過,那麼就會将finalize()扔到F-Queue隊列中;

若未覆寫該方法,則直接釋放對象記憶體。

執行F-Queue隊列中的finalize()方法 

虛拟機會以較低的優先級執行這些finalize()方法們,也不會確定所有的finalize()方法都會執行結束。如果finalize()方法中出現耗時操作,虛拟機就直接停止執行,将該對象清除。

對象重生或死亡 

如果在執行finalize()方法時,将this賦給了某一個引用,那麼該對象就重生了。如果沒有,那麼就會被垃圾收集器清除。

注意: 

強烈不建議使用finalize()函數進行任何操作!如果需要釋放資源,請使用try-finally。 

因為finalize()不确定性大,開銷大,無法保證順利執行。

我們知道,如果使用複制算法實作堆的記憶體回收,堆就會被分為新生代和老年代,新生代中的對象“朝生夕死”,每次垃圾回收都會清除掉大量的對象;而老年代中的對象生命較長,每次垃圾回收隻有少量的對象被清除掉。

由于方法區中存放生命周期較長的類資訊、常量、靜态變量,是以方法區就像是堆的老年代,每次垃圾收集的隻有少量的垃圾被清除掉。

方法區中主要清除兩種垃圾: 

1. 廢棄常量 

2. 廢棄的類

清除廢棄的常量和清除對象類似,隻要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除掉。

清除廢棄類的條件較為苛刻: 

1. 該類的所有對象都已被清除 

2. 該類的java.lang.Class對象沒有被任何對象或變量引用 

隻要一個類被虛拟機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區的時候建立,在方法區中該類被删除時清除。 

3. 加載該類的ClassLoader已經被回收

現在我們知道了判定一個對象是無效對象、判定一個類是廢棄類、判定一個常量是廢棄常量的方法,也就是知道了垃圾收集器會清除哪些資料,那麼接下來介紹如何清除這些資料。

首先利用剛才介紹的方法判斷需要清除哪些資料,并給它們做上标記;然後清除被标記的資料。

分析: 

這種算法标記和清除過程效率都很低,而且清除完後存在大量碎片空間,導緻無法存儲大對象,降低了空間使用率。

将記憶體分成兩份,隻将資料存儲在其中一塊上。當需要回收垃圾時,也是首先标記出廢棄的資料,然後将有用的資料複制到另一塊記憶體上,最後将第一塊記憶體全部清除。

這種算法避免了碎片空間,但記憶體被縮小了一半。 

而且每次都需要将有用的資料全部複制到另一片記憶體上去,效率不高。

解決空間使用率問題: 

在新生代中,由于大量的對象都是“朝生夕死”,也就是一次垃圾收集後隻有少量對象存活,是以我們可以将記憶體劃分成三塊:Eden、Survior1、Survior2,記憶體大小分别是8:1:1。配置設定記憶體時,隻使用Eden和一塊Survior1。當發現Eden+Survior1的記憶體即将滿時,JVM會發起一次MinorGC,清除掉廢棄的對象,并将所有存活下來的對象複制到另一塊Survior2中。那麼,接下來就使用Survior2+Eden進行記憶體配置設定。

通過這種方式,隻需要浪費10%的記憶體空間即可實作帶有壓縮功能的垃圾收集方法,避免了記憶體碎片的問題。

但是,當一個對象要申請記憶體空間時,發現Eden+Survior中剩下的空間無法放置該對象,此時需要進行Minor GC,如果MinorGC過後空閑出來的記憶體空間仍然無法放置該對象,那麼此時就需要将對象轉移到老年代中,這種方式叫做“配置設定擔保”。

什麼是配置設定擔保? 

當JVM準備為一個對象配置設定記憶體空間時,發現此時Eden+Survior中空閑的區域無法裝下該對象,那麼就會觸發MinorGC,對該區域的廢棄對象進行回收。但如果MinorGC過後隻有少量對象被回收,仍然無法裝下新對象,那麼此時需要将Eden+Survior中的所有對象都轉移到老年代中,然後再将新對象存入Eden區。這個過程就是“配置設定擔保”。

在回收垃圾前,首先将所有廢棄的對象做上标記,然後将所有未被标記的對象移到一邊,最後清空另一邊區域即可。

它是一種老年代的垃圾收集算法。老年代中的對象一般壽命比較長,是以每次垃圾回收會有大量對象存活,是以如果選用“複制”算法,每次需要複制大量存活的對象,會導緻效率很低。而且,在新生代中使用“複制”算法,當Eden+Survior中都裝不下某個對象時,可以使用老年代的記憶體進行“配置設定擔保”,而如果在老年代使用該算法,那麼在老年代中如果出現Eden+Survior裝不下某個對象時,沒有其他區域給他作配置設定擔保。是以,老年代中一般使用“标記-整理”算法。

将記憶體劃分為老年代和新生代。老年代中存放壽命較長的對象,新生代中存放“朝生夕死”的對象。然後在不同的區域使用不同的垃圾收集算法。

Java中根據生命周期的長短,将引用分為4類。

我們平時所使用的引用就是強引用。 

A a = new A(); 

也就是通過關鍵字new建立的對象所關聯的引用就是強引用。 

隻要強引用存在,該對象永遠也不會被回收。 

隻有當堆即将發生OOM異常時,JVM才會回收軟引用所指向的對象。 

軟引用通過SoftReference類實作。 

軟引用的生命周期比強引用短一些。 

隻要垃圾收集器運作,軟引用所指向的對象就會被回收。 

弱引用通過WeakReference類實作。 

弱引用的生命周期比軟引用短。 

虛引用也叫幽靈引用,它和沒有引用沒有差別,無法通過虛引用通路對象的任何屬性或函數。 

一個對象關聯虛引用唯一的作用就是在該對象被垃圾收集器回收之前會受到一條系統通知。 

虛引用通過PhantomReference類來實作。