JVM內存回收算法以及垃圾收集器

垃圾收集器需要完成的三件事

1)哪些內存需要回收?

2)什麼時候回收?

3)如何回收?

一:需要考慮內收回收的區域

wKioL1NLaiDgIrpAAAHfP883N7U891.jpg

也就說線程私有的區域(java stack、native java stack pc register)伴隨的線程的產生而產生伴隨的線程的消亡而回收。所以我們關心內存回收區域爲:heap、以及method area

對象是否已死

根搜索算法:

wKiom1NLaoOjDzCcAACpQEkVOsE743.jpg


這個算法的基本思路爲:就是通過一系列名爲“GC Roots “的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑爲稱爲引用鏈(Reference Chain),當一個對象到GC ROOT 沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象不可以用,也就是說這些對象可以回收

引用概念

根搜索算法涉及到引用,判斷對象的存活都跟“引用”有關。

如果引用類型的數據中存儲的數值代表的是另一個內存的起始地址,就稱這塊內存代表着一個引用。

JDK1.2以後又把引用分爲:強引用(strong reference)、軟引用(soft reference弱引用(weak reference)、虛引用(phantom  reference)四種,這四種引用強度一次逐漸減弱。

1)強引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()“,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

2)軟引用 用來描述一些還有用,但並非必需對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的內存,纔會拋出內存溢出異常。

3)弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

4)虛擬引用也稱爲幽靈引用或者幻影引用。它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象的設置虛引用關聯的唯一目的就是希望能在這個對象被收集回收時受到一個系統通知。

對象生存還是死亡?

在根搜索算法中不可達對象,也並非是該對象“非死不可“的,這時候它們暫時處於待“處理狀態”。要真正判斷一個對象可以回收(死亡),至少經歷2次標記:

一)如果對象在根搜索算法之後,有沒有與GCroots 相連接的引用鏈。這是第一個標記。

二)有第一次標記的之後的篩選的對象,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將兩種的情況都視爲“沒必要執行”。如果該對象“沒必要執行”finalize()方法時可以直接進行回收該對象。

如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。

如果對象這時候還沒逃脫,那它就真的離死不遠了。

wKiom1NLaujCFTrXAAH_janRX1U293.jpg

注意:

第一,finalize()方法只會被執行一次,所以對象只有一次復活的機會。

第二,執行GC後,要停頓半秒等待優先級很低的finalize()執行完畢。

第三:finalize()方法 並不是並需的,經查看一些資料,該方法儘量少用。因爲該方法的實現的”代價比較高昂“,一般在研發在編寫代碼的時候很少用finalize()方法來實現對象自救。

第四:即使該對象實現一次”自救“,但是面臨下次回收的時候,還是會直接被回收,而不會再次 執行 finalize()方法。

方法區回收:

主要垃圾回收區域:廢棄常量和無用的類。

廢棄常量:回收廢棄常量與回收java堆中的對象非常類似。比如以常量池中的字面量的回收爲例。

wKioL1NLauziGJ4rAACEzC5wd4Y985.jpg

假設字符串abc在常量池中,但是當前系統沒有任何一個string對象叫做“abc”的。換句話說是沒有任何的string對象引用常量池中的“abc”常量,也沒有其他的地方引用這個字面量。如果在這個時候發生內存回收的,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用頁與此類似。

無用類回收:判斷一個無用類需要同時滿足以下3類條件:

1)該類所有實例都已經被回收,也就是java堆中不存在該類的任何實例。

2)加載該類的ClassLoader 已經被回收。

3)該類對應的java.lang.Class(類運行標識符)對象沒有在任何地方被引用,無法再任何地方通過訪問該類的方法。

同時滿足這3個條件僅僅是“可以”回收,而不是對象經過2次標記之後就進行回收。

二:垃圾收集算法:

標記-清除算法:算法分爲“標記”和清除兩個階段。

首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的。

缺點:效率問題,標記和清除過程的效率都不高;

空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序以後的運行過程中需要分配較大對象的時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

wKiom1NLaz3Q38OxAADm0BYrYE8440.jpg

複製算法:

爲了解決標記清除算法的效率問題出現複製算法。

原理:它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活的對象複製到另一個塊上面。然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對其中的一塊進行內存回收,內存分配的時候也就不用考慮內存碎片等複雜情況。

缺點:用空間換效率導致內存使用率爲原來的一半。

wKiom1NLa2DSOUb7AAG76hzyLPY308.jpg

應用場景:

現在大多數的商業虛擬機都採用複製收集算法來回收新生代。主要是針對新生代存活對象較少的特點。其中由IBM研究指出不需要把可用內存分配成1:1的比例,而是8:1:1的比例。將可用內存分爲2個區,而這2個區分爲3個區域分別爲:Eden、Survivor、Survivor 3個區域。其中三者的比例爲:8:1:1。其中每次使用一個Eden和一個Survivor,而另一個Survivor爲保留區域。也就是每次新生代可使用的內存空間爲整個新生代容量的90%,只有10%的新生代容量會被浪費。

問題:那我們每次都能保證回收後剩餘的存活的對象都小於10%的新生代容量嗎?

事實上這種回收機制可以滿足98%的需求,當Survivor的內存空間大小不能滿足的時候,需要依賴其他內存(這裏指老年代)進行分配擔保。這些對象直接通過分配擔保機制進入老年代。

標記整理-算法:

複製算法主要用再新生代回收中,那老年代怎麼進行收集呢?

針對存活對象超過保留區域的情況,顯然老年代不適合這種算法(老年代存活對象較高的特點)。

原理:標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

wKioL1NLa2HhloS1AAGa8UA4_tU831.jpg

分代收集算法

現在大多數虛擬機收集都採用分代收集算法。

所謂的新生代和老年代是針對分代收集算法來定義。

新生代經過一次GC回收之後就立即進入老年代嗎?

新生代收集的算法是根據標記-複製算法。也就是數據首先分配到Eden區當中(特殊情況:如果是大對象那麼會直接放入到老年代(大對象需要大量連續的內存空間的java對象)),當Eden沒有足夠空間的時候就會觸發JVM發起一次Monitor GC。如果對象經過一次垃圾回收還存活,並且又能被Survivor空間接受的,那麼將移動到Survivor空間當中。並將其年齡設定爲1,對象在survivior每熬過一次垃圾回收,年齡會增加1,當年齡達到一定程度(默認是15)時,就會被晉升到老年代中。這個默認值可以進行修改。

-XX:MaxTenuringThreshold=0:設置垃圾最大年齡。如果設置爲0的話,則年輕代對象不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設置爲一個較大值,則年輕代對象會在Survivor區進行多次複製,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。

只是根據對象的存活週期的不用將內存劃分爲幾塊。一般分爲:heap分爲新生代和老年代,根據每個年代的特點採取相應的算法收集。

wKiom1NLa7njxFdIAAEBDPKTLQY336.jpg

垃圾收集器

如果說收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現。

Serial收集器

原理:Serial收集器是單線程收集器,當回收內存的時候會暫停其他的工作線程運行,直到回收完畢。

缺點:用戶體驗差。程序容忍度低(運行一半的時候,暫停運行。)

Java虛擬機在啓動包括2種模式:client模式和server模式。

Client模式:啓動速度較快,但是性能比server模式的差很多。

Server模式:啓動比client 慢10%,但是性能遠遠好於clent模式。

在java程序的啓動的時候指定:-client或者- server來判斷啓動模式

大多數應用環境採用的是server模式啓動。

如何查看java虛擬機啓動的模式:

1)查看啓動腳本:export JAVA_OPTS="-Djava.library.path=/X/X   -server -Xms2048m -Xmx2048m

2)使用java–version  查看:

/X/X/jdkX/bin/java-version

javaversion "X"

Java(TM)SE Runtime Environment (x)

JavaHotSpot(TM) 64-Bit Server VM (x)

性能比較:

wKiom1NLa-2T5iupAAHSx04AhFE038.jpg

適用場景:

當jvm用於啓動圖形用戶(GUI)界面交互的時候適合適用client模式,當jvm用於運行後臺程序的時候建議使用server模式。

Serial收集器雖然缺點比較明顯,但是它依然是jvm在client模式默認新生代收集器。

優點:

簡單高效(與其他的收集器的單線程相比),對於限定單個cpu的環境來說,serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。因爲client模式,大多數用於gui交互界面,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆或者幾百兆,停頓時間大約幾十毫秒最多一百毫秒以內,只要不頻繁發生,這點可以接受的。所以serial收集器對於運行在client模式下的虛擬機來說是一個很好的選擇。

ParNew收集器

  Parnew收集器是serial收集器的多線程版本。

適用場景:ParNew收集器大多運行在jvm的server模式下,首選的新生代收集器。其中與性能無關的很重要原因是除了serial收集器,目前只有ParNew收集器可以CMS收集器配合工作。

效率:ParNew收集器在單cpu的環境中絕對不會比serial收集器更好的效果,甚至由於存在線程交互開銷。該收集器在通過超線程技術實現的兩個的cpu的環境中都不能百分百超越serial收集器。

概念:並行指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

併發指用戶線程與垃圾收集線程同時執行(但不一定是並行,可能會交替執行),用戶程序繼續運行,而垃圾收集程序運行於另一個cpu上。


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