垃圾收集器(Garbage Collection, GC)的歷史可以追溯到1960年MIT的第一門真正使用內存動態分配和垃圾收集技術的語言——Lisp。人們當時就在思考GC需要完成的3件事:
哪些內存需要回收?
什麼時候回收?
如何回收?
經過半個世紀的發展,目前內存的動態分配與內存回收技術已經相當成熟。在Java這門語言中,
程序計數器、虛擬機棧、本地方法棧三個區域的內存隨線程的創建而創建,隨線程的銷燬而銷燬;
堆和方法區中,內存的分配和回收都是動態的。
棧中的棧幀隨着方法的進入和退出而有條不紊的執行着出棧和入棧操作,每一個棧幀中分配多少內存基本上是在類結構確定下來的時候就已知的(儘管在運行時期會由JIT編譯器進行一些優化,但基本還是可以認爲編譯期可知)。因此這幾個區域的內存分配和回收都具有確定性,因此不需要過多的考慮內存回收問題。
在堆和方法區中,因爲接口中多個實現類需要的內存可能不一樣,而一個方法中的多個分支需要的內存也可能不一樣,只有當程序處於運行期的時候,才能確定需要分配多大的內存。因此垃圾收集器所關注的便是這部分內存。
判斷對象已死
引用計數法
在傳統方法中獲取對象引用次數的方法爲引用計數法
(Reference Count),其基本的算法如下:
給對象添加一個引用計數器,每當有一個對象引用它時,計數器值加1;當引用實效時,計數器值減1;
任何時刻計數器值爲0的對象便是不可能被用的對象,此時可以判斷對象“已死”。
但是在主流的Java虛擬機中都沒有引用引用計數法
來管理內存,主要原因是該算法很難解決對象之間相互引用導致的相互循環引用問題。比如對象objA和objB都有字段instance,那麼令:
objA.instance = objB
objB.instance = objA
很顯然,兩個對象相互引用,導致引用計數不能爲0,於是引用計數器無法通知GC收集器回收它們。
可達性分析算法
Java, C#等主流程序語言中,均使用可達性分析
(Reachability Analysis)算法來判定對象是否存活的。該算法的基本思想如下:
將”GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路稱爲
引用鏈
(Reference Chain)`;
當一個對象到GC Roots沒有任何引用的鏈相連時(圖論中稱爲對象不可達),則證明此對象“已死”。
在Java語言中,可作爲GC Roots的對象包括下述幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象;
- 方法區中類靜態屬性和常量引用的對象;
- 本地方法棧中JNI(Native Method)引用的方法;
再談引用
無論是通過引用計數法
判斷對象的引用數量,還是通過可達性分析
算法判斷對象的引用鏈是否可達,判定對象是否“存活”都與引用相關。
JDK1.2之前,一個對象的只存在兩種狀態,即引用和被引用。
JDK1.2之後對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。
強引用
強引用就是代碼中普遍存在的,類似於
Object object = new Object()
這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
軟引用是用來描述一些還有用,但並非必需的對象。
如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。
軟引用可用來實現內存敏感的高速緩存。
Java1.2之後,提供了SoftReference
類來實現軟引用。
弱引用
弱引用也是用來描述非必須對象的,但是它的強度比軟引用更弱一些。
被軟引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
Java1.2之後,提供了WeakReference
類來實現弱引用。
虛引用
虛引用也稱爲幽靈引用或幻影引用,是最弱的一種引用關係。
虛引用並不會決定對象的生命週期,當然也無法通過一個虛引用來取得一個對象實例。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾收集器回收。爲一個對象設置虛引用的唯一目的就是在這個對象被收集器回收時可以得到一個系統通知。
Java1.2之後,提供了PhantomRefernce
類來實現虛引用。
生存還是死亡
在Java垃圾收集器中,對象並不會直接被標記爲“死亡”,宣告一個對象真正死亡需要經歷兩次標記過程:
可達性分析之後發現並沒有與GC Roots相連接的引用鏈,那麼它將被第一次標記;
對F-Queue中的對象第二次分析後,仍然沒有發現與GC Roots相連接的引用鏈,則標記對象“死亡”;
其完整過程如下圖所示:
注意:
finalize()方法是在Java剛誕生的時候,爲了方便C/C++而作出的一個妥協,其運行代價高昂,不確定性大,無法保證各個對象的調用順序。因此不建議在Java中使用finalize()這個方法
方法區的回收
方法區(即HotSpot中的永久代)中垃圾收集器是可有可無的。方法區中的垃圾回收主要回收兩部分內容:廢棄常量和無用的類。
判定廢棄常量可被回收
如果系統中某常量在其他地方未被引用,則該常量被系統清理出常量池;否則不清理。
判定類可被回收
- 該類所有的實例都已被回收,即Java堆中不存在任何類的引用;
- 加載該類的
ClassLoader
已經被回收; - 該類對應的
java.lang.Class
對象沒有在任何地方被引用,無法在任何地方通過反射機制訪問該類的方法;
虛擬機可以對滿足上述三個條件的類進行回收,但並不一定會回收。
而且虛擬機也針對這些無用類提供了部分參數來進行控制,比如
查看類加載和卸載信息: -verbose:class
、-XX:+TraceClasLoading
、-XX:+TraceClassUnLoading
其中-verbose:class
和-XX:+TraceClasLoading
可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading
則需要FastDebug版的虛擬機支持。
引用總結
引用類型 | 垃圾回收時間 | 用途 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 對象的一般狀態 | JVM停止運行時終止 |
軟引用 | 在內存不足時 | 對象緩存 | 內存不足時終止 |
弱引用 | 在垃圾回收時 | 對象緩存 | gc運行後終止 |
虛引用 | Unknown | Unknown | Unknown |
結束語
到這裏已經嘗試着回答了開始的時候提到的三個問題中的兩個:
- 哪些內存需要回收?
堆和方法區 - 如何回收?
可達性分析+引用類型
對於最後一個問題
什麼時候回收?
在新生代的Eden區中沒有足夠空間進行分配對象時,虛擬機將會發起一次Minor GC;
如果從新生代晉升到老年代的對象所需空間大於老年代的剩餘空間,虛擬機會發起一次Full GC;
這裏先說個概要,至於具體細節會在內存分配策略和回收策略中說明。