天天看點

java 記憶體回收機制

在Java中,它的記憶體管理包括兩方面:記憶體配置設定(建立Java對象的時候)和記憶體回收,這兩方面工作都是由JVM自動完成的,降低了Java程式員的學習難度,避免了像C/C++直接操作記憶體的危險。但是,也正因為記憶體管理完全由JVM負責,是以也使Java很多程式員不再關心記憶體配置設定,導緻很多程式低效,耗記憶體。是以就有了Java程式員到最後應該去了解JVM,才能寫出更高效,充分利用有限的記憶體的程式。

1.Java在記憶體中的狀态

首先我們先寫一個代碼為例子:

package test;

import java.io.Serializable;

public class Person implements Serializable {

    static final long serialVersionUID = 1L;

    String name; // 姓名

    Person friend;    //朋友

    public Person() {}

    public Person(String name) {
        super();
        this.name = name;
    }
}      
package test;


public class Test{

    public static void main(String[] args) {
        Person p1 = new Person("Kevin");
        Person p2 = new Person("Rain");
        Person p3 = new Person("Sunny");

        p1.friend = p2;
        p3 = p2;
        p2 = null;
    }
}      

把上面Test.java中main方面裡面的對象引用畫成一個從main方法開始的對象引用圖的話就是這樣的(頂點是對象和引用,有向邊是引用關系):

當程式運作起來之後,把它在記憶體中的狀态看成是有向圖後,可以分為三種:

1)可達狀态:在一個對象建立後,有一個以上的引用變量引用它。在有向圖中可以從起始頂點導航到該對象,那它就處于可達狀态。

2)可恢複狀态:如果程式中某個對象不再有任何的引用變量引用它,它将先進入可恢複狀态,此時從有向圖的起始頂點不能再導航到該對象。在這個狀态下,系統的垃圾回收機制準備回收該對象的所占用的記憶體,在回收之前,系統會調用finalize()方法進行資源清理,如果資源整理後重新讓一個以上引用變量引用該對象,則這個對象會再次變為可達狀态;否則就會進入不可達狀态。

3)不可達狀态:當對象的所有關聯都被切斷,且系統調用finalize()方法進行資源清理後依舊沒有使該對象變為可達狀态,則這個對象将永久性失去引用并且變成不可達狀态,系統才會真正的去回收該對象所占用的資源。

上述三種狀态的轉換圖如下:

2.Java對對象的4種引用

1)強引用 :建立一個對象并把這個對象直接賦給一個變量,eg :Person person = new Person(“sunny”); 不管系統資源有麼的緊張,強引用的對象都絕對不會被回收,即使他以後不會再用到。

2)軟引用 :通過SoftReference類實作,eg : SoftReference p = new SoftReference(new Person(“Rain”));,記憶體非常緊張的時候會被回收,其他時候不會被回收,是以在使用之前要判斷是否為null進而判斷他是否已經被回收了。

3)弱引用 :通過WeakReference類實作,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管記憶體是否足夠,系統垃圾回收時必定會回收。

4)虛引用 :不能單獨使用,主要是用于追蹤對象被垃圾回收的狀态。通過PhantomReference類和引用隊列ReferenceQueue類聯合使用實作,eg :

package test;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;


public class Test{

    public static void main(String[] args) {
        //建立一個對象
        Person person = new Person("Sunny");    
        //建立一個引用隊列    
        ReferenceQueue<Person> rq = new ReferenceQueue<Person>();
        //建立一個虛引用,讓此虛引用引用到person對象
        PhantomReference<Person> pr = new PhantomReference<Person>(person, rq);
        //切斷person引用變量和對象的引用
        person = null;
        //試圖取出虛引用所引用的對象
        //發現程式并不能通過虛引用通路被引用對象,是以此處輸出為null
        System.out.println(pr.get());
        //強制垃圾回收
        System.gc();
        System.runFinalization();
        //因為一旦虛引用中的對象被回收後,該虛引用就會進入引用隊列中
        //是以用隊列中最先進入隊列中引用與pr進行比較,輸出true
        System.out.println(rq.poll() == pr);
    }
}      

運作結果:

3.Java垃圾回收機制

其實Java垃圾回收主要做的是兩件事:1)記憶體回收 2)碎片整理

3.1垃圾回收算法

1)串行回收(隻用一個CPU)和并行回收(多個CPU才有用):串行回收是不管系統有多少個CPU,始終隻用一個CPU來執行垃圾回收操作,而并行回收就是把整個回收工作拆分成多個部分,每個部分由一個CPU負責,進而讓多個CPU并行回收。并行回收的執行效率很高,但複雜度增加,另外也有一些副作用,如記憶體碎片增加。

2)并發執行和應用程式停止 :應用程式停止(Stop-the-world)顧名思義,其垃圾回收方式在執行垃圾回收的同時會導緻應用程式的暫停。并發執行的垃圾回收雖然不會導緻應用程式的暫停,但由于并發執行垃圾需要解決和應用程式的執行沖突(應用程式可能在垃圾回收的過程修改對象),是以并發執行垃圾回收的系統開銷比Stop-the-world高,而且執行時需要更多的堆記憶體。

3)壓縮和不壓縮和複制 :

①支援壓縮的垃圾回收器(标記-壓縮 = 标記清除+壓縮)會把所有的可達對象搬遷到一端,然後直接清理掉端邊界以外的記憶體,減少了記憶體碎片。

②不壓縮的垃圾回收器(标記-清除)要周遊兩次,第一次先從跟開始通路所有可達對象,并将他們标記為可達狀态,第二次便利整個記憶體區域,對未标記可達狀态的對象進行回收處理。這種回收方式不壓縮,不需要額外記憶體,但要兩次周遊,會産生碎片

③複制式的垃圾回收器:将堆記憶體分成兩個相同空間,從根(類似于前面的有向圖起始頂點)開始通路每一個關聯的可達對象,将空間A的全部可達對象複制到空間B,然後一次性回收空間A。對于該算法而言,因為隻需通路所有的可達對象,将所有的可達對象複制走之後就直接回收整個空間,完全不用理會不可達對象,是以周遊空間的成本較小,但需要巨大的複制成本和較多的記憶體。

3.2堆記憶體的分代回收

1)分代回收的依據:

①對象生存時間的長短:大部分對象在Young期間就被回收

②不同代采取不同的垃圾回收政策:新(生存時間短)老(生存時間長)對象之間很少存在引用

2) 堆記憶體的分代:

①Young代 :

Ⅰ回收機制 :因為對象數量少,是以采用複制回收。

Ⅱ組成區域 :由1個Eden區和2個Survivor區構成,同一時間的兩個Survivor區,一個用來儲存對象,另一個是空的;每次進行Young代垃圾回收的時候,就把Eden,From中的可達對象複制到To區域中,一些生存時間長的就複制到了老年代,接着清除Eden,From空間,最後原來的To空間變為From空間,原來的From空間變為To空間。

Ⅲ對象來源 :絕大多數對象先配置設定到Eden區,一些大的對象會直接被配置設定到Old代中。

Ⅳ回收頻率 :因為Young代對象大部分很快進入不可達狀态,是以回收頻率高且回收速度快

②Old代 :

Ⅰ回收機制 :采用标記壓縮算法回收。

Ⅱ對象來源 :1.對象大直接進入老年代。

       2.Young代中生存時間長的可達對象

Ⅲ回收頻率 :因為很少對象會死掉,是以執行頻率不高,而且需要較長時間來完成。

③Permanent代 :

Ⅰ用 途 :用來裝載Class,方法等資訊,預設為64M,不會被回收

Ⅱ對象來源 :eg:對于像Hibernate,Spring這類喜歡AOP動态生成類的架構,往往會生成大量的動态代理類,是以需要更多的Permanent代記憶體。是以我們經常在調試Hibernate,Spring的時候經常遇到java.lang.OutOfMemoryError:PermGen space的錯誤,這就是Permanent代記憶體耗盡所導緻的錯誤。

Ⅲ回收頻率 :不會被回收

3.3常見的垃圾回收器

在此之前,我們先講一下下面将會涉及到的并發和并行兩個詞的解釋:

1)并行:指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态;

2)并發:指使用者線程與 垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),使用者程式繼續執行,而垃圾收集程式運作于另一個CPU上。

好啦,繼續講垃圾回收器:

1)串行回收器(隻使用一個CPU):Young代采用串行複制算法;Old代使用串行标記壓縮算法(三個階段:标記mark—清除sweep—壓縮compact),回收期間程式會産生暫停,

2)并行回收器:對Young代采用的算法和串行回收器一樣,隻是增加了多CPU并行處理; 對Old代的處理和串行回收器完全一樣,依舊是單線程。

3)并行壓縮回收器:對Young代處理采用與并行回收器完全一樣的算法;隻是對Old代采用了不同的算法,其實就是劃分不同的區域,然後進行标記壓縮算法:

① 将Old代劃分成幾個固定區域;

② mark階段(多線程并行),标記可達對象;

③ summary階段(串行執行),從最左邊開始檢驗知道找到某個達到數值(可達對象密度小)的區域時,此區域及其右邊區域進行壓縮回收,其左端為密集區域

④ compact階段(多線程并行),識别出需要裝填的區域,多線程并行的把資料複制到這些區域中。經此過程後,Old代一端密集存在大量活動對象,另一端則存在大塊空間。

4)并發辨別—清理回收(CMS):對Young代處理采用與并行回收器完全一樣的算法;隻是對Old代采用了不同的算法,但歸根待地還是标記清理算法:

① 初始辨別(程式暫停):标記被直接引用的對象(一級對象);

② 并發辨別(程式運作):通過一級對象尋找其他可達對象;

③ 再标記(程式暫停):多線程并行的重新标記之前可能因為并發而漏掉的對象(簡單的說就是防遺漏)

④ 并發清理(程式運作)

4.記憶體管理小技巧

1)盡量使用直接量,eg:String javaStr = “國小徒的成長曆程”;

2)使用StringBuilder和StringBuffer進行字元串連接配接等操作;

3)盡早釋放無用對象;

4)盡量少使用靜态變量;

5)緩存常用的對象:可以使用開源的開源緩存實作,eg:OSCache,Ehcache;