返回
Featured image of post Java - JVM GC

Java - JVM GC

垃圾收集的主要场所是 JVM 堆.

一、垃圾收集算法

下面这张图中存在 Permanent Space ,因此明显是基于 JDK 1.8 以前版本画的,在之后版本,元空间取代了永久代成为了 HotSpot 对方法区的实现

跟据 Object 生命周期分为三个层次

  • Young Generation
  • Old Generation
  • Permanent Generation

Young Generation 包括 Eden 区和两个存活区(From 和 To),采用“停止-复制(Stop-and-copy)”清理法。大部分对象在 Eden 区域分配,一次新生代垃圾回收后如果对象还存活,则升1岁进入 s0s1 ,清理 Eden 和使用过的一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证内存利用率有 90%。

Eden 区满时执行 Minor GC 清理可销毁对象,将不可销毁的迁移至其中一个存活区,而将另一个存活区直接清空,下一次 GC 时两个存活区角色交换,交换次数超过 15 (MaxTenuringThreshold)的进入 Old Generation。

复制算法(young代GC算法)

该算法会将内存区域分为两个大小一样的区域。GC回收时,遍历当前使用区域,只将正在引用的对象复制到另一个区域,因此复制成本较低,且复制过程中还会进行内存整理,不会出现“碎片”问题。缺点就是:需要两个大小一样的内存区域和生命周期短的对象。所以该算法不适合大内存对象和长生命周期的对象,适用于young代的SO/S1

Hotspot 的动态年龄阈值

Hotspot遍历对象时按年龄从小到大对其所占用大小进行累积,当累积的某个年龄大小超过了 survivor 区一半,取年龄与 MaxTenuringThreshold 中更小的作为新年龄阈值

Old Generation 通过“标记-整理”算法,标记处仍存活对象,并将所有存活对象向一端移动以保证内存连续,清理掉剩余部分内存。当进入的对象超过剩余空间大小,则触发 Full GC。“标记-整理”好处是不需要额外内存区域。

Permanent Generation 主要存放字节码、字符串常量池、静态变量、可持久化数据等。每次发生 Full GC 时,同时也会销毁 Permanent Generation 中的可销毁对象。

永久代实际上是HotSpot JVM对JVM方法区的实现。由于永久代内存经常不够或发生内存泄露,造成OOM(PermGen),从JDK8开始废弃了永久代,替换为了本地内存(native memory) 中的 Metaspace。

元空间与永久代最大区别在于它不在虚拟机中,而是使用本地内存。两者都是对JVM规范中方法区的实现,用于存储类的信息、常量池、方法数据、方法代码等。

字符串常量从JDK1.7开始由永久代转移到堆中(Java heap space)


二、经典垃圾收集器

HotSpot 中的安全点一般设置在方法调用、循环跳转、异常跳转等地方,只在安全点位置建立根节点枚举,强制到大安全点后才暂停,进行垃圾收集。

HotSpot 中有7个垃圾收集器,连线表示可以配合使用。

  1. Serial

    串行的单线程收集器,简单高效。在 Client 场景下为默认 Young Generation 收集器,单线程收集效率高。Server 场景用于和 Parallel Scavenge 搭配使用。

  2. ParNew

    Serial 的多线程版本。在 Server 场景下为默认 Young Generation 收集器,可以与 CMS 配合使用。

  3. Parallel Scavenge

    多线程。以“吞吐量”为优先考虑,即 CPU 运行用户代码的时间占总时间比值最高,CPU 用于垃圾回收的时间占总时间比值最低,而非其它垃圾收集器“尽可能缩短垃圾收集时用户线程的停顿时间”的目标,垃圾回收较为频繁。

    CPU效率更高,也适合后台运算任务,不适合对停顿和响应敏感的交互式程序。

    JVM中有配置以打开 GC 中新生代大小、Eden、S区自适应调节策略。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

  4. Serial Old 收集器

    ![](Serial Old.jpg)

    Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:

    • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
    • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
  5. Parallel Old 收集器

    ![](Parallel Old.jpg)

    是 Parallel Scavenge 收集器的老年代版本。

    在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

  6. CMS

    CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。

    分为以下四个流程:

    • 初始标记:仅仅只是标记一下 GC Roots 能直接关联(一级连接,不遍历)到的对象,同时遍历新生代可直达地老年对象,速度很快,需要停顿。
    • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
    • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
    • 并发清除: 清理删除掉标记阶段判断已经死亡的对象,不需要停顿。

    在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

    具有以下缺点:

    • 吞吐量低:对处理器资源敏感,低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。因此 CMS 适用于四核以上的处理器。
    • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
    • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
  7. G1

    G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

    堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

    G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

    通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

    每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

    但是这样设计也存在副作用,region 大小固定为 1MB 到 32 MB 间的 2的幂值数,尽量能划 2048 个左右同等大小的 region 。

因为大小固定,和大对象很难保证一致,容易造成空间浪费,也很容易令大对象很难找到连续空间存放。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记短暂停顿线程以标记 GC Roots 直接关联到的对象,并修改 TAMS (Next Top at Mark Start) 值,让下一并发阶段能在正确 Region 中创建新对象。
  • 并发标记:从 GC Roots 开始对堆对象进行可达性分析,递归扫描堆中的对象图,找出存活的对象,耗时长,但可以并发执行
  • 最终标记:为了修正在并发标记阶段遗留的因用户程序继续运作而导致变动的标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要短暂停顿线程,但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

具备如下特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

除经典垃圾收集器外还有 Shenandoah 收集器(CAS并发)、ZGC 收集器(通过染色体指针减少GC中内存屏障的使用)等低延迟垃圾收集器,见以下博客

https://blog.csdn.net/qq_31709249/article/details/106711606


三、内存分配与回收策略

Minor GC 和 Full GC

  • Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

1. 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

  • -Xmx: 最大堆大小
  • -Xms: 最小堆大小
  • -Xmn: 年轻代堆大小
  • -XXSurvivorRatio: 年轻代中Eden区与Survivor区的大小比值
2. 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

3. 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义对象进入老年期的年龄阈值。

4. 动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5. 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

JDK 6 Update 24 后,如果老年代连续空间大于新生代对象总大小或历次晋升的平均大小,则直接 Minor GC,否则 Full GC。

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

  1. 调用 System.gc()

    只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行不建议使用这种方式,而是让虚拟机管理内存。

  2. 老年代空间不足

    老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

    为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  3. 空间分配担保失败

    使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第 5 小节。

  4. JDK 1.7 及以前的永久代空间不足

    在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

    当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

    为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

  5. Concurrent Mode Failure

    执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。


四、可销毁对象

Java 虚拟机不使用引用计数算法,因为两个对象如果循环引用,则引用计数器永远无法为 0。

可达性分析

根搜索方法,将所有 Java 对象构成“搜索树”结构,有一个根节点 root,每次从根节点触发进行搜索,遍历完后,不存在的变量成为可销毁对象。

root 包括所有正在运行线程栈上的引用变量、所有全局变量、所有 ClassLoader

类的卸载

类卸载必须满足很多条件,最基本的有:所有实例都被回收;ClassLoader已被回收;对应的Class对象没有在任何地方被引用。

8u40 以后 G1 增加并默认开启 ClassUnloadingWithConcurrentMark ,在并发标记阶段结束后,JVM 直接进行类卸载。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus