JVM全文
概述
- JVM规范对垃圾回收器没有特别要求,可以由不同厂商、不同版本的JVM来实现
分类
线程数来分,串行垃圾收集器和并行垃圾收集器
- 串行回收,在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束;默认被应用在客户端模式下的JVM
- 在单CPU或者较小的应用内存等硬件平台不优越的场合,性能表现超过
并发和并行- 并行收集运用
多个CPU同时执行垃圾回收,采用独占式,也会导致STW,产生的停顿时间短于串行
工作模式来分,并发式垃圾回收器和独占式垃圾回收器
- 并发式,垃圾回收线程与用户线程交替执行,尽可能减少用户线程停顿时间,提高响应,减小延迟
- 独占式,一旦运行,就会
STW,停止应用程序的用户线程,直到垃圾回收过程完全结束
碎片处理方式来分,压缩式垃圾回收器和非压缩式垃圾回收器
- 压缩式,回收完成后,对存活对象进行压缩整理,消除回收后的碎片;指针碰撞
- 非压缩式,不进行整理;空闲列表来维护,记录空闲的内存空间
工作内存区间来分,年轻代垃圾回收器和老年代垃圾回收器
性能指标
吞吐量,运行用户代码的时间占总运行时间的比例,越大越好
- 总运行时间=程序运行时间+内存回收时间
- 100分钟,垃圾收集1分钟,吞吐量99%
- 高吞吐量的应用程序有更长的时间基准,不用去考虑暂停时间
- 垃圾收集开销,吞吐量的补数,垃圾收集所用的时间与总运行时间的比例,
越小越小 暂停时间,STW,执行垃圾收集时,程序的工作线程被暂停的时间,越短越好- 收集频率,相对于应用程序的执行,收集操作发生的频率,
- 频率低->暂停总时间长->某段时间内吞吐量低
- 频率高->暂停总时间短->某段时间内吞吐量高
内存占用,Java堆区所占的内存大小,越大越好- 快速,一个对象从诞生到被回收所经历的时间,
越快越好 - 吞吐量、暂停时间、内存占用,构成不可能三角,不能同时具备
- 抓住两点,吞吐量和暂停时间(最重要,追求低延迟)
- 内存空间大->收集频率低->某段时间吞吐量提高,但是只要进行GC,暂停时间提高,延迟提高,在包含GC的时间段中,吞吐量减少
- 吞吐量优先,单位时间内,STW总时间最短,单次STW时间长
让用户感觉只用用户线程在执行,用户线程连续执行的时间较长
- 暂停时间优先,注重低延迟,内存空间小,单次STW时间最短,但是回收频率高,单位时间的STW长,吞吐量下降
对于一个交互式的应用程序,具有较低的暂停时间很重要
高吞吐量和低暂停时间是相矛盾的
- 吞吐量优先,需要降低内存回收的频率,但是导致GC需要更长的暂停时间来执行回收
- 低延迟有限,频繁执行内存回收,引起年轻代内存的缩减,导致吞吐量下降
- 目前的标准,
G1垃圾回收器,以最大吞吐量优先情况下,降低最大停顿时间
垃圾回收器发展
SerialGC,大对象无法进入时,执行GC,先将Eden的对象放入老年代,将大对象放在新生代;ParallelGC,大对象无法进入时,直接进入老年代- 第一款GC,串行
Serial GC ParNew是Serial的多线程版本Parallel GC在JDK6之后称为HotSpot默认GC- JDK9中G1变成默认垃圾收集器,替代
CMS,Concurrent Mark Sweep,第一款并发垃圾回收器,达到低延迟 - JDK10中G1可以进行
并行完整垃圾回收,实现并行性来改善最坏情况下的延迟 - JDK11,引入
Epsilon垃圾回收器,称为No-Op(无操作);ZGC,可伸缩的低延迟垃圾回收器(实验版本) - JDK12,增强G1,自动返回未用堆内存给操作系统;引入
Shenandoah GC,低停顿时间的GC(实验班) - JDK13,增强ZGC,自动返回未用堆内存给操作系统
- JDK14,删除CMS垃圾收集器,扩展ZGC在mac和win上的应用,原先是在linux上测试
- 工作模式
- 串行 Serial、Serial Old
- 并行 ParNew、Parallel Scavenge(框架和CMS不一样,不能兼容)、Parallel Old
- 并发 CMS、G1
- 工作区域
- Young Gen: Serial GC、Parallel Scavenge GC、ParNew GC
- Old Gen: Serial Old GC、Parallel Old GC、CMS GC
- Young/Old Gen: G1 GC
- 组合关系
- Serial GC + Serial Old GC (MSC)
- ParNew GC + CMS GC(JKD14 被移除) + Serial Old GC (MSC 后备方案,如果CMS回收失败,就使用这个)
- Parallel Scavenge GC + Parallel Old GC(G1前使用的)
- 针对不同场景,提供不同的垃圾回收器,提高垃圾收集的性能
- 查看默认垃圾回收器
-XX:+PrintCommandLineFlags
//jdk8
-XX:InitialHeapSize=134217728
-XX:MaxHeapSize=2147483648
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseParallelGC //垃圾回收器 JDK8
//jdk16
-XX:ConcGCThreads=1
-XX:G1ConcRefinementThreads=4
-XX:GCDrainStackTargetSize=64
-XX:InitialHeapSize=134217728
-XX:MarkStackSize=4194304
-XX:MaxHeapSize=2147483648
-XX:MinHeapSize=6815736
-XX:+PrintCommandLineFlags
-XX:ReservedCodeCacheSize=251658240
-XX:+SegmentedCodeCache
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseG1GC
- 控制台查看
- jinfo -flag UseParallelGC 38077
- jinfo -flag UseParallelOldGC 38077
- jinfo -flag UseG1GC 38224
Serial 回收器
- 串行回收,Client模式下默认新生代垃圾收集器
- 采用复制算法、串行回收和
STW机制的方式执行内存回收 - Serial Old,针对老年代,使用
标记-压缩算法,是Client模式下默认的老年代垃圾收集器 - Serial Old在Server模式下的用途
- 与新生代Parallel Scavenge配合使用,现在已经取消
- 作为老年代CMS收集器的后备垃圾收集方案
- 只使用一个CPU或一条收集线程完成垃圾收集工作,在收集工作时,暂停其他所有工作线程
- 对于单个CPU环境中,没有线程交互的开销,专心做垃圾收集获得最高线程收集效率
-XX:+UseSerialGC指定使用串行收集器
ParNew 回收器
- Parallel New,只能处理新生代,并行回收
- Serial收集器的多线程版本,基本算法没有区别
- 复制算法,STW机制
- Server模式下新生代的默认垃圾回收器
- 对于新生代,回收次数频繁,并行方式高效
- 对于老年代,回收次数少,串行节省资源,CPU并行需要切换线程,串行可以省去切换线程的资源
- ParNew在多CPU环境下,重复利用物理硬件资源优势,快速完成垃圾收集
- 单CPU,串行回收不需要进行频繁线程切换
-XX:+UseParNewGC指定垃圾收集器,jdk9及以后弃用-XX:ParallelGCThreads=2设置线程数量,默认开启和CPU数量相同
Parallel Scavenge 回收器
- 吞吐量优先,组合JDK8默认
- 复制算法、并行回收、STW
- 目标是达到一个可控制的吞吐量,拥有自适应调节策略,调节内存分配,达到最优策略
- 尽快完成程序的运算任务,适合后台运算不需要太多交互任务,常见服务器环境下
- 框架和
CMS不同 - Parallel Old,针对老年代,采用标记-压缩算法,并发回收、STW
- 参数设置
-XX:UseParallelGC,互相激活,默认,跟老年代是一组搭配-XX:UseParallelOldGC-XX:ParallelGCThreads设置年轻代线程数量,默认开启和CPU数量相同;CPU数量小于8,默认为CPU数量;CPU数量大于8,值为3+[5*count]/8-XX:MaxGCPauseMills,最大停顿时间,STW的时间,自动调整java堆的大小和分配;暂停时间越长->比例越大,影响下面的参数;暂时时间越短->越频繁->总时间越长->吞吐量下降(某个时间段)-XX:GCTimeRatio,垃圾回收时间占总时间的比例,=1 / (N + 1),默认值N=99;-XX:+UseAdaptiveSizePolicy,自适应调节,年轻代大小,Eden和s1/s0的比例,晋升老年代的阈值自动调整
CMS回收器
- Concurrent Mark Sweep,并发性标记清除垃圾收集器,第一款真正意义上的并发收集器,实现了垃圾收集线程与用户线程同时工作(切换)
- 针对老年代
- 尽可能缩短用户线程停顿时间,低延迟,快速响应
- 标记清除算法、STW
- 初始标记,Initial-Mark,所有用户线程STW,
仅仅只是标记出GCRoots能直接关联到的对象,需要标记的对象很少,一旦标记完成后就恢复用户线程,执行速度非常快,STW时间短- 并发标记,Concurrent-Mark,从GCRoots的直接关联对象开始
遍历整个对象图的过程(可达性分析),耗时长,但不需要停顿用户线程,并发执行- 重新标记,Remark,为了修正标记期间,
因用户程序运行导致标记产生变动的那一部分对象的标记记录,怀疑是垃圾,需要确定是否为垃圾,进行STW,STW时间比初始标记稍长- 并发清除,Concurrent-Sweep,清理清除掉标记阶段判断的已经死亡的对象,释放内存空间,
不需要移动存活对象,并发执行
- 由于垃圾收集阶段用户线程没有中断,回收过程中,需要
确保用户线程有足够的内存可用,CMS不能等到老年代被填满后才进行收集,当堆内存使用率达到某一阈值,开始回收
CMS运行时预留的内存无法满足程序需要,出现
Concurrent Mode Failure,虚拟机启用后备方案,临时启用Serial Old收集器进行老年代的垃圾收集
- 采用标记清除算法,产生内存碎片,再分配空间时无法采用指针碰撞,需要
维护一个空闲列表
CMS采用的是并发清理,要保证用户线程还能继续执行,运行的资源不受影响,不能随意移动堆中对象的地址
- 优点,并发收集、低延迟
- 缺点
- 产生内存碎片,导致并发清除后,用户线程可用的空间不足,在无法分配大对象的情况下,提前触发Full GC
- 对CPU资源资源非常敏感,在并发阶段,占用一部分线程导致应用程序变慢,总吞吐量降低
- 无法处理浮动垃圾,在并发标记中,用户线程执行产生的新的垃圾对象,CMS无法对这些垃圾对象进行标记,导致这些新垃圾没有被及时回收
- 参数
-XX:+UseConcMarkSweepGC启用CMS,自动打开ParNew,采用ParNew+CMS+Serial Old-XX:CMSInitiatingOccupancyFraction=92,堆内存使用阈值,默认=-1,降低Full GC执行次数;如果内存增长很快,设置较小的值,给用户线程留出足够空间,避免频繁触发老年代串行收集器;内存增长很慢,设一个较大的值,降低CMS出发频率,减少老年代回收可以较明显地改善应用程序性能-XX:+UseCMSCompactAtFullCollection,执行完CMS后,进行压缩整理-XX:CMSFullGCsBeforeCompaction=0,执行多少次CMS Full GC后,进行压缩整理-XX:ParallelCMSThreads,设置CMS的线程数量,默认值为CPU个数,结果为(ParallelGCThreads+3)/4
小结
- 最小化使用内存和并行开销 Serial GC
- 最大化应用程序的吞吐量 Parallel GC
- 最小化GC的中断或停顿时间 CMS GC
G1 回收器
- 侧重点在于回收垃圾价值量最大的区间,垃圾优先,Garbage First
- 区域化分代式,在延迟可控的情况下尽可能提高吞吐量
- 用不同的Region来表示Eden、S0、S1、老年代等
- 有计划地避免在整个堆中进行全全区域的垃圾回收,跟踪各个Region里面的
垃圾堆积的价值大小(回收所获得的空间大小以及回收所需的时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region - 主要针对配备多核CPU及大容量内存的机器,极高概率满足GC停顿时间的同时,兼具高吞吐量的性能特征
-XX:UseG1GCjdk8时还不是默认的,设置启动- 适用场景
- 面向服务端应用,针对大内存、多处理器的机器
- 需要低GC延迟,并具有大堆的应用
- G1表现比CMS好的场景,超过50%的Java堆被活动数据占用;对象分配频率或年代提升频率变化很大;GC停顿时间过长
- HotSpot垃圾收集器中,其他垃圾收集器使用内置专门的JVM垃圾回收线程执行GC的多线程操作,而G1可以
采用应用程序线程承担后台运行的GC工作;当JVM的GC线程处理缓慢时,系统调用应用线程帮助加速垃圾回收处理 - 分区Region 化整为零
- 所有Region大小相同,在JVM生命周期不会改变,参数设置 1MB~32MB
- 新生代和老年代不再是物理隔离,逻辑上连续,都是Region的一部分集合所组成的
- 一个Region
只能是一个角色,当回收完后,角色可以转换- Survivor区
不再强调S0/S1 From/To,在处理上就会转换- Humongous,相当于
多个连续的Region和起来的区域,用于存储超大对象,如果超过1.5个region,就放到H;如果一个H区存不下,就寻找连续的H区进行存储;如果找不到,就需要启动Full GC;G1大多数时候会将H区作为老年代看待,回收的不频繁- 每一个region中存放的时候都是规整的,且通过指针碰撞来分配空间;region中也可以划分TLAB区域,线程私有堆空间,线程并行执行
优缺点
- 并行与并发
- 并行性,G1在回收期间,可以有多个GC线程同时工作,利用多核计算能力,进行STW
- 并发性,拥有与应有程序交替执行的能力,部分工作可以与应用程序同时执行
- 分代收集
- 区分年轻代和老年代,堆结构上,不要求区域是连续的,也
不坚持固定大小和固定数量- 将堆空间分为若干个Region,包含了逻辑上的年轻代和老年代,每次回收后,Region的角色可以变换
- 同时可以回收年轻代和老年代
- 空间整合
- Region之间是复制算法
- 整体上看是标记-压缩算法
- 避免碎片化,有利于长时间运行
- Java堆非常大的时候,G1优势明显
- 可预测的停顿时间模型,软实时,soft real-time,尽可能在给定的时间内完成垃圾回收
- 建议可预测的停顿时间模型,明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
- G1可以只选取部分区域进行内存回收,缩小回收的范围,避免全局停顿
- 跟踪各个Region里面的垃圾堆积的价值大小,在有限时间内,获取尽可能高的收集效率
- 下限高
- 缺点
- 内存占用较高
- 额外的执行负载较高
- 小内存 CMS表现好;大内存,G1表现好
参数
-XX:+UseG1GC,手动置顶-XX:G1HeapRegionSize=16m设置每个Region的大小,值是2的幂,范围是1~32MB,目标根据最小的Java堆大小,划分出2048个区域,堆空间的大小为2G~64G,默认是对内存的1/2000-XX:MaxGCPauseMillis,设置期望达到的最大GC停顿时间指标,不一定能达到,默认200ms
值小->处理的region比较少->如果用户线程内存使用速率快->容易满->导致Full GC
-XX:ParallelGCThread,设置STW时GC线程数的值,最多8-XX:ConcGCThreads,设置并发标记的线程数,将n设置为并行垃圾回收线程数 占ParallelGCThread的1/4左右-XX:InitiatingHeapOccupancyPercent,设置触发并发GC周期的Java堆占用率阈值,超过此值,触发GC,默认为45
使用步骤
- 确保开启
- 设置堆的最大内存
- 设置对大停顿时间
垃圾回收过程
- 用LinkedList空闲列表维护空闲的Region
Card Table
points-out我引用了谁的对象的结构YGC时,需要结合是否被老年代的对象引用的信息,如果扫描整个老年代,非常消耗性能- 使用
Card Table,将老年代分为一个个Card,一个Card有多个对象,如果其中有一个对象指向年轻代,那么就标记该Card为Dirty Card,进行YGC时,只需扫描该Card即可 - 底层数据结构以
Bit Map实现
Remembered Set
- 交叉引用是由
新生代对象引用老生代对象。当我们回收新生代的时候,这没有什么问题。但是当我们回收老生代的时候,如果只扫描老生代不扫描新生代,则老生代中的一些对象可能被误当作不可达对象回收掉!为了处理这种情况,可以做一个约定–如果回收老生代,那么比它年轻的新生代都要一起回收一遍,也就是说回收老年代时,往往同时对ygc进行回收 - 基于
CardTable的实现,记录了谁引用了我 - 记忆集 Remembered Set,避免进行全局扫描
- 一个对象会被不同区域所引用,一个对象不可能是孤立的,会被任意Region中对象引用
- Reset的作用是记录当前Region中哪些对象被外部引用指向,比如Old区中的对象会指向Eden区的对象,当我们要回收某个Region的时候,直接遍历遍历当前Region中的所有对象就可以了,然后针对性的去找到那些指向当前对象的其他对象,最终发现当前对象是否是根可达的,如果不是,那就应该被删除
- 每一个Region都有一个RSet,每次引用类型数据写操作时(A中的引用指向了B),都会产生一个
Write Barrier暂时中断操作- 检查将要
写入的引用指向的对象是否和该引用类型数据是否在不同的Region,其他收集器检查老年代对象是否引用了新生代对象- 如果不同,通过
CardTable把相关引用信息(在B中记录A的区域)记录到引用指向对象所在的Region对应的RSet中- 当进行垃圾收集时,在GCRoots的枚举范围加入RSet,避免全局扫描
- 案例举例,当进行MinorGC,通过GCRoots找到当前对象时,当前对象同时也被其他区域(年轻代的其他区域或者老年代区域)的对象引用,如果不进行全局扫描,从一般的GCRoots出发进行可达性分析,无法直接通过其他区域的对象到达当前对象,但是这个对象也不应该被删除;以前的垃圾回收器需要全局遍历,才能保证不被删除,但是G1,在引用数据写入时,就会将引用当前对象的其他对象的相关信息记录在RSet,在扫描当前Region时,GCRoots的范围就包括了这个RSet中的信息
- 案例二,当GCRoots可以到达A区域的某个对象,但是这个对象同时被B区域的某个对象引用,无法直接从GCRoots到达B区域的某个对象,通过RSets可以将B区域的对象加入A区域对象的对象图中,不被删除
年轻代
- 年轻代GC,Young GC
- Eden用尽时触发,并行的独占式的收集器,G1暂停所有应用程序线程,启动多线程执行,STW的
- 将Eden存活对象,移动到Survivor区,或者老年代;Survivor进行清理并复制到空闲区域,如果达到阈值,移动到老年代
- 采用复制算法
- 过程
- 扫描根/GCRoots,static变量指向的对象、正在执行的方法调用链条上的局部变量,Rset记录的外部引用作为扫描存活对象入口
- 更新RSet,处理dirty card queue(记录引用对象的操作,对象引用关系变化,先记录在队列中,保证性能,STW的时候进行更新)中的card,进行更新,保证RSet可以准确反映老年代对所在内存分段中对象的引用
- 处理RSet,识别被引用的对象,标记为存活
- 复制对象,复制算法,遍历对象树,Eden->Survivor中空的内存片段;Survivor存活对象未达阈值->年龄+1->到空闲区域;达到阈值->Old区;如果Survivor空间不够,Eden空间中的部分数据直接晋升到老年代
- 处理引用,软/弱/虚/终结器/JNI-Weak引用,最终Eden空间为空,GC停止工作,通过复制算法,新形成的Region对象是连续的,没有碎片;空闲区域记录在LinkedList列表中,
老年代并发标记过程
- 老年代并发标记过程,Concurrent Makring,包括YGC
- 过程
- 初始标记,标记从根节点直接可达的对象,
是STW的- 根区域扫描,Root Region Scanning,G1扫描Survivor区可直接可达的老年代区域对象,并标记被引用的对象,这一过程必须在YGC之前完成,如果发生YGC,会处理年轻代
- 并发标记,Concurrent Marking,在整个堆中进行并发标记,可能被YGC中断;若发现区域中的所有对象都是垃圾,就会立即回收(实时回收),同时计算每个区域的对象活性(区域中存活对象的比例)
- 再次标记,Remark,修正上一次的标记结果,
STW,采用比CMS更快的初始快照算法,snapshot-at-the-begining,解决并发标记的对象消失问题- 独占清理,计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域,
STW,不会去做实际的垃圾收集- 并发清理阶段,识别并清理完全空闲的区域(百分之百要被回收的区域),将空闲的区域加入空闲列表
混合回收
- 混合回收 Mixed GC,包括YoungGC和OldGC
- 标记完成后,每个Region中的垃圾的内存分段就被计算出来,进行混合回收过程
- 这个过程中老年代和新生代是一起被回收的
- 整理,G1从老年代移动存活对象到空闲空间,这些空闲区间成为老年代一部分
- 不需要整个老年代被回收,一次只需要回收老年代中的部分Region
- 过程
- 老年代的内存分段会分8次进行回收
-XX:G1MixedGCCountTarget设置- Collection Set混合回收的回收集,包括八分之一的老年代内存片段(老年代的总Region1/8部分),Eden区内存分段,Survivor区内存分段,采用复制算法
- G1优先考虑回收垃圾多的内存分段,并且有一个阈值决定内存分段(单个Region)是否被回收
-XX:G1MixedGCLiveThresholdPercent,默认为65%,指的是垃圾占内存分段比例高达65%才会被回收;垃圾太少,存活对象过多,导致复制的时候花费更多的时间- 混合回收不一定要进行8次,
-XX:G1HeapWastePercent,默认值为10%,允许整个堆内存有10%的空间被浪费,如果回收的垃圾占堆内存的比例低于10%(并发标记过程能记录垃圾总和),则不再进行混合回收,回收比很低
Full GC
- FUll GC,单线程、独占式、高强度
针对GC的评估失败,提供了一种保护机制,强力回收
- 导致的原因
- 复制算法转移对象时,没有足够的空间来存放,堆内存太小,复制对象没有空的内存分段可用Region,增大内存
- 并发处理过程完成前,空间耗尽
- 最大GC停顿时间太短,导致规定的时间间隔内无法完成垃圾回收,加大GC停顿时间
优化
- 回收阶段与用户线程并发执行
- 面向低延迟,停顿用户线程能够达到最大的幅度提高垃圾收集效率
- 年轻代大小
- 避免使用
-Xmn或-XX:NewRatio设置年轻代大小- 固定年轻代的大小会覆盖暂停时间目标
- 暂停时间不要太小
- G1吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
- 评估G1吞吐量时,暂停时间太小表示承受更多的垃圾回收开销,影响到吞吐量
并发标记的优化
三色标记法
- 让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的
CMS/G1的标记法- 黑色:自身以及可达对象都已经被标记
- 灰色:自身被标记,可达对象还未标记完成
- 白色:还未被标记,垃圾对象
- 步骤
- 创建三个集合:白、灰、黑
- 所有对象放入白色集合
- 从
CGRoots开始遍历所有对象,只遍历一层,把遍历到的对象从白色集合,放到灰色集合- 遍历灰色集合,将灰色对象引用的对象,从白色集合放入灰色集合,再将此灰色对象放入黑色集合
- 知道灰色中无任何对象
- 通过写屏障,检测对象是否有引用变化
- 收集所有白色对象
- 存在的问题
- 浮动垃圾,并发标记过程中,一个已经被标记成黑色或灰色的对象,突然变成垃圾,但是不会对黑色的对象再次扫描,因此不会发现,只能在下一次GC时处理
- 漏标,需要的对象被回收,在并发标记过程中,扫描到一个垃圾对象,但是在后续,黑色对象对这个垃圾对象再次产生引用,而后续仍然不会再次扫描,就会被删除
写屏障
- 对一个对象引用进行写操作(引用赋值)之前或者之后附加执行的逻辑,为引用赋值挂上一小段钩子代码
- 当写屏障检测到变化时,会维护当前对象的引用关系,防止其被误删
漏标
- 并发标记时,应用线程给一个黑色对象的引用类型字段赋值了该白色对象,新增
解决方案:利用post-write barrier,记录所有新增的引用关系,然后根据这些引用关系为根重新扫描一遍
- 并发标记时,应用线程删除所有灰色对象到该白色对象的引用,减少,但是有可能白色对象后来被黑色对象引用
解决方案:利用pre-write barrier,将所有既将被删除的引用关系的旧引用记录下来,最后以这些旧引用为根重新扫描一遍
三色标记法案例
- 并发标记时扫描到
- 同时程序进行了执行
A.c=C B.c=null,同时标记将c标记为垃圾 - 结果
CMS优化
- 增量更新,
post-write barrier,只要在写屏障里发现要有一个白对象的引用被赋值到一个黑对象的字段里,那么该黑色对象就要被改成灰色对象 - 仍然存在漏标的可能,对于一个本身就是灰色对象的,正在被扫描其其他引用,当指向一个白色对象后,该灰色对象被放入黑色集合,该白色对象有可能也不会被扫描到
G1优化
- SATB,删除的时候记录所有对象,解决cms重新标记长时间的stw
- 在开始标记的时候生成一个快照图标记存活对象
- 在并发标记的时候,将所有被改变的对象入
GC队,将所有即将pre-write barrier被删除的引用关系的旧引用记录下来,再以这些旧引用为根重新扫描一遍(放入灰色集合中),配合Rset查看哪些Region引用了当前白色对象 - 可能存在游离的垃圾,在下次被收集
SATB效率高于增量更新的原因
- STAB在重新标记的环节只需要重续扫描那些
GC队的引用,配合Rset判断当前对象是否被引用来进行回收 - G1并不会回收所有垃圾对象,根据当前
Region的回收价值进行选择回收
Region详解
- TAMS,
top-at-mark-start,发生并发标记的位置 - region包含5个指针
- A初始标记阶段。
next TAMS尚未标记任何存活对象,而此时的previous TAMS被初始化为region内存地址起始值,next TAMS被初始化为top。top实际上就是一个region未分配区域和已分配区域的分界点 - B并发标记之后,进入了再次标记阶段。此时存活对象的扫描已经完成了,因此next bitmap构造好了,代表的是当下状态中region中的内存使用情况。注意的是,此时
top已经不再与next TAMS重合了,top和next TAMS之间的就是在前面标记阶段之时,新分配的对象 - C代表的是clean up阶段。C和B比起来,
next bitmap变成了previous bitmap,而在bitmap中标记为垃圾(也就是白色区域的)的对应的region的区域也被染成了浅灰色。这并不是指垃圾对象已经被清扫了,仅仅是标记出来了。此时SATB对这部分区域[bottom, previous TAMS]进行快照,将发生引用变化过的对象加入队列satb mark queue中,在重新标记时,进行处理 - D下一个初始标记阶段
- EF是BC的重复
垃圾回收器总结
- 发展阶段 Serial->Parallel 并行->CMS 并发->G1->ZGC
- 如何选择垃圾回收器
- 让JVM自适应完成调整堆大小
- 内存小于100M,串行
- 单核、单机程序,没有停顿时间要求,串行
- 多CPU,高吞吐量、允许停顿时间超过1秒,并行或者JVM自行选择
- 多CPU,第停顿时间,快速响应,并发
日志分析
SerialGC,大对象无法进入时,执行GC,先将Eden的对象放入老年代,将大对象放在新生代;ParallelGC,大对象无法进入时,直接进入老年代- 参数,对于G1,345参数都不能使用
-XX:+PrintGC,输出GC日志,类似-verbose:gc-XX:+PrintGCDetails,输出GC的详细日志-XX:+PrintGCTimeStamps,输出GC的时间戳(以基准时间的形式,JVM启动以来到现在的时间)0.389-XX:+PrintGCDateStamps,输出GC的时间戳(以日期的形式)2021-10-15T19:07:13.682+0800-XX:+PrintHeapAtGC,进行GC的前后打印出堆的信息-Xloggc:/Users/apple/Desktop/java/jvm/logs/gc.log日志文件的输出
- JDK16参数改变
-Xlog:gc*-Xlog:gc:details-Xlog:gc+heap=trace-Xlog:gc:/Users/apple/Desktop/java/jvm/logs/gc.log
- PrintGC JDK8
//GC-新生代 Allocation Failure-新对象分配失败导致的GC
//16296K->14970K-前后变化 59392K-当前堆大小
[GC (Allocation Failure) 16296K->14970K(59392K), 0.0085846 secs]
[GC (Allocation Failure) 31292K->31288K(59392K), 0.0084852 secs]
[Full GC (Ergonomics) 31288K->31188K(59392K), 0.0090026 secs]
[Full GC (Ergonomics) 47541K->47182K(59392K), 0.0074301 secs]
- PrintGCDetails
//PSYoungGen-Parallel收集器 ParOldGen-ParallelOld收集器
//user-垃圾收集器花费的总CPU时间
//sys-花费在等待系统调用或系统事件的时间
//real-GC从开始到结束的时间,包括其他进程占用时间片的实际时间
//16296K->2024K(18432K) 回收之前的大小 回收后的大小 总大小
[GC (Allocation Failure) [PSYoungGen: 16296K->2024K(18432K)] 16296K->15034K(59392K), 0.0098301 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 18346K->2008K(18432K)] 31356K->31328K(59392K), 0.0105358 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 2008K->0K(18432K)] [ParOldGen: 29320K->31188K(40960K)] 31328K->31188K(59392K), [Metaspace: 3241K->3241K(1056768K)], 0.0165703 secs] [Times: user=0.01 sys=0.01, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 16352K->6601K(18432K)] [ParOldGen: 31188K->40581K(40960K)] 47541K->47182K(59392K), [Metaspace: 3241K->3241K(1056768K)], 0.0193033 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
- -XX:+PrintHeapAtGC
//GC堆的名字 total 总容量 used 已分配空间 [申请的虚拟空间下限,已分配的虚拟空间上限,申请的虚拟空间上限) 左闭右开
{Heap before GC invocations=1 (full 0):
PSYoungGen total 18432K, used 16296K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 16384K, 99% used [0x00000007bec00000,0x00000007bfbea280,0x00000007bfc00000)
from space 2048K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007c0000000)
to space 2048K, 0% used [0x00000007bfc00000,0x00000007bfc00000,0x00000007bfe00000)
ParOldGen total 40960K, used 0K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000)
object space 40960K, 0% used [0x00000007bc400000,0x00000007bc400000,0x00000007bec00000)
Metaspace used 3239K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
- G1信息,Region变化,-Xlog:gc*
[0.170s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) //STW
[0.170s][info][gc,task ] GC(0) Using 2 workers of 4 for evacuation //执行的CPU数
[0.178s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms //回收集
[0.178s][info][gc,phases ] GC(0) Merge Heap Roots: 0.0ms //RSet
[0.178s][info][gc,phases ] GC(0) Evacuate Collection Set: 8.1ms //执行回收时间
[0.178s][info][gc,phases ] GC(0) Post Evacuate Collection Set: 0.1ms //发布执行回收时间
[0.178s][info][gc,phases ] GC(0) Other: 0.2ms
[0.178s][info][gc,heap ] GC(0) Eden regions: 13->0(7) //region变化,括号里的是将7个再利用的意思吗?
[0.178s][info][gc,heap ] GC(0) Survivor regions: 0->2(2)
[0.178s][info][gc,heap ] GC(0) Old regions: 0->10
[0.178s][info][gc,heap ] GC(0) Archive regions: 2->2
[0.178s][info][gc,heap ] GC(0) Humongous regions: 0->0
[0.178s][info][gc,metaspace] GC(0) Metaspace: 629K(832K)->629K(832K) NonClass: 591K(704K)->591K(704K) Class: 37K(128K)->37K(128K) //元空间变化
[0.178s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 13M->12M(60M) 8.586ms //结果
[0.178s][info][gc,cpu ] GC(0) User=0.01s Sys=0.01s Real=0.01s //事件信息
日志工具
垃圾回收器新发展
Epsilon No-Op,只做内存分配,不做垃圾回收,运行完之后直接退出程序Shenandoah GC,低停顿时间
- Open JDK中的,不是Oracle公司主导开发的
- 低停顿的需求,暂停时间与对大小无关,限制在10ms之内
- 越频繁,总时间长,吞吐量下降
ZGC低延迟垃圾回收器,可伸缩低延迟
- 尽可能对吞吐量影响不大的情况下,实现任意堆内存大小都可以将停顿时间控制到10ms以内
- 基于Region内存布局,不设分代,使用了
读屏障、染色指针和内存多重映射等技术实现可并发的标记-压缩算法的,以低延迟为首要目标的垃圾收集器- 并发标记->并发预备重分配->并发重映射
- 几乎所有地方都是并发执行,
初始标记是STW的,停顿时间几乎都耗费在初始标记上-XX:+UnlockExperimentalVMOptions -XX:+UseZGC使用
AloiGC,阿里,基于G1算法,面向大堆Zing,低延迟GC