深入理解Java虛擬機(四)之垃圾回收算法

垃圾回收概述

什麼是垃圾?

➢垃圾是指在運行程序中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾。
● 如果不及時對內存中的垃圾進行清理,那麼,這些垃圾對象所佔的內存空間會一直保留到應用程序結束,被保留的空間無法被其他對象使用。甚至 可能導致內存溢出。

爲什麼需要GC?

  • 對於高級語言來說,一個基本認知是如果不進行垃圾回收,內存遲早都會被消耗完,因爲不斷地分配內存空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。
  • 除了釋放沒用的對象,垃圾回收也可以清除內存裏的記錄碎片。碎片整理將所佔用的堆內存移到堆的一端,以便JVM將整理出的內存分配給新的對象。
  • 隨着應用程序所應付的業務越來越龐大、複雜,用戶越來越多,沒有GC就不能保證應用程序的正常進行。而經常造成STW的GC又跟不上實際的需求,所以纔會不斷地嘗試對GC進行優化。

垃圾回收相關算法

1、標記階段:引用計數算法
2、 標記階段:可達性分析算法
3、對象的finalization機制
4、MAT與JProfiler的GC Roots溯源
5、清除階段:標記清除算法
6、清除階段:複製算法
7、清除階段:標記-壓縮算法
8、小結
9、分代收集算法
X、增量收集算法、分區算法

垃圾標記階段,對象存活判斷

  • 在堆裏存放着幾乎所有的Java對象實例,在GC執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記爲己經死亡的對象,GC纔會在執行垃圾回收時,釋放掉其所佔用的內存空間,因此這個過程我們可以稱爲垃圾標記階段。

  • 那麼在JVM中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判爲已經死亡。

判斷對象存活一般有兩種方式,引用計數算法和可[達性分析算法

引用計數算法

  • 引用計數算法(Reference Counting)比較簡單,對每個對象保存一 個整型的引用計數器屬性。用於記錄對象被引用的情況。

  • 對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值爲0,即表示對象A不可能再被使用,可進行回收。

  • 優點: 實現簡單,垃圾對象便於辨識;判定效率高,回收沒有延遲性。

  • 缺點:
    ➢它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。
    ➢每次賦值都需要更新計數器,伴隨着加法和減法操作,這增加了時間開銷。
    ➢引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,
    導致在Java的垃圾回收器中沒有使用這類算法。
    引用計數算法

可達性分析(或根搜索算法、追蹤性垃圾收集)

  • 相對於引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用問題,防止內存泄漏的發生。
  • 相較於引用計數算法,這裏的可達性分析就是Java、C#選擇的。這種類的垃圾收集通常也叫作追蹤性垃圾收集(Tracing GarbageCollection)。
  • 所謂"GC Roots"根集合就是一組必須活躍的引用。
  • 基本思路:
    ➢可達性分析算法是以根對象集合(GC Roots) 爲起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。
    ➢使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接連接着,搜索所走過的路徑稱爲引用鏈 ( Reference Chain )
    ➢如果目標對象沒有任何引用鏈相連,則是不可達的,就意味着該對象己經死亡,可以標記爲垃圾對象。
    ➢在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象纔是存活對象。

在這裏插入圖片描述

GC Roots

在Java語言中,GC Roots 包括以下幾類元素:

  • 虛擬機棧中引 用的對象
    ➢比如:各個線程被調用的方法中使用到的參數、局部變量等。

  • 本地方法棧內JNI (通常說的本地方法)引用的對象

  • 方法區中類靜態屬性引用的對象!
    ➢比如:Java類的引用類型靜態變量

  • 方法區中常量引用的對象
    ➢比如:字符串常量池(String Table) 裏的引用

  • 所有被同步鎖synchroni zed持有的對象

  • Java虛擬機內部的引用對象
    ➢基本數據類型對象,一.些常駐的異常對象(如:NullPointerException 、OutOfMemoryError),系統類加載器。

  • 反映j ava虛擬機內部情況JMXBean,JVMTI中註冊的回調、本地代碼緩存等。

在這裏插入圖片描述

對象的finalization機制

  • Java語言提供了對象終止( finalization)機制來允許開發人員提供對象被銷燬之前的自定義處理邏輯。

  • 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法。

  • finalize()方法允許在子類中被重寫,用於在對象被回收時進行資源釋放。

  • 通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。

  • 永遠不要 主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用。理由包括下面三點:
    ➢在finalize() 時可能會導致對象復活。
    ➢finalize() 方法的執行時間是沒有保障的,它完全由Gc線程決定,極端情況下,若不發生GC,則finalize() 方法將沒有執行機會。
    ➢一個糟糕的finalize()會嚴重影響GC的性能。

  • 從功能上來說,finalize ()方法與C+ +中的析構函數比較相似,但是Java採用的是基於垃圾回收器的自動內存管理機制,所以finalize ()方法在本質上不同於C+ +中的析構函數。

  • 由於finalize()方法的存在,虛擬機中的對象一般處於三種可能的狀態。

  • 如果從所有的根節點都無法訪問到某個對象,說明對象已經不再使用了。一般來說,此對象需要被回收。但事實上,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段。一個無法觸及的對象有可能在某-一個條件下“復活”自己,如果這樣,那麼對它的回收就是不合理的,爲此,定義虛擬機中的對象可能的三種狀態。如下:
    可觸及的:從根節點開始,可以到達這個對象。
    可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()中復活。
    不可觸及的:對象的finalize()被調用,並且沒有復活,那麼就會進入不可觸及狀態。不可 觸及的對象不可能被複活,因爲finalize()只會被調用一次。
  • 以上3種狀態中,是由於finalize ()方法的存在,進行的區分。只有在對象不可觸及時纔可以被回收。

在這裏插入圖片描述

標記清除算法

  • 背景:
    標記-清除算法(Mark-Sweep)是一種非常基礎和常見的垃圾收集算法,
    該算法被J. McCarthy等人在1960年提出並並應用於Lisp語言。

  • 執行過程:
    當堆中的有效內存空間(available memory) 被耗盡的時候,就會停止整個程序(也被稱爲stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。
    標記: Collector 從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄爲可達對象。
    清除: Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記爲可達對象,則將其回收。
    在這裏插入圖片描述

  • 缺點
    ➢效率不算高
    ➢在進行GC的時候,需要停止整個應用程序,導致用戶體驗差
    ➢這種方式清理出來的空閒內存是不連續的,產生內存碎片。需要維護一個空閒列表

  • 注意:何爲清除?
    ➢這裏所謂的清除並不是真的置空,而是把需要清除的對象地址保存在空閒的地址列表裏。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放。

複製算法

  • 背景:
    爲了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky1963年發表了著名的論文,“ 使用雙存儲區的Lisp語言垃圾收集器CA9 LISP Garbage Collector Algorithm Using SerialSecondary storage )”。M. L.Minsky在該論文中描述的算法被人們稱爲複製(Copying)算法,它也被M. L. Minsky本人成功地引入到了Lisp語言的一個實現版本中。
  • 核心思想:
    將活着的內存空間分爲兩塊,每次只使用其中- -塊,在垃圾回收時將正在使用的內存中的存活對象複製到未被使用的內存塊中,之後清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最後完成垃圾收。
    在這裏插入圖片描述
  • 優點:
    沒有標記和清除過程,實現簡單,運行高效
    複製過去以後保證空間的連續性,不會出現“碎片”問題。
  • 缺點:
    此算法的缺點也是很明顯的,就是需要兩倍的內存空間。
    對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC需要維護region之間對象引用關係,不管是內存佔用或者時間開銷也不小。
  • 特別的:
    如果系統中的垃圾對象很多,複製算法需要複製的存活對象數量並不會太大,或者說非常低纔行。
  • 應用場景:
    在新生代,對常規應用的垃圾回收,一 次通常可以回收70%-99%的內存空間。回收性價比很高。所以現在的商業虛擬機都是用這種收集算法回收新生代。
    在這裏插入圖片描述

標記-壓縮(整理)算法

  • 背景
    複製算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用複製算法,由於存活對象較多,複製的成本也將很高。因此,基於老年代垃圾回收的特性,需要使用其他的算法。
    標記-清除算法的確可以應用在老年代中,但是該算法不僅執行效率低下,而且在執行完內存回收後還會產生內存碎片,所以JVM的設計者需要在此基礎之上進行改進。標記-壓縮(Mark - Compact) 算法由此誕生。1970年前後,G. L. Steele 、C. J. Chene和D.S. Wise等研究者發佈標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。
  • 執行過程

在這裏插入圖片描述
標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,因此,也可以把它稱爲標記-清除-壓縮(Mark- Sweep-Compact)算法。
二者的本質差異在於標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收後的存活對象是一-項優缺點並存的風險決策。
可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一-來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

  • 優點:
    消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM只 需要持有一個內存的起始地址即可。消除了複製算法當中,內存減半的高額代價。
  • 缺點:
    從效率上來說,標記-整理算法要低於複製算法。移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址。移動過程中,需要全程暫停用戶應用程序。即: STW

對比三種算法

在這裏插入圖片描述
效率.上來說,複製算法是當之無愧的老大,但是卻浪費了太多內存。而爲了儘量兼顧上面提到的三個指標,標記-整理算法相對來說更平滑一些,但是效率上不盡如人意,它比複製算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。

增量收集算法

上述現有的算法,在垃圾回收過程中,應用軟件將處於一一種stop the world的狀態。在Stop the World狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。爲了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集( Incremental Collecting) 算法的誕生。
基本思想
如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集- -小片區域的內存空間,接着切換到應用程序線程。依次反覆,直到垃圾收集完成。總的來說,增量收集算法的基礎仍是傳統的標記-清除和複製算法。增量收集算法通過對線程間衝突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或複製工作。
缺點: .
使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因爲線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。

分代收集算法

  • 當前JVM垃圾收集都採用的是"分代收集(Generational Collection)"算法,這個算法並沒有新思想,只是根據對象存活週期的不同將內存劃分爲幾塊。
  • 一般是把Java堆分爲新生代和老年代。
  • 新生代中98%的對象都是"朝生夕死"的,所以並不需要按照複製算法所要求1 : 1的比例來劃分內存空間,而是將內存(新生代內存)分爲一塊較大的Eden(伊甸園)空間和兩塊較小的Survivor(倖存者)空間,每次使用Eden和其中一塊Survivor(兩個Survivor區域一個稱爲From
    區,另一個稱爲To區域)。HotSpot默認Eden與Survivor的大小比例是8 : 1,也就是說Eden :Survivor From : Survivor To = 8 : 1 : 1。所以每次新生代可用內存空間爲整個新生代容量的90%,只有10%的內存會被”浪費“。
  • 在新生代中,每次垃圾回收都有大批對象死去,只有少量存活,因此我們採用複製算法;而老年代
    中對象存活率高、沒有額外空間對它進行分配擔保,就必須採用"標記-清理"或者"標記-整理"算
    法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章