重識JVM(四):GC

垃圾收集(GC)是java的重中之重,相信大家很多人喜歡使用java語言進行開發,就是因爲喜歡這一特性吧。

要了解垃圾收集,那麼我們肯定得弄明白三個問題:

(1)哪些內存需要回收?

(2)什麼時候回收?

(3)如何回收?

1.哪些內存需要回收?

堆裏存放着幾乎所有對象實例,垃圾收集器在堆上進行回收之前,一定要做的事就是要確認哪些對象還“存活”,而哪些對象已經“死亡”了,這些已經死亡的對象無疑就是要回收的了。如何判斷對象已經死亡了呢?

1.1引用計數法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計算器爲0 的對象就是不可能再被使用的。
主流的Java 虛擬機中沒有選用計數算法來管理內存,最主要的原因是它很難就解決對象之間相互循環引用的問題。A引用B,B引用A,除此之外再無引用,但是計數器不爲0,所以就不會被回收。

1.2可達性分析算法

主流的商用程序語言的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱爲GC Roots 的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots 沒有任何引用鏈相連時,則證明此對象是不可達的。

在Java 中,可作爲GC Roots 的對象包括:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象
(2)方法區中類靜態屬性引用的對象
(3)方法區中常量引用的對象
(4)本地方法棧中JNI(一般說的Native 方法)引用的對象

1.3細談引用

無論是通過哪種算法判斷對象存活都與“引用”有關。引用分爲強引用,軟引用,弱引用,虛引用4種,這4種引用的強度依次減弱。

強引用:

平時我們編程的時候例如:Object object=new Object();那object就是一個強引用了。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

軟引用(SoftReference):

如果一個對象只具有軟引用,那就是有用但非必須的對象。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。 軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

弱引用(WeakReference):

如果一個對象只具有弱引用,那就是非必須的對象。弱引用與軟引用的區別在於:只具有弱引用的對象擁有 短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。 弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯 的引用隊列中。

虛引用(PhantomReference):

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。 虛引用主要用來跟蹤對象被垃圾回收的活動。爲一個對象設置虛引用的唯一目的就是能在這個對象被回收時收到一個系統通知。

1.4對象究竟是生存還是死亡呢?

即使在可達性分析中不可達的對象,也並非是非死不可。要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots 相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件就是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()已經被虛擬機調用過,虛擬機將這兩種情況視爲沒有必要執行。
如果這個對象被判爲有必要執行finalize() 方法,那麼這個對象將會放置在一個叫做F-Queue 隊列之中,並在稍後由一個虛擬機自動建立的、低優先級的Finalizer 線程去執行它。這裏所謂的執行是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環,將很可能會導致F-Queue 隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC 將會對F-Queue 中的對象進行第二次小規模的標記,如果對象要在finalize()中拯救自己,只要重新與引用鏈上的任何一個對象建立聯繫即可,比如把自己this 複製給某個類變量或對象的成員變量,那在第二次標記時它將被移出即將回收的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。任何一個對象的finalize()方法都只會被系統調用一次,如果對象面臨下一次回收,它的finalize()方法不
會被再次執行。

finalize()能做的所有工作,使用try/finally或其他方式都能做的更好,更及時,所以完全可以忘記它的存在。

1.5回收方法區

在方法區(永久代)中進行垃圾收集的性價比較低:在堆中,尤其在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量和回收Java堆中的對象類似。以常量池中字面量的回收爲例,沒有任何String 對象引用常量池中的某個字符串常量,這個常量就會被系統清理出常量池。常量池中的其他類、方法、字段的符號引用也與此類似。
判定一個類是否是無用的類的條件比較苛刻,需要同時滿足以下三個條件:
1)該類的所有實例都已經被回收
2)加載該類的類加載器已經被回收
3)該類對應的Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3 個條件的無用類進行回收,這裏說的僅僅是可以,而不是和對象一樣,不適用了就必然會被回收。是否對類回收,HotSpot 虛擬機提供了參數進行控制。在大量使用反射、動態代理、CGLib 等ByteCode 框架,動態生成JSP 以及OSGi 這類頻繁自定義ClassLoader 的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

2.GC 算法

2.1標記-清除算法
最基礎的收集算法是標記-清除算法(Mark-Sweep),算法分爲標記清除兩個階段。首先標記處所有需要回收的對象,在標記完成後統一回收所有被標記的對象。他的不足主要有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後再程序運行過程中需要分配較大對象時, 無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

2.2複製算法
爲了解決效率問題,出現了複製算法。它將可用內存按容量劃分爲大小相等的兩塊,每次只是用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存空間,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免太高了一點。

現在的商業虛擬機都採用這種收集算法來回收新生代。將內存分爲一塊較大的Eden 空間兩塊較小的Survivor 空間,每次都使用Eden 和其中一塊Survivor。當回收時,將Eden 和Survivor 中還存活着的對象一次性地複製到另外一塊Survivor 空間上,最後清理掉Eden 和剛纔用過的Survivor 空間。HotSpot 虛擬機默認Eden 和Survivor 的大小比例是8:1當Survivor 空間不夠用時,需要依賴其他內存(老年代)進行分配擔保。

2.3標記-整理算法
複製收集算法在對象存活率較高時,效率就會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,所以老年代一般不能直接選用這種算法。根據老年代的特點,有人提出一種標記-整理算法(Mark-Compact),標記過程仍然與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的內存。

2.4分代收集算法
當前商業虛擬機的垃圾收集都採用分代收集算法(Generational Collection),這種算法是根據對象存活週期的不同將內存劃分爲適當的幾塊。一般是把Java 堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清除或者標記-整理算法來進行回收。

3.GC

Minor GC
從年輕代空間(包括Eden 和Survivor 區域)回收內存被稱爲Minor GC。
非常頻繁,回收速度較快。
各種Young GC 的觸發原因都是eden 區滿了
Full GC
收集整個堆,包括年輕代、老年代、元數據區等所有部分。
速度較慢。
觸發原因不確定,因具體垃圾收集器而異。
比如老年代內存不足,ygc 出現promotion failure,System.gc()等。
CMS 垃圾收集器不能像其他垃圾收集器那樣等待年老代機會完全被填滿之後再進行收集,
需要預留一部分空間供併發收集時的使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章