zl程序教程

您现在的位置是:首页 >  Java

当前栏目

《深入理解Java虚拟机》读书笔记(七)

2023-03-07 09:02:03 时间

经典垃圾收集器

HotSpot虚拟机垃圾收集器.png

衡量垃圾收集器的三项重要指标:

  • 内存占用
  • 吞吐量
  • 延迟

三者共同构成了一个“不可能三角”,一款优秀的收集器通常最多可以同时达成其中的两项

垃圾收集下的并发与并行

  • 并发

描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条垃圾收集线程同时工作,通常默认此时用户程序处于等待状态

  • 并行

描述的是垃圾收集器与用户线程之间的关系;同一时间收集器线程与用户线程都在运行;由于垃圾收集线程也占用一部分系统资源,所以应用程序的处理吞吐量将收到一定程度的影响

新生代垃圾收集器

Serial收集器

  • 最基础的、单线程的、简单高效的(与其他单线程的收集器相比)、基于标记-整理法的新生代垃圾收集器
  • 所有垃圾收集器里额外内存消耗最小的,对于内存受限的环境下更加友好
  • HotSpot虚拟机运行在客户端模式下的默认新生代收集器
  • 进行垃圾收集时必须使用STW
Serial+Serial Old组合运行示意图.png

ParNew收集器

  • Serial收集器的多线程并行版本,新生代垃圾收集器(ParNew收集器作为Serial收集器的并行版本,在控制参数、收集算法、STW、对象分配规则、回收策略等上都与Serial收集器完全一致,ParNew收集器在实现时共用了Serial收集器的代码)
  • 使用-XX:+/-UseParNewGC参数选择强制指定或者禁用ParNew收集器
  • JDK8之后(JDK8声明Serial+CMS为废弃,JDK9完全取消了这个组合),ParNew是唯一可以与CMS组合使用收集器;是激活CMS后(-XX:+UseConcMarkSweepGC)的默认新生代收集器
  • JDK9之前,ParNew+CMS的组合是官方推荐的运行在服务端模式下的收集器解决方案(JDK9开始,被G1所取代,G1是面向全堆的一款收集器)
  • ParNew对于垃圾收集时系统资源的高效利用很有好处,默认开启的收集线程数与处理器核心数量相同(开启线程数可以通过-XX:ParallelGCThreads来限制)
ParNew+Serial Old组合运行示意图.png

Parallel Scavenge收集器

  • 基于标记-复制算法实现的一款新生代垃圾收集器
  • 吞吐量优先垃圾收集器,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量
  • 自适应的调节策略,通过参数-XX:+UseAdaptiveSizePolicy开启;用户使用-XX:MaxGCPauseMillis参数(设置最大停顿时间,关注最大停顿时间)或-XX:GCTimeRatio参数(设置吞吐量的大小,更关注吞吐量)给虚拟机设立一个最大停顿或是吞吐量的优化目标,具体的调优细节和内存管理策略交给虚拟机完成
  • 在要求吞吐量或者处理器资源较为稀缺的场合,优先考虑Parallel Scavenge + Parallel Old的组合
垃圾收集器中吞吐量的定义.png
Parallel Scavenge+Parallel Old组合运行示意图.png

老年代垃圾收集器

Serial Old收集器

  • Serial收集器的老年代版本,基于标记-整理法,单线程工作
  • 供客户端模式下的HotSpot虚拟机使用
  • 在服务端模式下,要么与Parallel Scavenge收集器搭配使用(主要是JDK5以及之前的版本),要么作为CMS收集器发生失败时的后备预案

Parallel Old收集器

  • Parallel Scavenge收集器的老年代版本;基于标记-整理法,支持多线程并发收集
  • 为弥补先前Parallel Scavenge + Serial Old组合在吞吐量上的劣势而产生(主要是Serial Old收集器在服务端应用性能上的“拖累”),因此,在吞吐量或者处理器资源较为稀缺的场合,优先考虑Parallel Scavenge + Parallel Old的组合

CMS收集器

  • 基于标记-清除法的、以获得最短回收停顿时间为目标的收集器
  • 并发低停顿收集器,多用于关注服务响应速度、基于浏览器的B/S系统的服务端上
  • CMS收集器的垃圾收集过程,分为四个步骤;其中初始标记、重新标记仍需要STW

初始标记(CMS initial mark):标记GC Roots能直接关联到的对象,速度很快,但需要STW 并发标记(CMS concurrent mark):从每一个被初始标记获得的对象开始遍历整个对象图,耗时长,但用户线程可以与垃圾收集线程并发运行 重新标记(CMS remark):修正并发标记过程中由于和用户线程并发而导致标记产生变动的那部分对象,这个阶段虽然耗时,但远比并发标记的耗时大大缩短,此阶段仍需要STW 并发清除(CMS concurrent sweep):清理删除掉已经死亡的对象,由于不需要移动对象(标记-清除法),所以可以和用户线程并发进行

CMS运行示意图..png
  • CMS收集器对处理器资源非常敏感,CMS默认启动的回收线程数是(处理器核心数+3)/4,如果应用本来的处理器负载就很高,那么CMS所占用的那部分系统资源无疑会使应用程序变慢、降低总的吞吐量

虚拟机曾提供了i-CMS(增量式并发收集器),尽量减少垃圾收集线程的独占资源的时间,减少对用户程序的影响;但因为实践证明i-CMS效果一般,从JDK7开始,i-CMS就已经声明为废弃了

  • CMS收集器无法清理“浮动垃圾”,可能出现“Concurrent Mode Failure”并发失败进而导致另一次完全STW的Full GC的产生

浮动垃圾区别于空间碎片,其来源于并发标记阶段和并发清理阶段用户线程和GC线程并发时程序运行自然产生的新的垃圾,由于错过了本次的标记过程只能留待下次处理,因此称为浮动垃圾。 浮动垃圾的存在使得CMS收集器不能等待老年代几乎完全填满时才进行收集,而必须预留一部分空间供GC线程与用户线程并发运行时使用 JDK提供参数-XX:CMSInitiatingOccupancyFraction参数来设置当老年代空间达到多少时触发收集,JDK6时CMS收集器的启动阈值已经默认提升至92%;但此参数不宜设置过高,过高时容易导致给浮动垃圾预留空间不足,出现“并发失败”,这时候虚拟机不得不启动后备预案:冻结用户线程,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这将使得停顿时间更长

  • CMS使用标记-清除算法作为底层实现,会产生大量的空间碎片;空间碎片对大对象的分配极不友好而不得不提前出发一次Full GC

CMS收集器提供参数-XX:+UseCMSCompactAtFullCollection,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,同时,搭配-XX:CMSFullGCsBeforeCompaction参数,此参数要求CMS收集器在执行过若干次不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(这两个参数在JDK9开始废弃)

Garbage First收集器

  • G1收集器是一款主要面向服务端应用的垃圾收集器,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
  • G1收集器的垃圾收集是Mixed GC,它可以面向堆内存任何部分来组成回收机进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多、回收收益最大
  • G1收集器的一个强大的功能:由用户指定期望的停顿时间;设置不同的期望停顿时间,可使的G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡

基于Region的内存布局:G1仍是遵循分代收集理论设计的,但区别于其他垃圾收集器对堆内存划分,G1不再坚持固定大小以及固定数量的分代区域划分,而是将连续的Java堆划分为多个大小相等的独立区域,称为Region,新生代和老年代也不再固定,它们都是一系列区域的动态集合;每个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间;通过参数-XX:G1HeapRegionSize设置每个Region的大小 Humongous区域:Region中专门用来存储大对象的区域。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待 区域的优先级回收方式:使用Region作为单次回收的最小单元,避免了在整个Java堆上进行全区域的垃圾收集,让G1收集器跟踪各个Region里面的垃圾堆积的回收收益(回收所获的空间以及回收所需的时间)的大小,然后在后台维护一个优先级列表,每次根据用户设定的收集停顿大小(-XX:MaxGCPauseMillis),优先处理回收收益最大的Region,提高回收的效率 原始快照处理并发:在与用户线程并发时,必须保证其不能打破原本的对象图结构,CMS收集器采用了增量更新算法实现,G1则采用了原始快照(SATB)算法实现;G1为每个Region设计了两个名为TAMS的指针,将TAMS指针位置以上的空间用于并发回收过程中新对象的分配 并发失败:与CMS一样,如果内存回收速度赶不上内存分配速度,也会导致Concurrent Mode Failure从而导致一次Full GC

  • G1从整体来看是基于“标记-整理”算法实现的垃圾收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现的
  • G1收集器的运作过程大致分为四个步骤

初始标记(Initial Marking):标记GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配对象,这个阶段需要STW,而且是借用Minor GC的时候同步完成的,耗时很短,没有额外的停顿 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,递归扫描整个对象图,耗时较长,但可与用户程序并发执行,当对象图扫描完成后,还要重新处理SATB记录下的在并发时有引用变动的对象 最终标记(Final Marking):对用户线程做另一个短暂的暂停,STW,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来执行回收计划,可以选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,这里的操作涉及存活对象的移动,必须STW,由多条收集器线程并行完成

G1收集器运行示意图.png
  • 目前小内存应用上CMS的表现大概率仍然要优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间

低延迟垃圾收集器

Shenandoah收集器、ZGC收集器

Shenandoah收集器

  • Shenandoah收集器是一款不由Oracle公司开发的HotSpot垃圾收集器,只有OpenJDK才会包含,而在OracleJDK中被完全排除
  • Shenandoah收集器在实现上和G1收集器更加相似,它们两者有着相似的对内存布局(Region的堆内存布局、Humongous Region的大对象存储结构、区域的优先级回收方式等),在初始标记、并发标记等阶段的处理思路都高度一致,甚至共享了部分实现代码
  • Shenandoah收集器默认是不使用分代收集的,同时摒弃了在G1中需要耗费大量内存和计算资源去维护的记忆集,改用了名为“连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题发生的概念
  • Shenandoah收集器工作过程大致分为九个阶段

初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段需要STW,但停顿时间与堆大小无关,只与GC Roots的数量有关 并发标记(Concurrent Marking):与G1一样,遍历对象图,标记全部可达对象,这个过程与用户线程并发,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region组成回收集,这一阶段存在短暂的STW 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域连一个存活对象都没有的Region 并发回收(Concurrent Evacuation):并发回收阶段是Shenandoah收集器与之前HotSpot中其他收集器的核心差异。在这个阶段,Shenandoah要把回收集里存活对象先复制一份到其他未使用的Region之中,Shenandoah通过读屏障和“Brooks Pointers”转发指针处理了这期间用户线程和GC线程并发存在的对象移动问题 初始引用更新(Initial Update Reference):建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务,这一阶段需要短暂的STW 并发引用更新(Concurrent Update Reference):并发回收阶段复制对象结束且已完成了初始引用更新阶段的处理后,需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个阶段与用户线程一起并发,时间长短取决于内存中涉及的引用数量,且搜索对象的方式不是按图搜索而是按照内存物理地址线性地搜索出引用类型,将旧值改为新值 最终引用更新(FInal Update Reference):解决了堆中的引用更新后,还要修正存在于GC Roots中的引用,这个阶段需要STW,停顿时间只与GC Roots数量相关 并发清理(Concurrent Cleanup):经过并发回收和引用更新之后,整个回收集中所有Region已再无存活对象,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象的分配

ZGC收集器

  • ZGC收集器是一款基于Region内存布局的、不设分代的、使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的、以低延迟为首要目标的一款垃圾收集器

虚拟机及垃圾收集器日志

  • 查看GC基本信息,
# JDK9之前
-XX:+PrintGC

# JDK9之后
-Xlog:gc
  • 查看GC详细信息
# JDK9之前
-XX:+PrintGCDetails

# JDK9之后
-Xlog:gc*
  • 查看GC前后堆、方法区可用容量变化
# JDK9之前
-XX:+PrintHeapAtGC

# JDK9之后
-Xlog:gc+heap=debug
  • 查看GC过程中用户线程并发时间以及停顿时间
# JDK9之前
-XX:+PrintGCApplicationConcurrentTime
或者
-XX:+PrintGCApplicationStoppedTime

# JDK9之后
-Xlog:safepoint

一些常用参数

参数

描述

UseSerialGC

虚拟机运行在Client模式下的默认值,使用Serial+Serial Old的收集器组合

UseParNewGC

使用ParNew+Serial Old的收集器组合,JDK9后不再支持 |

UseConcMarkSweepGC

使用ParNew+CMS+Serial Old的收集器组合,Serial Old作为CMS并发失败后的后备方案

UseParallelGC

JDK9之前运行在Sever模式下的默认值,使用Parallel Scavenge+Serial Old的收集器组合

UseParallelOldGC

使用Parallel Scavenge+Parallel Old的垃圾收集器组合

SurvivorRatio

新生代Eden与Survivor区域容量比值,默认是8,即8:1

PretenureSizeThreshold

直接晋升到老年代对象大小,大于这个参数大小的对象将直接在老年代分配,此参数只对Serial和ParNew两款新生代收集器有效

MaxTenuringThreshold

晋升到老年代的年龄,每个对象坚持过一次Minor GC年龄就会+1

UseAdaptiveSizePolicy

自动调整Java堆中各区域的大小以及进入老年代的年龄

UseG1GC

JDK9后Server的默认值