JVM13-垃圾回收算法


1. 标记阶段算法

简单来讲,垃圾回收 分红两步, 第一步找出垃圾,第二步进行回收,而标记阶段使用的算法,就是 为了找出谁是垃圾html

  1. 在堆里存放着几乎全部的Java对象实例,在GC执行垃圾回收以前,首先须要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
  2. 只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,所以这个过程咱们能够称为垃圾标记阶段。
  3. 那么在JVM中到底是如何标记一个死亡对象呢?简单来讲,当一个对象已经再也不被任何的存活对象继续引用时,就能够宣判为已经死亡。
  4. 判断对象存活通常有两种方式:引用计数算法和可达性分析算法。

1.1 引用计数算法

  1. 引用计数算法(Reference Counting)比较简单,对每一个对象保存一个整型的引用计数器属性。用于记录对象被引用的状况。
  2. 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。

优势:java

  • 实现简单,垃圾对象便于辨识;
  • 断定效率高,回收没有延迟性。

缺点:算法

  1. 它须要单独的字段存储计数器,这样的作法增长了存储空间的开销。
  2. 每次赋值都须要更新计数器,伴随着加法和减法操做,这增长了时间开销。
  3. 引用计数器有一个严重的问题,即没法处理循环引用的状况。这是一条致命缺陷,致使在Java的

由于第三点的严重性,JAVA 垃圾回收器中没有使用这类算法。数据库

什么是 循环引用缓存

1606039051270

上面的图中 , 对象一引用了对象二 , 对象二引用了对象三, 而对象三又从新指向了对象一,并发

而对象一是被外部引用的,因此它的计数器是2,eclipse

可是当外部的引用断掉时, 计数器减1,仍然是1, 不会被清除,致使这三个对象 没法清除,形成内存泄漏jvm

使用代码证实JAVA 中没有使用引用计数算法ide

public class RefCountGC {
    //这个成员属性惟一的做用就是占用一点内存
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB

    Object reference = null;

    public static void main(String[] args) {
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;

        //显式的执行垃圾回收行为,这里发生GC,obj1和obj2可否被回收?
        // System.gc();
    }
}复制代码

上面代码的内存示意图:函数

1606039876939

下面运行验证一下

jvm参数: -XX:+PrintGCDetails 打印GC日志

不进行垃圾回收时:使用了 16798k

1606039759529

手动进行GC: 只剩下了655k , 说明 这两个对象确实被回收了

1606039825511

引用计数小结

  1. 引用计数算法,是不少语言的资源回收选择,例如因人工智能而更加火热的Python,它是同时支持引用计数和垃圾收集机制。
  2. 具体哪一种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提升吞吐量的尝试。
  3. Java并无选择引用计数,是由于其存在一个基本的难题,也就是很难处理循环引用关系。Python如何解决循环引用?
  • 手动解除:很好理解,就是在合适的时机,代码中手动解除引用关系。
  • 使用弱引用weakref,weakref是Python提供的标准库,旨在解决循环引用。

1.2 可达性分析算法

可达性分析算法:也能够称为根搜索算法、追踪性垃圾收集

  1. 相对于引用计数算法而言,可达性分析算法不只一样具有实现简单和执行高效等特色,更重要的是该算法能够有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
  2. 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集一般也叫做追踪性垃圾收集(Tracing Garbage Collection)

基本思路以下:

  1. 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所链接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接链接着,搜索所走过的路径称为引用链(Reference Chain)
  3. 若是目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,能够标记为垃圾对象。
  4. 在可达性分析算法中,只有可以被根对象集合直接或者间接链接的对象才是存活对象。

示意图:

1606045953206

GC Roots 能够是哪些元素

列举:

  1. 虚拟机栈中引用的对象,好比:各个线程被调用的方法中使用到的参数、局部变量等。
  2. 本地方法栈内JNI(一般说的本地方法)引用的对象方法区中类静态属性引用的对象,好比:Java类的引用类型静态变量
  3. 方法区中常量引用的对象,好比:字符串常量池(StringTable)里的引用
  4. 全部被同步锁synchronized持有的对象
  5. Java虚拟机内部的引用。
  6. 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。
  7. 反映java虚拟机内部状况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

总结一句话就是,除了堆空间外的一些结构,好比:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,均可以做为GC Roots进行可达性分析

扩展:

除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不一样,还能够有其余对象“临时性”地加入,共同构成完整GC Roots集合。好比:在进行分代收集和局部回收时(PartialGC)。

若是只针对Java堆中的某一块区域进行垃圾回收(好比:典型的只针对新生代),必须考虑到这个区域的对象彻底有可能被其余堆区域的对象所引用,例如老年代等,这时候就须要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。

总结:

  • 在进行可达性分析时, 要对目标区域进行隔离, 通常将目标区域外的对象做为 GC Roots,
  • 例如在大多数GC收集整个堆空间时, 就将堆外的对象,例如方法区做为GC Roots 的对象
  • 若是在特殊的 GC 中,单独收集新生代,,就须要将新生代除外的区域的对象都考虑到,好比 老年代中引用新生代对象,此时老年代的对象也能够做为 GC Roots

可达性分析注意事项

  1. 若是要使用可达性分析算法来判断内存是否可回收,那么分析工做必须在一个能保障一致性的快照中进行。这点不知足的话分析结果的准确性就没法保证。
  2. 这点也是致使GC进行时必须“Stop The World”的一个重要缘由。即便是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必需要停顿的。

回到顶部

2. 对象的 finalization 机制

  1. Java语言提供了对象终止(finalization)机制来容许开发人员提供对象被销毁以前的自定义处理逻辑。
  2. 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象以前,标记阶段,总会先调用这个对象的finalize()方法。
  3. finalize() 方法容许在子类中被重写,用于在对象被回收时进行资源释放。一般在这个方法中进行一些资源释放和清理的工做,好比关闭文件、套接字和数据库链接等。

注意 finaliza() 方法并非一定是销毁前调用的, 它也是肯定此对象可不能够被销毁的一个判断因素,在标记阶段调用

Object 类中 finalize() 源码

// 等待被重写
protected void finalize() throws Throwable { }复制代码

2.1 finalize() 方法使用的注意事项

  1. 永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。

  • finalize() 方法是能够在标记阶段致使对象复活的,可是若是手动执行过,那么将不在判断(finalize方法只可调用一次)
  • finalize()方法的执行时间是没有保障的,它彻底由GC线程决定,极端状况下,若不发生GC,则finalize()方法将没有执行机会。由于jvm中有专门执行对象finalize方法的线程,此线程优先级比较低,即便主动调用该方法,也不会所以就直接进行回收

一个糟糕的finalize()会严重影响GC的性能(写个多重循环, 每一个对象在标记时调用时,均可能执行)。

从功能上来讲,finalize()方法与C++中的析构函数比较类似,可是Java采用的是基于垃圾回收器的自动内存管理机制,因此finalize()方法在本质上不一样于C++中的析构函数。

2.2 对象的三种可能的状态

因为finalize()方法的存在,可能会将对象复活,因此虚拟机中的对象通常处于三种可能的状态。

若是从全部的根节点都没法访问到某个对象,说明对象己经再也不使用了。通常来讲,此对象须要被回收。

但事实上,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段(关入大牢)。一个没法触及的对象有可能在某一个条件下“复活”本身,若是这样,那么对它当即进行回收就是不合理的

为此,定义虚拟机中的对象可能的三种状态。以下:

  1. 可触及的:从根节点开始,能够到达这个对象。
  2. 可复活的:对象的全部引用都被释放,可是对象有可能在finalize()中复活。(全部引用所有释放,第一次标记)
  3. 不可触及的:对象的finalize()被调用,并无从新使GC Roots跟节点的对象引用本身(关入大牢的对象没有找到关系),因此没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,由于finalize()只会被调用一次。(finalize方法没有复活本身,第二次标记)

以上3种状态中,是因为finalize()方法的存在,进行的区分。只有在对象两次标记,不可触及时才能够被回收。

2.3 finalize() 具体执行过程

上一节已经说到, 断定一个对象objA是否可回收,至少要经历两次标记过程:

  1. 若是对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法
  • 若是对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA直接 断定为不可触及的。(没有关系,或者关系已经找过了)
  • 若是对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动建立的、低优先级的Finalizer线程触发其finalize()方法执行。(若是关入大牢的有可能有关系,并且没有找过,则把他们都放到一个房间中,让他们打电话)
finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。若是objA在finalize()方法中与引用链上的任何一个对象创建了联系,那么在第二次标记时,objA会被移出“即将回收”集合。(若是找到打电话攀上关系,则复活)以后,对象若再次出现没有引用存在的状况。在这个状况下,finalize()方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法只会被调用一次。(若是复活后,再次关进来,则不会再让他找关系,直接标记第二次)

使用代码证实上述观点

下面的代码中, 使用类变量做为 GC Roots ,而且在对象回收时,在finalize 方法里自救

public class CanReliveObj {
    public static CanReliveObj obj;//类变量,属于 GC Root


    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj创建了联系
    }


    public static void main(String[] args) {
        try {
            obj = new CanReliveObj();
            // 对象第一次成功拯救本身
            obj = null;
            System.gc();//调用垃圾回收器
            System.out.println("第1次 gc");
            // 由于Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
            System.out.println("第2次 gc");
            // 下面这段代码与上面的彻底相同,可是此次自救却失败了
            obj = null;
            System.gc();
            // 由于Finalizer线程优先级很低,暂停2秒,以等待它
            Thread.sleep(2000);
            if (obj == null) {
                System.out.println("obj is dead");
            } else {
                System.out.println("obj is still alive");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}复制代码
  • 将finalize 方法注释的状况

打印:

第1次 gc
obj is dead
第2次 gc
obj is dead复制代码

说明此对象在第一次 gc时直接就回收了

  • 将注释放开

打印:

调用当前类重写的finalize()方法
第1次 gc
obj is still alive
第2次 gc
obj is dead复制代码

在第一次gc时 ,调用了 finalize 方法,并又从新使类变量指向本身,复活

可是在第二次gc 时,发现finalize 方法 就没有再执行了,直接被回收

回到顶部

3. GC Roots 溯源

本节将介绍使用各个工具查看 GC Roots 集合

3.1 MAT 工具查看

  1. MAT是Memory Analyzer的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗状况。
  2. MAT是基于Eclipse开发的,是一款免费的性能分析工具。
  3. 你们能够在http://www.eclipse.org/mat/下载并使用MAT

获取 dump 文件

jvm的一个内存快照,能够被各个软件分析,

下面将 演示如何将正在运行的 程序导出 dump文件

public class GCRootsTest {
    public static void main(String[] args) {
        List<Object> numList = new ArrayList<>();
        Date birth = new Date();

        for (int i = 0; i < 100; i++) {
            numList.add(String.valueOf(i));
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("数据添加完毕,请操做:");
        new Scanner(System.in).next();
        numList = null;
        birth = null;

        System.out.println("numList、birth已置空,请操做:");
        new Scanner(System.in).next();

        System.out.println("结束");
    }
}复制代码

先将程序跑起来,将阻塞

方式一:命令行使用jmap

1606136218303

方式二:使用JVisualVM

第一步: 选中监视tab, 点击堆 Dump

1606136419481

第二步: 右击另存为

1606136477149

第三步: 将上面程序 键盘输入,继续执行, 捕获第二个快照

1606136547696

这样咱们就获取到了两个 内存快照, 一个是被局部变量引用的,一个是释放掉的

如何使用MAT 查看堆内存快照

打开 MAT ,选择 File --> Open Heap Dump, 选择 须要查看的Dump文件

1606136708108

选择 Java Basics --> GC Roots

1606137000897

前后查看两个快照, 因为 局部变量再也不引用对象, 因此不在是GC Roots

1606136886631

释 放后:

1606136908431

3.2 JProfiler 工具使用

不用 dump文件, 查看实时的 运行时程序

查看当前程序中堆中最多的对象类型,并查看其GC Roots

点击 :Live Memory --> All Object ,查看 堆中最多的对象, 并右击 ,点击Show Selection In Heap Walker

1606137684003

在显示界面 选择 References tab,查看堆中该类型的全部实例, 而后能够选中某一个对象,选择 Incoming References 选项, 再点击 Show Paths To FC Roots 按钮,弹出框点击确认

1606137956342

而后就能够看到 选中的对象的GC Roots , 例以下面的案例中, 字符串 "添加完毕,请操做" 对象 的 GC Roots 就是 out 对象, 由于被 System.out.println("添加完毕,请操做")打印

1606138263123

使用JProfiler 分析OOM

代码:

public class HeapOOM {
    byte[] buffer = new byte[1 * 1024 * 1024];//1MB

    public static void main(String[] args) {
        ArrayList<HeapOOM> list = new ArrayList<>();

        int count = 0;
        try{
            while(true){
                list.add(new HeapOOM());
                count++;
            }
        }catch (Throwable e){
            System.out.println("count = " + count);
            e.printStackTrace();
        }
    }
}复制代码

上面的代码 将会致使OOM, 能够开启jvm指令,在出现OOM 时 自动生成 dump文件

运行程序,jvm指令: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

输出日志: 出现了OOM,生成的dump文件在 工程目录下

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid14608.hprof ...
java.lang.OutOfMemoryError: Java heap space
	at com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12)
	at com.atguigu.java.HeapOOM.main(HeapOOM.java:20)
Heap dump file created [7797849 bytes in 0.010 secs]
count = 6复制代码

打开JProfiler, 能够在 超大对象 里面找到它

1606138829393

也能够 查看出现OOM的线程:

1606138873872

回到顶部

4. 垃圾清除阶段

上面第一节中, 说到了如何标记垃圾,那么下面就开始清除垃圾,关于清除垃圾,也有不一样的算法

目前在JVM中比较常见的三种垃圾收集算法是

  1. 标记-清除算法(Mark-Sweep)
  2. 复制算法(Copying)
  3. 标记-压缩算法(Mark-Compact)

4.1 标记——清除 算法

标记-清除算法(Mark-Sweep)是一种很是基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。

执行过程

当堆中的有效内存空间(available memory)被耗尽的时候,就会中止整个程序(也被称为stop the world),而后进行两项工做,第一项则是标记,第二项则是清除

**标记:**垃圾收集器从引用根节点开始遍历,标记全部被引用的对象。(这里是标记不是垃圾的对象)

  • 通常是在对象的Header中记录为可达对象。
  • 注意:标记的是引用的对象,不是垃圾!!

**清除:**垃圾收集器对堆内存从头至尾进行线性的遍历,若是发现某个对象在其Header中没有标记为可达对象,则将其回收

流程示意图, 先从跟节点 找出全部的 可达对象, 标记为"绿色",再遍历整个对象列表,将没有标记为绿色的清除

1606140274876

何为清除?

这里所谓的清除并非真的置空,而是把须要清除的对象地址回收,保存在空闲的地址列表里。下次有新对象须要加载时,判断垃圾的位置空间是否够,若是够,就覆盖原有的地址。 (跟电脑硬盘的删除同样)

关于空闲列表是在为对象分配内存的时候提过:

若是内存规整

  • 采用指针碰撞的方式进行内存分配

若是内存不规整

  • 虚拟机须要维护一个空闲列表
  • 采用空闲列表分配内存

标记-清除算法的缺点

标记清除算法的优势很明显, 简单 易理解 易于实现,可是缺点也很明显

  1. 标记清除算法的效率不算高
  2. 在进行GC的时候,须要中止整个应用程序,用户体验较差
  3. 这种方式清理出来的空闲内存是不连续的,产生内存碎片,须要维护一个空闲列表

4.2 复制算法

  1. 为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。
  2. M.L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

核心思路:

将存放对象的内存空间分为两块,每次只使用其中一块,在垃圾回收时,垃圾回收器也从跟节点开始遍历,找到全部的可达对象,可是此时不标记, 而是直接将此对象复制到未被使用的内存块中,以后全盘清除正在使用的内存块中的全部对象,交换两个内存的角色,最后完成垃圾回收

示意图:

1606141538254

把可达的对象,直接复制到另一个区域中复制完成后,from区里面的对象就没有用了,新生代里面就用到了复制算法

1606141576843

复制算法的优缺点

优势

  1. 没有标记和清除过程,实现简单,运行高效
  2. 复制过去之后保证空间的连续性,不会出现“碎片”问题。

缺点

  1. 此算法的缺点也是很明显的,就是须要两倍的内存空间。
  2. 由于对象的地址发生了改变,全部对此对象的使用的地方的引用都须要改变

注意事项

若是系统中的垃圾对象不少,复制算法须要复制的存活对象数量并不会太大,效率较高,可是若是垃圾对象很是少的状况, 每次拷贝都几乎所有拷贝了,而后清除也就清除了个寂寞,

因此在jvm 中新生代中, 因为垃圾回收频率高,数量多,一次一般能够回收70% - 99% 的内存空间 ,回收性价比很高。因此如今的商业虚拟机都是用这种收集算法回收新生代。

4.3 标记 - 压缩(或标记-整理,Mark - Compact) 算法

复制算法的高效性是创建在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常发生,可是在老年代,更常见的状况是大部分对象都是存活对象。

若是依然使用复制算法,因为存活对象较多,复制的成本也将很高。所以,基于老年代垃圾回收的特性,须要使用其余的算法。

标记-清除算法的确能够应用在老年代中,可是该算法不只执行效率低下,并且在执行完内存回收后还会产生内存碎片,因此JVM的设计者须要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。

1970年先后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

执行流程:

  1. 第一阶段和标记清除算法同样,从根节点开始标记全部被引用对象
  2. 第二阶段将全部的存活对象压(或者说是整理)到内存的一端,按顺序排放。以后,清理边界外全部的空间。

示意图:

1606142659064

标记-压缩算法与标记-清除算法的比较

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,所以,也能够把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

两者的本质差别在于

  • 标记-清除算法是一种非移动式的回收算法,
  • 标记-压缩是移动式的。

并且在对象分配内存时能够看到,若内存区域是零散的,须要访问空闲列表(标记-清除算法回收地址到空闲列表)

可是若是使用标记-压缩算法, 标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销。

是否移动回收后的存活对象是一项优缺点并存的风险决策。

标记-压缩算法的优缺点

优势

  1. 消除了标记-清除算法当中,内存区域分散的缺点,咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可。
  2. 消除了复制算法当中,内存减半的高额代价。

缺点

  1. 从效率上来讲,标记-整理算法要低于复制算法和标记清除-算法
  2. 移动对象的同时,若是对象被其余对象引用,则还须要调整引用的地址
  3. 移动过程当中,须要全程暂停用户应用程序。即:STW

4.4 三种清除算法的VS

三种算法的横纵对比:

标记清除

标记整理

复制

速率

中等

最慢

最快

空间开销

少(但会堆积碎片)

少(不堆积碎片)

一般须要活对象的2倍空间(不堆积碎片)

移动对象

  1. 效率上来讲,复制算法是当之无愧的老大,可是却浪费了太多内存。
  2. 而为了尽可能兼顾上面提到的三个指标,标记-整理算法相对来讲更平滑一些,可是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。

总结: 没有最好的算法,只有最适合的算法

4.5 分代收集算法

前面全部这些算法中,并无一种算法能够彻底替代其余算法,它们都具备本身独特的优点和特色。

分代收集算法应运而生。他的目标不是替换上面的算法,而是具体问题 具体对待

分代收集算法,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。

通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色使用不一样的回收算法,以提升垃圾回收的效率。

在Java程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关:

  • 好比Http请求中的Session对象、线程、Socket链接,这类对象跟业务直接挂钩,所以生命周期比较长。
  • 可是还有一些对象,主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短,好比:String对象,因为其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次便可回收。

目前几乎全部的GC都采用分代收集算法执行垃圾回收的

每一个代的各个特色和适合的回收算法

年轻代(Young Gen)

  • 年轻代特色:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
  • 这种状况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,所以很适用于年轻代的回收。而复制算法内存利用率不高的问题,经过hotspot中的两个survivor的设计获得缓解。(整体占用比例较小)

老年代(Tenured Gen)

  • 老年代特色:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
  • 这种状况存在大量存活率高的对象,复制算法明显变得不合适。通常是由标记-清除或者是标记-清除与标记-整理的混合实现。
    • Mark(标记)阶段的开销与存活对象的数量成正比。标记的是存活的对象,存活的越多,标记的越多,越慢
    • Sweep阶段(标记清除的清除阶段)的开销与所管理区域的大小成正相关。全盘遍历全部须要清理的区域, 管理的区域越大, 越慢
    • Compact阶段(标记整理的整理阶段)的开销与存活对象的数据成正比。存活的越多,整理的对象越多,越慢

简单介绍CMS 回收器

  1. 以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对象的回收效率很高。
  2. 对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片致使的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。
  3. 分代的思想被现有的虚拟机普遍使用。几乎全部的垃圾回收器都区分新生代和老年代

4.6 增量收集算法

上述现有的算法,在垃圾回收过程当中,应用软件将处于一种Stop the World的状态。在Stop the World状态下,应用程序全部的线程都会挂起,暂停一切正常的工做,等待垃圾回收的完成。

若是垃圾回收时间过长,应用程序会被挂起好久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的诞生。

基本思路:

  1. 若是一次性将全部的垃圾进行处理,须要形成系统长时间的停顿,那么就可让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
  2. 总的来讲,增量收集算法的基础还是传统的标记-清除和复制算法。增量收集算法经过对线程间冲突的妥善处理,容许垃圾收集线程以分阶段的方式完成标记、清理或复制工做

使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。

缺点:

由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低。

4.7 分区算法

  1. 通常来讲,在相同条件下,堆空间越大,一次GC时所须要的时间就越长,有关GC产生的停顿也越长。
  2. 为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减小一次GC所产生的停顿。
  3. 分代算法将按照对象的生命周期长短划分红两个部分,分区算法将整个堆空间划分红连续的不一样小区间。每个小区间都独立使用,独立回收。这种算法的好处是能够控制一次回收多少个小区间。

1606145774237


5. 写在最后

注意,这些只是基本的算法思路,实际GC回收器过程要复杂的多,目前还在发展中的前沿GC都是复合算法,而且并行和并发兼备。 因此这里以为模糊的,到后面把各个GC 回收器的实现说明完,就清晰了.

相关文章
相关标签/搜索