JVM理論與實踐【堆內存結構與垃圾回收】

本文轉自:點擊打開鏈接


        在生產環境下,通常都需要對JVM進行參數優化,其中對垃圾回收器的參數優化是一個非常重要的一方面。下面重點介紹Java的堆內存,垃圾回收算法,常用的垃圾回收器以及Java堆內存的分配策略,這些內容將作爲對JVM進行垃圾回收參數優化的重要基礎。然後通過簡單示例驗證Java的垃圾回收機制。

 

【Java堆內存結構】

       Java的堆(Heap)是存放對象的內存區域。在邏輯上我們可以把堆細分爲新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)。

  1.  新生代:可以再劃分爲Eden(伊甸)、From Survivor(存活者)和To Survivor三個邏輯區域, 對象優先存放在新生代的Eden區域。尷尬

  2. 老年代:新生代的對象經過幾次垃圾回收之後,仍然存活的將存放到老年代,並且大對象可以不經過新生代而直接存放在老年代。微笑

  3. 永久代:方法區使用永久代作爲存儲區域,在邏輯上,永久代是Java堆的一部分、但通常稱之爲“非堆”(Non-Heap)內存以示區別。方法區(Method Area)通常用來存放類的相關信息 (類加載器所加載的類的字段、方法簽名等)、運行時常量池(如字符串常量池)、靜態引用變量等。叫喊

    Java的堆內存結構可下圖簡單描述,其中Eden、From Survivor和To Survivor區域這三部分將構成堆內存中的新生堆區域。尷尬

 【對象是否存活】

       在進行垃圾回收(Garbage Collection,GC)之前,需要判斷堆中哪些對象是可回收的(不再被引用的)、哪些對象是不能被回收的。在面向對象的語言中,通常使用如下兩種方式來進行對象是否存活的判斷。

  1. 引用計數法:Reference Counting

    可以給每個對象添加引用計數器,對象有新的引用時、計數器+1操作,引用失效時、計數器-1操作,計數器的值爲0時、該對象就是可回收的。Python語言的垃圾回收機制就採用引用計數法,但是這種方法很難解決對象的循環引用問題。

  2. 根搜索算法:GC Roots Tracing酷

    如果對象到GC Roots(比如,線程棧中的對象、靜態引用變量等就可作爲GC Roots)之間有引用鏈相連,表示該對象仍然被使用着的、不能被回收的,否則即認爲對象沒有被引用、是可以進行回收的。典型的高級語言如Java、C#都採用該方法。爲了說明Java語言確實是採用根搜索算法判斷對象是否存活的,編寫程序: 

Java代碼  收藏代碼
  1. public class CircularRefTest {  
  2.     private CircularRefTest instance = null;  
  3.     private byte[] buffer = new byte[1024 * 1024];  
  4.       
  5.     public static void main(String[] args) {  
  6.         CircularRefTest a = new CircularRefTest();  
  7.         CircularRefTest b = new CircularRefTest();  
  8.         a.instance = b;  
  9.         b.instance = a;  
  10.         a = null;  
  11.         b = null;  
  12.         System.gc();  
  13.     }  
  14. }       

     設置該程序運行時的VM Arguments參數:  

Text代碼  收藏代碼
  1. -Xms3m -Xmx3m -XX:+PrintGCDetails  

     運行該程序,可看到控制檯輸出內容: 

Text代碼  收藏代碼
  1. 2014-09-13T16:07:35.998+0800: [GC [DefNew: 623K->64K(960K), 0.0028993 secs][Tenured: 1407K->1471K(2048K), 0.0045221 secs] 1647K->1471K(3008K), [Perm : 1732K->1732K(12288K)], 0.0075367 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
  2. 2014-09-13T16:07:35.998+0800: [Full GC (System) [Tenured: 2495K->446K(3484K), 0.0050437 secs] 2536K->446K(4636K), [Perm : 1734K->1734K(12288K)], 0.0051196 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]   
  3. Heap  
  4.  def new generation   total 1664K, used 15K [0x02a500000x02c100000x02c50000)  
  5.   eden space 1536K,   1% used [0x02a500000x02a53dd80x02bd0000)  
  6.   from space 128K,   0% used [0x02bd00000x02bd00000x02bf0000)  
  7.   to   space 128K,   0% used [0x02bf00000x02bf00000x02c10000)  
  8.  tenured generation   total 3484K, used 446K [0x02c500000x02fb70000x03050000)  
  9.    the space 3484K,  12% used [0x02c500000x02cbf9d00x02cbfa000x02fb7000)  
  10.  compacting perm gen  total 12288K, used 1739K [0x030500000x03c500000x07050000)  
  11.    the space 12288K,  14% used [0x030500000x03202e800x032030000x03c50000)  
  12. No shared spaces configured.  

     說明:在該程序中,先後定義了兩個對象,並且每個對象先後被引用了兩次,然後每個對象其中的一個引用失效,如果採用引用計數法,這兩個對象是不能被回收的,因爲每個對象都還有一個未失效的引用。但是通過控制檯的觀察發現,這兩個對象確實是被回收了的,這說明Java並未採用引用計數法。在上述程序中,引用變量a和b是線程棧引用變量,都可以作爲GC Roots,天真a和b先後被置爲null,這意味着對象通過instance引用無法和GC Roots建立一個有效的引用鏈,因此這兩個對象都被回收了。這說明Java確實是採用根搜索算法來判斷對象是否可回收的。

 

【引用類型的擴展】

     強引用(Strong):傳統意義的引用。

     軟引用(soft):在內存緊張時、會回收軟引用對象(結合使用SoftReference類)。尷尬

     弱引用:對象只能生存到下一次垃圾回收之前。

     虛引用:引用關係最弱、無法通過虛引用獲取對象。

Java代碼  收藏代碼
  1. public class SoftRefTest {  
  2.     private byte[] buffer = new byte[2 * 1024 * 1024];   
  3.           
  4.     public static void main(String[] args) {  
  5.         SoftRefTest objA = new SoftRefTest();  
  6.         SoftReference<SoftRefTest> softRef = new SoftReference<SoftRefTest>(objA);  
  7.         objA = null;  
  8.         SoftRefTest objB = new SoftRefTest();  
  9.         //System.gc();  
  10.     }  
  11. }  

     設置VM Arguments參數: 

Text代碼  收藏代碼
  1. -Xms3m -Xmx3m -XX:+PrintGCDetails -XX:+PrintGCDateStamps  

    運行該程序,通過控制檯觀察到軟引用對象objA確實被回收了。微笑

 

【關於finalize()方法】

    如果堆中的對象到GC Roots之間沒有任何引用鏈,GC就可以對其進行回收. 在回收之前會調用對象的finalize()方法,可以通過覆蓋該方法、把當前對象的引用重新和GC Roots連接起來、以阻止GC進行回收。 需要注意的是,大笑一個對象的finalize()方法只會被執行一次、如果GC再次回收該對象,無法阻止被GC回收。

 

【永久代的垃圾回收】

    在Sun公司的HotSpot虛擬機中,方法區存放在Java堆的永久代(Permanent Generation)。在大量涉及反射、動態代理、cglib等字節碼(bytecode)技術的場景(如項目中使用Spring、Hibernate等框架),需要虛擬機具有類卸載的功能,皺眉保證永久代不會溢出。

 

【垃圾收集算法】

  1. 複製算法:Copying 蠢話

    將堆內存劃分爲兩塊,當其中一塊正在使用中的的內存空間緊張時、把其中“存活”(仍然被引用)着的對象複製到另外一塊空閒着的內存區域,然後清空當前內存空間. 複製算法通常作爲新生代的垃圾回收策略。

  2. 標記-清除算法:Mark-Sweep吐舌頭

    先標記出可回收的對象,然後進行統一清除. 缺點:效率低、並且產生大量不連續的內存碎片。

  3. 標記-整理算法:Mark-Compact

    標記出可回收的對象、將所有存活的對象向其中一端移動,然後直接清理掉另一端的內存區域。

  4. 分代收集算法:Generational Collection大笑

    將Java堆劃分爲新生代、老年代,新生代中的大多數對象都是可回收的,而老年代中的對象大多數都是不可回收的 。新生代採用複製算法:大多數對象都是可回收的、只需複製少數存活的對象、回收效率較高。老年代只有少數對象可回收、標記效率較高,因此採用標記-清除(無須移動對象)、標記-整理(移動存活對象到其中一側)算法相結合進行回收。

 

【垃圾收集器】

  1. Serial收集器:串行收集器(collector)驚訝

    單線程的垃圾收集器,是JVM運行在client模式下的默認收集器,進行垃圾回收時、必須暫停其他所有的工作線程(Sun稱之爲“Stop The World”)。

  2. ParNew收集器:並行收集器

    Serial收集器的多線程版本、多條線程並行進行垃圾回收、以減少暫停時間,通常用於JVM在server模式下新生代的收集器。並行:(Parallel):多個垃圾回收線程並行工作、仍需暫停其他工作線程。哭

  3. CMS收集器:Concurrent Mark Sweep吻

    併發標記清除收集器,通常作爲老年代的收集器。併發(Concurrent):多條垃圾回收線程和工作線程交替運行、無須暫停工作線程,最大程度的提高垃圾效率、減少工作線程的停頓時間。

 

【堆內存分配策略】吐舌頭

   1. 新創建的對象將存放在新生代的Eden(伊甸)區域、以及其中一個Survivor(存活者)區域(From Survivor)。

  2. 堆內存緊張時、進行新生代對象的回收,存活着的對象將從Eden和From Survivor區域複製到To Survivor區域,如果To Survivor區域內存緊張、一部分存活對象將直接複製到老年代存放,然後清空Eden和From Survivor區域.。在下一次新生代垃圾回收時、From Survivor和To Survivor區域的角色互換.

 3. 大對象(通常是指內容很長的字符串或者數組)直接放入老年代、以避免大對象在新生代的反覆拷貝。

 4. (新生代中)長期存活的對象將放入老年代,新生代中的對象每在Survivor區域完成一次拷貝、該對象的

 年齡(Age)加1,當對象的年齡增加到一定值(默認爲15)時、該對象將被存放到老年代,以避免該對象在

 新生代的反覆拷貝。

 

【Minor/Major GC】

  新生代GC(Minor GC):新生代的垃圾回收非常頻繁(儘可能快的釋放出可用空間)、效率很高(採用複製算法,大多數對象可回收、只需複製少數存活對象)。微笑

  老年代GC(Major/Full GC):老年代的垃圾回收、效率通常比新生代的Minor GC慢至少10倍,(採用標記-清除、標記-整理算法),每次Full GC會同時進行至少一次Minor GC, 通常在堆內存緊張、或者顯示的調用System.gc()時觸發Full GC。叫喊

 

=================================

垃圾回收機制的學習,確實枯燥乏味,但這卻是進行JVM參數調優的重要基礎!

大笑


發佈了30 篇原創文章 · 獲贊 27 · 訪問量 67萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章