Java虚拟机垃圾回收——7种垃圾收集器

概述

垃圾回收器是垃圾回收算法(标记清除算法,复制算法,标记整理算法,火车算法)的具体实现,不同版本的jvm所提供的垃圾回收器可能会有很大的差别,本文主要说明HotSpot虚拟机中的垃圾收集器;

总体介绍

回收器以及其中组合的介绍

JDK7/8后,HotSpot虚拟机的所有收集器以及组合(连线)
在这里插入图片描述
说明

  1. 上图中展示了7种不同分代的收集器
    Serial,ParNew,Parallel Scavenge ,Serial Old ,Parallel Old,CMS,G1
  2. 他们的所处区域,表明是属于新生代收集器还是老年代收集器:
    • 新生代收集器:Serial,ParNew,Parallel Scavenge
    • 老年代收集器:Serial Old,Parallel Old,CMS
  3. 两个收集器间有联系表示他们可以搭配使用:
    Serial/Serial Old ,Serial/CMS ,ParNew/CMS,ParNew/Serial Old,Parallel Scavenge/Serial Old,Parallel Scavenge/Parallel Old ,G1;
  4. 其中Serial Old 作为CMS出现“Concurrent Mode Failure” 失败的后备预案;
并发垃圾收集器和并行垃圾收集器的区别
  • 并行(Parallel)
    指多条垃圾收集器并行工作,但是此时用户线程仍然处于等待的状态;
    如:ParNew,Parallel Scavenge,Parallel Old;
  • 并发(Concurrent)
    指用户线程和垃圾线程同时执行(但是不一定是并行的,可能会交替执行);
    用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
    如 CMS G1 (也有并行);
Minor GC和Full GC的区别
  • Minor GC:又成为新生代GC,指发生在新生代的垃圾收集动作;
    因为Java 对象大多是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快;
  • Full GC:又称Major GC或者老年GC,指发生在老年代的GC;
    出现Full GC经常伴随至少一次的Minor GC(不是绝对的,Parallel Scavenge收集器就可以设置Majar GC的策略)

Major GC速度一般比Minor GC慢10倍以上;

收集器的详细介绍

在介绍各种收集器之前,首先明确一个观点:没有完美的垃圾收集器,只有在不同的场景下选择最适合的垃圾回收器;

1. Serial收集器

Serial(串行)垃圾收集器是最基本,发展历史最悠久的收集器;jdk1.3.1前是HotSpot新生代收集器的唯一选择;

特点
  • 针对新生代;
  • 采用复制算法;
  • 单线程收集;
  • 进行垃圾回收时候必须暂停所有的工作线程,直到完成;
  • 即会“Stop The World”;
    Serial/Serial Old 组合收集器运行示意图如下:
    在这里插入图片描述
应用场景

依然是HotSpot在client模式下默认的新生代收集器;也有优于其它收集器的地方:

  • 简单高效(和其它收集器的单线程相比);
  • 对于限定单个cpu环境来说,Serial收集器没有线程交互(切换)的开销,可以获取最高的单线程收集效率;
  • 在用户的桌面应用场景种,可用内存一般不大于(几十M至一两百M),可以在较短时间内完成垃圾回收,只要不频繁发生,是可以接受的;
设置参数

“-XX:+UseSerialGC”:添加改参数显示的使用串行垃圾收集器;

Stop The World 说明

Jvm 在后台自动发起和自动完成,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;会带给用户不良的体验

从JDK1.3到现在,从 Serial收集器->Parallel收集器->CMS->G1,用户线程停顿时间不断缩短,但仍然无法完全消除;

2. ParNew 收集器

ParNew 垃圾收集器是Serial收集器的多线程版本

特点
  • 除了多线程外,其余的行为,特点和Serial收集器完全一样;
  • 如Serial收集器可用的控制参数,收集算法,stop the world,内存分配规则,回收策略等;
  • 两个收集器公用了不少的代码;
  • ParNew/Serial Old组合收集器运行示意图如下:
    在这里插入图片描述
应用场景
  • 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除了Serial外,目前只有它能与CMS收集器配合使用;
  • 但是在单个cpu环境下,不会比Serial收集器有更好的效果,因为存在线程交互的开销;
设置参数

“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads”:指定垃圾回收的线程数量,ParNew默认开始的线程数和cpu的数目相同;

为什么只有ParNew能和CMS配合

CMS是Hotspot在jdk1.5推出的真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程和用户线程同时工作;
CMS作为老年代收集器,但却无法与1.4已经存在的新生代收集器Parallel Scavenge 配合工作;
因为Parallel Scavenge (以及G1)都没有使用传统的GC收集器代码框架,而是另外独立实现,而其余的几种收集器公用了部分框架代码;

3. Parallel Scavenge 收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(througthput Collector)

特点
  • 有一些特点和ParNew 类似
    • 新生代收集器
    • 采用复制算法
    • 多线程收集
  • 主要特点:它的关注点与其它的收集器不同
    • CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
    • 而Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(ThroughPut);
应用场景

Parallel Scavenge收集器提供两个参数用户精确控制吞吐量:

  • “-XX:MaxGCPauseMillis”
    控制最大的垃圾回收停顿时间,大于0的毫秒数;
    MaxGCPauseMillis 设置稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因此可能导致垃圾发生的更频繁;

  • “-XX:GCTimeRatio”
    设置垃圾收集时间占总时间的比率,0<n<100的整数;
    GCTimeRotio相当于设置吞吐量大小;
    垃圾收集执行时间占用程序执行时间的比例计算方法:

    1/(1+n)
    

    例如,设置-XXTimeRatio=19 设置垃圾收集时间占总时间的1/(1+19)=5%
    默认值是1%=1/(1+99),即n=99

  • 还有一个值得关注的参数 “-XX:+UseAdptiveSizePolicy”
    开启这个参数后,就不用手工指定一些细节参数,如:

新生代的大小(-Xmn),Eden与Surviver区的比例(-XX:SurvivorRation),晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;

这是一个值得推荐的方式:
1. 只需设置好内存数据大小(如“-Xmx”设置最大堆)
2. 然后使用"-XX:MaxGCPauseMillis" 或者 "-XX:GCTimeRatio"给jvm设置一个优化目标
3. 其它具体细节参数的调节就由JVM自适应完成;
这也是Parallel Scavenge 收集器与ParNew收集器一个重要区别;

吞吐量和收集器
  • 吞吐量(Throughput)
    CPU用于运行用户代码的时间与CPU总消耗时间的比值;
    即吞吐量=运行用户代码时间 / (运动用户代码时间+垃圾回收时间);
    高吞吐量即减少垃圾收集时间,让用户代码获取更长的运行时间;
  • 垃圾收集器关注的目标
    • 停顿时间
      停顿时间越短就适合需要与用户交互的程序;
      良好的响应速度提升用户体验;
    • 吞吐量
      高吞吐量则可以高效率利用CPU时间,尽快完成运算的任务;
      主要适合在后台计算而不需要太多交互的任务;
    • 覆盖率
      在达到前面两个目标的情况下,尽量减少堆内存的空间;
      可以获取更好的空间局部性;
4.Serial Old收集器

Serial Old是Serial 收集器的老年代版本;

特点
  1. 针对老年代;
  2. 采用“标记-整理”算法(还有压缩,Mark-Sweep-Compact);
  3. 单线程收集;
  4. Serial/Serial Old收集器运行示意图:
    在这里插入图片描述
应用场景

主要用户client模式;
在server模式由两大用用途:

  • 在jdk 1.5之前以及之前的版本(parallel Old衍生之前)中与Parallel Scavenge收集器配合使用;
  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure 时使用;
5 Parallel Old 收集器

Parallel Old 收集器是Parallel Scavenge收集器的老年代版本;

特点
  1. 针对老年代;
  2. 使用多线程和“标记-整理”算法。
  3. 采用多线程;

Parallel Old 收集器的工作流程与Parallel Scavenge工作流程相同:
在这里插入图片描述

应用场景
  1. JDK1.6及之后用来代替老年代的Serial Old 收集器;
  2. 特别是在server模式下,多cpu的场景;
  3. 这样在注重吞吐量以及cpu资源敏感的场景,就可以用Parallel Scavenge和Parallel Old这样的组合;
设置参数

“-XX:+UseParallelOldGC”:指定使用Parallel Old 收集器;

6.CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或者低延迟(Low-latency)垃圾收集器;

特点
  1. 针对老年代;
  2. 以获取最短回收停顿时间为目标;
  3. 并发收集,低停顿;
  4. 需要更多的内存;
  5. 是Hotspot在jdk1.5推出的第一款真正意义上的并发(Concurrent)收集器;
  6. 第一次实现了让垃圾收集器与用户线程(基本上同时工作)
应用场景
  1. 与用户交互较多的场景;
  2. 希望系统停顿的时间最短,注重服务的响应速度;
  3. 给用户较好的体验;
  4. 常见web,B/S系统的服务器上应用;
设置参数

“-XX:+UseConcMarkSweepGC”:指定使用CMS收集器;

CMS收集器运作的过程
  1. 初始标记
    仅标记一下GC Roots能直接关联到的对象;
    速度很快,但是需要stop the would;
  2. 并发标记
    进行GC Roots Tracing的过程;
    刚才产生的集合中标记出存活对象;
    应用程序也在进行;
    并不能保证可以标记出所有的存活对象;
  3. 重新标记(CMS remark)
    为了修改并发标记期间用户程序继续运作而导致标记变动的那一部分对象的标记记录;
    需要"stop the world" ,且停顿时间比初始标记稍长,但是远比并发标记短;
    采用多线程并发执行来提升效率;
  4. 并发清除
    回收所有的垃圾对象;

整个过程中消耗时间最长的并发标记和并发清除,都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程与用户线程一起并发执行;

cms收集器运行示意图:
在这里插入图片描述

明显的缺陷
  1. 对cpu资源非常敏感
    • 并发收集虽然不会暂停用户线程,但占用一部分cpu资源,还是会导致应用程序变慢,总吞吐量变低;
    • CMS的默认收集器数量= (cpu数量+3)/4;
    • 当cpu数量多于4个,收集线程占用CPU的资源多余25%,对用户程序影响可能很大;不足4个时,影响更大,可能无法接受;
  2. 无法处理浮动垃圾,可能出现“Concurrent Mode Failure” 失败
    • 浮动垃圾(Floating Garbage)
      在并发清除时,用户线程新产生的垃圾就是浮动垃圾;
      这使得并发清除时须留一定的内存空间,不能像其它的收集器在老年代几乎填满再进行收集;
      也可以认为CMS所需要的空间比其他垃圾收集器大;
      “-XX:CMSInitiatingOccupancyFraction”:设置CMS预留内存空间;
      jdk1.5默认值68%;
      jdk1.6默认值92%;
    • “Concurrent Mode Failure” 失败
      如果CMS预留空间无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败;
      这时JVM启动后备预案,临时启用Serial Old收集器,而导致一次Full GC的产生;
      这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置太大。
  3. 产生大量的内存碎片
    由于CMS基于“标记-清除”算法,清楚后不进行压缩操作;

产生大量不联系的内存碎片会导致分配大内存对象时,无法找到内存的连续内存,从而需要提前触发另一次Full GC动作。

解决办法

  • “-XX:+UseCMSCompactAtFullConllection”
    使得CMS出现上面这种情况时不进行fullGC,而开启内存碎片的合并整理过程;但是合并整理过程无法并发,停顿时间会比较长;
    默认开启(但是不会进行,结合下面的CMSFullGCBeforeCompaction);
  • “-XX:+CMSFullGCBeforeCompaction”
    设置执行多少次不压缩的Full GC后,来一次压缩整理;
    为减少合并整理过程的停顿时间;
    默认为0,也就是说每次都执行Full GC,不会进行压缩整理;

由于空间连续,CMS需要使用可用“空闲列表”内存分配方式,这比简单使用“碰撞指针”分配的内存消耗大;

总体来说:
与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;但是却增加了新生代垃圾收集时应用暂停的时间,降低了吞吐量而且需要占用大量的堆空间;

7 G1收集器

G1(Garbage-First)是jdk-u4才推出商用的收集器

特点
  1. 并行与并发
    能充分利用多的cpu,多核环境下的硬件优势;
    可以并行来缩短STW停顿时间;
    可以并发让垃圾收集和用户程序同时进行;
  2. 分代收集,收集范围包括新生代和老年代;
    能独立管理整个GC堆(新生代和老年代),而不需要与其他的收集器搭配;
    能够采用不同的方式处理不同时期的对象;
    虽然保留分代的概念,但是java堆的内存布局由很大的差异;
    将整个堆划分为多个大小相等的独立区域(Region);
    新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
  3. 结合多种垃圾收集算法,空间整理,不产生碎片
    从整体看,是基于标记-整理算法;
    从局部(两个Region)看,是基于复制算法;
    这是一种类似火车算法的实现;
    都不会产生内存碎片,有利于长时间运行;
  4. 可预测停顿:低停顿的同时实现高吞吐量
    G1除了追求低停顿外,还能建立可预测的停顿时间模型;
    可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
应用场景

面对服务器的应用,针对具有大内存,多处理器的机器;
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;

如:在堆大小约6GB或者更大时,可预测的暂停时间低于0.5s;

用来替换jdk1.5中的CMS收集器:
在下面情况下,使用G1可能比CMS好:

  1. 超过50%的java堆被活动数据占用;
  2. 对象分配频繁,或者年代提升频率变化很大;
  3. GC停顿时间过长(长于0.5至1秒);

是否一定要采用G1? 未必

  1. 如果目前使用的收集器没有问题,不必使用G1;
  2. 如果程序追求低停顿,可以尝试勇士G1;
  3. 是否替换CMS需要实际场景测试才知道;
设置参数

“-XX:+UseG1GC”:指定使用G1收集器;
“-XX:InitiatingHeapOccupancyPercent”:当整个java堆的占用率达到参数值时,开始开发标记阶段,默认为45;
“-XX:MaxGCPauseMillis”:为G1设置暂停时间,默认为200毫秒;
“-XX:G1HeapRegionSize”:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region

为什么G1收集器可以实现可预测的停顿

G1可以建立可预测的停顿时间模型,是因为:

  • 可以有计划地避免在Java堆的进行全区域的垃圾收集;
  • G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;
  • 每次根据允许得收集时间,优先回收价值最大的Region(名称Garbage-First的由来);
  • 这就保证了有限的时间内尽可能高的收集效率;
一个对象被不同区域引用的问题

一个Region 不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?在其他的分代收集器中也存在这个问题(G1更加突出)

回收新生代的也不得不同时扫描老年代?
这样的话会降低MinorGC的效率;
解决方案:

无论是G1还是其他分代收集器,jvm都是使用Remembered Set来避免全局扫描;

  • 每个Region都有一个对应的Remembered Set;
  • 每次Reference类型数据写操作,都会产生一个Write Barrir暂时中断操作;
  • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同Redion(其他收集器:检查老年代对象是否引用了新生代对象);
  • 如果不同,通过CardTable把相关的引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
  • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;
  • 就可以保证不进行全局扫描,也不会有遗漏。
G1收集器运作流程

不算维护Remembered Set的操作,可以分为四步(与CMS较为类似)

  1. 初始标记(Initial Marking)
    仅标记一下GC Roots能直接关联到的对象;
    且修改TAMS(Next Top at Mark Start),让下一个阶段并发运行时,用户程序能在正确可用的region中创建对象;
    需要STW,但是速度很快;
  2. 并发标记(Concurrent Marking)
    进行GC Roots Tracing 的过程;
    刚才产生的集合中标记出存活的对象;
    耗时较长,但应用程序也在运行;
    并不能保证可以标记出所有可以存活的对象;
  3. 最终标记(final Marking)
    为了修正并发标记期间因用户程序继续运行而导致标记变动的那一部分对象的标记记录;
    上一阶段对象的变化记录在线程的Remembered Set Log;
    这里把Remembered Set Log 合并到Remembered Set中;
    需要STW,且停顿时间比初始标记时间稍长,但是远比并发标记短;
  4. 筛选回收(Live Data Counting and Evacuation)
    首先排序各个Region的回收价值和成本;
    然后根据用户期望的GC停顿时间来制定回收计划;
    最后按照计划回收一些价值高的region中垃圾对象;
    回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
    可以并发进行,降低停顿时间并添加吞吐量;

在这里插入图片描述

总结

收集器串行/并行/并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式,CMS后备预案
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并行老年代标记-整理响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并行老both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMS
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值