JVM之垃圾回收

1 什么是垃圾

没有任何引用指向的一个对象或者多个对象

coding-JVM之垃圾回收-

1.1 引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1,当引用失效时,计数器减 1。
coding-JVM之垃圾回收-

1.2 可达性分析

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
作为 GC Roots 的对象主要包括下面几种:
(1) 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个现场被调用方法堆栈中使用到的参数、局部变量、临时变量等。
(2) 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
(3) 方法区中常量引用的对象;比如:字符串常量池里的引用。
(4) 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
coding-JVM之垃圾回收-

2 引用类型

2.1 强引用

一般的 Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。

2.2 软引用 SoftReference

一些有用但是并非必需,用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象就会被回收(如果这次回收后还是没有足够的空间,才会抛出内存溢出)。

/**
* 软引用
* -Xms20m -Xmx20m -XX:+PrintGC
*/
public class TestSoftRef {
    public static class User {
        public int id;
        public String name;
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }
    }
    public static void main(String[] args) {
        User u = new User(1, "King"); //new是强引用
        SoftReference<User> userSoft = new SoftReference<>(u);//软引用
        u = null;//干掉强引用,确保这个实例只有userSoft的软引用
        System.gc();//进行一次GC垃圾回收  千万不要写在业务代码中。
        //往堆中填充数据,导致OOM
        List<byte[]> list = new LinkedList<>();
        try {
            for (int i = 0; i < 100; i++) {
                System.out.println("*************" + userSoft.get());
                list.add(new byte[1024 * 1024 * 5]); //5M的对象
            }
        } catch (Throwable e) {
            //抛出了OOM异常时打印软引用对象
            System.out.println("Exception*************" + userSoft.get());
        }
    }
}

coding-JVM之垃圾回收-

2.3 弱引用 WeakReference

一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。

 /**
* 弱引用 ,不需要填JVM参数
*/
public class TestWeakRef {
    public static class User {
        public int id;
        public String name;
        public User(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", name=" + name + "]";
        }
    }
    public static void main(String[] args) {
        User u = new User(1, "King");
        WeakReference<User> userWeak = new WeakReference<User>(u);
        u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
        System.out.println(userWeak.get());
        System.gc();//进行一次GC垃圾回收,千万不要写在业务代码中。
        System.out.println("After gc");
        System.out.println(userWeak.get());
    }
}

coding-JVM之垃圾回收-

2.4 虚引用 PhantomReference

幽灵引用,最弱(随时会被回收掉),垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。

3 对象的分配策略

coding-JVM之垃圾回收-

4 垃圾回收算法

垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的方式都有不同,所以不需要去了解算法的实现,我们重点讲解 3 种算法的思想。
coding-JVM之垃圾回收-

4.1 复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

Appel 式回收
一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1 和Survivor2)专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
coding-JVM之垃圾回收-

4.2 标记- 清除算法

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
coding-JVM之垃圾回收-

4.3 标记- 整理算法

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新( 直接指针需要调整)。所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
coding-JVM之垃圾回收-

匿名

发表评论

匿名网友