千家信息网

java中的垃圾回收概念与算法是怎样的

发表于:2024-11-20 作者:千家信息网编辑
千家信息网最后更新 2024年11月20日,今天就跟大家聊聊有关java中的垃圾回收概念与算法是怎样的,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。1. 常用的垃圾回收算法1.1 引用计
千家信息网最后更新 2024年11月20日java中的垃圾回收概念与算法是怎样的

今天就跟大家聊聊有关java中的垃圾回收概念与算法是怎样的,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

1. 常用的垃圾回收算法

1.1 引用计数法

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1.只要对象A的引用计数器值为0,则对象A就不可能再被使用。

引用计数器的实现非常简单,但有两个严重问题:

  1. 无法处理循环引用。

  2. 引用计数器要求在每次引用产生和消除的时候,伴随一个加法操作和减法操作,对系统性能有一定影响。

因此,java虚拟机并未选择此算法作为垃圾回收算法。

1.2 标记清除算法

标记清除法是现在垃圾回收算法的思想基础。标记清除法将垃圾回收分为两个阶段:标记和清除阶段。

  • 标记阶段:首先通过根节点标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。

  • 清除阶段:清除所有未被标记的对象。

标记清除法的最大问题是可能产生空间碎片。

1.3 复制算法

复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

复制算法优点:如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量就会相对较少。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。又由于对象是在垃圾回收过程中统一被复制到新的内存空间的,可确保回收后的内存空间是没有碎片的。

复制算法缺点:复制算法需要将内存分成两块,每次只使用其中一块(真正使用的内存只有其中一半).

1.4 标记压缩算法

标记压缩算法标记清除算法 的基础上做了一些优化。和 标记清除算法 一样,票房压缩算法也是需要从根节点开始,对所有可达对象做一次标记。但之后,它并不只是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。然后清理边界外所有的空间。

这种方法既避免了碎片的产生,又不需要两块相同的内存空间。

1.5 分代算法

在前面介绍的几种算法中,没有一种算法是可以完全替代其他算法,它们都有自己的优势和特点,根据垃圾回收对象的特性,使用合适的算法,才是明智的选择。

分代算法就是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点使用不同的回收算法,以提高垃圾回收的效率。

一般来说,java虚拟机会将所有的新建对象都放入新生代的内存区域,新生代的特点是朝生夕灭,大约90%的新建对象会被很快回收,因此新生代比较适合使用复制算法。

当一个对象经过几次回收后依然存活,对象就会晋升到老年代内存空间中。在老年代中,几乎所有的对象都是经过几次垃圾回收依然得存活的,因此可以认为这些对象在一段时期内,都将是常驻内存的。如果依然使用复制算法,将需要复制大量对象,再加上老年代的回收性价比也低于新生代,因此这种做法不可取。根据分代的思想,可以对老年代使用标记压缩或标记清除算法。

通常新生代回收的频率很高,但每次回收的耗时很短,而老年代回收的频率比较低,但耗时长。为了支持高频率的新生代回收,虚拟机可能使用一种叫作卡表的数据结构。

卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用。这样在新生代gc时,可以不用花大量时间扫描所有的老年代对象来确定每一个对象的引用关系,可以先扫描卡表,只有当卡表的标记位为1时,才需要扫描给定区域的老年代对象,而卡表为0的老年代对象肯定不含有新生代对象的引用。使用这种方式,可以大大加快新生代的回收速度。

1.6 分区算法

分区将整个堆空间划分成连续的不同小区间,每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收小区间的数量。

一般来说,在相同条件下,堆空间越大,一次GC所需要的时间就越长,从而产生的停顿也越长。为了更好地控制gc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目录停顿时间,每次合理地回收若干个小区间,而不是回收整个堆空间,从而减少一次gc所产生的停顿。

2. 判断可触及性

对象的可触及性包含以下三种状态:

  1. 可触及的:从根节点开始,可以到达这个对象;

  2. 可复活的:对象的所有引用都被释放,但是对象有可能在 finallize() 函数中复活;

  3. 不可触及的:对象的 finallize() 函数被调用,并且没有复活,那么就会进入不可触及状态,不可触及状态的对象不可能被复活,因为 finallize() 函数只会被调用一次。

以上3种状态中,只有在对象不可触及时才可以被回收。

2.1 对象的复活

对象很有可能在 finalize() 函数中使自己复活,这里给出一个例子:

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 可用");        }    }}

结果:

can rellive obj finalize calledobj 可用第2次gcobj 是 null

可以看到,将obj设置为null后,进行gc,结果发现obj对象复活了。接着,再次释放对象引用并进行gc,对象才真正回收。这是因为第一次进行gc时,在 finalize() 函数调用前,虽然系统中的引用已经被清除,但是作为实例方法finalize(),对象的this引用依然会被传入方法内部,如果引用外泄,对象就会复活,此时对象又变为可触及状态。而finalize() 函数只会被调用一次,在第2次清除对象时,对象就再无机会复活,因此就会被回收。

注意:finalize() 函数是一个非常糟糕的模式,不推荐使用finalize()函数释放资源,原因如下:

  1. finalize() 函数有可能发生引用外泄,在无意中复活对象;

  2. finalize() 函数是被系统调用的,调用时间不明确,因此不是一个好的资源释放方案,推荐在try-catch-finally语句中进行资源的释放。

2.2 引用和可触及强度

在java中,提供了4个级别的引用:强引用、软引用、弱引用和虚引用。队强引用外,其他3种引用均可以在java.lang.ref 包中找到。其中,FinalReference 为 "最终" 引用,它用以实现对象的 finallize() 函数。

强引用就是程序中一般使用的引用类型,强引用的对象是可触及的,不会被回收。软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下都是可以被回收的。

强引用示例:

//这里str就是强引用String str = "aaa";

强引用有如下特点:

  • 强引用可以直接访问目标对象;

  • 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向的对象;

  • 强引用可能导致内存泄露。

2.3 软引用

软引用是指强引用弱一点的引用类型。如果一个对象只持有软引用,那么当空间不足时,就会被回收。软引用使用java.lang.ref.SoftReference 类实现。

以下示例演示了软引用会在系统堆内存不足时被回收:

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 userSoftReference = new SoftReference<>(new User(1, "geym"));        System.out.println(userSoftReference.get());        //第一次垃圾回收        System.gc();        System.out.println("after gc");        System.out.println(userSoftReference.get());        //初始化数组后        byte[] b = new byte[1024 * 973 * 7];        System.gc();        System.out.println(userSoftReference.get());    }}

使用参数-Xmx10m运行,结果如下:

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

上述代理,首先声明了User对象的软引用,接着进行了一次垃圾回收,发现软引用对象依然存在;接着分配了一个大对象,系统此时认为内存紧张,于是进行了一次gc,此时会回收软引用。

每一个软引用都可以附带一个引用队列,当对象的可达性发生改变时,软引用对象就会进入引用队列,通过这个引用队列,可以跟踪对象的回收情况,代码示例如下:

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 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");                    }                }            }        }    }    //自定义一个软引用类,扩展软引用的目的是记录User.uid,    //后续在引用队列中就可以通过这个uid字段知道哪个User实例被回收了。    public static class UserSoftReference extends SoftReference {        int uid;        public UserSoftReference(User referent, ReferenceQueue 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();        //创建软引用时,指定了一个软引用队列,当给定的对象实例被回收时,就会被加入这个引用队列,        //通过访问訪队列可以跟踪对象的回收情况        User u = new User(1, "geym");        softQueue = new ReferenceQueue<>();        UserSoftReference userSoftReference = new UserSoftReference(u, softQueue);        u = null;        System.out.println(userSoftReference.get());        System.gc();        //内存足够,不会被回收        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);    }}

使用参数 -Xmx10m 运行上述代码就可以得到:

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

弱引用是一种比软引用弱的引用类型。在系统gc时,只要发现弱引用,不管堆空间使用情况如何,都会将对象进行回收。但是,由于垃圾回收器的线程通常优先级很低,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。 一旦一个弱引用对象被垃圾回收器回收,便会加入一个注册的引用队列,这一点和软引用很相似。软引用使用java.lang.ref.WeakReference类实现。

以下示例显示了弱引用的特点

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 userWeakReference = new WeakReference<>(new User(1, "geym"));        System.out.println(userWeakReference.get());        System.gc();        // 不管当前空间足够与否,都会回收它的内存        System.out.println("after gc");        System.out.println(userWeakReference.get());    }}

运行结果:

User{id=1, name='geym'}after gcnull

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

2.5 虚引用

虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的 get() 方法取得强引用时,总会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。

下面给出一个示例,使用虚引用跟踪一个可复活对象的回收:

public class Demo05 {    public static Demo05 obj = null;    static ReferenceQueue phantomQueue = null;    public static class CheckRefQueue extends Thread {        @Override        public void run() {            while(true) {                if(phantomQueue != null) {                    PhantomReference objt = null;                    try {                        objt = (PhantomReference) 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();        //构造一个虚引用        PhantomReference phantomReference = new PhantomReference<>(obj, phantomQueue);        //去除强引用,进行垃圾回收,由于对象可复活,gc无法回收该对象        obj = null;        System.gc();        Thread.sleep(1000);        if(obj == null) {            System.out.println("obj 是 null");        } else {            System.out.println("obj 可用");        }        //第2次进行gc,由于 finalize()函数只会被调用一次,因此第2次gc会回收对象,        //同时其引用队列应该也会捕获取对象的回收        System.out.println("第二次gc");        obj = null;        System.gc();        Thread.sleep(1000);        if(obj == null) {            System.out.println("obj 是 null");        } else {            System.out.println("obj 可用");        }    }}

执行代码,结果如下:

demo05 obj finalize calledobj 可用第二次gcdemo05 obj is delete by gcobj 是 null

由于虚引用可以跟踪对象的回收时间,所以也可以将一些资源的释放操作放在虚引用中执行和记录。

3. 垃圾回收时的停顿现象:Stop-The-World(stw)

为了让垃圾回收器正常且高效地执行,在大部分情况下,会要求系统进入一个停顿的状态,停顿的目的是终止所有应用线程的执行,只有这样系统才不会有新地垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也有益于垃圾回收器更好地标记垃圾对象。因此,在垃圾回收时,都会产生应用程序的停顿。停顿产生时,整个应用程序会被卡死,没有任何响应,因此这个停顿也叫"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();    }}

以上代码创建了两个线程:一个用来分配空间,另一个用来打印时间,使用参数 -Xmx1g -Xms1g -Xmn512k -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails 运行,部分输出如下:

34.73234.83334.94035.810  (从此处开始,程序中设置每隔0.1秒输出,但此处时间间隔明显大于0.1秒)36.60437.3838.23039.2039.81340.59041.420

此处对应的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,但是如果发生full gc,那么gc时间就会变长,在35.895、36.689和37.469时,gc所用时间接近0.8s,分别为real=0.79 secsreal=0.78 secsreal=0.84 secs

使用jvisualvm观察gc过程,如图所求,可以看到:

  • 新生代(eden space) GC共进行了2828次,共耗时7.571s,平均耗时 0.002677s;

  • 老年代(Old Gen) GC共进行31次,共耗时24.084s,平均耗时 0.7769s;

  • 整个堆发生GC共2859次,共耗时31.655s.

看完上述内容,你们对java中的垃圾回收概念与算法是怎样的有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注行业资讯频道,感谢大家的支持。

0