长大后想做什么?做回小孩!

0%

JVM垃圾回收机制

主要是针对堆内存的对象分配与回收,堆事垃圾收集器管理的主要区域,因此被称为 GC 堆。

内存分配

  • 对象优先分配在 Eden 区,如果 Eden 区空间不够进行分配,则发起一次 Minor GC(新生代 GC),期间可能会通过分配担保机制把新生代对象转移到老年代中。

  • 大对象直接进入老年代,避免新生代垃圾回收太过频繁。G1 收集器通过设置堆区域大小和 Mixed GC 的阈值参数来确定哪些对象直接进入老年代。Parallel Seavenge 收集器由虚拟机根据堆内存使用情况和历史数据动态决定。

  • 长期存活的对象将进入老年代,new 对象首先会分配在 Eden 区域,经过一次新生代垃圾回收之后,如果对象还存活,则分配到 S0 或 S1,并且对象的年龄加一(Eden 区 >> Survivor 区后对象初始年龄变为 1),年龄增加到阈值(默认值根据收集器不同而不同,可以通过 -XX:MaxTenuringThreshold 来设置)就会被晋升到老年代中。

    PS:“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加到某个年龄时,所累加的大小超过了 Survivor 区的一半,则取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值”。

  • 空间分配担保,JDK6 之前新生代 GC 会先判断老年代是否有足够存下当前新生代对象的空间,空间足够,则直接进行新生代 GC,否则检查是否开启了允许担保失败-XX:HandlePromotionFailure,如果开启则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则新生代 GC,如果小于平均大小或者没有开启允许担保失败,则 Full GC。

对象死亡判断

  • 引用计数法:每个对象有个引用计数器,每次被一个地方引用就加 1,引用失效就减 1,简单高效,但是难以解决对象循环引用问题,所以主流虚拟机都没有选择。

  • 可达性分析:关键是找到 GC Roots 对象作为起点,从这个节点往下搜,节点走过的路径称为引用链,如果一个对象到 GC Roots 没有任何引用链相关的话,说明该对象可以被回收。

    GC Roots:被虚拟机栈(局部变量表)、本地方法栈、方法区的类静态属性和常量、同步锁持有、JNI 这些引用的对象。

    即使可达性分析中,一个对象没有任何引用链相关,也不会被立刻清理。还要再次判断这些不可达对象是否有必要执行 finalize 方法,当对象有覆盖 finalize 方法且未被虚拟机调用过时,对象会被放到一个队列中等待专门的线程去调用 finalize 方法,方法执行后对象才会被回收释放内存。(因为 finalize 方法的不确定性、安全性、影响 GC 效率的问题,在 JDK9 之后被废除了)

  • 无用常量:JDK1.7 之前运行时常量池包含字符串常量池存放在方法区(hotspot 中永久代实现的方法区);JDK1.7 字符串常量池拿到了堆中,运行时常量池剩下的东西还在方法区;JDK1.8 用元空间实现方法区,字符串常量池还在堆中,运行时常量池剩下的东西还在方法区。堆中字符串常量池的回收也很简单,当没有任何 String 对象引用该字符常量的话,该字符常量就可以被清理出常量池了。

  • 无用的类:一个类需要被卸载,需要同时满足三种情况:1. 类的所有实例都被回收。2. 加载该类的类加载器已经被回收。3. 该类的类对象没有被引用。

引用类型

  • 强引用:使用最普遍的引用,如果一个对象具有强引用,那么虚拟机即使抛出 OOM 也绝对不会回收这个对象。
  • 软引用:如果一个对象只有软引用,垃圾回收器只有在内存不足的时候才会考虑回收这个对象。适合用来实现内存敏感的高速缓存。
  • 弱引用:如果一个对象只有弱引用,垃圾回收器无论内存是否充足都会考虑回收这个对象。
  • 虚引用:如果一个对象只有虚引用,那么和没有引用一样,任何时候都可以被回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用必须和引用队列联合使用,当垃圾收集器要回收一个有虚引用的对象时,会把虚引用加入到与之关联的引用队列中。程序就可以根据引用队列中虚引用了解被引用的对象是否将要被回收,可以在内存回收之前采取必要的措施。

程序设计中很少使用弱引用和虚引用,更多的使用是软引用,软引用可以加速 JVM 对垃圾内存回收速度,可以维护系统的运行安全,一定程度上防止 OOM 问题的产生。

垃圾收集算法

  • 标记-清除:标记阶段标记出所有不需要回收的对象,然后统一回收未标记的对象。缺点是标记和清除的过程效率都不高,最重要的是容易产生大量的内存碎片。
  • 复制:旨在解决内存碎片问题,使用两块一样大的内存,每次收集是把所有不需要回收的对象复制到另一块内存中区。缺点是内存只有一半可用,对于老年对象不友好,如果存活数量多,性能很差。
  • 标记-整理:标记之后,让存活的对象向内存的一端移动,然后回收掉端边界之外的内存。缺点依然是效率比较低。
  • 分代收集:主流的算法,根据存活对象的周期把内存分为几块,根据每块内存中对象的特点使用不同的收集算法。例如,新生代中通常每次都有大量的对象死去,可以选择复制算法,每次收集只需要复制少量存活代码即可,对于老年代存活几率比较高的对象,则使用标记-清除或者标记-整理算法进行收集。

垃圾收集器

Serial收集器

单线程收集器,不仅仅只有一个线程来进行垃圾收集,而且垃圾收集的过程中必须暂停其他所有工作线程。

新生代采用标记-复制算法,老年代采用标记-整理算法。

Serial 收集器没有线程交互的开销,获得相对较高的单线程收集效率。对于运行在 Client 模式下的虚拟机来说是个不错的选择。

ParNew 收集器

Serial 收集器的多线程版本,除了垃圾收集使用多线程之外,其他的回收策略和 Serial 收集器完全一样。

新生代采用标记-复制算法,老年代采用标记-整理算法。

对于运行在 Server 模式下的虚拟机是个还不错的选择。

Parallel Scavenge 收集器

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old ,即新生代何老年代都采用多线程并行垃圾收集。

新生代采用标记-复制算法,老年代采用标记-整理算法。

Parallel Scavenge 收集器关注的是吞吐量,高效地利用 CPU,提供了很多参数供用户找到合适的停顿时间和最大吞吐量,而且可以使用自适应调节策略,把内存管理优化交给虚拟机本身。

Serial Old 收集器

Serial 收集器的老年代版本,同样是单线程收集器。主要是在 JDK1.5 以及更早的版本中与 Parallel Scavenge 收集器搭配使用,同时也是 CMS 收集器的后备方案。

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用多线程和和标记-整理算法。在注重吞吐量以及 CPU 资源的场景里都可以优先考虑 Parallel Scavenge + Parallel Old 。

CMS 收集器

CMS 收集器是标记-清除算法的实现,旨在获取最短回收停顿时间,是 HotSpot 虚拟机第一款真正意义上的并发收集器(实现垃圾收集线程和用户线程同时工作)。

整个过程分为四步:

  1. 初始标记:暂停其他线程,标记与 GC root 相连的对象,速度很快。

  2. 并发标记:同时开启 GC 和用户线程,用一个闭包结构来记录可达对象。

    PS:结束后,不能保证包含所有可达对象,因为用户线程可能不断更新引用域,所以 GC 线程不能保证实时性,所以算法会跟踪记录发生引用更新的地方。

  3. 重新标记:暂停其他线程,修正并发标记期间因为用户线程继续运行导致变动那一部分对象的标记记录。这个阶段的停顿时间比初始标记阶段稍长,但是远远低于并发标记阶段的时间。

  4. 并发清除:同时开启 GC 和用户线程,GC 线程开始对未标记的区域做垃圾收集。

CMS 收集器的主要优点是并发收集和低停顿。但是对 CPU 资源敏感、不能处理浮动垃圾、采用标记-清除算法导致大量内存碎片。在 Java9 中被标记为过时,在 JAVA14 中被移除。

G1 收集器

面向服务器的垃圾收集器,主要针对多处理器和大内存机器。极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。在 JDK7 中引入,从 JDK9 开始作为默认垃圾收集器。

G1 收集器最大的变化就是采取了 Region 分区的方式,把内存空间化整为零,在后台维护了一个优先队列,每次根据允许的收集时间,优先选择回收价值最大的 Region。

G1 收集器的步骤:

  • 初始标记:STW,和 CMS 类似,通过 GC Roots 进行可达分析,通常耗时非常短。
  • 并发标记:递归的追踪所有可达对象。
  • 最终标记:STW,处理并发标记过程中新产生的对象引用关系,确保所有在并发标记阶段漏掉的对象都被正确标记。
  • 筛选回收:根据每个 Region 的垃圾堆积情况和回收价值进行排序,选择性的回收部分 Region 中的垃圾对象。回收过程中包括将存活的对象从一个 Region 复制到另一个 Region,并更新相关的引用。这个过程也是并发的,同时可能会涉及到对象的整理和压缩,以减少内存碎片。

G1 收集器提供了两种垃圾回收模式:

  • Young GC:主要负责回收新生代中的垃圾对象,Eden 区和 Survivor 区的存活对象被复制到另一个 Survivor 区或者晋升到 Old 区,这个过程是 STW 的,但是新生代中对象通常较少,所以暂停时间较短。
  • Mix Gc:G1 收集器特有的回收策略,不仅回收新生代中所有的 Region,还回收部分老年代的 Region。目的是在保证停顿时间不超过预期的情况下,尽可能多的回收垃圾对象。Mix GC 的过程中,首先会进行全局的并发标记,标记出所有存活对象。然后回收阶段,根据标记结果选择收益较高的老年代和新生代Region 一起回收。相较于 Young GC,Mix GC 的停顿时间更长,因为涉及到老年代中对象的扫描和回收。但是 Mix GC 可以收集更多的垃圾,更有效的释放内存空间。

G1 收集器的优点:

  • 并发和并行:充分利用 CPU、多核环境,使用多个 CPU 或 CPU 核心来缩短 Stop-The-World 的时间,部分收集器原本需要停顿用户线程的 GC 操作,在 G1 收集器中仍然可以通过并发的方式继续执行。
  • 分区域收集:G1 虽然不需要其他收集器配合就能采用 Region 分区的思想独立管理整个 GC 堆,但是还是保留了分代的概念,即每个 Region 会根据其中对象的特点进行分区标记。
  • 空间整合:与 CMS 不同,G1 整体上看是基于标记-整理算法实现,局部上看是基于标记-复制算法实现。
  • 可预测的停顿:这是 G1 相对于 CMS 最大的优势,虽然 G1 和 CMS 都关注减低停顿时间,但是 G1 还建立了可预测的停顿时间模型,让用户可以指定在一个长度为 M 毫秒的时间片段内, 消耗在垃圾收集上的时间不得超过 N 毫秒。

CMS 与 G1 的区别:

  • CMS 中堆被划分为 PermGen、YoungGen、OldGen,其中 YoungGen 又被分为了两个 survivor 区域。G1 中堆被平均分为了很多 Region,虽然每个 Region 也保留了 Eden、Survivor、Old、Humongous 的概念,但是收集的时候是整个 Region 为单位收集的。
  • G1 回收内存后,会立刻合并空闲内存的工作;而 CMS 则是默认在 STW 的时候做。
  • G1 可以在整个堆上使用;CMS 只能在 OldGen 上使用,需要配合上 Serial 收集器或者 ParNew 收集器。