GC收集器整理

1 篇文章 0 订阅
垃圾回收器
  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1、ZGC
新生代垃圾回收器一般采用复制算法,复制算法优点是效率高,缺点是内存利用率低
老年代垃圾回收器一般采用标记-整理算法进行垃圾回收
   


  • Serial收集器(复制算法): 
        它是一个单线程收集器,它在工作时必须暂停其他所有的工作线程,直到收集结束。这里要知道一个很严重的问题就是暂停一切线程的结果就是当前运行在这个JDK的所有用户线程全部暂停,也就是说这一瞬间都是死掉的,用户看到的现象就是页面无任何响应,如果这种这种现象出现的时间长且频繁用户就奔溃了。
     越优秀的收集器,他的停顿时间一定越短,这也是所有收集器共同追求的目标


  • ParNew收集器 (复制算法): 

        新生代收并行集器,实际上是Serial收集器的多线程版本。在单CPU环境下ParNew的性能没办法超过Serial,在多核CPU环境下有着比Serial更好的表现;ParNew还可以和CMS搭配使用

        

  • Parallel Scavenge收集器 (复制算法): 

        新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),例如虚拟机运行100分钟,其中垃圾收集时间使用1分钟,那么吞吐量就是99%,高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;


  • Serial Old收集器 (标记-整理算法):
         老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 
         老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

    并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。如:ParNew、Parallel Scavenge、Parallel Old
    并发:用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。如:CMS、G1
 
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):

         老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。它采用标记-清除算法,因此会产生大量空间碎片。为了解决这个问题,CMS可以通过配置以下两种参数解决:

  1. -XX:+UseCMSCompactAtFullCollection:参数,强制JVM在Full GC完成后対老年代迸行圧縮,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置
  2. -XX:+CMSFullGCsBeforeCompaction=n :参数,在执行了n次Full GC后, JVM再在老年代执行空间碎片整理

        初始标记:停止一切用户线程,仅使用一条初始化标记线程对所有与GC Roots直接关联的老年代对象进行标记,速度很快
        并发标记:使用多条并发标记线程并行执行,并与用户线程并发执行,此过程进行可达性分析,标记所有这些对象可达的存活对象,速度很慢
        重新标记:因为并发标记时有用户线程在执行,标记结果可能有变化,停止一切用户线程,并使用多条重新标记线程并行执行,重新遍历所有在并发期间有变化的对象进行最后的标记,这个过程的运行时间介于初始标记和并发标记之间
        并发清除:只使用一条并发清除线程,和用户线程并发执行,清除刚才标记的对象

  • G1(Garbage First)收集器 (复制、标记-整理算法): 
        G1收集器的特点        
  • 并行与并发
    • 能充分利用多CPU、多核环境下的硬件优势
    • 可以并行来缩短STW停顿时间
    • 可以并发让垃圾收集与用户程序同时进行
  • 分代收集、收集范围包含新生代和老年代
    • 能独立管理整个GC堆(新生代和老年代),不需要其他收集器配合
    • 能够采用不同方式处理不同时期的对象
    • 虽然保留分代概念,单Java堆的内存布局有很大差别
    • 将整个堆划分成多个大小相等的独立区域(Region)
    • 新生代和老年代不再是物理隔离,他们都是一部分独立Region的集合(不需要连续)
  • 结合多种垃圾收集算法,空间整合,不产生碎片
    • 从整体看是基于标记-整理算法
    • 从局部看(两个Region)看,是基于复制算法
    • 不会产生内存碎片,有利于长时间运行
  • 可预测的停顿:低停顿的同时实现高吞吐量
    • G1除了追求低停顿还能建立可预测的停顿时间模型
    • 可以明确指定M毫秒内,垃圾收集消耗的时间不超过N毫秒
        
         G1的内存模型
                G1将Java堆空间分割成若干相同大小的的区域,即Region,Humongous是特殊的Old类型,专门放置大型对象,JDK11中,已经将G1设为默认垃圾回收器
    G1垃圾收集过程
  1.  初始标记:标记与GC Roots直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程
  2. 并发标记:进行全面的可达性分析,开启一条并发标记线程与用户线程并行执行,这个过程比较长
  3. 最终标记:标记出并发标记过程中用户线程新产生的垃圾,停止用户线程,并使用多条最终标记线程并行执行
  4. 筛选回收:回收废弃的对象,此时也需要停止一切用户线程,并使用多条筛选回收线程并行执行(G1会根据设置的系统暂停时间回收尽可能多的垃圾)
    G1最大的特点就是可以让我们设置一个垃圾回收的预期停顿时间,也就是说比如我们可以指定:希望G1在垃圾回收的时候可以保证,在一个小时内G1垃圾回收导致的STW时间,也就是系统停顿时间不能超过一分钟。G1要做到这一点就必须追踪每个Region里的回收价值(它必须搞清楚每个Region里的对象有多少是垃圾,如果这个Region进行垃圾回收需要耗费多少时间,可以回收多少垃圾)
                                                        
        如图,G1通过追踪发现,1个Region中的垃圾对象有10M,回收他们需要耗费1秒,另一个Region中的垃圾对象有20M,回收他们需要200毫秒,然后在垃圾回收的时候,G1会发现在最近的一个时间段内,垃圾已经导致了几百毫秒的系统停顿,现在又要执行一次垃圾回收,那么必须是回收图中那个只需要200毫秒就能回收掉的20M垃圾的Region,于是G1触发了一次垃圾回收,虽然可能会导致系统停顿了200毫秒,但是一下子回收了更多垃圾,如下图
                                                              
    其中,[0~4TB) 对应Java堆,[4TB ~ 8TB) 称为M0地址空间,[8TB ~ 12TB) 称为M1地址空间,[12TB ~ 16TB) 预留未使用,[16TB ~ 20TB) 称为Remapped空间。
    当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效。ZGC之所以设置三个虚拟地址空间,是因为它使用“空间换时间”思想,去降低GC停顿时间。“空间换时间”中的空间是虚拟空间,而不是真正的物理空间。后续章节将详细介绍这三个空间的切换过程。与上述地址空间划分相对应,ZGC实际仅使用64位地址空间的第0~41位,而第42~45位存储元数据,第47~63位固定为0。
读屏障
    当对象从堆中加载的时候,就会使用到读屏障(Load Barrier)。这里使用读屏障的主要作用就是检查指针上的三色标记位,根据标记位判断出对象是否被移动过,如果没有可以直接访问,如果移动过就需要进行“自愈”(对象访问会变慢,但也只会有一次变慢),当“自愈”完成后,后续访问就不会变慢了。
  ZGC特点:
  1. 停顿时间不超过10ms
  2. 停顿时间不会随着堆的大小或者活跃对象的大小而增加
  3. 支持8M~4T级别的堆
ZGC原理
    与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。
    ZGC垃圾回收周期如下图所示:
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
    ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
  • 初始标记:与G1一样标记与GC Roots直接关联的对象,停止所有用户线程
  • 并发标记:使用多条并发标记线程并行执行,并与用户线程并发执行,此过程进行可达性分析,标记所有这些对象可达的存活对象,速度很慢
  • 重新标记:因为并发标记时有用户线程在执行,标记结果可能有变化,停止一切用户线程,并使用多条重新标记线程并行执行,重新遍历所有在并发期间有变化的对象进行最后的标记,这个过程的运行时间介于初始标记和并发标记之间,这几个标记阶段只会更新染色体指针中的M0和M1标志位
  • 并发预备重分配(并发转移准备):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
  • 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
  • 并发重映射:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。
ZGC的染色指针因为“自愈”(Self-Healing)能力,所以只有第一次访问旧对象会变慢,而Shenandoah的Brooks转发指针是每次都会变慢。一旦重分配集中某个Region的存活对象都复制完毕后,这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉,因为可能还有访问在使用这个转发表。
ZGC存在的问题
    ZGC最大的问题是浮动垃圾,ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。
ZGC触发时机
    ZGC有多种GC触发机制,总结如下:
  1. 阻塞内存分配请求触发:当垃圾来不及回收,垃圾将堆占满时,会导致部分线程阻塞。我们应当避免出现这种触发方式。日志中关键字是“Allocation Stall”。
  2. 基于分配速率的自适应算法:最主要的GC触发方式,其算法原理可简单描述为”ZGC根据近期的对象分配速率以及GC时间,计算出当内存占用达到什么阈值时触发下一次GC”。自适应算法的详细理论可参考彭成寒《新一代垃圾回收器ZGC设计与实现》一书中的内容。通过ZAllocationSpikeTolerance参数控制阈值大小,该参数默认2,数值越大,越早的触发GC。我们通过调整此参数解决了一些问题。日志中关键字是“Allocation Rate”。
  3. 基于固定时间间隔:通过ZCollectionInterval控制,适合应对突增流量场景。流量平稳变化时,自适应算法可能在堆使用率达到95%以上才触发GC。流量突增时,自适应算法触发的时机可能会过晚,导致部分线程阻塞。我们通过调整此参数解决流量突增场景的问题,比如定时活动、秒杀等场景。日志中关键字是“Timer”。
  4. 主动触发规则:类似于固定间隔规则,但时间间隔不固定,是ZGC自行算出来的时机,我们的服务因为已经加了基于固定时间间隔的触发机制,所以通过-ZProactive参数将该功能关闭,以免GC频繁,影响服务可用性。 日志中关键字是“Proactive”。
  5. 预热规则:服务刚启动时出现,一般不需要关注。日志中关键字是“Warmup”。
  6. 外部触发:代码中显式调用System.gc()触发。 日志中关键字是“System.gc()”。
  7. 元数据分配触发:元数据区不足时导致,一般不需要关注。 日志中关键字是“Metadata GC Threshold”。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值