JVM系列(五) - JVM垃圾回收算法

前言

第二篇介绍了Java内存运行时区域,其中程序计数器虚拟机栈本地方法栈 三个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操做。每个栈帧中分配多少内存基本上是在类结构肯定下来时就已知的,所以这几个区域的内存分配和回收都具有肯定性。在这几个区域内不须要过多考虑回收的问题,由于方法结束或线程结束时,内存天然就跟随着回收了算法

Java方法区 则不同,一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同。咱们只有在程序处于运行期间时才能知道会建立哪些对象,这部分内存的分配和回收都是动态的垃圾收集器 所关注的是这部份内存。编程

正文

(一). 对象生死断定

如何判断Java中一个对象应该 “存活” 仍是 “死去”,这是 垃圾回收器要作的第一件事。后端

1. 引用计数算法

Java 中每一个具体对象(不是引用)都有一个引用计数器。当一个对象被建立并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出做用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象能够被看成垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。缓存

  • 优势多线程

    引用计数收集器执行简单,断定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。架构

  • 缺点框架

    难以检测出对象之间的循环引用。同时,引用计数器增长了程序执行的开销。因此Java语言并无选择这种算法进行垃圾回收。异步

2. 可达性分析算法

可达性分析算法也叫根搜索算法,经过一系列的称为 GC Roots 的对象做为起点,而后向下搜索。搜索所走过的路径称为引用链 (Reference Chain), 当一个对象GC Roots 没有任何引用链相连时, 即该对象不可达,也就说明此对象是 不可用的分布式

以下图所示: Object5Object6Object7 虽然互有关联, 但它们到GC Roots是不可达的, 所以也会被断定为可回收的对象。微服务

GC根对象

Java中, 可做为GC Roots的对象包括如下四种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 本地方法栈JNINative方法)引用的变量

  • 方法区类静态属性引用的变量

  • 方法区常量引用的变量

    JVM中用到的全部现代GC算法在回收前都会先找出全部仍存活的对象。可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图。下图展现的JVM中的内存布局能够用来很好地阐释这一律念:

(二). 对象引用分类

1. 强引用(Strong Reference)

在代码中广泛存在的,相似Object obj = new Object()这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

2. 软引用(Sofe Reference)

有用但并不是必需 的对象,可用SoftReference类来实现软引用。在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。

3. 弱引用(Weak Reference)

非必需 的对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生以前,JDK提供了WeakReference类来实现弱引用。不管当前内存是否足够,用软引用相关联的对象都会被回收掉。

4. 虚引用(Phantom Reference)

虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。为一个对象设置虚引用的惟一目的是:能在这个对象在垃圾回收器回收时收到一个系统通知

(三). finalize()二次标记

一个对象是否应该在垃圾回收器在GC时回收,至少要经历两次标记过程

第一次标记过程,经过可达性分析算法分析对象是否与GC Roots可达。通过第一次标记,而且被筛选为不可达的对象会进行第二次标记。

第二次标记过程,判断不可达对象是否有必要执行finalize方法。执行条件是当前对象的finalize方法被重写,而且还未被系统调用过。若是容许执行那么这个对象将会被放到一个叫F-Query的队列中,等待被执行。

注意:因为finalize由一个优先级比较低的Finalizer线程运行,因此该对象的的finalize方法不必定被执行,即便被执行了,也不保证finalize方法必定会执行完。若是对象第二次小规模标记,即finalize方法中拯救本身,只须要从新和引用链上的任一对象创建关联便可。

(四). 垃圾回收算法

本节具体介绍一下各类垃圾回收算法的思想:

1. 标记-清除算法

标记-清除算法对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。

  • 优势

    实现简单,不须要进行对象进行移动。

  • 缺点

    标记、清除过程效率低,产生大量不连续的内存碎片,提升了垃圾回收的频率。

2. 复制算法

这种收集算法解决了标记清除算法存在的效率问题。它将内存区域划分红相同的两个内存块。每次仅使用一半的空间,JVM生成的新对象放在一半空间中。当一半空间用完时进行GC,把可到达对象复制到另外一半空间,而后把使用过的内存空间一次清理掉。

  • 优势

    按顺序分配内存便可,实现简单、运行高效,不用考虑内存碎片。

  • 缺点

    可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

3. 标记-整理算法

标记-整理算法 采用和 标记-清除算法 同样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将全部的存活对象往一端空闲空间移动,而后清理掉端边界之外的内存空间。

  • 优势

    解决了标记-清理算法存在的内存碎片问题。

  • 缺点

    仍须要进行局部对象移动,必定程度上下降了效率。

4. 分代收集算法

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。通常包括年轻代老年代永久代,如图所示:

新生代(Young generation)

绝大多数最新被建立的对象会被分配到这里,因为大部分对象在建立后会很快变得不可达,因此不少对象被建立在新生代,而后消失。对象从这个区域消失的过程咱们称之为 minor GC

新生代 中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(若是新对象过大,会直接分配在老年代中)。在GC中,Eden中的对象会被移动到Survivor中,直至对象知足必定的年纪(定义为熬过GC的次数),会被移动到老年代

能够设置新生代老年代的相对大小。这种方式的优势是新生代大小会随着整个大小动态扩展。参数 -XX:NewRatio 设置老年代新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代8/1. 老年代 占堆大小的 7/8新生代 占堆大小的 1/8(默认便是 1/8)。

例如:

-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
复制代码

老年代(Old generation)

对象没有变得不可达,而且重新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正因为其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,能够称之为major GC(或者full GC)。

永久代(permanent generation)

像一些类的层级信息方法数据方法信息(如字节码变量大小),运行时常量池JDK7以后移出永久代),已肯定的符号引用虚方法表等等。它们几乎都是静态的而且不多卸载和回收,在JDK8以前的HotSpot虚拟机中,类的这些**“永久的”** 数据存放在一个叫作永久代的区域。

永久代一段连续的内存空间,咱们在JVM启动以前能够经过设置-XX:MaxPermSize的值来控制永久代的大小。可是JDK8以后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace) 的本地内存区域

小结

JDK8堆内存通常是划分为年轻代老年代不一样年代 根据自身特性采用不一样的垃圾收集算法

对于新生代,每次GC时都有大量的对象死亡,只有少许对象存活。考虑到复制成本低,适合采用复制算法。所以有了From SurvivorTo Survivor区域。

对于老年代,由于对象存活率高,没有额外的内存空间对它进行担保。于是适合采用标记-清理算法标记-整理算法进行回收。

参考

周志明,深刻理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社


欢迎关注技术公众号:零壹技术栈

零壹技术栈

本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。