本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等java
堆是java建立對象的區域(String對象在常量池中),也是垃圾回收最多的地方。可是除了堆空間還有方法區存在須要回收的垃圾git
回收方法區github
廢棄的常量面試
在常量池中存在一個字面量A,若是系統中沒有一個地方引用`A``,這時候發生垃圾回收,若是有必要這個字面量就會被清理出常量池。算法
注意是若是有必要。好比上一篇文章中引用的例子,就沒有回收字符串。安全
無用的類數據結構
當知足如下條件時,這個類就能夠被回收,而不是必定會回收。ide
java有一個很是大的好處就是會自動進行垃圾回收,而不用手動釋放對象所佔用的內存。當以一個對象再也不被引用的時候就能夠進行垃圾回收,那麼如何判斷一個對象是否在被使用呢?學習
引用計數法線程
引用計數法很簡單,只須要在對象建立之初給對象加一個引用計數器,每當有一個地方引用他就+1,引用失效就-1,當引用計數器爲0,則對象再也不被引用。每次垃圾回收,只須要遍歷一遍全部的引用計數器就能夠。可是對於循環引用,引用計數法則沒法釋這兩個對象。
可達性分析算法
經過一系列被稱爲GC Root的對象爲起點,從這些節點往下搜索,搜索走過的路徑稱之爲引用鏈,當一個對象到GC Root沒有任何引用鏈的時候,則證實此對象不可達。
在JVM中,能夠被用做GC Root的對象有:
枚舉根節點
對於根節點的枚舉有以下的問題:
解決方法
是否能夠用額外的空間記錄下每一個Reference的位置,這樣的話GC的時候從這個結構中直接讀取這個結構,而不用進行全棧掃描。事實上,大部分主流的虛擬機也確實是這樣作的,以HotSpot爲例,它使用一種OopMap的數據結構來保存這類信息。
一個棧意味着一個線程,而一個棧楨表明了一個方法,每一個被JIT編譯事後的方法會在一些特定的位置記錄下OopMap記錄了執行到該方法的某條指令的時候,棧上和寄存器的哪些位置是引用,這樣GC在掃描到這些棧的時候就會查詢這些OopMap就知道哪裏是引用。這些位置主要在:
而這些位置就被稱之爲「安全點」,之因此要選擇一些特定位置來記錄OopMap,是由於若是對每條指令的位置都記錄OopMap的話,這些記錄就會比較大,那麼空間開銷就會顯得不值得。
GC發生時,程序首先運行到最近的一個安全點停下來,而後更新本身的OopMap,枚舉根節點時,遞歸遍歷每一個棧楨的OopMap,經過棧中記錄的被引用的對象的內存地址,便可找到這些對象。
安全點與安全區域
安全點
程序在執行時並非任什麼時候間均可以進行GC,只有到達有OopMap記錄的位置才能夠執行GC,整個位置稱之爲安全點
安全點的選定基本是以程序「是否具備讓程序長時間執行的特徵」爲標準選定的。程序通常不會由於指令流太長而長時間執行(每一個指令執行的時間都很短)。「長時間執行」的典型特徵就是指令序列的服用,例如:循環、遞歸、方法調用。因此具備這些功能的指令纔會產生安全點。
安全區域
安全區域指在這一段代碼之中,引用關係不會發生變化,在這一段代碼之中,任一點都是安全點。任何一個地方均可以中斷線程開始GC。
當線程執行到安全區域後,首先標識本身已經進入安全區域,那麼這段時間JVM要發起GC時就不用管標記本身進入安全區的線程。線程要離開安全區時,首先須要先檢查系統是否已經完成了根節點的選舉,若是完成則線程繼續執行,不然要繼續等待收到能夠安全離開安全區的信號。
如何保證GC發生時,全部的線程都跑到了安全點上呢?
當要進行GC的時候,會讓全部的線程都在安全點中斷,就有兩種方式:
標識的設置和安全點是重合的,標識的設置和安全點是重合的。除此以外還有一個建立對象須要分配內存的地方。
假設存在以下的內存區域:
下文將以這塊內存爲例進行垃圾收集算法的分析
標記-清除算法
顧名思義,標記清除算法會爲兩個階段,1-標記,2-清除。
標記:垃圾收集器從GC Roots出發,進行搜索,而後對全部能夠訪問的對象打上標識,標記其爲可達的對象,標記通常保存在header中
清除:垃圾收集器對堆內存進行線性遍歷,若是發現某個對象沒有被標記爲可達,就會將其回收,回收後效果以下圖
優勢
缺點
複製算法
複製算法,就是將內存劃分爲相等的兩塊,每次只是用其中一塊,當這塊內存使用完了就將還存活的對象複製到另外一塊,而後將這塊空間清理掉,這樣使得每次對內存的回收都是半區回收。
複製算法的示意圖以下圖:
優勢
缺點
標記—整理算法
複製算法在對象存活較多的時候會進行較多的操做,若是對象所有存活複製將會進行100%,而且浪費50%的內存空間做爲擔保。
標記—整理算法和標記—清除算法前半部分同樣,只是後續不是清理,而是讓全部存活的對象都向一端移動,而後清理掉邊界之外的內存。
在當前主流的垃圾收集器當中(g1除外),基本都採用一種分代收集算法。根據對象存活週期,將java堆分爲新生堆和老年堆。對於新生堆,採用複製算法,對於老年堆採用標記-清除或者標記-整理算法。
研究人員發現大多數的對象都是「朝生夕滅」,對於這樣的對象,生存週期很短,能夠將其放入新生堆,由於其生存時間很短,因此新生堆採用複製算法的時候沒有必要使用1:1的比例劃份內存。
而是分爲較大的Eden空間和兩塊較小的Suvivor空間;HotSpot的Eden和Suvivor的比例爲8:1。回收時將Eden和一塊Suvivor上還存活的對象,一次性copy到另外一塊Suvivor上,而後清理掉之前的兩塊區域。這樣每次新生代可用的內存空間佔整個新生堆的90%,只有10%會被浪費。
咱們沒有辦法保證新生代回收的時候只剩下很少於10%的對象存活。當Suvivor空間不夠用時,就須要依賴其餘內存(老年堆)進行分配擔保。對於存活過必定gc次數的對象放進老年堆。
老年堆對象存活率高,使用複製算法可能就須要1:1的空間,這樣就會浪費內存,所以使用的是標記-清除或者標記-整理算法。
保守式GC
HotSpot虛擬機在棧上使用OopMap記錄下了哪些位置是引用類型,根據記錄的類型類型開始查找堆中存活的對象。
虛擬機最初的實現當中是沒有記錄每一個數據的類型的,JVM也沒法區份內存裏某個位置的數據到底應該解讀爲引用類型仍是其餘數據類型,這種條件下,實現出來的GC就是「保守式GC」。在進行GC時,JVM開始從一些已知的位置(例如棧)開始掃描內存,掃描的時候每看到一個數字就看看它「像不像是一個指向GC堆中的指針」。
這裏會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(一般分配空間的時候會有對齊要求,假如說是4字節對齊,那麼不能被4整除的數字就確定不是指針),之類的。而後遞歸的這麼掃描出去。
優勢
缺點
有一種辦法能夠在使用保守式GC的同時支持對象的移動,那就是增長一個間接層,不直接經過指針來實現引用,而是添加一層「句柄」(handle)在中間,全部引用先指到一個句柄表裏,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表裏的內容便可。
半保守式GC
保守式GC沒有在JVM中記錄任何類型信息,半保守式GC會在對象上記錄類型信息,這樣的話,掃描棧的時候仍然和保守式GC同樣,可是掃描到堆上的時候,對象上帶了足夠的類型信息,JVM就能判斷出棧中這個位置是否是一個指向堆中對象的指針,以及這個對象內什麼位置數據是引用類型,這種是「半保守式GC」,也稱之爲「根上保守」。
因爲半保守式GC在堆內部的數據是準確的,因此它能夠在直接使用指針來實現引用的條件下支持部分對象的移動,方法是隻將保守掃描能直接掃到的對象設置爲不可移動(pinned),而從它們出發再掃描到的對象就能夠移動了。
準確式GC
對於垃圾回收,JVM關心的就是掃描的根節點是否是一個指向堆內存的指針,那麼就是在棧上記錄下那個位置式引用類型,是指向堆上對象的指針,在HotSpot虛擬機中這個數據結構就是OopMap