1. 程式人生 > >對GC自動回收機制的理解

對GC自動回收機制的理解

參考文章:https://www.cnblogs.com/wjtaigwh/p/6635484.html,在他的基礎上加入自己理解的一些東西

GC的簡單瞭解

        GC:(Garbage Collection),是垃圾回收器,釋放垃圾佔用的記憶體。讓建立的物件不需要像c、c++那樣delete、free掉。對於c、c++開發來說,記憶體是開發人員分配的,也就是說還要對記憶體進行維護和釋放。對於Java程式設計師來說,一個物件的記憶體分配是在虛擬機器的自動記憶體分配機制的幫助下,不再需要為每一個new操作去寫配對的delete/free程式碼,而且不容易出現記憶體洩漏和記憶體溢位的問題。但是,如果出現了記憶體洩漏和記憶體溢位的問題,而開發者又不瞭解怎麼分配記憶體的話,那麼定位錯誤和排除錯誤將是一件很困難的事情。

下面是JVM的記憶體管理結構:

或者是這種:

1.程式計數器(Program Counter Register)程式計數器是一個比較小的記憶體區域,用於指示當前執行緒所執行的位元組碼執行到了第幾行,可以理解為是當前執行緒的行號指示器。位元組碼直譯器在工作時,會通過改變這個計數器的值來取下一條語句指令。

        每個程式計數器只用來記錄一個執行緒的行號,所以它是執行緒私有(一個執行緒就有一個程式計數器)的。

        如果程式計數器執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值為Undefined,由於程式計數器只是記錄當前指令地址,所以不存在記憶體溢位的情況,因此,程式計數器也是所有JVM記憶體區域中唯一一個沒有定義OutOfMemoryError的區域。

 2.虛擬機器棧(JVM Stack):一個執行緒的每個方法在執行的同事時,都會建立一個棧幀(Stack Frame),棧幀中儲存的有區域性變量表、操作站、動態連結、方法出口等,當方法被呼叫時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。

區域性變量表中儲存著方法的相關區域性變數,包括各種基本資料型別,物件的引用,返回地址等。在區域性變量表中,只有long和double型別會佔用2個區域性變數空間(Slot,對於32位機,一個Slot就是32個bit),其他都是1個Slot。需要注意的是區域性變量表在編譯時就已經確定好的,方法執行所需要分配的空間在棧幀中是完全確定的,在方法的生命週期內都不會改變。

虛擬機器棧中定義了兩種異常,如果執行緒呼叫的棧深度大於虛擬機器允許的最大深度,則丟擲StackOverFlowError(棧溢位);不過多數Java虛擬機器都允許動態擴充套件虛擬機器棧的大小(有少部分是固定長度的),所以執行緒可以一直申請棧,直到記憶體不足,此時,會丟擲OutOfMemoryError(記憶體溢位)。

每個執行緒對應著一個虛擬機器棧,因此虛擬機器棧也是執行緒私有的。

3、本地方法棧(Native Method Stack):本地方法棧在作用、執行機制、異常型別等方面都與虛擬機器棧相同,唯一的區別是:虛擬機器棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機器中(如Sun的JDK預設的HotSpot虛擬機器),會將本地方法棧與虛擬機器棧放在一起使用。

本地方法 棧也是執行緒私有的。

4、堆區(Heap):堆區是理解Java GC最重要的區域沒有之一。在JVM所管理的記憶體中,堆區是最大的一塊,堆區也是Java GC機制所管理的主要記憶體區域,堆區由所有執行緒共享,在虛擬機器啟動時建立。堆區的存在是為了儲存物件例項,原則上講,所有的物件都在堆區上分配記憶體。

一般,根據Java虛擬機器規範規定,堆記憶體需要在邏輯上是連續的(在物理上不需要),在實現時,可以說固定大小的,也可以是可擴充套件的,目前主流的虛擬機器都是可擴充套件的。如果在執行垃圾回收之後,仍沒有足夠的記憶體分配,也不能再擴充套件,將會丟擲OutOfMemoryError:Java heap space 異常。

5、方法區(Method Area):在Java虛擬機器規範中,將方法區作為堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的部落格中,將Java GC的分代收集分為3個代:青年代,老年代,永久代,這些作者將方法區定義為“永久代”,這是因為,對於之前的HotSpot Java虛擬機器的實現方法中,將分代收集的思想擴充套件到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機器,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。

        方法區是各個執行緒共享的區域,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常量、靜態變數、編譯器即時編譯的程式碼等。

        方法區在物理上也不需要是連續的,可以選擇固定大小或可擴充套件大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱為永久代的原因之一(HotSpot),但這也不代表著在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常量池的記憶體回收和對已載入類的解除安裝。

        在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以後做進一步深入研究時使用。

        執行常量池(Runtime Constant Pool)是方法區的一部分,用於儲存編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是字串表示某個變數、介面的位置,直接引用就是根據符號引用翻譯出來的地址,將在類連結階段完成翻譯);執行時常量池除了儲存編譯器常量外,也可以儲存在執行時間產生的常量(比如String型別的intern()方法,作用是String維護了一個常量池,如果呼叫的字串“abc”已經在常量池中,則返回池中的字串地址,否則,新建一個常量加入池中,並返回地址)。

6、直接記憶體(Direct Memory):直接記憶體並不是JVM管理的記憶體,是JVM以外的機器記憶體,比如,你有4G的記憶體,JVM佔用了1G,則其餘的3G就是直接記憶體,JDK中有一種基於通道(Channel)和緩衝區(Buffer)的記憶體分配方式,將由C語言實現的native函式庫分配在直接記憶體中,用儲存在JVM堆中的DirectByteBuffer來引用。由於直接記憶體收到及其記憶體的限制,所以也可能出現OutOfMemoryError的異常。

JVM將堆分成了兩個大區新生代(Young)和老年代(Old),新生代又被進一步劃分為Eden和Survivor區,而Servivor由FromSpace和ToSpace組成,也有些人喜歡用Survivor1和Survivor2來代替。這裡為什麼要將Young劃分為Eden,Survivor1,Survivor這三塊,給出的解釋是:

Young中的98%的物件是朝生夕死,所以將記憶體分為一塊較大的Eden和兩塊較小的Survivor1、Survivor2,JVM預設分配是8:1:1,每次呼叫Eden和其中的Survivor1(FromSpace),當發生回收的時候,將Eden和Survivor1(FromSpace)存活的物件複製到Survivor2(ToSpace),然後直接清理掉Eden和Survivor1的空間。

堆結構如下圖:

新生代:新建立的物件都是新生代分配記憶體,Eden記憶體不足時,觸發Minor GC,這時會把存活的物件轉移進Survivor區。

老年代:老年代用於存放經過多次Minor GC之後仍然存活的物件。

新生代的GC(Minor GC):新生代通常存活時間較短基於Copying演算法進行回收,所謂Copying演算法就是掃描出存活的物件,並複製到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和FromSpace或ToSpace之間copy。新生代採用空閒指標的方式來控制GC觸發,指標保持最後一個分配的物件在新生代區間的位置,當有新的物件要分配記憶體時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配物件時,物件會逐漸從Eden到Survivor,最後到老年代。

老年代的GC(Major GC/Full GC):老年代與新生代不同,老年代物件存活時間比較長、比較穩定,因此採用標記(Mark)演算法進行回收,所謂標記就是掃描出存活的物件,然後再進行回收未被標記的物件,回收後對空出的空間要麼進行合併、要麼標記下來便於下次進行分配,總之目的就是要減少記憶體碎片帶來的效率損耗。

下面介紹幾種垃圾收集演算法:

1)      Mark-Sweep(標記-清除)演算法:                                                     

這是最基礎的垃圾回收演算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除演算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的物件,清除階段就是回收被標記的物件所佔用的空間。具體過程如下圖所示:

從圖中可以很容易看出標記-清除演算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生記憶體碎片,碎片太多可能會導致後續過程中需要為大物件分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

2)              Copy(複製)演算法

為了解決Mark-Sweep演算法的缺陷,Copying演算法就被提出來了。它將可用的記憶體按照容量劃分為大小相等的兩塊,每次使用其中的一塊。當這一塊的記憶體用完了,就將還活著的物件複製到另一塊上面,然後再把已使用的記憶體空間一次清理掉,這樣一來就不容易出現記憶體碎片的問題。具體過程如下圖所示:

這種演算法雖然實現簡單,執行高效且不容易產生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能夠使用的記憶體縮減到原來的一半。很顯然,Copying演算法的效率跟存活物件的數目多少有很大的關係,如果存活的物件很多,那麼Copying演算法的效率將會大大降低。我們的新生代GC演算法採用的就是這種演算法。

3)           Mark-Compact(標記-整理)演算法:

          為了解決Copying演算法的缺陷,充分利用記憶體空間,提出了Mark-Compact演算法。該演算法標記階段和Mark-Sweep一樣,但是完成標記之後,它不是直接清理掉可回收物件,而是將存活物件都向一端移動,然後清理掉端邊界以外的記憶體。具體過程如下圖所示:

                      

在一般廠商JVM中老年代GC就是使用的這種演算法,因為老年代的特點是每次都只回收少量物件。

上面是一些常見的垃圾收集演算法,垃圾收集演算法是記憶體回收的理論基礎,而垃圾收集器就是記憶體回收的具體實現。下面有幾種建立的垃圾收集器,使用者可以根據自己的需求組合出新生代和老年代使用的收集器。下面是常見的劃分辦法:

新生代GC:序列GC(SerialGC)、並行回收GC(ParallelScavenge)和並行GC(ParNew)

什麼時候使用GC

Java堆記憶體不足時,GC會被呼叫。當應用執行緒在執行,並在執行過程中建立新物件,若這時記憶體空間不足,JVM就會強制地呼叫GC執行緒,以便回收記憶體用於新的分配。若GC一次之後仍不能滿足記憶體分配的要求,JVM會再進行兩次GC做進一步的嘗試,若仍無法滿足要求,則JVM將報“out of memory”的錯誤,Java應用將停止。