深入学习JVM- (3)理解并运用不同的垃圾收集器

  • 3.1 经典垃圾收集器

    • HotSpot虚拟机的垃圾收集器
      • 没有最好的收集器,只有在具体场景最适合的收集器
    • (1) Serial收集器

      • “单线程”工作:强调垃圾收集时必须暂停其它所有工作线程,直到收集结束;==>STW时间过长用户难以接受
      • 但依然是客户端模式下的默认新生代收集器,原因包括:➀简单高效,额外内存消耗最小,因此非常适用于内存资源受限的情况;➁对于单核或者核心数少的处理器环境,Serial收集器没有线程交互开销,可以专心做垃圾收集,因此可以获得最高的单线程收集效率;
    • (2) ParNew收集器

      • 实质上是Serial收集器的多线程并行版本;
      • 是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工;
      • 自JDK 9开始,ParNew加CMS收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了,且ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能够和它们配合了;
      • 并行vs并发
        • 并行:描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态;
        • 并发:描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。用户线程并未被暂停,程序依然能响应服务请求,只是应用程序的吞吐量会由于垃圾收集线程占用资源而收到影响;
    • (3) Parallel Scavenge收集器

      • 不能与CMS配合使用,Parallel Scavenge的目标是面向高吞吐量,而CMS的目标是面向低延迟;
      • 吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值,吞吐量=运行用户代码的时间/(运行用户代码的时间+运行垃圾收集时间);
      • 低延迟vs高吞吐量:低延迟意味着停顿时间短,适合需要与用户交互或者需要保证服务响应质量的程序;高吞吐量意味着处理器的资源利用效率,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务;
      • Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间(设置得较小时吞吐量也随之下降),-XX:GCTimeRatio直接设置吞吐量大小;
      • 自适应调整策略:通过参数-XX:+UseAdaptiveSizePolicy设置,激活后只需要设置基本内存数据以及吞吐量控制目标,虚拟机可以根据当前运行情况自动调整堆内部的细节参数,相当于把内存调优的任务交给虚拟机完成;
    • (4) Serial Old收集器

      • 标记-整理算法,
      • 主要是供客户端模式下使用,另外在服务器模式下,一是在JDK5及之前的版本中与Parallel Scavenge收集器搭配使用,二是作为CMS发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用;
    • (5) Parallel Old收集器

      • 是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现;
      • 在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
    • (6) CMS收集器(JDK5)

      • 是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作;以获取最短回收停顿时间为目的,适合关注服务器响应速度的应用;
      • 基于标记-清除算法。运作过程分为四个步骤:(1)初始标记(STW);(2)并发标记;(3)重新标记(STW,增量更新);(4)并发清除;
      • 优点:并发标记和并发清除两个最耗时的阶段实现了与用户线程并发执行,是HotSpot虚拟机追求低停顿的第一次成功尝试;
      • 缺点:(1) 对处理器资源很敏感,当处理器核心数不足四个时,CMS对于用户线程的影响可能变得很大,需要处理器分出可能超过一般的运算能力去执行垃圾收集线程,导致用户程序的执行速度突然大幅下降;(2) 无法处理并发标记阶段程序产生的“浮动垃圾”,这一部分垃圾出现在标记阶段完成之后,清理结束之前,进而导致出现再一次Full GC[Concurrent Mode Failure],因此CMS不能等空间几乎填满再收集,需要留出一定的空间提供给并发阶段的用户线程使用;(3) 收集结束会产生大量的空间碎片,因此再进行若干次不整理空间的Full GC之后,需要整理一次碎片;
    • (7) G1收集器

      • 开创了收集器面向局部收集的设计思路和基于Region的内存布局形式;
        • JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用的收集器;规划JDK 10功能目标时,HotSpot虚拟机提出了“统一垃圾收集器接口”,将内存回收的“行为”与“实现”进行分离,CMS以及其他收集器都重构成基于这套接口的一种实现;
        • Mixed GC模式:可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大
        • 目标:建立起“停顿时间模型”,能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标;
        • 基于Region的堆内存布局:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代的Eden空间、Survivor空间,或者老年代空间,不同的角色只是代表不同的回收策略。​​​​​​​​​​​​​​

        • G1收集器以Region作为回收单元,有计划避免对整个堆进行全区域的垃圾收集。G1收集器跟踪各个Region垃圾堆积的“价值”,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值收益最大的那些Region;
      • 需要解决的关键细节问题:
        • ➀ 跨Region引用对象:使用记忆集解决,每个region都维护自己的记忆集,在G1中记忆集的存储本质是双向的哈希表,其实施相当复杂且有更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
        • ➁ 并发阶段线程互不干扰:用户线程改变对象引用关系==>原始快照;新创建对象的内存分配==>创建在两个TAMS(Top at Mark Start)指针标记的空间内,不纳入回收范围;回收速率需要能跟上内存分配速率,否则STW;
        • ➂ 建立可靠的停顿预测模型:以衰减均值为理论基础来实现,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。衰减平均值更准确地代表“最近的”平均状态。
      • 运作过程:
        • (1)初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,需要短暂停顿用户线程,
        • (2)并发标记:递归扫描整个堆里的对象图,并重新处理SATB记录下在并发时有引用变动的对象;
        • (3)最终标记:短暂停顿用户线程,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录;
        • (4)筛选回收:必须暂停用户线程,更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后清理回收集;
      • G1 vs CMS
        • G1追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净;设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡;G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存;
        • CMS的内存占用和程序运行时的额外执行负载比G1低;
  • 3.2 低延迟垃圾收集器

    • 衡量收集器的“不可能三角”:内存占用,吞吐量,延迟
    • (1) Shenandoah收集器

      • 目标是实现在任何堆大小下都可以把垃圾收集的停顿时间限制在10ms以内的垃圾收集器 ==》Shenandoah除了进行并发标记外,还会并发进行对象清理后的整理动作;
      • Shenandoah相对于G1的改进:
        • (1)支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发;
        • (2)Shenandoah(目前)是默认不使用分代收集的;
        • (3)Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“ 连接矩阵”的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题的发生概率;
      • 工作过程
      • Brooks Pointer 转发指针 ==》解决并发回收阶段(复制阶段)用户线程对被移动对象进行读写访问的问题;
    • (2) ZGC收集器

      • 目标与Shenandoah收集器目标高度相似,但是实现思路两者有显著差异;
      • ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器
  • 3.3 选择合适的垃圾收集器

    • 收集器的权衡——主要受三个因素影响
      • (1)应用程序的主要关注点:如果是数据分析、科学计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的;
      • (2)运行应用的基础设施:譬如硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM /Aarch64;处理器的数量多少,分配内存的大小;选择的操作系统是Linux、Solaris还是Windows等;
      • (3)使用JDK的发行商和版本号:是ZingJDK/Zulu、OracleJDK、Open-JDK、OpenJ9抑或是其他公司的发行版?该JDK对应了《Java虚拟机规范》的哪个版本?
    • 虚拟机及垃圾收集器日志
      • 从JDK9开始,HotSpot所有功能的日志都收归到了“-Xlog”参数上;
      • 命令行:-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
        • 最关键的参数是选择器(Selector),由标签(Tag)和日志级别(Level)共同组成。
          • 垃圾收集器的标签名称为“gc”,
          • 日志级别从低到高,共有Trace, Debug, Info, Warning, Error, Off六种级别,日志级别决定了输出信息的详细程度,默认级别 为Info;
        • 还可以使用修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加在日志行上的信息包括:
          • time:当前日期和时间;
          • uptime:虚拟机启动到现在经过的时间,以秒为单位;
          • timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
          • uptimemillis:虚拟机启动到现在经过的毫秒数。
          • timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
          • uptimenanos:虚拟机启动到现在经过的纳秒数。
          • pid:进程ID。
          • tid:线程ID。
          • level:日志级别
        • 如果不指定, 默认值是uptime、level、 tags这三个;
  • 3.4 实战:内存分配与回收策略

    • ⚠️:要学习怎么写程序验证分配规则,因为使用不同的垃圾收集器时内存分配和回收策略存在差异;以下均为客户端模式下的案例,
    • (1)对象优先在Eden区分配
      • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC;
    • (2)大对象直接进入老年代
      • ⼤对象就是指需要⼤量连续内存空间的Java对象,HotSpot虚拟机提供了-XX:PretenureSizeThreshold参数,指定⼤于该设置值的对象直接在⽼年代分配,⽬的在于避免在Eden区及两个Survivor区之间来回复制,产⽣⼤量的内存复制操作。
    • (3)长期存活的对象将进入老年代
      • 虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
    • (4)动态对象年龄判定
      • 为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄
    • (5)空间分配担保
      • 发生Minor GC之前,检查➀老年代最大可用的连续空间是否大于新生代所有对象总空间➁-XX:HandlePromotionFailure参数的设置值是否允许担保失败➂检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;==》➀不满足时进行➁,➁允许进行➂,➂满足尝试一次MinorGC,➁不允许或者➂不满足则进行Full GC;
      • JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值