《深入理解虚拟机》阅读笔记-垃圾收集器

垃圾收集器

  • 概述
    • 连线代表可组合使用

    • 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收集器的工作流程示意图

  • 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 为虚拟机优化目标即可。
  • 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)
    • 缺点
      • 对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)。
        • 每次对内存的回收,都是对指定分区的卡片进行处理。
    • 可预测时间模型的基础
      • 可以有计划地避免在整个堆中进行全区域的垃圾扫描。
      • 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,时间是用户可控制的,停顿用户线程能大幅提高收集效率。
    • 垃圾收集器相关参数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值