JVM垃圾回收

目錄

一、哪些內存需要回收?

1.1  引用計數法

1.2  可達性分析算法

1.3 Java中的引用

1.4 方法區如何判斷是否被回收

二、垃圾收集算法

2.1 標記-清除算法

2.2 複製算法

2.3 標記-整理算法

2.4 分代收集算法


JVM在對內存進行回收之前,需要考慮一下問題:

  1. 哪些內存需要回收?
  2. 什麼時候回收?
  3. 如何回收?

一、哪些內存需要回收?

我們都知道,JVM在執行Java程序的過程中把它管理的內存劃分爲不同的數據區域。分爲線程私有區域和線程共享區域。

線程私有區域:程序計數器、虛擬機棧、本地方法棧

線程共享區域:Java堆、方法區、運行時常量池

其中線程私有區域的生命週期與相關線程有關,隨線程而生,隨線程而滅,當方法結束或線程結束時,內存就自然跟着線程回收了,所以線程私有區域的內存分配和回收具有確定性。因此,我們這裏談的內存回收中回收的是Java堆和方法區。

那麼垃圾收集器在對堆區和方法區進行回收之前,首先要確定這些區域的哪些對象可以回收,哪些不能被回收。這就涉及到判斷對象是否存活的算法。

1.1  引用計數法

1)算法分析

引用計數是垃圾收集器中的早期策略。在這種方法中,給堆中的每個對象附加一個引用計數器,每當有一個地方引用此對象,該對象的引用計數器+1;每當有一個地方不再引用此對象,計數器-1。在任意時刻,只要對象的引用計數器值爲0,認爲此對象已死。

2)優缺點:

優點:實現簡單,判斷效率高,大多數情況下是一個不錯的算法。

缺點:無法處理對象的循環引用問題。

1.2  可達性分析算法

1)算法分析

通過一系列稱爲“GC Roots”的對象作爲起點,從這些節點開始向下搜索。搜索走過的路徑稱爲“引用鏈”。當一個對象到“GC Roots”沒有任何引用鏈相連時,這些對象被判定爲是不可用的。

可以作爲“GC Roots”的對象有:

  1. 虛擬機棧(本地變量表)中引用的對象
  2. 本地方法棧中引用的對象
  3. 方法區中類靜態變量引用的對象
  4. 方法區中常量引用的對象

2)不可達的對象並非“非死不可”

需要注意,在可達性分析算法中不可達的對象,並不是非死不可的。它們只是暫時處於“緩刑階段”。究竟死不死,還取決於finalize()方法。

  a.  如果該對象所在的類覆寫了finalize()方法,並且未被JVM調用過。那麼JVM就會調用finalize()方法。調用之後,如果“GC Roots”到該對象可達了,那麼該對象拯救成功。如果調用之後仍舊不可達,宣告死亡;

  b.  如果該對象所在的類沒有覆寫finalize()方法或者finalize()方法已經被JVM調用過,則宣告死亡。

1.3 Java中的引用

上面反覆提到了引用這個概念 ,包括引用鏈、引用計數器等。這裏的引用並不是簡單的用了。JDK1.2之後,Java對引用的概念進行了擴充。將引用分爲強引用、軟引用、弱引用、虛引用(引用強度逐漸降低)。

  • 強引用(StrongReference):描述的是必不可少的對象。即程序代碼中普遍存在的。比如:Object obj = new Object( ) 這類的引用。如果一個對象具有強引用,垃圾收集器永遠不會回收此對象實例。即使當內存空間不足時,Java虛擬機寧願拋出OOM錯誤,也不會回收它。
  • 軟引用(SoftReference):描述的是一些有用但不必須的對象。如緩存對象。如果內存空間足夠,垃圾收集器就不會回收這類對象,如果內存空間不夠了(即將拋出OOM時),就會回收這些對象的內存。
  • 弱引用(WeakReference):也是用來描述非必需對象的,但強度比軟引用更弱一些。被弱引用關聯的對象只能存活到下一次垃圾收集之前,當垃圾收集器開始工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用(PhatomReference):顧名思義,形同虛設。與其他幾種引用不同,虛引用並不會決定對象的生存週期。當然了,也無法通過虛引用來取得一個對象實例。它的作用是能在這個對象被垃圾收集器回收時收到一個系統通知。

在程序中一般很少用弱引用和虛引用,使用軟引用的情況較多。這是因爲軟引用可以加快JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出等問題的產生。

1.4 方法區如何判斷是否被回收

方法區的垃圾回收主要包括兩部分:廢棄常量、無用的類。

1.4.1 如何判斷一個常量是廢棄常量?

對於廢棄常量也可以通過引用的可達性來判斷。

1.4.2 如何判斷一個類是無用的類?

一個無用的類必須滿足一下三點:

1)該類的所有實例都已被垃圾回收(即在Java堆中不存在任何該類的實例);

2)加載該類的ClassLoad已被回收;

3)該類的Class對象沒有在其他地方被引用。

JVM可以對滿足以上三點的無用類進行回收。注意,僅僅是可以,並不是像對象一樣不使用了就必然會回收。

二、垃圾收集算法

談完了哪些垃圾需要回收,接下來就是如何回收的問題了。由於垃圾收集算法的實現設計大量的程序細節,而且各個平臺的虛擬機操作內存的方法又各不相同,因此這裏不會過多的討論算法的實現,只是介紹幾種算法的思想。

2.1 標記-清除算法

1)算法分析

標記-清除算法是最基礎的收集算法,如同它的名字一樣,算法分爲“標記”和“清除”兩個階段。首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。

2)缺點:

a.  效率問題,標記和清除兩個過程的效率都不高;

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

2.2 複製算法

1)算法分析

爲了解決效率問題,複製算法出現了。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,然後再把已使用過的內存空間一次清理掉。這樣每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況了。實現簡單,運行高效。

只是這種算法的代價是將內存縮小成原來的一半,浪費太大。

現在的商用虛擬機都採用這種收集算法來回收新生代,IBM公司的研究表明,新生代中的對象 98% 是朝生夕死的,所以並不需要按照 1:1 的比例來劃分內存空間,而是將內存分爲一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 區和其中一塊 Survivor 區。當回收時,將 Eden 區和 Survivor 區中還存活的對象一次性地複製到另外一塊 Survivor 空間上,最後清理掉Eden 和剛纔用過的 Survivor 空間。HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1 ,也就是每次新生代可用內存佔整個內存空間的 90% 。當然,我們沒有辦法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其他內存(這裏指老年代)進行分配擔保。

2.3 標記-整理算法

對於存活率較高的對象,如果使用複製算法就要進行較多次的複製操作,效率會變低。所以爲了避免這個問題,就提出了標記-整理算法。標記過程仍然與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後清理掉端邊界以外的內存。

2.4 分代收集算法

當前商業虛擬機的垃圾收集都是採用“分代收集”算法。這種算法其實就是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆分成新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

在新生代中,每次垃圾回收都發現有大批對象死去,只有少量存活,因此選用複製算法。(在前面的複製算法部分已經介紹)。

在老年代中,對象的存活率高,可以使用“標記-清除”或者“標記-整理”算法來進行回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章