这篇文章将讲解垃圾回收的概念以及对那些区域进行垃圾回收,最后讲解几种常见的垃圾回收算法。

概述

什么叫垃圾收集器?

需要思考GC需要完成的3件事情:

  • 哪些内存需要回收?
  • 什么时候需要回收?
  • 如何回收?

下面介绍一下Java内存运行时区域的各个部分,为什么有些区域需要回收,有些区域不需要回收?以及怎么去回收?

  • 1、程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随方法的进入和退出而有条不絮地执行着出栈和入栈操作。因此这几个区域的内存分配和回收都具备确定性,在这几个区域就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了
  • 2、Java堆方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。

对象已死吗?

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在堆进行回收前,第一件事情就是确定这些对象之中哪些还“存活”着,哪些已经“死去”(即不能再被任何途径使用的对象)

引用计数算法

很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能在被使用的。

可达性分析算法

通过可达性分析算法(Reachability Analysis)来判定对象是否存活的。

基本思路:通过一系列的称为GC Roots 的对象作为起始点,从这些起始点往下搜索,搜索走过的路径称为引用链,当一个对象和GC Roots没有任何引用链(即GC Roots到这个对象时不可达的),说明对象时无用的。

img

在Java中可作为GC Roots的对象有下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中引用的对象。

再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。

引用的四种类型:

  • 强引用:就是指在程序代码之中普遍存在的,类似Object obj = new Object() 这类引用,只要强引用还存在,垃圾收集器永远不会回收被引用的对象。
  • 软引用:是用来描述一个还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用: 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得这个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时受到一个系统通知

生存还是死亡

可达性分析法中不可达的对象也不是非死不可的,而是处于缓刑阶段。要宣告一个对象的死亡至少要经过两次标记过程:

  • 1、当经过可达性分析后发现对象与GC Roots不可达,那么它会被第一次标记并且进行一次刷选,刷选的条件是此对象是否有必要执行finalize方法。
  • 2、当对象没有覆盖finalize方法或对象的finalize方法已经被虚拟机执行过。这两种情况都会被视为不需要执行finalizef方法。

如果这个对象有必要执行finalize方法,那么对象会被放在F-Queue的队列中,并且会被由Java虚拟机自动创建的、低优先级的Finalizer线程去执行。finalize方法是对象最后一次逃脱死亡的机会,在finalize方法后,GC将会对对象进行第二次标记。如果对象在finalize方法中成功拯救自己,那么在第二次标记时会被移出回收集合,否则就真的被回收了。

回收方法区

很多人认为方法区(虚拟机中的永久代)是没有垃圾回收的,Java虚拟机规范也确实说过不要求虚拟机在方法区实现圾回收,因为方法区的垃圾收集效率很低。

方法区的垃圾收集主要回收两部分内容:废弃常量无用的类

  • 回收废弃常量:回收废弃常量与回收Java堆中的对象类似,以常量池中的字面量的回收为例:如果“abc”字符串存储在常量池中,其他地方没有任何对象引用常量池中的“abc”常量,那么进行垃圾回收时“abc”常量会被清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
  • 无用的类 判断无用的类比废弃常量条件苛刻得多。必须满足以下三个条件:
    • 该类的所有实例都已被回收。
    • 加载该类的ClassLoader已被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类。

垃圾收集算法

标记-清除算法

工作原理:算法主要分为两个阶段标记和清除,首先标记出所有需要回收的对象,标记完成后统一进行清除。

img

缺点:

  • 1、效率问题:标记和清除两个过程效率都不高
  • 2、空间问题:对象清除后会产生大量不连续的空间碎片,当需要分配给大对象较大的内存空间时会因为找不到足够的连续空间而不得不提前出发下一次垃圾收集。

复制算法

为解决效率问题,复制算法出现了:它将内存空间分为大小相等的两块区域,每次只使用其中一块,当进行垃圾收集时,将这块区域中还存活的对象复制到另一块,然后将这一块内存回收。这样就不会产生内存碎片的问题。

img

缺点:这种算法实现简单,运行高效,只是代价是每次只能使用内存的一半,代价过高。

现在的商用虚拟机都采用这种收集算法回收新生代内存。根IBM公司的研究表明,新生代中的内存对象98%是朝生夕死的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden区域,两块较小的Survivor区域。每次只使用一块Eden区域和一块Survivor区域,当进行垃圾收集时,将Eden区域和Survivor区域仍然存活的对象复制到另一块Survivor区域,然后将Eden区域和使用过的Survivor区域清除HotSpot虚拟机默认的Eden和Survivor区域大小比例为8:1,这样只会浪费10%的内存。

标记-整理算法

复制算法在对象成活率较低的新生代比较适用,而对于对象成活率较高的老年代就需要进行较多的复制操作,效率明显会减低。所以针对老年代的特点,提出了标记-整理算法:标记清除过程仍然与标记清楚算法一样,只是在清除后将存活的对象都向一端移动。

img

分代收集算法

当前商业的虚拟机的垃圾收集算法都采用分代收集算法:根据对象存活周期的不同将内存划分为几块,一般把Java分为新生代和老年代,在根据各个年代的特点选择合适的收集算法。在新生代中,对象存活率低,适合使用复制算法,而老年代对象存活率较高,适合使用标记清除算法或标记-整理算法。

参考资料

https://juejin.im/post/5aa0e8176fb9a028d663be1e

http://www.ityouknow.com/jvm/2017/08/29/GC-garbage-collection.html