天天看點

JVM記憶體模型及垃圾收集政策解析(一)

JVM記憶體模型是Java的核心技術之一,之前51CTO曾為大家介紹過JVM分代垃圾回收政策的基礎概念,現在很多程式設計語言都引入了類似Java JVM的記憶體模型和垃圾收集器的機制,下面我們将主要針對Java中的JVM記憶體模型及垃圾收集的具體政策進行綜合的分析。

一 JVM記憶體模型

1.1 Java棧

Java棧是與每一個線程關聯的,JVM在建立每一個線程的時候,會配置設定一定的棧空間給線程。它主要用來存儲線程執行過程中的局部變量,方法的傳回值,以及方法調用上下文。棧空間随着線程的終止而釋放。StackOverflowError:如果線上程執行的過程中,棧空間不夠用,那麼JVM就會抛出此異常,這種情況一般是死遞歸造成的。

1.2 堆

Java中堆是由所有的線程共享的一塊記憶體區域,堆用來儲存各種JAVA對象,比如數組,線程對象等。

1.2.1 Generation

JVM堆一般又可以分為以下三部分:

JVM記憶體模型及垃圾收集政策解析(一)

◆ Perm

Perm代主要儲存class,method,filed對象,這部門的空間一般不會溢出,除非一次性加載了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被解除安裝掉,這樣就造成了大量的class對象儲存在了perm中,這種情況下,一般重新啟動應用伺服器可以解決問題。

◆ Tenured

Tenured區主要儲存生命周期長的對象,一般是一些老的對象,當一些對象在Young複制轉移一定的次數以後,對象就會被轉移到Tenured區,一般如果系統中用了application級别的緩存,緩存中的對象往往會被轉移到這一區間。

◆ Young

Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻隻有其中一個是被使用的,另外一個留做垃圾收集時複制對象用,在Young區間變滿的時候,minor GC就會将存活的對象移到空閑的Survivor區間中,根據JVM的政策,在經過幾次垃圾收集後,任然存活于Survivor的對象将被移動到Tenured區間。

1.2.2 Sizing the Generations

JVM提供了相應的參數來對記憶體大小進行配置。正如上面描述,JVM中堆被分為了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。

JVM記憶體模型及垃圾收集政策解析(一)

◆ Total Heap

-Xms :指定了JVM初始啟動以後初始化記憶體

-Xmx:指定JVM堆得最大記憶體,在JVM啟動以後,會配置設定-Xmx參數指定大小的記憶體給JVM,但是不一定全部使用,JVM會根據-Xms參數來調節真正用于JVM的記憶體

-Xmx -Xms之差就是三個Virtual空間的大小

◆ Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,這樣eden+2*survivor=1/9

堆記憶體

-XX:SurvivorRatio=32意味着eden和一個survivor的比值是32:1,這樣一個Survivor就占Young區的1/34.

-Xmn 參數設定了年輕代的大小

◆ Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

1.3 堆棧分離的好處

呵呵,其它的先不說了,就來說說面向對象的設計吧,當然除了面向對象的設計帶來的維護性,複用性和擴充性方面的好處外,我們看看面向對象如何巧妙的利用了堆棧分離。如果從JAVA記憶體模型的角度去了解面向對象的設計,我們就會發現對象它完美的表示了堆和棧,對象的資料放在堆中,而我們編寫的那些方法一般都是運作在棧中,是以面向對象的設計是一種非常完美的設計方式,它完美的統一了資料存儲和運作。

二 JAVA垃圾收集器

2.1 垃圾收集簡史

垃圾收集提供了記憶體管理的機制,使得應用程式不需要在關注記憶體如何釋放,記憶體用完後,垃圾收集會進行收集,這樣就減輕了因為人為的管理記憶體而造成的錯誤,比如在C++語言裡,出現記憶體洩露時很常見的。Java語言是目前使用最多的依賴于垃圾收集器的語言,但是垃圾收集器政策從20世紀60年代就已經流行起來了,比如Smalltalk,Eiffel等程式設計語言也內建了垃圾收集器的機制。

2.2 常見的垃圾收集政策

JVM記憶體模型及垃圾收集政策解析(一)

所有的垃圾收集算法都面臨同一個問題,那就是找出應用程式不可到達的記憶體塊,将其釋放,這裡面得不可到達主要是指應用程式已經沒有記憶體塊的引用了,而在JAVA中,某個對象對應用程式是可到達的是指:這個對象被根(根主要是指類的靜态變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。

2.2.1 Reference Counting(引用計數)

引用計數是最簡單直接的一種方式,這種方式在每一個對象中增加一個引用的計數,這個計數代表目前程式有多少個引用引用了此對象,如果此對象的引用計數變為0,那麼此對象就可以作為垃圾收集器的目标對象來收集。

優點:

簡單,直接,不需要暫停整個應用

缺點:

     1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作,比如每次将對象指派給新的引用,或者者對象的引用超出了作用域等。

     2.不能處理循環引用的問題

2.2.2 跟蹤收集器

跟蹤收集器首先要暫停整個應用程式,然後開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用,這裡面有三個問題需要搞清楚:

1.如果每次掃描整個堆,那麼勢必讓GC的時間變長,進而影響了應用本身的執行。是以在JVM裡面采用了分代收集,在新生代收集的時候minor gc隻需要掃描新生代,而不需要掃描老生代。

2.JVM采用了分代收集以後,minor gc隻掃描新生代,但是minor gc怎麼判斷是否有老生代的對象引用了新生代的對象,JVM采用了卡片标記的政策,卡片标記将老生代分成了一塊一塊的,劃分以後的每一個塊就叫做一個卡片,JVM采用卡表維護了每一個塊的狀态,當JAVA程式運作的時候,如果發現老生代對象引用或者釋放了新生代對象的引用,那麼就JVM就将卡表的狀态設定為髒狀态,這樣每次minor gc的時候就會隻掃描被标記為髒狀态的卡片,而不需要掃描整個堆。具體如下圖:

JVM記憶體模型及垃圾收集政策解析(一)

3.GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:Strong reference,Soft reference,Weak reference,Phantom reference.

◆ Strong Reference

強引用是JAVA中預設采用的一種方式,我們平時建立的引用都屬于強引用。如果一個對象沒有強引用,那麼對象就會被回收。

  1. public void testStrongReference(){  
  2. Object referent = new Object();  
  3. Object strongReference = referent;  
  4. referent = null;  
  5. System.gc();  
  6. assertNotNull(strongReference);  

◆ Soft Reference

軟引用的對象在GC的時候不會被回收,隻有當記憶體不夠用的時候才會真正的回收,是以軟引用适合緩存的場合,這樣使得緩存中的對象可以盡量的再記憶體中待長久一點。

  1. Public void testSoftReference(){  
  2. String  str =  "test";  
  3. SoftReference<String> softreference = new SoftReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNotNull(softreference.get());  
  7. }  

Weak reference

弱引用有利于對象更快的被回收,假如一個對象沒有強引用隻有弱引用,那麼在GC後,這個對象肯定會被回收。

  1. Public void testWeakReference(){  
  2. String  str =  "test";  
  3. WeakReference<String> weakReference = new WeakReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNull(weakReference.get());  
  7. }  

Phantom reference

 Phantom Reference(幽靈引用) 與 WeakReference 和 SoftReference 有很大的不同,  因為它的 get() 方法永遠傳回 null, 這也正是它名字的由來.PhantomReference 唯一的用處就是跟蹤 referent  何時被 enqueue 到 ReferenceQueue 中.

<插入部分>

RererenceQueue 

當一個 WeakReference 開始傳回 null 時, 它所指向的對象已經準備被回收, 這時可以做一些合适的清理工作.   将一個 ReferenceQueue 傳給一個 Reference 的構造函數, 當對象被回收時, 虛拟機會自動将這個對象插入到 ReferenceQueue 中, WeakHashMap 就是利用 ReferenceQueue 來清除 key 已經沒有強引用的 entries.

Java代碼

  1. [email protected]  
  2. 2.public void referenceQueue() throws InterruptedException {  
  3. 3.    Object referent = new Object();       
  4. 4.    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();  
  5. 5.    WeakReference<Object> weakReference = new WeakReference<Object>(referent, referenceQueue);  
  6. 6.      
  7. 7.    assertFalse(weakReference.isEnqueued());  
  8. 8.    Reference<? extends Object> polled = referenceQueue.poll();  
  9. 9.    assertNull(polled);  
  10. 10.      
  11. 11.    referent = null;  
  12. 12.    System.gc();  
  13. 13.  
  14. 14.    assertTrue(weakReference.isEnqueued());  
  15. 15.    Reference<? extends Object> removed = referenceQueue.remove();  
  16. 16.    assertNotNull(removed);  
  17. 17.}  

複制代碼

6.  PhantomReference  vs WeakReference 

PhantomReference  有兩個好處, 其一, 它可以讓我們準确地知道對象何時被從記憶體中删除, 這個特性可以被用于一些特殊的需求中(例如 Distributed GC,  XWork 和 google-guice 中也使用 PhantomReference 做了一些清理性工作).

其二, 它可以避免 finalization 帶來的一些根本性問題, 上文提到 PhantomReference 的唯一作用就是跟蹤 referent 何時被 enqueue 到 ReferenceQueue 中,  但是 WeakReference 也有對應的功能, 兩者的差別到底在哪呢 ?

這就要說到 Object 的 finalize 方法, 此方法将在 gc 執行前被調用, 如果某個對象重載了 finalize 方法并故意在方法内建立本身的強引用,  這将導緻這一輪的 GC 無法回收這個對象并有可能

引起任意次 GC, 最後的結果就是明明 JVM 内有很多 Garbage 卻 OutOfMemory, 使用 PhantomReference 就可以避免這個問題, 因為 PhantomReference 是在 finalize 方法執行後回收的,也就意味着此時已經不可能拿到原來的引用,  也就不會出現上述問題,  當然這是一個很極端的例子, 一般不會出現.

<插入部分/>

2.2.2.1 Mark-Sweep Collector(标記-清除收集器)

标記清除收集器最早由Lisp的發明人于1960年提出,标記清除收集器停止所有的工作,從根掃描每個活躍的對象,然後标記掃描過的對象,标記完成以後,清除那些沒有被标記的對象。

優點:

1 解決循環引用的問題

2 不需要編譯器的配合,進而就不執行額外的指令

缺點:

1.每個活躍的對象都要進行掃描,收集暫停的時間比較長。

2.2.2.2 Copying Collector(複制收集器)複制收集器将記憶體分為兩塊一樣大小空間,某一個時刻,隻有一個空間處于活躍的狀态,當活躍的空間滿的時候,GC就會将活躍的對象複制到未使用的空間中去,原來不活躍的空間就變為了活躍的空間。複制收集器具體過程可以參考下圖:

JVM記憶體模型及垃圾收集政策解析(一)

優點:

1 隻掃描可以到達的對象,不需要掃描所有的對象,進而減少了應用暫停的時間

缺點:

1.需要額外的空間消耗,某一個時刻,總是有一塊記憶體處于未使用狀态

2.複制對象需要一定的開銷

2.2.2.3 Mark-Compact Collector(标記-整理收集器)标記整理收集器汲取了标記清除和複制收集器的優點,它分兩個階段執行,在第一個階段,首先掃描所有活躍的對象,并标記所有活躍的對象,第二個階段首先清除未标記的對象,然後将活躍的的對象複制到堆得底部。标記整理收集器的過程示意圖請參考下圖:Mark-compact政策極大的減少了記憶體碎片,并且不需要像Copy Collector一樣需要兩倍的空間。

JVM記憶體模型及垃圾收集政策解析(一)

2.3 JVM的垃圾收集政策

GC的執行時要耗費一定的CPU資源和時間的,是以在JDK1.2以後,JVM引入了分代收集的政策,其中對新生代采用"Mark-Compact"政策,而對老生代采用了“Mark-Sweep"的政策。其中新生代的垃圾收集器命名為“minor gc”,老生代的GC命名為"Full Gc 或者Major GC".其中用System.gc()強制執行的是Full Gc.

2.3.1 Serial Collector

Serial Collector是指任何時刻都隻有一個線程進行垃圾收集,這種政策有一個名字“stop the whole world",它需要停止整個應用的執行。這種類型的收集器适合于單CPU的機器。

Serial Copying Collector

此種GC用-XX:UseSerialGC選項配置,它隻用于新生代對象的收集。1.5.0以後。-XX:MaxTenuringThreshold來設定對象複制的次數。當eden空間不夠的時候,GC會将eden的活躍對象和一個名叫From survivor空間中尚不夠資格放入Old代的對象複制到另外一個名字叫To Survivor的空間。而此參數就是用來說明到底From survivor中的哪些對象不夠資格,假如這個參數設定為31,那麼也就是說隻有對象複制31次以後才算是有資格的對象。這裡需要注意幾個個問題:

◆  From Survivor和To survivor的角色是不斷的變化的,同一時間隻有一塊空間處于使用狀态,這個空間就叫做From Survivor區,當複制一次後角色就發生了變化。

◆  如果複制的過程中發現To survivor空間已經滿了,那麼就直接複制到old generation.

◆  比較大的對象也會直接複制到Old generation,在開發中,我們應該盡量避免這種情況的發生。

Serial  Mark-Compact Collector

串行的标記-整理收集器是JDK5 update6之前預設的老生代的垃圾收集器,此收集使得記憶體碎片最少化,但是它需要暫停的時間比較長。

2.3.2 Parallel Collector 

Parallel Collector主要是為了應對多CPU,大資料量的環境。Parallel Collector又可以分為以下兩種:

Parallel Copying Collector

此種GC用-XX:UseParNewGC參數配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。1.4.1以後Parallel Mark-Compact Collector,此種GC用-XX:UseParallelOldGC參數配置,此GC主要用于老生代對象的收集。1.6.0

Parallel scavenging Collector

此種GC用-XX:UseParallelGC參數配置,它是對新生代對象的垃圾收集器,但是它不能和CMS配合使用,它适合于比較大新生代的情況,此收集器起始于jdk 1.4.0。它比較适合于對吞吐量高于暫停時間的場合,Serial gc和Parallel gc可以用如下的圖來表示:

JVM記憶體模型及垃圾收集政策解析(一)

2.3.3 Concurrent Collector

Concurrent Collector通過并行的方式進行垃圾收集,這樣就減少了垃圾收集器收集一次的時間,這種GC在實時性要求高于吞吐量的時候比較有用。此種GC可以用參數-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

JVM記憶體模型及垃圾收集政策解析(一)

JVM記憶體模型是Java的核心技術之一,之前51CTO曾為大家介紹過JVM分代垃圾回收政策的基礎概念,現在很多程式設計語言都引入了類似Java JVM的記憶體模型和垃圾收集器的機制,下面我們将主要針對Java中的JVM記憶體模型及垃圾收集的具體政策進行綜合的分析。

一 JVM記憶體模型

1.1 Java棧

Java棧是與每一個線程關聯的,JVM在建立每一個線程的時候,會配置設定一定的棧空間給線程。它主要用來存儲線程執行過程中的局部變量,方法的傳回值,以及方法調用上下文。棧空間随着線程的終止而釋放。StackOverflowError:如果線上程執行的過程中,棧空間不夠用,那麼JVM就會抛出此異常,這種情況一般是死遞歸造成的。

1.2 堆

Java中堆是由所有的線程共享的一塊記憶體區域,堆用來儲存各種JAVA對象,比如數組,線程對象等。

1.2.1 Generation

JVM堆一般又可以分為以下三部分:

JVM記憶體模型及垃圾收集政策解析(一)

◆ Perm

Perm代主要儲存class,method,filed對象,這部門的空間一般不會溢出,除非一次性加載了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang.OutOfMemoryError : PermGen space 的錯誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被解除安裝掉,這樣就造成了大量的class對象儲存在了perm中,這種情況下,一般重新啟動應用伺服器可以解決問題。

◆ Tenured

Tenured區主要儲存生命周期長的對象,一般是一些老的對象,當一些對象在Young複制轉移一定的次數以後,對象就會被轉移到Tenured區,一般如果系統中用了application級别的緩存,緩存中的對象往往會被轉移到這一區間。

◆ Young

Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中Survivor區間中,某一時刻隻有其中一個是被使用的,另外一個留做垃圾收集時複制對象用,在Young區間變滿的時候,minor GC就會将存活的對象移到空閑的Survivor區間中,根據JVM的政策,在經過幾次垃圾收集後,任然存活于Survivor的對象将被移動到Tenured區間。

1.2.2 Sizing the Generations

JVM提供了相應的參數來對記憶體大小進行配置。正如上面描述,JVM中堆被分為了3個大的區間,同時JVM也提供了一些選項對Young,Tenured的大小進行控制。

JVM記憶體模型及垃圾收集政策解析(一)

◆ Total Heap

-Xms :指定了JVM初始啟動以後初始化記憶體

-Xmx:指定JVM堆得最大記憶體,在JVM啟動以後,會配置設定-Xmx參數指定大小的記憶體給JVM,但是不一定全部使用,JVM會根據-Xms參數來調節真正用于JVM的記憶體

-Xmx -Xms之差就是三個Virtual空間的大小

◆ Young Generation

-XX:NewRatio=8意味着tenured 和 young的比值8:1,這樣eden+2*survivor=1/9

堆記憶體

-XX:SurvivorRatio=32意味着eden和一個survivor的比值是32:1,這樣一個Survivor就占Young區的1/34.

-Xmn 參數設定了年輕代的大小

◆ Perm Generation

-XX:PermSize=16M -XX:MaxPermSize=64M

Thread Stack

-XX:Xss=128K

1.3 堆棧分離的好處

呵呵,其它的先不說了,就來說說面向對象的設計吧,當然除了面向對象的設計帶來的維護性,複用性和擴充性方面的好處外,我們看看面向對象如何巧妙的利用了堆棧分離。如果從JAVA記憶體模型的角度去了解面向對象的設計,我們就會發現對象它完美的表示了堆和棧,對象的資料放在堆中,而我們編寫的那些方法一般都是運作在棧中,是以面向對象的設計是一種非常完美的設計方式,它完美的統一了資料存儲和運作。

二 JAVA垃圾收集器

2.1 垃圾收集簡史

垃圾收集提供了記憶體管理的機制,使得應用程式不需要在關注記憶體如何釋放,記憶體用完後,垃圾收集會進行收集,這樣就減輕了因為人為的管理記憶體而造成的錯誤,比如在C++語言裡,出現記憶體洩露時很常見的。Java語言是目前使用最多的依賴于垃圾收集器的語言,但是垃圾收集器政策從20世紀60年代就已經流行起來了,比如Smalltalk,Eiffel等程式設計語言也內建了垃圾收集器的機制。

2.2 常見的垃圾收集政策

JVM記憶體模型及垃圾收集政策解析(一)

所有的垃圾收集算法都面臨同一個問題,那就是找出應用程式不可到達的記憶體塊,将其釋放,這裡面得不可到達主要是指應用程式已經沒有記憶體塊的引用了,而在JAVA中,某個對象對應用程式是可到達的是指:這個對象被根(根主要是指類的靜态變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。

2.2.1 Reference Counting(引用計數)

引用計數是最簡單直接的一種方式,這種方式在每一個對象中增加一個引用的計數,這個計數代表目前程式有多少個引用引用了此對象,如果此對象的引用計數變為0,那麼此對象就可以作為垃圾收集器的目标對象來收集。

優點:

簡單,直接,不需要暫停整個應用

缺點:

     1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數的操作,比如每次将對象指派給新的引用,或者者對象的引用超出了作用域等。

     2.不能處理循環引用的問題

2.2.2 跟蹤收集器

跟蹤收集器首先要暫停整個應用程式,然後開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用,這裡面有三個問題需要搞清楚:

1.如果每次掃描整個堆,那麼勢必讓GC的時間變長,進而影響了應用本身的執行。是以在JVM裡面采用了分代收集,在新生代收集的時候minor gc隻需要掃描新生代,而不需要掃描老生代。

2.JVM采用了分代收集以後,minor gc隻掃描新生代,但是minor gc怎麼判斷是否有老生代的對象引用了新生代的對象,JVM采用了卡片标記的政策,卡片标記将老生代分成了一塊一塊的,劃分以後的每一個塊就叫做一個卡片,JVM采用卡表維護了每一個塊的狀态,當JAVA程式運作的時候,如果發現老生代對象引用或者釋放了新生代對象的引用,那麼就JVM就将卡表的狀态設定為髒狀态,這樣每次minor gc的時候就會隻掃描被标記為髒狀态的卡片,而不需要掃描整個堆。具體如下圖:

JVM記憶體模型及垃圾收集政策解析(一)

3.GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用主要有四種:Strong reference,Soft reference,Weak reference,Phantom reference.

◆ Strong Reference

強引用是JAVA中預設采用的一種方式,我們平時建立的引用都屬于強引用。如果一個對象沒有強引用,那麼對象就會被回收。

  1. public void testStrongReference(){  
  2. Object referent = new Object();  
  3. Object strongReference = referent;  
  4. referent = null;  
  5. System.gc();  
  6. assertNotNull(strongReference);  

◆ Soft Reference

軟引用的對象在GC的時候不會被回收,隻有當記憶體不夠用的時候才會真正的回收,是以軟引用适合緩存的場合,這樣使得緩存中的對象可以盡量的再記憶體中待長久一點。

  1. Public void testSoftReference(){  
  2. String  str =  "test";  
  3. SoftReference<String> softreference = new SoftReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNotNull(softreference.get());  
  7. }  

Weak reference

弱引用有利于對象更快的被回收,假如一個對象沒有強引用隻有弱引用,那麼在GC後,這個對象肯定會被回收。

  1. Public void testWeakReference(){  
  2. String  str =  "test";  
  3. WeakReference<String> weakReference = new WeakReference<String>(str);  
  4. str=null;  
  5. System.gc();  
  6. assertNull(weakReference.get());  
  7. }  

Phantom reference

 Phantom Reference(幽靈引用) 與 WeakReference 和 SoftReference 有很大的不同,  因為它的 get() 方法永遠傳回 null, 這也正是它名字的由來.PhantomReference 唯一的用處就是跟蹤 referent  何時被 enqueue 到 ReferenceQueue 中.

<插入部分>

RererenceQueue 

當一個 WeakReference 開始傳回 null 時, 它所指向的對象已經準備被回收, 這時可以做一些合适的清理工作.   将一個 ReferenceQueue 傳給一個 Reference 的構造函數, 當對象被回收時, 虛拟機會自動将這個對象插入到 ReferenceQueue 中, WeakHashMap 就是利用 ReferenceQueue 來清除 key 已經沒有強引用的 entries.

Java代碼

  1. [email protected]  
  2. 2.public void referenceQueue() throws InterruptedException {  
  3. 3.    Object referent = new Object();       
  4. 4.    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();  
  5. 5.    WeakReference<Object> weakReference = new WeakReference<Object>(referent, referenceQueue);  
  6. 6.      
  7. 7.    assertFalse(weakReference.isEnqueued());  
  8. 8.    Reference<? extends Object> polled = referenceQueue.poll();  
  9. 9.    assertNull(polled);  
  10. 10.      
  11. 11.    referent = null;  
  12. 12.    System.gc();  
  13. 13.  
  14. 14.    assertTrue(weakReference.isEnqueued());  
  15. 15.    Reference<? extends Object> removed = referenceQueue.remove();  
  16. 16.    assertNotNull(removed);  
  17. 17.}  

複制代碼

6.  PhantomReference  vs WeakReference 

PhantomReference  有兩個好處, 其一, 它可以讓我們準确地知道對象何時被從記憶體中删除, 這個特性可以被用于一些特殊的需求中(例如 Distributed GC,  XWork 和 google-guice 中也使用 PhantomReference 做了一些清理性工作).

其二, 它可以避免 finalization 帶來的一些根本性問題, 上文提到 PhantomReference 的唯一作用就是跟蹤 referent 何時被 enqueue 到 ReferenceQueue 中,  但是 WeakReference 也有對應的功能, 兩者的差別到底在哪呢 ?

這就要說到 Object 的 finalize 方法, 此方法将在 gc 執行前被調用, 如果某個對象重載了 finalize 方法并故意在方法内建立本身的強引用,  這将導緻這一輪的 GC 無法回收這個對象并有可能

引起任意次 GC, 最後的結果就是明明 JVM 内有很多 Garbage 卻 OutOfMemory, 使用 PhantomReference 就可以避免這個問題, 因為 PhantomReference 是在 finalize 方法執行後回收的,也就意味着此時已經不可能拿到原來的引用,  也就不會出現上述問題,  當然這是一個很極端的例子, 一般不會出現.

<插入部分/>

2.2.2.1 Mark-Sweep Collector(标記-清除收集器)

标記清除收集器最早由Lisp的發明人于1960年提出,标記清除收集器停止所有的工作,從根掃描每個活躍的對象,然後标記掃描過的對象,标記完成以後,清除那些沒有被标記的對象。

優點:

1 解決循環引用的問題

2 不需要編譯器的配合,進而就不執行額外的指令

缺點:

1.每個活躍的對象都要進行掃描,收集暫停的時間比較長。

2.2.2.2 Copying Collector(複制收集器)複制收集器将記憶體分為兩塊一樣大小空間,某一個時刻,隻有一個空間處于活躍的狀态,當活躍的空間滿的時候,GC就會将活躍的對象複制到未使用的空間中去,原來不活躍的空間就變為了活躍的空間。複制收集器具體過程可以參考下圖:

JVM記憶體模型及垃圾收集政策解析(一)

優點:

1 隻掃描可以到達的對象,不需要掃描所有的對象,進而減少了應用暫停的時間

缺點:

1.需要額外的空間消耗,某一個時刻,總是有一塊記憶體處于未使用狀态

2.複制對象需要一定的開銷

2.2.2.3 Mark-Compact Collector(标記-整理收集器)标記整理收集器汲取了标記清除和複制收集器的優點,它分兩個階段執行,在第一個階段,首先掃描所有活躍的對象,并标記所有活躍的對象,第二個階段首先清除未标記的對象,然後将活躍的的對象複制到堆得底部。标記整理收集器的過程示意圖請參考下圖:Mark-compact政策極大的減少了記憶體碎片,并且不需要像Copy Collector一樣需要兩倍的空間。

JVM記憶體模型及垃圾收集政策解析(一)

2.3 JVM的垃圾收集政策

GC的執行時要耗費一定的CPU資源和時間的,是以在JDK1.2以後,JVM引入了分代收集的政策,其中對新生代采用"Mark-Compact"政策,而對老生代采用了“Mark-Sweep"的政策。其中新生代的垃圾收集器命名為“minor gc”,老生代的GC命名為"Full Gc 或者Major GC".其中用System.gc()強制執行的是Full Gc.

2.3.1 Serial Collector

Serial Collector是指任何時刻都隻有一個線程進行垃圾收集,這種政策有一個名字“stop the whole world",它需要停止整個應用的執行。這種類型的收集器适合于單CPU的機器。

Serial Copying Collector

此種GC用-XX:UseSerialGC選項配置,它隻用于新生代對象的收集。1.5.0以後。-XX:MaxTenuringThreshold來設定對象複制的次數。當eden空間不夠的時候,GC會将eden的活躍對象和一個名叫From survivor空間中尚不夠資格放入Old代的對象複制到另外一個名字叫To Survivor的空間。而此參數就是用來說明到底From survivor中的哪些對象不夠資格,假如這個參數設定為31,那麼也就是說隻有對象複制31次以後才算是有資格的對象。這裡需要注意幾個個問題:

◆  From Survivor和To survivor的角色是不斷的變化的,同一時間隻有一塊空間處于使用狀态,這個空間就叫做From Survivor區,當複制一次後角色就發生了變化。

◆  如果複制的過程中發現To survivor空間已經滿了,那麼就直接複制到old generation.

◆  比較大的對象也會直接複制到Old generation,在開發中,我們應該盡量避免這種情況的發生。

Serial  Mark-Compact Collector

串行的标記-整理收集器是JDK5 update6之前預設的老生代的垃圾收集器,此收集使得記憶體碎片最少化,但是它需要暫停的時間比較長。

2.3.2 Parallel Collector 

Parallel Collector主要是為了應對多CPU,大資料量的環境。Parallel Collector又可以分為以下兩種:

Parallel Copying Collector

此種GC用-XX:UseParNewGC參數配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。1.4.1以後Parallel Mark-Compact Collector,此種GC用-XX:UseParallelOldGC參數配置,此GC主要用于老生代對象的收集。1.6.0

Parallel scavenging Collector

此種GC用-XX:UseParallelGC參數配置,它是對新生代對象的垃圾收集器,但是它不能和CMS配合使用,它适合于比較大新生代的情況,此收集器起始于jdk 1.4.0。它比較适合于對吞吐量高于暫停時間的場合,Serial gc和Parallel gc可以用如下的圖來表示:

JVM記憶體模型及垃圾收集政策解析(一)

2.3.3 Concurrent Collector

Concurrent Collector通過并行的方式進行垃圾收集,這樣就減少了垃圾收集器收集一次的時間,這種GC在實時性要求高于吞吐量的時候比較有用。此種GC可以用參數-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

JVM記憶體模型及垃圾收集政策解析(一)

繼續閱讀