概述:java
目前內存的動態分配和內存的回收技術已經至關成熟,一切看起來都已經進入了「自動化」時代,爲何還要去了解GC和內存分配呢?緣由很簡單:當須要排查各類內存泄漏、內存溢出問題時,當垃圾收集器成爲系統達到更高併發量的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。算法
以前的博客講到了Java虛擬機運行時內存的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨方法的進入和退出執行着入棧和出棧操做,所以方法結束或者線程結束時,內存就天然跟隨着回收了。而Java堆和方法區組不一樣,咱們在運行時才知道會建立哪些對象,這部分的內存和回收都是動態的,垃圾回收器所關注的也是這一部份內存。後文所講的內存分配和回收也是僅指這一部份內存。併發
不少教科書或者開發人員,對於回答「如何判斷對象死活?」這個問題的答案大可能是引用計數法。實現起來是這樣的:給對象添加一個引用計數器,每當一個地方引用它時就+1,當引用失效時就-1,任什麼時候刻計數器爲0的對象就是不願能再被引用的。框架
客觀的說,引用計數法實現簡單,斷定效率也很高,可是至少主流的Java虛擬機都沒有采用引用計數法來管理內存,最主要的緣由是它很難解決對象之間互相循環引用的問題。jvm
可達性算法(Reachability Anlysis)是目前Java、C#的主流實現中來斷定對象是否存活的。主要思路是:經過一些類成爲「GC Roots」的對象作爲起點,從這些結點開始往下搜索,搜索所走過的路稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈時,則證實這個對象是不可用的。ide
下圖所示:高併發
Object1-Object4均有引用鏈和GC Roots相鏈接,而Object5-Oject7沒有引用鏈相連,因此他們被斷定爲能夠回收的對象this
在Java中,能夠作爲GC Roots結點的有一下四種spa
在JDK1.2以前,Java中的引用定義爲:若是reference類型的數據存儲的是數值表明的是另外一塊內存的起始地址,就稱爲這塊內存表明着一個引用。在JDK1.2以後,Java對引用概念進行了擴充,分爲下面四種:線程
即便在可達性分析算法中不可達的對象也不是「非死不可」的,這時候他們處於「緩刑」的階段,真正要宣告一個對象死亡,至少要通過兩次標記的過程。
下面代碼顯示一次對象的自我拯救:
1 //代碼演示了兩點 2 //1.對象能夠實現自救 3 //2.這種自救只能一次,由於finalize方法只會被執行一次 4 5 public class jvmtest1 { 6 public static jvmtest1 obj = null; 7 8 public void isAlive() { 9 System.out.println("I am alive"); 10 } 11 12 @Override 13 protected void finalize() throws Throwable { 14 super.finalize(); 15 System.out.println("finalize method executed!"); 16 obj = this; 17 } 18 19 public static void main(String[] args) throws Exception { 20 obj = new jvmtest1(); 21 22 obj = null; 23 System.gc(); 24 // 由於finalize方法優先級很低,因此暫停0.5s執行 25 Thread.sleep(500); 26 if (obj != null) { 27 obj.isAlive(); 28 } else { 29 System.out.println("I am dead"); 30 } 31 32 // 與上面的代碼徹底相同,可是自救失敗了 33 obj = null; 34 System.gc(); 35 // 由於finalize方法優先級很低,因此暫停0.5s執行 36 Thread.sleep(500); 37 if (obj != null) { 38 obj.isAlive(); 39 } else { 40 System.out.println("I am dead"); 41 } 42 } 43 }
程序輸出爲:
能夠看出obj的finalize()方法,確實被GC觸發過,並且成功自救了一次。要注意的是下半部分的代碼和上半部分的代碼相同,可是自救失敗,這是由於一個對象的finalize()方法只能被系統自動調用一次,若是對象面臨第二次回收,其finalize()方法不會被調用,所以第二次自救失敗。
方法區的垃圾收集「性價比」很低,在堆中,尤爲是新生代中,常規應用進行一次垃圾收集通常能夠回收70%-95%的空間,而方法區(HatSport虛擬機中的永久代)的回收效率遠低於 此。
永久代中的垃圾收集主要爲兩種:廢棄常量和無用的類
(1)該類的全部實例都已經被回收,也就是堆中不存在該類的任何實例
(2)加載該類的ClassLoader已經被回收
(3)該類對應的java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
對於知足上面三個條件的類,也是能夠回收,不是必然回收。在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
標記-清除算法(Mark-Sweep)是最基礎的收集算法。首先標記出所須要回收的對象,在標記完成後統一回收全部被標記的對象。
不足之處主要有兩點:
過程以下:
爲了解決效率問題,複製算法(Copying)出現了,他將內存分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就把存活的對象複製到另外一塊上,而後把這一塊空間所有清理掉。這樣是的每次是對整個半區就行內存回收,內存分配時也不用考慮內存碎片的複雜狀況,只須要移動指針,按順序分配內存便可。
只是這種方法的代價是將內存分爲以前的一半,未免過高了。
過程以下:
如今的商業虛擬機都是採用這種方法來回收新生代,IBM公司的專門研究代表,新生代中98%的對象是「朝生夕死」的,因此並不須要1:1來劃份內存空間。另外一種方法是:將內存塊分爲Eden空間(80%)和兩塊Survivor空間(10%),每次使用Eden空間和一塊Survivor空間,回收時將存活的對象複製到另外一塊Survivor空間上,這樣每次只有10%的空間被「浪費」。當Survivor空間不夠用時,須要依賴其餘內存(老年代)來進行分配擔保(同過度配擔保機制進入老年代)。
當複製收集算法在對象存活率較高時就要進行較多的複製動做,效率會變慢。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此老年代通常不能選用這種方法。標記-整理算法(Mark-Compact)就被提出了。
標記過程和「標記-清除」算法同樣,可是後續步驟不是直接對可回收部分進行清理,而是把存活對象都向一端移動,而後直接清理掉端之外的內存。
過程以下:
當前商業虛擬機都採用分代收集算法(Generational Collection),通常是把堆分爲新生代和老年代,根據各個年代的不一樣特色採用最適合的算法。新生代通常採用複製算法,只須要付出少許存活對象的複製成本便可;老年代由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收。