在面試java後端開發的時候通常都會問到java的自動回收機制(GC)。在瞭解java的GC回收機制以前,咱們得先了解下Java虛擬機的內存區域。html
java虛擬機在執行的過程當中會將其管理的內存劃分爲不用的數據區域,不一樣的區域有不一樣的做用以及線程時間。java
數據區劃分以下:面試
下面將介紹不一樣區域的做用,若是已經瞭解能夠跳過算法
程序計數器(線程私有)後端
程序計數器的做用很簡單,就是記錄當前線程所執行的位置(因此爲線程私有),能夠當作當前線程所執行的字節碼的行號指示器。若是執行的是native方法,則這個計數器爲空。數組
Java虛擬機棧(線程私有,生命週期與線程相同)spa
虛擬機棧描述的是Java方法執行的內存模型:每一個Java方法在執行的時候都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。線程
本地方法棧(線程共享)code
本地方法棧與虛擬機棧發揮的做用相似,不過它執行的是虛擬機使用的Native方法。htm
Java堆(線程共享)
Java堆是Java虛擬機管理內存中最大的一塊,在虛擬機啓動的時候建立。此區域的惟一目的就是存放對象示例,幾乎全部的對象實例都是在這分配內存的。
方法區(線程共享)
剛開始的時候,看到方法區域,第一想法就是Java中的方法
,不過實際上並非這樣。方法區儲存的是已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。咱們能夠想想,當咱們須要建立一個對象的時候,咱們須要根據類的信息去建立,那麼類的信息在哪?固然是在方法區!
運行時常量池
運行時常量池是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用。
前面說了這麼多,如今咱們終於能夠來講說垃圾回收機制了。
首先咱們得說下垃圾回收回收的是哪一部份內存區域。在前面咱們知道:程序計數器,虛擬機棧,本地方法棧都是線程私有的,隨着線程生或滅。這部分咱們就不須要考慮了。因此咱們須要考慮的就是Java堆
和方法區
。
對象是否能夠被回收
判斷對象是否被回收就是當一個對象死了的時候就須要進行回收。那麼如何判斷一個對象是否死亡,在Java中,咱們使用了可達性分析算法來判斷對象是否存活。
當一個對象到GC Roots沒有任何鏈(稱爲引用鏈
)相連(也就是對象到GC Roots不可達)則斷定對象已經死亡(如圖中的Object5,Object6),可進行回收。
可做爲GC Roots的對象:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即通常說的Native方法)引用的對象
在前面中,咱們知道,不可達就意味着回收,但是當咱們的內存很夠時,有一些對象又是「食之無味棄之惋惜」的時候,咱們怎麼辦呢?在JDK1.2中,Java對引用進行擴張,分爲如下引用:
- 強引用(Strong Reference):只要強引用還在,則不回收
- 軟引用(Soft Reference):描述一些有用但非必須的對象,在系統將要發生內存溢出以前,將這些對象列入回收範圍之中進行第二次回收。<java.lang.ref.SoftReference>
- 弱引用(Weak Reference):比軟引用還要弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。<java.lang.ref.WeakReference>
- 虛引用(Phantom Reference):不會對生存時間構成影響,惟一的做用就是這個對象被回收的時候會收到一個通知。<java.lang.ref.PhantomReference>
最終判斷對象是否可以存活
在可達性分析算法中,若是一個對象不可達,那麼這個對象就進入到了「緩刑」階段,真正宣告一個對象死亡還須要進行兩次標記。
第一次標記進行篩選
對不可達的對象進行第一次標記並進行篩選。篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過(意思就是finalize()方法只能被調用一次,也就是對象只可以有一次避免被回收),虛擬機將這兩種狀況都視爲「沒有必要執行」,對象被回收。
第二次標記
若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣作的緣由是,若是一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的狀況),將極可能會致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至致使整個內存回收系統崩潰。
finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,若是對象要在finalize()中成功拯救本身----只要從新與引用鏈上的任何的一個對象創建關聯便可,譬如把本身賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出「即將回收」的集合。若是對象這時候還沒逃脫,那基本上它就真的被回收了。
在Java虛擬機規範中說過不要求方法區實現垃圾收集,而且進行垃圾收集的「性價比」也較低。不過既然寫了,那一定有方法區的垃圾收集,主要回收如下兩部份內容:
廢棄常量:字面量和符號引用
無用的類:
- 該類的全部實例都被回收,即:Java堆中不存在該類的任何實例
- 該類的Classloader已經被回收
- 該類對用的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問到該類的方法。
當知足以上三個條件時,也未必說是必定要被回收。也僅僅是能夠。
咱們經過對象的存活週期來將JVM堆中內存空間劃分爲新生代和老年代。
新生代:主要是用來存放新生的對象。通常佔據堆的1/3空間。
老年代:主要存放應用程序中生命週期長的內存對象。
OK,說了這麼多,咱們如今終於能夠來講說垃圾收集的算法了。
下面的圖片來源於這位大佬,這位大佬講的真滴不錯。
標記-清除算法(Mark-Sweep)
標記:首先標記須要回收的對象,標記完成統一回收
清除:就是清除對象,釋放空間
缺點:標記和清除的效率不高,同時產生大量不連續的內存碎片(可能不利於下次的空間分配)。
標記整理法
標記整理算法相比較於標記清除算法,標記-整理算法在清除的時候並非一個一個的清除對象釋放空間,而是一次清除所有的可回收的空間。這樣使得空間變得連續,有利於對象空間的分配。
複製算法
優勢:速度快,效率高,不會產生內存碎片。
缺點:顯而易見,空間浪費大,縮小了一半。
解決方法:
IBM研究代表:新生代98%的對象是「朝生夕死」,因此咱們並不須要將空間劃分爲1:1,而是將空間劃分爲Eden:Survivor:Survivor = 8:1:1
。每次使用Eden和其中一塊Survivor。
若是第二步中Survivor的空間不足,則依賴於其餘內存(老年代)進行分配擔保(也就是講存活的對象放入老年代)。
分代收集算法
分代收集算法其實就是前面幾種算法的應用。根據年代使用不一樣的算法
參考書籍:《深刻理解Java虛擬機》——周志明,這本書寫的太好了,寫的通熟易懂。強烈推薦去看看。