溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java中的垃圾回收概念與算法是怎樣的

發(fā)布時間:2021-09-27 09:56:29 來源:億速云 閱讀:127 作者:柒染 欄目:編程語言

今天就跟大家聊聊有關(guān)java中的垃圾回收概念與算法是怎樣的,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

1. 常用的垃圾回收算法

1.1 引用計數(shù)法

對于一個對象A,只要有任何一個對象引用了A,則A的引用計數(shù)器就加1,當(dāng)引用失效時,引用計數(shù)器就減1.只要對象A的引用計數(shù)器值為0,則對象A就不可能再被使用。

引用計數(shù)器的實(shí)現(xiàn)非常簡單,但有兩個嚴(yán)重問題:

  1. 無法處理循環(huán)引用。

  2. 引用計數(shù)器要求在每次引用產(chǎn)生和消除的時候,伴隨一個加法操作和減法操作,對系統(tǒng)性能有一定影響。

因此,java虛擬機(jī)并未選擇此算法作為垃圾回收算法。

1.2 標(biāo)記清除算法

標(biāo)記清除法是現(xiàn)在垃圾回收算法的思想基礎(chǔ)。標(biāo)記清除法將垃圾回收分為兩個階段:標(biāo)記和清除階段。

  • 標(biāo)記階段:首先通過根節(jié)點(diǎn)標(biāo)記所有從根節(jié)點(diǎn)開始的可達(dá)對象。因此,未被標(biāo)記的對象就是未被引用的垃圾對象。

  • 清除階段:清除所有未被標(biāo)記的對象。

標(biāo)記清除法的最大問題是可能產(chǎn)生空間碎片。

1.3 復(fù)制算法

復(fù)制算法的核心思想是:將原有的內(nèi)存空間分為兩塊,每次只使用其中一塊,在進(jìn)行垃圾回收時,將正在使用的內(nèi)存中的存活對象復(fù)制到未使用的內(nèi)存塊中,之后清除正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色,完成垃圾回收。

復(fù)制算法優(yōu)點(diǎn):如果系統(tǒng)中的垃圾對象很多,復(fù)制算法需要復(fù)制的存活對象數(shù)量就會相對較少。因此,在真正需要垃圾回收的時刻,復(fù)制算法的效率是很高的。又由于對象是在垃圾回收過程中統(tǒng)一被復(fù)制到新的內(nèi)存空間的,可確?;厥蘸蟮膬?nèi)存空間是沒有碎片的。

復(fù)制算法缺點(diǎn):復(fù)制算法需要將內(nèi)存分成兩塊,每次只使用其中一塊(真正使用的內(nèi)存只有其中一半).

1.4 標(biāo)記壓縮算法

標(biāo)記壓縮算法標(biāo)記清除算法 的基礎(chǔ)上做了一些優(yōu)化。和 標(biāo)記清除算法 一樣,票房壓縮算法也是需要從根節(jié)點(diǎn)開始,對所有可達(dá)對象做一次標(biāo)記。但之后,它并不只是簡單地清理未標(biāo)記的對象,而是將所有的存活對象壓縮到內(nèi)存的一端。然后清理邊界外所有的空間。

這種方法既避免了碎片的產(chǎn)生,又不需要兩塊相同的內(nèi)存空間。

1.5 分代算法

在前面介紹的幾種算法中,沒有一種算法是可以完全替代其他算法,它們都有自己的優(yōu)勢和特點(diǎn),根據(jù)垃圾回收對象的特性,使用合適的算法,才是明智的選擇。

分代算法就是基于這種思想,它將內(nèi)存區(qū)間根據(jù)對象的特點(diǎn)分成幾塊,根據(jù)每塊內(nèi)存區(qū)間的特點(diǎn)使用不同的回收算法,以提高垃圾回收的效率。

一般來說,java虛擬機(jī)會將所有的新建對象都放入新生代的內(nèi)存區(qū)域,新生代的特點(diǎn)是朝生夕滅,大約90%的新建對象會被很快回收,因此新生代比較適合使用復(fù)制算法。

當(dāng)一個對象經(jīng)過幾次回收后依然存活,對象就會晉升到老年代內(nèi)存空間中。在老年代中,幾乎所有的對象都是經(jīng)過幾次垃圾回收依然得存活的,因此可以認(rèn)為這些對象在一段時期內(nèi),都將是常駐內(nèi)存的。如果依然使用復(fù)制算法,將需要復(fù)制大量對象,再加上老年代的回收性價比也低于新生代,因此這種做法不可取。根據(jù)分代的思想,可以對老年代使用標(biāo)記壓縮或標(biāo)記清除算法。

通常新生代回收的頻率很高,但每次回收的耗時很短,而老年代回收的頻率比較低,但耗時長。為了支持高頻率的新生代回收,虛擬機(jī)可能使用一種叫作卡表的數(shù)據(jù)結(jié)構(gòu)。

卡表為一個比特位集合,每一個比特位可以用來表示老年代的某一區(qū)域中的所有對象是否持有新生代對象的引用。這樣在新生代gc時,可以不用花大量時間掃描所有的老年代對象來確定每一個對象的引用關(guān)系,可以先掃描卡表,只有當(dāng)卡表的標(biāo)記位為1時,才需要掃描給定區(qū)域的老年代對象,而卡表為0的老年代對象肯定不含有新生代對象的引用。使用這種方式,可以大大加快新生代的回收速度。

1.6 分區(qū)算法

分區(qū)將整個堆空間劃分成連續(xù)的不同小區(qū)間,每一個小區(qū)間都獨(dú)立使用,獨(dú)立回收。這種算法的好處是可以控制一次回收小區(qū)間的數(shù)量。

一般來說,在相同條件下,堆空間越大,一次GC所需要的時間就越長,從而產(chǎn)生的停頓也越長。為了更好地控制gc產(chǎn)生的停頓時間,將一塊大的內(nèi)存區(qū)域分割成多個小塊,根據(jù)目錄停頓時間,每次合理地回收若干個小區(qū)間,而不是回收整個堆空間,從而減少一次gc所產(chǎn)生的停頓。

2. 判斷可觸及性

對象的可觸及性包含以下三種狀態(tài):

  1. 可觸及的:從根節(jié)點(diǎn)開始,可以到達(dá)這個對象;

  2. 可復(fù)活的:對象的所有引用都被釋放,但是對象有可能在 finallize() 函數(shù)中復(fù)活;

  3. 不可觸及的:對象的 finallize() 函數(shù)被調(diào)用,并且沒有復(fù)活,那么就會進(jìn)入不可觸及狀態(tài),不可觸及狀態(tài)的對象不可能被復(fù)活,因?yàn)?finallize() 函數(shù)只會被調(diào)用一次。

以上3種狀態(tài)中,只有在對象不可觸及時才可以被回收。

2.1 對象的復(fù)活

對象很有可能在 finalize() 函數(shù)中使自己復(fù)活,這里給出一個例子:

public class Demo01 {
    public static Demo01 obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("obj finalize called");
        obj = this;
    }

    @Override
    public String toString() {
        return "I am can rellive obj";
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new Demo01();
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj 是 null");
        } else {
            System.out.println("obj 可用");
        }

        System.out.println("第2次gc");
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj 是 null");
        } else {
            System.out.println("obj 可用");
        }

    }
}

結(jié)果:

can rellive obj finalize called
obj 可用
第2次gc
obj 是 null

可以看到,將obj設(shè)置為null后,進(jìn)行g(shù)c,結(jié)果發(fā)現(xiàn)obj對象復(fù)活了。接著,再次釋放對象引用并進(jìn)行g(shù)c,對象才真正回收。這是因?yàn)榈谝淮芜M(jìn)行g(shù)c時,在 finalize() 函數(shù)調(diào)用前,雖然系統(tǒng)中的引用已經(jīng)被清除,但是作為實(shí)例方法finalize(),對象的this引用依然會被傳入方法內(nèi)部,如果引用外泄,對象就會復(fù)活,此時對象又變?yōu)榭捎|及狀態(tài)。而finalize() 函數(shù)只會被調(diào)用一次,在第2次清除對象時,對象就再無機(jī)會復(fù)活,因此就會被回收。

注意:finalize() 函數(shù)是一個非常糟糕的模式,不推薦使用finalize()函數(shù)釋放資源,原因如下:

  1. finalize() 函數(shù)有可能發(fā)生引用外泄,在無意中復(fù)活對象;

  2. finalize() 函數(shù)是被系統(tǒng)調(diào)用的,調(diào)用時間不明確,因此不是一個好的資源釋放方案,推薦在try-catch-finally語句中進(jìn)行資源的釋放。

2.2 引用和可觸及強(qiáng)度

在java中,提供了4個級別的引用:強(qiáng)引用、軟引用、弱引用和虛引用。隊強(qiáng)引用外,其他3種引用均可以在java.lang.ref 包中找到。其中,FinalReference 為 “最終” 引用,它用以實(shí)現(xiàn)對象的 finallize() 函數(shù)。

java中的垃圾回收概念與算法是怎樣的

強(qiáng)引用就是程序中一般使用的引用類型,強(qiáng)引用的對象是可觸及的,不會被回收。軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在一定條件下都是可以被回收的。

強(qiáng)引用示例:

//這里str就是強(qiáng)引用
String str = "aaa";

強(qiáng)引用有如下特點(diǎn):

  • 強(qiáng)引用可以直接訪問目標(biāo)對象;

  • 強(qiáng)引用所指向的對象在任何時候都不會被系統(tǒng)回收,虛擬機(jī)寧愿拋出OOM異常,也不會回收強(qiáng)引用所指向的對象;

  • 強(qiáng)引用可能導(dǎo)致內(nèi)存泄露。

2.3 軟引用

軟引用是指強(qiáng)引用弱一點(diǎn)的引用類型。如果一個對象只持有軟引用,那么當(dāng)空間不足時,就會被回收。軟引用使用java.lang.ref.SoftReference 類實(shí)現(xiàn)。

以下示例演示了軟引用會在系統(tǒng)堆內(nèi)存不足時被回收:

public class Demo02 {

    public static class User {
        public int id;

        public String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        SoftReference<User> userSoftReference = new SoftReference<>(new User(1, "geym"));
        System.out.println(userSoftReference.get());
        //第一次垃圾回收
        System.gc();
        System.out.println("after gc");
        System.out.println(userSoftReference.get());
        //初始化數(shù)組后
        byte[] b = new byte[1024 * 973 * 7];
        System.gc();
        System.out.println(userSoftReference.get());
    }
}

使用參數(shù)-Xmx10m運(yùn)行,結(jié)果如下:

User{id=1, name='geym'}
after gc
User{id=1, name='geym'}
null

上述代理,首先聲明了User對象的軟引用,接著進(jìn)行了一次垃圾回收,發(fā)現(xiàn)軟引用對象依然存在;接著分配了一個大對象,系統(tǒng)此時認(rèn)為內(nèi)存緊張,于是進(jìn)行了一次gc,此時會回收軟引用。

每一個軟引用都可以附帶一個引用隊列,當(dāng)對象的可達(dá)性發(fā)生改變時,軟引用對象就會進(jìn)入引用隊列,通過這個引用隊列,可以跟蹤對象的回收情況,代碼示例如下:

public class Demo03 {

    private static class User {
        public int id;

        public String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    static ReferenceQueue<User> softQueue = null;

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while(true) {
                if(softQueue != null) {
                    UserSoftReference obj = null;
                    try {
                        obj = (UserSoftReference) softQueue.remove();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if(obj == null) {
                        System.out.println("user id " + obj.uid + "is delete");
                    }
                }
            }
        }
    }

    //自定義一個軟引用類,擴(kuò)展軟引用的目的是記錄User.uid,
    //后續(xù)在引用隊列中就可以通過這個uid字段知道哪個User實(shí)例被回收了。
    public static class UserSoftReference extends SoftReference<User> {

        int uid;

        public UserSoftReference(User referent, ReferenceQueue<? super User> q) {
            super(referent, q);
            this.uid = referent.id;
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new CheckRefQueue();
        t.setDaemon(true);
        t.start();
        //創(chuàng)建軟引用時,指定了一個軟引用隊列,當(dāng)給定的對象實(shí)例被回收時,就會被加入這個引用隊列,
        //通過訪問訪隊列可以跟蹤對象的回收情況
        User u = new User(1, "geym");
        softQueue = new ReferenceQueue<>();
        UserSoftReference userSoftReference = new UserSoftReference(u, softQueue);
        u = null;
        System.out.println(userSoftReference.get());
        System.gc();
        //內(nèi)存足夠,不會被回收
        System.out.println("after gc:");
        System.out.println(userSoftReference.get());

        System.out.println("try to create byte array and GC");
        byte[] b = new byte[1024 * 973 * 7];
        System.gc();
        System.out.println(userSoftReference.get());
        Thread.sleep(1000);
    }
}

使用參數(shù) -Xmx10m 運(yùn)行上述代碼就可以得到:

User{id=1, name='geym'}
after gc:
User{id=1, name='geym'}
try to create byte array and GC
null
2.4 弱引用

弱引用是一種比軟引用弱的引用類型。在系統(tǒng)gc時,只要發(fā)現(xiàn)弱引用,不管堆空間使用情況如何,都會將對象進(jìn)行回收。但是,由于垃圾回收器的線程通常優(yōu)先級很低,并不一定能很快地發(fā)現(xiàn)持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。 一旦一個弱引用對象被垃圾回收器回收,便會加入一個注冊的引用隊列,這一點(diǎn)和軟引用很相似。軟引用使用java.lang.ref.WeakReference類實(shí)現(xiàn)。

以下示例顯示了弱引用的特點(diǎn)

public class Demo04 {
    private static class User {
        public int id;

        public String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    public static void main(String[] args) {
        WeakReference<User> userWeakReference = new WeakReference<>(new User(1, "geym"));
        System.out.println(userWeakReference.get());
        System.gc();
        // 不管當(dāng)前空間足夠與否,都會回收它的內(nèi)存
        System.out.println("after gc");
        System.out.println(userWeakReference.get());
    }
}

運(yùn)行結(jié)果:

User{id=1, name='geym'}
after gc
null

弱引用和軟引用一樣,在構(gòu)造弱引用時,也可以指定一個引用隊列,當(dāng)弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。

2.5 虛引用

虛引用是所有引用類型中最弱的一個。一個持有虛引用的對象,和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。當(dāng)試圖通過虛引用的 get() 方法取得強(qiáng)引用時,總會失敗。并且,虛引用必須和引用隊列一起使用,它的作用在于跟蹤垃圾回收過程。

下面給出一個示例,使用虛引用跟蹤一個可復(fù)活對象的回收:

public class Demo05 {

    public static Demo05 obj = null;

    static ReferenceQueue<Demo05> phantomQueue = null;

    public static class CheckRefQueue extends Thread {
        @Override
        public void run() {
            while(true) {
                if(phantomQueue != null) {
                    PhantomReference<Demo05> objt = null;
                    try {
                        objt = (PhantomReference<Demo05>) phantomQueue.remove();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if(objt != null) {
                        System.out.println("demo05 obj is delete by gc");
                    }
                }
            }
        }
    }

    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("demo05 obj finalize called");
        obj = this;
    }

    @Override
    public String toString() {
        return "I am Demo05";
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new CheckRefQueue();
        t.setDaemon(true);
        t.start();

        phantomQueue = new ReferenceQueue<>();
        obj = new Demo05();
        //構(gòu)造一個虛引用
        PhantomReference<Demo05> phantomReference = new PhantomReference<>(obj, phantomQueue);
        //去除強(qiáng)引用,進(jìn)行垃圾回收,由于對象可復(fù)活,gc無法回收該對象
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj 是 null");
        } else {
            System.out.println("obj 可用");
        }
        //第2次進(jìn)行g(shù)c,由于 finalize()函數(shù)只會被調(diào)用一次,因此第2次gc會回收對象,
        //同時其引用隊列應(yīng)該也會捕獲取對象的回收
        System.out.println("第二次gc");
        obj = null;
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj 是 null");
        } else {
            System.out.println("obj 可用");
        }
    }
}

執(zhí)行代碼,結(jié)果如下:

demo05 obj finalize called
obj 可用
第二次gc
demo05 obj is delete by gc
obj 是 null

由于虛引用可以跟蹤對象的回收時間,所以也可以將一些資源的釋放操作放在虛引用中執(zhí)行和記錄。

3. 垃圾回收時的停頓現(xiàn)象:Stop-The-World(stw)

為了讓垃圾回收器正常且高效地執(zhí)行,在大部分情況下,會要求系統(tǒng)進(jìn)入一個停頓的狀態(tài),停頓的目的是終止所有應(yīng)用線程的執(zhí)行,只有這樣系統(tǒng)才不會有新地垃圾產(chǎn)生,同時停頓保證了系統(tǒng)狀態(tài)在某一個瞬間的一致性,也有益于垃圾回收器更好地標(biāo)記垃圾對象。因此,在垃圾回收時,都會產(chǎn)生應(yīng)用程序的停頓。停頓產(chǎn)生時,整個應(yīng)用程序會被卡死,沒有任何響應(yīng),因此這個停頓也叫“Stop-The-World”(STW).

下面這個示例顯示了停頓的情況:

public class Demo06 {

    private static class MyThread extends Thread {
        HashMap map = new HashMap();

        @Override
        public void run() {
            try {
                while (true) {
                    if(map.size() * 512 /1024 / 1024 >= 900) {
                        map.clear();
                        System.out.println("clean map");
                    }
                    byte[] b1;
                    for(int i = 0; i < 100; i++) {
                        b1 = new byte[512];
                        map.put(System.nanoTime(), b1);
                    }
                    Thread.sleep(1);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static class PrintThread extends Thread {
        public static final long startTime = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while(true) {
                    long t = System.currentTimeMillis() - startTime;
                    System.out.println(t / 1000 + "." + t % 1000);
                    Thread.sleep(100);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyThread t = new MyThread();
        PrintThread p = new PrintThread();
        t.start();
        p.start();
    }
}

以上代碼創(chuàng)建了兩個線程:一個用來分配空間,另一個用來打印時間,使用參數(shù) -Xmx1g -Xms1g -Xmn512k -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails 運(yùn)行,部分輸出如下:

34.732
34.833
34.940
35.810  (從此處開始,程序中設(shè)置每隔0.1秒輸出,但此處時間間隔明顯大于0.1秒)
36.604
37.38
38.230
39.20
39.813
40.590
41.420

此處對應(yīng)的gc日志如下:

35.100: [GC (Allocation Failure) 35.100: [DefNew: 447K->64K(448K), 0.0015667 secs] 1047853K->1047838K(1048512K), 0.0016257 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
35.110: [GC (Allocation Failure) 35.110: [DefNew: 448K->448K(448K), 0.0000200 secs]35.110: [Tenured: 1047774K->1048063K(1048064K), 0.7820350 secs] 1048222K->1048208K(1048512K), [Metaspace: 8969K->8969K(1058816K)], 0.7821444 secs] [Times: user=0.78 sys=0.00, real=0.78 secs] 
35.895: [Full GC (Allocation Failure) 35.895: [Tenured: 1048063K->1048063K(1048064K), 0.7921236 secs] 1048511K->1048417K(1048512K), [Metaspace: 8974K->8974K(1058816K)], 0.7922297 secs] [Times: user=0.78 sys=0.00, real=0.79 secs] 
36.689: [Full GC (Allocation Failure) 36.689: [Tenured: 1048063K->1048063K(1048064K), 0.7799881 secs] 1048510K->1048439K(1048512K), [Metaspace: 8976K->8976K(1058816K)], 0.7800798 secs] [Times: user=0.78 sys=0.00, real=0.78 secs] 
37.469: [Full GC (Allocation Failure) 37.469: [Tenured: 1048063K->1047758K(1048064K), 0.8430948 secs] 1048511K->1047758K(1048512K), [Metaspace: 8965K->8965K(1058816K)], 0.8432301 secs] [Times: user=0.84 sys=0.00, real=0.84 secs] 
38.316: [GC (Allocation Failure) 38.316: [DefNew: 384K->384K(448K), 0.0000200 secs]38.316: [Tenured: 1047758K->1048010K(1048064K), 0.7867043 secs] 1048142K->1048010K(1048512K), [Metaspace: 8955K->8955K(1058816K)], 0.7868497 secs] [Times: user=0.78 sys=0.00, real=0.79 secs]

注意看gc日志中的[Times: ... real=...],正常情況下,gc所用時間為real=0.00 secs,但是如果發(fā)生full gc,那么gc時間就會變長,在35.895、36.689和37.469時,gc所用時間接近0.8s,分別為real=0.79 secs、real=0.78 secsreal=0.84 secs。

使用jvisualvm觀察gc過程,如圖所求,可以看到:

  • 新生代(eden space) GC共進(jìn)行了2828次,共耗時7.571s,平均耗時 0.002677s;

  • 老年代(Old Gen) GC共進(jìn)行31次,共耗時24.084s,平均耗時 0.7769s;

  • 整個堆發(fā)生GC共2859次,共耗時31.655s.

java中的垃圾回收概念與算法是怎樣的

看完上述內(nèi)容,你們對java中的垃圾回收概念與算法是怎樣的有進(jìn)一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI