• 深入理解Java虚拟机读书笔记--5垃圾收集器


    对应《深入理解Java虚拟机》3.5 3.6 3.7 3.8节的内容

    传统垃圾收集器(3.5)

    《Java虚拟机规范》中对垃圾收集器应该如何实现并没有做出任何规定,因此不同的厂商、不同版本的虚拟机所包含的垃圾收集器都可能会有很大差别,不同的虚拟机一般也都会提供各种参数供用户根据自己的应用特点和要求组合出各个内存分代所使用的收集器。

    JVM学习笔记22 垃圾回收理论知识

    • 如果两个收集器之间存在连线,就说明它们可以搭配使用
    • 图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器
    • 我们选择的只是对具体应用最合适的收集器
    • 在 JDK8 时将 Serial + CMSParNew + Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK9 中完全取消了这些组合的支持(JEP214)

    Serial收集器

    • 最基础、历史最悠久的收集器
    • 单线程工作的收集器
    • Stop The World 带给用户恶劣的体验
    • 迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器
    • 简单而高效
    • 所有收集器里额外内存消耗最小的

    ParNew收集器

    • ParNew 收集器实质上是Serial收集器的多线程并行版本
    • 除了同时使用多条线程进行垃圾收集之外,其余的行为都与Serial收集器完全一致
    • 在 JDK 7 之前的遗留系统中首选的新生代收集器就是 ParNew收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与 CMS 收集器配合工作
    • ParNew收集器是激活 CMS后(-XX:+UseConcMarkSweepGC)的默认新生代收集器,也可以使用 -XX:+/-UseParNewGC 选项来强制指定或者禁用它。
    • G1是一个面向全堆的收集器,不再需要其他新生代收集器的配合工作。所以自JDK 9开始,ParNew 加 CMS 收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了 ParNew 加
      Serial Old 以及 Serial 加 CMS 这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了 -XX:+UseParNewGC 参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了。
    • ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时系统资源的高效利用还是很有好处的;它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

    Parallel Scavenge收集器

    • 基于标记-复制算法实现的收集器
    • 并行收集的多线程收集器
    • Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput 吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值)
    • 提供了两个参数用于精确控制吞吐量,
      • 控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数;允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值(垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的)
      • 设置吞吐量大小的 -XX:GCTimeRatio 参数;譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%,即1/(1+19),默认值为99,即允许最大1%,即1/(1+99) 的垃圾收集时间
    • 也经常被称作“吞吐量优先收集器”
    • Parallel Scavenge 收集器还有一个参数 -XX:+UseAdaptiveSizePolicy 。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
      • 只需要把基本的内存数据设置好(如-Xmx设置最大堆),
      • 然后使用 -XX:MaxGCPauseMillis 参数(更关注最大停顿时间) -XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。

    Serial Old收集器

    • Serial Old是Serial收集器的老年代版本
    • 单线程收集器
    • 使用标记-整理算法
    • 主要意义也是供客户端模式下的HotSpot虚拟机使用
    • 在服务端模式下,可以作为 CMS 收集器发生失败时的后备预案,在并发收集发生 ConcurrentMode Failure 时使用

    Parallel Old收集器

    • Parallel Old是Parallel Scavenge收集器的老年代版本
    • 支持多线程并发收集
    • 基于标记-整理算法实现
    • 在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。

    CMS收集器 (Concurrent Mark Sweep)

    • 一种以获取最短回收停顿时间为目标的收集器
    • 基于“标记-清除”算法实现
    • 基于浏览器的B/S系统的服务端上的应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS 收集器就非常符合这类应用的需求。

    运作过程:

    • 1)初始标记(CMS initial mark)
      • 仍然需要“Stop The World”
      • 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
    • 2)并发标记(CM Sconcurrent mark)
      • 从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程
    • 3)重新标记(CM Sremark)
      • 仍然需要“Stop The World”
      • 修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
    • 4)并发清除(CM Sconcurrent sweep)
      • 清理删除掉标记阶段判断的已经死亡的对象

    由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

    缺点:

    • 对处理器资源非常敏感;事实上,面向并发设计的程序都对处理器资源比较敏感
    • 由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop The World”的 Full GC 的产生。
    • CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
      • 为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认是开启的,此参数从JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长
      • 因此虚拟机设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表示每次进入Full GC时都进行碎片整理)。

    Garbage First收集器

    演化:

    • 从JDK6 Update 14开始就有Early Access版本的G1收集器供开发人员实验和试用
    • 直至JDK 7 Update 4,Oracle才认为它达到足够成熟的商用程度,移除了“Experimental”的标识
    • 到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”
    • JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

    特点:

    • G1 面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的 Mixed GC 模式。
    • G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
    • 每个Region的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为2的N次幂。
    • Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
    • 让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数 -XX:MaxGCPauseMillis 指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。

    回收过程:

    当代垃圾收集器

    Shenandoah收集器

    最初Shenandoah是由RedHat公司独立发展的新型收集器项目,在2014年RedHat把Shenandoah贡献给了OpenJDK,并推动它成为OpenJDK 12的正式特性之一,也就是后来的JEP 189。

    ZGC收集器 (Z Garbage Collector)

    ZGC就更像是Azul System公司独步天下的PGC(Pauseless GC)和C4(Concurrent Continuously Compacting Collector)收集器的同胞兄弟。

    选择合适的垃圾收集器

    收集器的权衡

    如何选择一款适合自己应用的收集器呢:

    • 应用程序的主要关注点是什么?
      • 如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点;
      • 如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点;
      • 而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
    • 运行应用的基础设施如何?譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM /Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows等。
    • 使用JDK的发行商是什么?版本号是多少?是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?

    假设某个直接面向用户提供服务的B/S系统准备选择垃圾收集器:

    • 如果你有充足的预算但没有太多调优经验,那么一套带商业技术支持的专有硬件或者软件解决方案是不错的选择,Azul公司以前主推的Vega系统和现在主推的Zing VM 是这方面的代表,这样你就可以使用传说中的C4收集器了。
    • 如果你虽然没有足够预算去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟,那ZGC很值得尝试。
    • 如果你对还处于实验状态的收集器的稳定性有所顾虑,或者应用必须运行在Windows操作系统下,那ZGC就无缘了,试试Shenandoah吧。
    • 如果你接手的是遗留系统,软硬件基础设施和JDK版本都比较落后,那就根据内存规模衡量一下,对于大概4GB到6GB以下的堆内存,CMS一般能处理得比较好,而对于更大的堆内存,可重点考察一下G1。

    以上都是仅从理论出发的分析,实战中切不可纸上谈兵,根据系统实际情况去测试才是选择收集器的最终依据。

    虚拟机及垃圾收集器日志

    • 查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc
    • 查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*
    • 查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug
    • 查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用 -XX:+PrintGCApplicationConcurrentTime以及-XX:PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog:safepoint
    • 查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptiveSizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace
    • 查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuringDistribution,JDK 9之后使用-Xlog:gc+age=trace

    垃圾收集器参数总结

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    实战:内存分配与回收策略(对应3.8节)

    对象优先在Eden分配

    大对象直接进入老年代

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

    动态对象年龄判定

    空间分配担保

  • 相关阅读:
    用IDEA操作数据库--MySQL
    【Pytorch with fastai】第 20 章 :结语与问题
    推荐收藏,机器学习7种特征编码太好用了
    会话与终端
    Hive三种模式安装部署
    4.从中缀向后缀转换表达式
    Structured Streaming
    研发merge请求合并代码触发检测(gitlab + jenkins)
    【kafka专栏】消费偏移量4种提交方式以及如何避免消费数据丢失与重复消费
    单片机的实验板
  • 原文地址:https://blog.csdn.net/u013837825/article/details/126513855