本文轉自:點擊打開鏈接
在生產環境下,通常都需要對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語言確實是採用根搜索算法判斷對象是否存活的,編寫程序:
- public class CircularRefTest {
- private CircularRefTest instance = null;
- private byte[] buffer = new byte[1024 * 1024];
- public static void main(String[] args) {
- CircularRefTest a = new CircularRefTest();
- CircularRefTest b = new CircularRefTest();
- a.instance = b;
- b.instance = a;
- a = null;
- b = null;
- System.gc();
- }
- }
設置該程序運行時的VM Arguments參數:
- -Xms3m -Xmx3m -XX:+PrintGCDetails
運行該程序,可看到控制檯輸出內容:
- 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]
- 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]
- Heap
- def new generation total 1664K, used 15K [0x02a50000, 0x02c10000, 0x02c50000)
- eden space 1536K, 1% used [0x02a50000, 0x02a53dd8, 0x02bd0000)
- from space 128K, 0% used [0x02bd0000, 0x02bd0000, 0x02bf0000)
- to space 128K, 0% used [0x02bf0000, 0x02bf0000, 0x02c10000)
- tenured generation total 3484K, used 446K [0x02c50000, 0x02fb7000, 0x03050000)
- the space 3484K, 12% used [0x02c50000, 0x02cbf9d0, 0x02cbfa00, 0x02fb7000)
- compacting perm gen total 12288K, used 1739K [0x03050000, 0x03c50000, 0x07050000)
- the space 12288K, 14% used [0x03050000, 0x03202e80, 0x03203000, 0x03c50000)
- No shared spaces configured.
說明:在該程序中,先後定義了兩個對象,並且每個對象先後被引用了兩次,然後每個對象其中的一個引用失效,如果採用引用計數法,這兩個對象是不能被回收的,因爲每個對象都還有一個未失效的引用。但是通過控制檯的觀察發現,這兩個對象確實是被回收了的,這說明Java並未採用引用計數法。在上述程序中,引用變量a和b是線程棧引用變量,都可以作爲GC Roots,a和b先後被置爲null,這意味着對象通過instance引用無法和GC Roots建立一個有效的引用鏈,因此這兩個對象都被回收了。這說明Java確實是採用根搜索算法來判斷對象是否可回收的。
【引用類型的擴展】
強引用(Strong):傳統意義的引用。
軟引用(soft):在內存緊張時、會回收軟引用對象(結合使用SoftReference類)。
弱引用:對象只能生存到下一次垃圾回收之前。
虛引用:引用關係最弱、無法通過虛引用獲取對象。
- public class SoftRefTest {
- private byte[] buffer = new byte[2 * 1024 * 1024];
- public static void main(String[] args) {
- SoftRefTest objA = new SoftRefTest();
- SoftReference<SoftRefTest> softRef = new SoftReference<SoftRefTest>(objA);
- objA = null;
- SoftRefTest objB = new SoftRefTest();
- //System.gc();
- }
- }
設置VM Arguments參數:
- -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參數調優的重要基礎!