JVM GC之一找出不可達對象並回收

JAVA運行時數據區域
     1、程序計數器:當前線程所執行的字節碼的行號指示器。一個處理器只會執行一條線程中的指令,爲了線程切換後能回覆到正確的執行位置,所以每條線程都需要一個獨立的計數器。各條線程之間互不影響,獨立存儲,屬於‘線程私有’內存

     2、java虛擬機棧:描述的是JAVA方法執行的內存模型:每個方法執行的時候都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法的被調用直至執行完成的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。所以也是線程私有的

     3、本地方法棧:和java虛擬機棧發揮的作用類似,只不過Java虛擬機棧是爲JAVA方法服務的,而本地方法棧是爲Native方法服務。所以也是線程私有的。

     4、JAVA堆:JAVA堆是被所有線程共享的區域。所有的對象實例及數組都要在堆上分配。

     5、方法區:是各個線程共享的內存區域。主要存儲被虛擬機加載的類信息,常量、靜態變量、編譯後的字節碼數據等。有一個別名:非堆。

     6、運行時常量池:方法區的一部分,當然也是線程共享的咯。除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放各種字面量和符合引用。

     7、直接內存:並不是虛擬機運行時的數據區的一部分。是在NIO中基於通道和緩衝區的I/O方式,使用Native函數庫直接分配堆外內存。避免了JAVA堆和Native堆中來回複製數據。和(操作系統中內存頁的用戶空間和系統空間的虛擬映象類似)

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

垃圾收集器
     1、程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此內存的分配和回收都具備確定性。因此不需要考慮回收問題,因爲線程結束或者方法結束,內存自然就回收了。而Java堆和方法去不一樣,一個接口中的多個實現類需要的內存不一樣,一個方法中的多個分支也不一樣,只有在程序處於運行時才能知道創建哪些內存,所以這部分的內存的分配和回收都是動態的。

     2、引用計數算法
          引用計數:每當被引用時引用計數加1,有引用斷開時引用計數減1.當引用計數爲0時表示該對象可以被回收。JVM並不是通過引用計數算法來進行回收的,主要原因是很難解決對象之間的相互循環引用的問題。

     3、根搜索算法:GC Roots
         JVM是通過根搜索算法判定對象算法存活的。算法的基本思路是:通過一系列的名爲GC Roots (GC 根節點)的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑,當一個對象到GC Roots沒有任何引用鏈相連(圖論說:從GC Roots到這個對象不可達)時, 證明此對象是不可用的。
     
     4、可以作爲GC Roots的對象
     虛擬機棧(棧幀中的本地變量表)中引用的對象。
     方法區中的類靜態屬性引用的對象
     方法區中的常量引用的對象
     本地方法棧JNI中的引用的對象。

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

對象Life or death

     1、根搜索算法中不可達的對象也並非是‘非死不可’的,暫時是‘緩刑’階段,要真正判斷一個對象死亡要經歷兩次標記過程:如果對象在進行根搜索後發現對象不可達,那它將會進行被第一次標記並且進行篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機掉用過,這兩種情況都視爲‘沒有必要執行’。
     如果對象被認爲有必要執行finalize()方法,那麼這個方法會被放置在一個名爲F-Queue的隊列之中,並在稍後由一條由虛擬機自動建立的、低優先級的Finalizer線程去執行。這裏的‘執行’也只是指虛擬機會觸發這個方法,但並不承諾一定會執行。
     finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC會對F-Queue中的對象進行第二次小規模的標記,如果對象在finalize()中重新與引用鏈上的任何一個對象建立了關聯,就會被移出‘即將回收’集合,如果沒有移出,那就真的離死亡不遠了。
     finalize()方法只會被系統自動調用一次。

============================================
 
回收方法區(HotSpot虛擬機中的永久代)

     1、Java虛擬機規範中說過可以不要求虛擬機在方法區實行垃圾收集,在方法區進行垃圾回收的‘性價比’一般比較低。在堆中尤其是新生代中,常規的一次垃圾回收就可以回收70%--90%的空間,而永久代的垃圾收集效率遠低於此。

     2、永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

     3、如果字符串‘abc’進入了常量池,但是當前系統中沒有任何String對象引用‘abc’常量,也沒有其他地方引用了這個字面量,如果此時發生內存回收,而且必要的話 這個‘abc’常量就會被回收掉。

     4、判斷無用的類必須同時滿足三個條件:
          該類的所有實例都已經被回收,即JAVA堆中不存在該類的任何實例。
          加載該類的ClassLoader已經被回收
          加載該類的Class對象沒有任何地方引用,而且不能通過反射訪問。

    5、在大量使用反射、動態代理、CGLib及動態生成JSP和頻繁自定義ClassLoader的場景需要虛擬機自動卸載,以防止永久代不會溢出。

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

垃圾收集算法:標記--清楚算法、複製算法、標記-整理算法、分代收集算法

1、標記--清除算法
     最基礎的算法,分爲標記和清除兩個階段:
          標記:首先標記出需要清除的對象,標記的過程就是上面的對象Life or death。之所以說是基礎的算法,是因爲後續的算法都是基於這個算法改進的。
     該算法有兩個缺點:效率和空間問題(其實計算機最糾結的無外乎這兩個地方:時間和空間問題)。標記和清除的效率都不高。空間問題:標記清除之後會產生大量的碎片,可能會導致,當程序在以後的運行過程中需要分配較大的對象時無法找到足夠的連續內存而不得不提前觸發另一次收集動作。

2、複製算法
     複製算法可以解決算法1的效率問題。將可用內存按容量劃分成大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完時,就吧還活着的對象複製到另一塊上面,然後再把已使用的內存一次性清楚乾淨。這樣使得每次都是對其中的一塊內存回收,內存分配時也不用考慮內存碎片問題,只要移動堆頂指針即可。但是代價是內存縮小爲原來的一半。

     現在的商業虛擬機都採用這中算法來回收新生代,因爲IBM研究表明新生代的的對象98%都是朝生夕死的,所以不需要按照1:1的比例來劃分空間。
     而是把內存分成一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor空間。當回收時,把Eden和Survivor中還活着的對象一次性拷貝到Survivor空間中,最後清理掉Eden和Survivor空間。HotSpot默認Eden和Survivor空間的大小是8:1,這樣可以保證每次可用內存是90%(80%+10),只有10%是浪費的。
     當Survivor空間不足時就要依賴老年代進行分配擔保(就是說 有大頭(老年代)在後面,才放心只留出10%來存放Eden和Survivor中活着的對象,萬一Survivor不足時還有老年代在後面撐腰)。


3、標記-整理算法

     複製算法在對象存活率較高時就要執行交多的複製操作,效率會變低。重點是如果不想浪費掉50%的空間就需要有額外的空間進行擔保,以應對被使用的內存中有100%存活的極端情況,所以老年代一般不能直接使用這一算法。因爲老年代沒有其他的內存可擔保了。

     根據老年代的特定,提出了 ‘標記--整理’算法。原理是:標記過程仍然與‘標記--清除’算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有活着的對象都向一端移動,然後直接清理邊界以外的內存。


4、分代算法
     當前商業虛擬機都採用這種算法回收。沒有什麼新的思想,只是根據對象的存活週期把內存劃分成幾塊,一般是新生代和老生代。這樣可以根據各個年代的特定採用合適的算法進行垃圾收集。
     在新生代中,對象的存活週期較短,朝生夕死,採用複製算法。只需要付出少量存活對象的複製成本就可以完成收集。
     在老生代中,對象的存活率較高,沒有額外的空間對它進行分配擔保,必須使用‘標記--清除’或‘標記--整理’算法進行回收。















































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