《深入理解虚拟机》阅读笔记-垃圾收集器
垃圾收集器
- 概述
- 连线代表可组合使用
- Parallel Scanvenge与G1没有使用传统的gc收集器代码框架,其余都共用了部分框架代码
- 连线代表可组合使用
- 并发与并行
- 并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
- Serial收集器
- 最基本、历史最悠久的收集器,是1.3.1以前新生代收集的唯一选择
- 单线程收集器:只有一条收集线程去执行垃圾收集,并且必须暂停其他所有工作线程。
- Client模式下的默认新生代收集器,简单高效,适用于单cpu的环境。
- Serial收集器的工作流程示意图
- ParNew收集器
- Serial收集器的多线程版本
- 不同:多条线程执行垃圾收集
- 相同
- 控制参数:-XX:SurvivroRatio/-XX:PretenureSizeThreshold/-XX:HandlePromotionFailure等
- 收集算法
- STW
- 对象分配规则
- 回收策略
- Server模式下的新生代首选,并且可以与CMS收集器配合使用。
- 使用-XX:UserConcMarkSweepGC后的默认新生代收集器,或使用-XX:UseParNewGC强制使用
- 单cpu下的性能并不一定比Serial好,但是随着cpu数量增多,对GC时系统资源的有效利用越好。
- 默认开启线程与CPU数量相同
- -XX:ParallelGCThreads限制垃圾收集的线程数
- ParNew收集器的工作流程示意图
- Serial收集器的多线程版本
- Parallel Scavenge收集器
- 新生代-复制算法的并行多线程收集器,“吞吐量优先”收集器
- 收集器的目标是达到可控制的吞吐量(Throughput = 用户代码运行时间/CPU总消耗时间)
- 高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
- 主要参数
- -XX:MaxGCPauseMills
- 控制最大垃圾收集停顿时间
- 大于0的毫秒数,GC停顿时间缩短以牺牲吞吐量和新生代空间为代价
- -XX:GCTimeRatio
- 设置吞吐量大小,默认为99
- 大于0小于100的整数,即垃圾收集时间占总时间的比率,吞吐量的倒数
- 若设置为N,则允许最大GC时间为N/(N+1)
- -XX:+UseAdaptiveSizePolicy
- GC自适应的调节策略(GC Ergonomics)
- 不需要手动设置新生代大小,新生代分区比例、晋升老年代对象大小等参数,虚拟机根据系统运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大吞吐量
- 只需设置基本内存参数,设置-XX:MaxGCPauseMills或-XX:GCTimeRatio 为虚拟机优化目标即可。
- -XX:MaxGCPauseMills
- Serial Old 收集器
- Serial收集器的老年代版本,使用“标记-整理”算法
- 主要在Client模式下的虚拟机使用
- Server模式的作用
- JDk1.5及以前与Parallel Scavenge收集搭配使用
- 作为CMS收集器的后备预案,当并发收集发生Concurrent Mode Failure时使用。
- Serial Old收集器的工作流程示意图
- Parallel Old收集器
- Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
- 主要搭配Parallel Scavenge使用。
- CMS(Concurrent Mark Sweep)收集器
- 一种以获取最短回收停顿时间为目标的收集器。
- 基于“标记-清除”算法。
- 过程
- CMS收集器的工作流程示意图
- 初始标记(CMS initial mark)-STW
- 标记GC Roots直接关联的对象
- 停顿时间很短
- 并发标记(CMS concurrent mark)
- GC Roots Tracing
- 在并发标记过程中,应用线程的继续运行导致有些对象会从新生代晋升到老年代、有些老年代的对象引用会被改变、有些对象会直接分配到老年代,这些受到影响的老年代对象所在的card会被标记为dirty,用于重新标记阶段扫描。
- 预清理阶段
- 用于标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短.
- 这个阶段的目标是在并发标记阶段被应用线程影响到的老年代对象,包括:(1)老年代中card为dirty的对象;(2)幸存区(from和to)中引用的老年代对象。因此,这个阶段也需要扫描新生代+老年代。
- 可中断的预清理
- 跟“预清理”阶段相同,也是为了减轻重新标记阶段的工作量。
- 在进入重新标记阶段之前尽量等到一个Minor GC,尽量缩短重新标记阶段的停顿时间。
- 另外可中断预清理会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时间,这个还有另一个意义,即避免短时间内连着的两个停顿。
- 若满足以下两个条件,则不开启“可中断的预清理”
- Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;
- Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。
- 若不满足,则进入可中断的预清理,可中断预清理可能会执行多次,那么退出这个阶段的出口有两个
- 设置了CMSMaxAbortablePrecleanLoops,并且执行的次数超过了这个值,这个参数的默认值是0;
- CMSMaxAbortablePrecleanTime,执行可中断预清理的时间超过了这个值,这个参数的默认值是5000毫秒。
- 有可能可中断预清理过程中一直没等到Minor gc,这时候进入重新标记阶段的话,新生代还有很多活着的对象,就回导致STW变长,因此CMS还提供了CMSScavengeBeforeRemark参数,可以在进入重新标记之前强制进行依次Minor gc。
- 重新标记(CMS remark)-STW
- 为了修正并发标记期间,因用户程序运作而导致标记产生变动的那部分对象的标记记录
- 新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老年代对象。
- 停顿时间比初始标记稍长,比并发标记短
- 并发清除(CMS concurrent sweep)
- CMS收集器的工作流程示意图
- 缺点
- 对CPU资源非常敏感。默认启动的回收线程数是(CPU数量+3)/4。
- 无法处理浮动垃圾(Floating Garbage)。
- 可能出现“Concurrent Mode Failure”失败导致另一次full gc的发生。
- 出现在标记之后,用户程序的继续运作产生了新的垃圾,无法在本次回收处理。
- 由于预留内存空间,实际使用空间:-XX:CMSInitiatingOccupancyFraction。1.5为默认老年代的60%;1.6默认为92%。
- 若预留内存无法满足用户程序的分配,则导致“Concurrent Mode Failure”,临时启动Serial Old收集器进行老年代的收集。
- 会产生大量的内存碎片。
- 若无法找到足够的内存分配对象,则提前触发full GC。
- -XX:+UseCMSCompactAtFullCollection
- 默认开启
- 当CMS收集顶不住要进行Full Gc时,开启内存碎片的合并整理过程。该过程是无法并发的。
- -XX:CMSFullGCsBeforeCompaction
- 默认为0,即每次Full Gc 都压缩
- 用于设置执行多少次不压缩的FullGc后,执行一次带压缩的。
- CMS并发周期失败的情况
- 并发模式失败(Concurrent mode failure):在并发周期执行期间,用户的线程依然在运行。如果这时候如果应用线程向老年代请求分配的空间超过预留的空间(担保失败),就回触发concurrent mode failure,然后CMS的并发周期就会被一次Full GC代替——停止全部应用进行垃圾收集,并进行空间压缩。如果我们设置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,其中CMSInitiatingOccupancyFraction的值是70,那预留空间就是老年代的30%。
- 晋升失败:新生代做minor gc的时候,需要CMS的担保机制确认老年代是否有足够的空间容纳要晋升的对象,担保机制发现不够,则报concurrent mode failure,如果担保机制判断是够的,但是实际上由于碎片问题导致无法分配,就会报晋升失败。
- 永久代空间(或Java8的元空间)耗尽,默认情况下,CMS不会对永久代进行收集,一旦永久代空间耗尽,就回触发Full GC。
- G1收集器
- 面向服务端的收集器
- 特性
- 并行与并发
- 充分利用多cpu、多核环境的硬件优势。使用多个CPU来缩短STW时间。
- 其他收集器需要STW的GC动作,G1依然可以通过并发的方式进行。
- 分代收集
- 空间整合
- 从整体是基于“标记-整理”,从局部(region之间)是基于“复制”算法。
- 上述算法意味着G1运作期间不会产生内存空间碎片。
- 可预测的停顿
- 建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒
- 并行与并发
- 内存布局
- 分区(Region)
- 将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。
- 只保留保留新生代和老年代的逻辑概念,不再是物理隔离,而是一部分(region)的集合。
- 每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。
- 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
- 卡片(Card)
- 每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中。
- 分配的对象会占用物理上连续的若干个卡片。
- 当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。
- 每次对内存的回收,都是对指定分区的卡片进行处理。
- 分区(Region)
- 可预测时间模型的基础
- 可以有计划地避免在整个堆中进行全区域的垃圾扫描。
- G1跟踪各个Region内来及堆积的价值大小(回收空间以及回收所需时间的经验值),维护一个优先列表,根据允许的收集时间,优先回收价值最大的Region。
- Collected Set
- 一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。
- Remembered Set--避免全堆扫描
- 每个Region都存在一个Remembered Set。
- JVM发现程序在对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中。
- 如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。
- 进行垃圾回收时,在GC根节点的枚举范围加入Remembered Set即可避免全堆扫描。
- RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。
- Snapshot-At-The-Beginning(SATB)
- SATB是维持并发GC的正确性的一个手段,G1GC的并发理论基础就是SATB,是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法SATAB的标记优化主要针对标记-清除垃圾收集器的并发标记阶段。按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。
- SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。
- previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。
- 步骤
- 并发周期包括初始标记、并发标记与最终标记。
- 在初始标记阶段,NTAMS字段被设置到每个分区当前的顶部。并发周期启动后分配的对象会被放在TAMS之上,同时被明确定义为隐式存活对象,而TAMS之下的对象则需要被明确地标记。
- 在Top与Bottom存在一个PTAMS指针,表示着上一次标记周期结束时属于隐式存活的,而下一个周期需要被明确标记的对象区域的位置,即上一个标记周期结束时NTAMS的位置。
- 并发周期开始后,Bottom与PTAMS之间是需要被明确标记的对象区域,并记录在previous位图中。
- Top和PATMS之间的对象均为隐式存活对象,同时也记录在previous位图中。
- 最终标记的最后,所有NTAMS之前的对象都会被标记。
- 在并发标记阶段分配的对象会被分配到NTAMS之后的空间,它们会作为隐式存活对象被记录在next位图中。一次并发标记周期完成后,这个next位图会覆盖previous位图,然后将next位图清空。
- 步骤
- 初始标记(Initial mark)-STW
- 标记GC Roots直接关联的对象,并修改NTAMS字段(Next Top at Mark Start)为当前分区的top位置
- 停顿时间很短
- 并发标记(Concurrent mark)
- GC Roots Tracing
- 并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;
- 记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace。
- 这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中。
- 如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。
- 最终标记(Finalremark)STW
- 为了修正并发标记期间,因用户程序运作而导致标记产生变动的那部分对象的标记记录。
- G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。
- JVM将期间对象变化记录到线程Remembered Set Logs中。最终合并到Remembered Set。
- 筛选回收(Live Data Counting and Evacuation)、
- 首先根据Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。
- 允许并发,但是因为只回收一部分Region,时间是用户可控制的,停顿用户线程能大幅提高收集效率。
- 初始标记(Initial mark)-STW
- 垃圾收集器相关参数
-