1. 程式人生 > >JVM GC之一找出不可達物件並回收

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