【JVM从小白学成大佬】4.Java虚拟机何谓垃圾及垃圾回收算法

原文来自公众号:猿人谷java

在Java中内存是由虚拟机自动管理的,虚拟机在内存中划出一片区域,做为知足程序内存分配请求的空间。内存的建立仍然是由程序猿来显示指定的,可是对象的释放却对程序猿是透明的。就是解放了程序猿手动回收内存的工做,交给垃圾回收器来自动回收。面试

在虚拟机中,释放哪些再也不被使用的对象所占空间的过程称为垃圾收集(Garbage Collection,GC)。负责垃圾收集的程序模块,成为垃圾收集器(Garbage Collector)算法

既然虚拟机已经帮咱们把垃圾自动处理了,为何还要去了解GC和内存分配呢?

当须要排查各类内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,咱们就须要对虚拟机的自动管理技术实施必要的监控和调节了。这也是JVM调优,故障排查,重点须要掌握的知识了。安全

本篇咱们的重点是介绍何谓垃圾及垃圾回收算法,那咱们就要弄清到底什么是垃圾?能不能设计一种强大的垃圾回收算法来解决垃圾回收的全部问题?确定是没有的,后面介绍的每一种垃圾回收算法都有它得天独厚的优势,也有它避之不及的缺点。针对具体的场景,灵活运用方是上策。markdown

但愿你们能带着以下问题进行学习,会收获更大。多线程

  1. 什么是垃圾?
  2. 如何回收垃圾?
  3. 有没有一种垃圾回收算法能像银弹同样解决全部垃圾全部?
  4. GC的分类是什么样的?(Minor GC、Major GC、Full GC)
  5. Stop-the-world是什么?
  6. 如何避免全堆扫描?

垃圾收集算法.png

1 垃圾回收

在堆里面存放着Java世界中几乎全部的对象实例,垃圾收集器在堆进行回收前,第一件事就是要肯定这些对象之中哪些还“存活”着,哪些已经“死亡”(即不可能再被任何途径使用的对象)。垃圾回收,其实就是将已经分配出去的,但再也不使用的内存回收,以便可以再次分配。在Java虚拟机的规范中,垃圾指的就是死亡的对象所占据的堆空间并发

那怎么肯定一个对象是存活仍是死亡呢?

1.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。也就是说,须要截获全部的引用更新操做,而且相应地增减目标对象的计数器app

题外话:记得研一那段时间对iOS开发感兴趣,找个公司去实习,现学现搞iOS开发,当时是作了一个模拟炒股的app。用的就是Objective-C,这门语言起初管理内存的方式就是用的这种引用计数算法,不事后面也有了自动管理内存。接触的对象多了,发现不少东西在本质的原理有很是多的类似之处。高并发

引用计数算法缺点:

  • 须要额外的空间来存储计数器,以及繁琐的更新操做。
  • 没法处理循环引用对象

其中没法处理循环引用对象,算是引用计数法的一个重大漏洞。学习

1.2 可达性分析算法

可达性是指,若是一个对象会被至少一个在程序中的变量经过直接或间接的方式被其余可达的对象引用,则称该对象是可达的(reachable)。更准确的说,一个对象只有知足下述两个条件之一,就会被判断为可达的:

  • 自己是根对象。根(Root)是指由堆之外空间访问的对象。JVM中会将一组对象标记为根,包括全局变量、部分系统类,以及栈中引用的对象,如当前栈帧中的局部变量和参数。
  • 被一个可达的对象引用。

这个算法的基本思路就是经过一系列的成为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),则证实此对象是不可用的。

可达性分析算法.jpeg

GC Roots又是什么呢?能够暂时理解为由堆外指向堆内的引用。

在Java语言中,能够做为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即通常说的Native方法)引用的对象。
  • 已启动且未中止的Java线程。

可达性分析算法能够解决引用计数算法不能解决的循环引用问题。举个例子,即使对象a和b相互引用,只要从GC Roots出发没法到达a或者b,那么可达性分析便不会将它们加入存活对象合集之中。

关于Java中的引用的定义及分类(强引用、软引用、弱引用、虚引用)会在单独出一篇进行详细介绍,Java引用的内容虽然有点冷门,可是不少公司面试的常考点。

可达性分析算法自己虽然很简明,可是在实践中仍是有很多其余问题须要解决的。好比,在多线程环境下,其余线程可能会更新已经访问过的对象中的引用,从而形成误报(将引用设置为null)或者漏报(将引用设置为未被访问过的对象)。误杀还能够接受,Java虚拟机至多损失了部分垃圾回收的机会。漏报就问题大了,由于垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则颇有可能会直接致使Java虚拟机奔溃。

2 垃圾回收算法

上面咱们介绍什么是Java中的垃圾,接下来咱们就开始介绍如何高效的回收这些垃圾。

2.1 标记-清除算法

标记-清除(Mark-Sweep)算法能够分为两个阶段:

  • 标记阶段:标记出全部能够回收的对象。
  • 清除阶段:回收全部已被标记的对象,释放这部分空间。

该算法存在以下不足:

  1. 内存碎片。因为Java虚拟机的堆中对象必须是连续分布的,所以可能出现总空闲内存足够,可是没法分配的极端状况。没法找到足够的连续内存,而不得不提早触发一次垃圾收集动做。
  2. 分配效率较低。若是是一块连续的内存空间,那么咱们能够经过指针加法(pointer bumping)来作分配。而对于空闲列表,Java虚拟机则须要逐个访问列表中的项,来查询可以放入新建对象的空闲内存。

标记-清除算法的示意图以下:标记清除算法.png

2.2 复制算法

复制算法的过程以下:

  • 划分区域:将内存区域按比例划分为1个Eden区做为分配对象的“主战场”和2个幸存区(即Survivor空间,划分为2个等比例的from区和to区)。
  • 复制:收集时,打扫“战场”,将Eden区中仍存活的对象复制到某一块幸存区中。
  • 清除:因为上一阶段已确保仍存活的对象已被妥善安置,如今能够“清理战场”了,释放Eden区和另外一块幸存区。
  • 晋升:如在“复制”阶段,一块幸存区接纳不了全部的“幸存”对象。则直接晋升到老年代。

复制算法.png

该算法解决了内存碎片化问题,但堆空间的使用效率极其低下。在对象存活率较高时,须要进行较多的复制操做,效率会变得很低。

2.3 标记-整理算法

该算法分为两个阶段:

  • 标记阶段:标记出全部能够回收的对象。
  • 压缩阶段:将标记阶段的对象移动到空间的一端,释放剩余的空间。

该算法的标记过程与标记-清除算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但它对内存变更更频繁,须要整理全部存活对象的引用地址,在效率上比复制算法要差不少。

标记-整理算法的示意图以下:标记-整理算法.png

2.4 分代收集算法

分代收集算法倒并无什么新的思想,只是根据对象存活周期的不一样将内存划分为几块。通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。JVM堆分代.png

新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理算法或标记-整理算法来进行回收。

3 HotSpot算法实现

3.1 枚举根节点

以可达性分析中从GC Roots节点找引用链这个操做为例,可做为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。上面介绍可达性分析算法时有详细介绍GC Roots,能够参看上面。

3.2 安全点(Safepoint)

安全点,即程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。

安全点的初始目的并非让其余线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便可以“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便可以在垃圾回收的同时,继续运行这段本地代码。

程序运行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生Safepoint。

对于安全点,另外一个须要考虑的问题就是如何在GC发生时让全部线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。

两种解决方案:

  • 抢先式中断(Preemptive Suspension)

抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。

  • 主动式中断(Voluntary Suspension)

主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起。轮询标志地地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。

3.3 安全区域

指在一段代码片断中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也能够把Safe Region看做是被扩展了的Safepoint。

4 扩展知识

4.1 GC分类

Minor GC:

  • 针对新生代。
  • 指发生在新生代的垃圾收集动做,由于java对象大多都具有朝生夕死的特性,因此Minor GC很是频繁,通常回收速度也比较快。
  • 触发条件:Eden空间满时。

Major GC:

  • 针对老年代。
  • 指发生在老年代的GC,出现了Major GC,常常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度通常会比Minor GC慢10倍以上。
  • 触发条件:Minor GC 会将对象移到老年代中,若是此时老年代空间不够,那么触发 Major GC。

Full GC:

  • 清理整个堆空间。必定意义上Full GC 能够说是 Minor GC 和 Major GC 的结合。
  • 触发条件:调用System.gc();老年代空间不足;空间分配担保失败。

4.2 Stop-the-world

GC进行时必须停顿全部Java执行线程,这就是Stop-the-world

可达性分析时必须在一个能确保一致性的快照中进行,这里“一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不能够出现分析过程当中对象引用关系还在不断变化的状况,这一点不知足的话分析结果准确性就没法获得保证。

Stop-the-world是经过安全点机制来实现的。当Java虚拟机接收到Stop-the-world请求,它便会等待全部的线程都到达安全点,才容许请求Stop-the-world的线程进行独占的工做。

4.3 卡表

有个场景,老年代的对象可能引用新生代的对象,那标记存活对象的时候,须要扫描老年代中的全部对象。由于该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又作全堆扫描?成本过高了吧。

HotSpot给出的解决方案是一项叫作卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,而且维护一个卡表,用来存储每张卡的一个标识位。这个标识位表明对应的卡是否可能存有指向新生代对象的引用。若是可能存在,那么咱们就认为这张卡是脏的。

在进行Minor GC的时候,咱们即可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成全部脏卡的扫描以后,Java虚拟机便会将全部脏卡的标识位清零。

想要保证每一个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机须要截获每一个引用型实例变量的写操做,并做出对应的写标识位操做。

卡表能用于减小老年代的全堆空间扫描,这能很大的提高GC效率