JavaJAVA

JVM(九)内存与垃圾回收|垃圾回收基本概念及算法

2020-08-07  本文已影响0人  TiaNa_na

  本文介绍垃圾回收的基本概念及相关算法。

目录
 1 垃圾回收概述
  1.1 什么是垃圾(GC)
  1.2 为什么需要GC
  1.3 早期垃圾回收
  1.4 Java垃圾回收机制
    1.4.1 可能出现的问题
    1.4.2 垃圾回收的区域
 2 垃圾回收相关算法
  2.1 垃圾标记阶段算法
    2.1.1 引用计数法
    2.1.2 可达性分析算法
  2.2 对象的finalization机制
  2.3 MAT与JProfiler的GC Roots溯源
  2.4 清除阶段算法
    2.4.1 标记-清除算法
    2.4.2 复制算法
    2.4.3 标记-压缩(整理)算法
    2.4.4 小结
  2.5 分代收集算法
  2.6 增量收集算法、分区算法
    2.6.1 标记-清除算法
    2.6.2 复制算法
 3 关于GC的面试题


1 垃圾回收概述


1.1 什么是垃圾(GC)

1.2 为什么需要GC

1.3 早期垃圾回收

1.4 Java垃圾回收机制

1.4.1 可能出现的问题
1.4.2 垃圾回收的区域

2 垃圾回收相关算法


2.1 垃圾标记阶段算法

2.1.1 引用计数法

小结

2.1.2 可达性分析算法

也叫根搜索算法或追踪性垃圾收集

GC Roots

  所谓"GC Roots"根集合就是一组必须活跃的引用。在Java语言中,GC Roots包括以下几类元素:

注意

2.2 对象的finalization机制

对象是否"死亡"

判定是否可以回收具体过程

判定一个对象objA是否可回收,至少要经历两次标记过程:

①如果对象objA到GC Roots没有引用链,则进行第一 次标记。
②进行筛选,判断此对象是否有必要执行finalize()方法

/**
 * 测试Object类中finalize()方法,即对象的finalization机制。
 *
 */
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();
        }
    }
}

2.3 MAT与JProfiler的GC Roots溯源

  MAT(Memory Analyzer)是一款基于Eclipse开发的功能强大的Java堆内存分析器,用于查找内存泄漏以及查看内存消耗情况。可以在https://www.eclipse.org/mat/downloads.php下载并使用。
  JProfiler是由ej-technologies GmbH公司开发的一款性能瓶颈分析工具,idea可集成JProfiler。
  当我们的java程序遇到频繁full gc或者oom的时候,我们常常需要将当前的heap 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("结束");
    }
}

获取dump文件

jps
jmap -dump:format=b,live,file=test1.bin {进程id}

使用MAT查看GC Roots


查看GC Roots

使用jProfiler进行GC溯源

GC溯源
使用Jprofiler分析OOM
/**
 * -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
 *  此命令:当程序出现OOM时在项目目录下生成dump文件
 */
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分析

2.4 清除阶段算法

  当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
  目前在JVM中比较常见的三种垃圾收集算法是标记-清除算法( Mark-Sweep)复制算法(Copying)标记-压缩算法(Mark-Compact)

2.4.1 标记-清除算法

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

执行过程

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

缺点

注意:何为清除?

2.4.2 复制算法

背景

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

核心思想

  将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在.使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
堆中幸存者S0和S1使用的就是复制算法

优点

缺点

应用场景

  在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
2.4.3 标记-压缩(整理)算法

背景

  复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
  标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记-压缩(Mark-Compact) 算法由此诞生。
  1970年前后,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

执行过程

优点

缺点

2.4.4 小结
Mark-Sweep Mark-Compact Copying
速度 中等 最慢 最快
空间开销 少(但会堆积碎片) 少(不堆积碎片) 通常需要活对象的2倍大小(不堆积碎片)
移动对象

2.5 分代收集算法

  前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
  分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
  目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的。

  在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。

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

2.6 增量收集算法、分区算法

2.6.1 增量收集算法

  上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the World的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,增量收集(Incremental Collecting)算法随之诞生。

基本思想

  如果一次性将所有的垃圾进行处理,会造成系统长时间的停顿。如何解决?可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
  总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点

  使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

2.6.2 分区算法

  一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
  分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。

  每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

注意,这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是复合算法,并且并行和并发兼备。

3 关于GC的面试题:


1. 你知道哪几种垃圾回收器,各自的优缺点,重点讲一下 cms和g1,包括原理,流程,优缺点。垃圾回收算法的实现原理
2. JVM GC算法有哪些,目前的JDK版本采用什么回收算法
3. G1回收器回收过程
4. GC是什么?为什么要有GC?
5. GC的两种判定方法? CMS收集器与G1收集器的特点。
6. jvm GC原理,JVM怎么回收内存
7. 垃圾回收算法有哪些?各自的优缺点,他们共同的缺点是什么?
8. 平时你是如何搭配使用垃圾回收器的
9. 什么情况下触发垃圾回收?
10. 如何选择合适的垃圾收集算法?
11. system.gc ()和runtime.gc()会做什么事情?
12. CMS回收停顿了几次,为什么要停顿两次?

上一篇下一篇

猜你喜欢

热点阅读