Day18 JVM垃圾收集器二

  • 多标-浮动垃圾 在并发垃圾回收过程中,由于应用程序线程与GC线程同时运行,导致某些本应该被回收的垃圾仍旧被标记到引用(对象标记期间变成垃圾或新分配的对象未被标记),从而未能回收的那部分垃圾,称之为浮动垃圾。浮动垃圾通常不会影响垃圾回收的正确性,只是需要等到下一轮才会被清除。针对并发标记/并发清理期间产生的新对象,通常都是全部标记为黑色

  • 漏标-读写屏障 漏标是指在并发标记阶段,由于应用线程同时修改对象引用关系,导致本应存活的对象被错误的标记为垃圾,从而在回收时被错误的释放,这会直接导致程序崩溃,是一种严重的程序错误。两种解决方案

    • 写屏障 当应用线程写入一个引用字段时,JVM自动加入一些处理操作来确保GC不会漏掉新建立的引用或丢失旧引用

      增量更新(Incremental Update) 当黑色对象插入新的指向白色对象的引用关系时,会将新插入的引用记录下来,等并发扫描结束之后,再以这个加入了新引用的黑色对象为根,进行重新扫描。简单理解就是,黑色对象一旦有对白色对象的新引用,就记为灰色。 原始快照(Snapshot At The Beginning SATB) 当灰色对象要删除指向白色对象的引用关系时,会将删除的引用记录下来,等并发扫描结束之后,再以这个被删除了的对象为根,进行重新扫描,确保白色对象能被正确标记(即使有可能是浮动垃圾,也让它先活过这一轮)。缺点是浮动垃圾增多,以及会有额外开销和内存占用 为什么G1选择用SATB,而CMS用增量更新? 效率和成本的综合考虑,SATB相对于增量更新效率会更高,因为不需要在重新标记阶段再次进行深度扫描,G1的很多对象都位于不同的Regin,CMS就只负责老年代一个区域,如果G1也进行深度扫描的话代价会高很多

    • 读屏障 当应用线程读取一个引用时插入屏障,在访问对象前修正指针、触发对象转移,以确保对象已标记

  • 记忆集和卡表 新生代做GC Roots可达性扫描过程中可能会碰到跨代引用的对象,如果又到老年代再去扫描的话效率太低了,为此引入了记忆集(Remember Set)

    • 卡表是记忆集的一种实现,使用一个字节数组CARD_TABLE[],数组每个元素对应着其标识的一块特定大小的内存区域,成为卡页,hotspot卡页大小是2^9=512字节。一个卡页中可能包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表元素标识就会变成1,表示该元素变脏,否则为0,同样也是使用写屏障维护卡表状态。GC时只筛选本收集区的卡表变脏的元素加入GC Roots里
  • G1收集器(-XX:UseG1GC JDK9+默认) Garbage-First收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足GC停顿时间要求(-XX:MaxGCPauseMillis设置,默认200ms)的同时,还具备高吞吐量性能特征。 G1将Java堆划分为多个大小相等的独立区域,目标是不超过2048个Region,一般Region的大小等于堆大小除以2048,可以使用-XX:G1HeapRegionSize手动指定region大小,一般使用默认方式。 G1保留了年轻代和老年代的概念,但并非是物理隔离,而是以分区的形式存在。默认年轻代再堆内存的占比是5%,可通过-XX:G1NewSizePercent设置比例,在实际运行中,并不会完全严格按照这个比例,JVM在垃圾回收过程中的优化有可能会不停的给年轻代增加更多的Region,最多占比不会超过60%(-XX:G1MaxNewSizePercent),年轻代Eden/S0/S1占比依旧为8:1:1。除了年轻代和老年代之外,G1还新增了一个Humongous区来用于专门保存大对象,而不是让大对象直接进入老年代region中,只要对象的大小超过了region的50%,就会被放入Humongous区(FullGC时也会回收),如果对象太大,会横跨多个region区来存放。 回收主要还是采用复制算法,将一个region的存活对象复制到另一个region,这样就不用像CMS一样回收完存在碎片还需要整理一次。

    • 特性

      • 并行与并发 充分利用多核CPU硬件优势,缩短stw停顿时间
      • 分代回收 保留分代概念,做区域分代收集 YoungGC:Eden区满立即触发,会STW,预测回收所需时间,如果时间远小于MaxGCPauseMillis,就新增年轻代region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区满回收时间接近设定值才做GC MixedGC:老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有年轻代和部分老年代。需要把各个region存活的对象拷贝到别的region区,如果拷贝过程中发现没有足够的空间承载对象就会触发FullGC FullGC:停止系统,然后采用单线程进行标记、清理和压缩整理,过程非常耗时,应尽量避免
      • 可预测停顿 对历史GC数据建模,预测每次回收的耗时 1)优先选择垃圾比例高+回收成本低的region 2)动态决定回收多少个region
    • G1收集器的运行过程(主要是指MixedGC)

      • 初始标记 暂停所有其他线程STW,并记录下GC Roots直接能引用的对象。每个region有都有自己的remember sets记录跨区引用,由写屏障维护
      • 并发标记 同CMS,基于SATB
      • 最终标记 同CMS
      • 筛选回收 对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来定制回收计划,尽可能保证总停顿≤MaxGCPauseMillis(不与用户线程并发执行) G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的region,保证有限时间内垃圾回收的高效率。

      image.png

    • G1参数配置

      -XX:+UseG1GC:使用G1收集器

      -XX:ParallelGCThreads:指定GC工作的线程数量

      -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区

      -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

      -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)

      -XX:G1MaxNewSizePercent:新生代内存最大空间

      -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

      -XX:MaxTenuringThreshold:最大年龄阈值(默认15)

      -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了

      -XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。

      -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

      -XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

    • 调优建议

      • 不要盲目调整 MaxGCPauseMillis如设为 10ms→ G1 会频繁 GC,吞吐下降,如设为1000ms→G1单次处理时间长,存活的对象可能比较多,有可能触发FullGC
      • 监控FullGC 一旦发生需要即使优化
      • 尽量避免大对象
      • 使用场景
        • 大堆内存 8G以上(例如Kafka服务器)
        • 停顿时间要求高≤500ms
        • 50%以上的堆被存活对象占用
  • ZGC收集器(-XX:UseZGC 15+转正) jdk11引入的一款低延迟、可伸缩、基于分区的垃圾收集器,目标是在任意堆大小下(TB级),实现始终低于10ms的GC停顿时间,并对吞吐量的影响极小,适用于超大内存、对延迟敏感度极低的系统

    • 内存布局 Region-Based

      • 小型region(Small) 固定容量为2MB,用于放置小于256KB的对象。
      • 中型region(Medium) 固定容量为32MB,用于放置256KB≤对象大小≤4MB的对象
      • 大型region(Large) 不固定容量,动态变化,用于放置4MB以上的大对象,每个大region中只会存放一个大对象。所以它的实际容量最小有可能仅有4MB image.png

      image.png

      • 在JDK11-JDK20中,ZGC是不支持分代的,自JDK21起试验性引入分代模式(需通过-XX:+ZGenerational手动启用),默认还是非分代模式,但在JDK24之后,该参数被废弃,默认是强制分代模式。
    • NUMA-aware(Non Uniform Memory Access Architecture)

      早期的内存是CPU通过总线访问,随着核心数增多,都走一条总线的话争抢会越来越严重,也就相应的造成了性能瓶颈,于是就把CPU和内存集成到一个单元访问,形成了NUMA,每次CPU访问内存时,优先访问自己分配到的这块,如果本地内存空间不够了,就会从远程调配,以防出现内存使用倾斜。

      image.png image.png

    • ZGC的回收过程

      • 并发标记(Concurrent Mark) 遍历对象图做可达性分析,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,ZGC的标记是在指针上用64位地址的高四位记录,标记阶段会更新染色指针
      • 并发预备重分配(Concurrent Prepare for Relocate) 需要根据特定的查询条件统计得出本次收集过程要清理的region,并将这些region组成重分配集(Relocation Set)。用更大的扫描成本换G1记忆集的维护成本。
      • 并发重分配(Concurrent Relocate) 将重分配集中的存活对象复制到新的region上,并为分配集中的每个region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。 ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。只有第一次触发时会有较大消耗
      • 并发重映射(Concurrent Remap) 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以ZGC可以很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,节省了一次遍历对象图的开销,一旦所有指针修正后,原来记录新旧对象关系的转发表就会被释放。
    • 染色指针(Colored Pointer)

      以往的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在64位地址的指针中,使得ZGC在扫描引用时,不需要访问对象本身,只看指针就能知道对象的状态(也使得ZGC高度依赖64位机器,32位系统无法使用)

      • 18位:预留给以后使用【46-63】;
      • 1位:Finalizable标识【45】,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;
      • 1位:Remapped标识【44】,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);
      • 1位:Marked1标识【43】;
      • 1位:Marked0标识【42】,和上面的Marked1都是标记对象用于辅助GC;
      • 42位:对象的地址【0-41】(所以它可以支持2^42=4T内存):

      使用双缓冲标记,在每一轮GC周期开始时交替使用,提高并发性能,第一轮GC使用Marked0,第二轮使用Marked1

    • 读屏障

      之前的GC都是采用Write Barrier,这次ZGC采用了完全不同的方案读屏障,这个是ZGC一个非常重要的特性。 在标记和移动对象的阶段,每次「从堆里对象的引用类型中读取一个指针」的时候,都需要加上一个Load Barriers。

      image.png

    • 缺点

      • 浮动垃圾多,每次停顿时间都在10ms以下,并且不做分代,意味着每次GC都有极大概率清理不完朝生夕死的垃圾,增大堆内存能有更多的喘息时间,但也是治标不治本
      • CPU消耗较高,因为读屏障的存在,每次读取引用都会触发检查
    • ZGC触发时机

      • 定时触发 默认禁用,可通过-XX:ZCollectionInterval配置
      • 预热触发 最多三次,当堆内存达到10%、20%、30%时触发,主要统计GC时间,为其他GC机制使用
      • 分配速率 基于正态分布统计,计算内存99.9%可能的最大分配速率,以此速率下内存将要耗尽的时间点,在耗尽之前触发
      • 主动触发 (默认,通过-XX:ZProactive配置)距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发
  • 如何选择垃圾收集器?

    1. 优先调整堆的大小让服务器自己来选择
    2. 如果内存小于100M,使用串行收集器
    3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
    4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
    5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
    6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC(JDK9+默认使用G1)

    image.png

  • 安全点与安全域

    安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。

    这些特定的安全点位置主要有以下几种:

    1. 方法返回之前
    2. 调用某个方法之后
    3. 抛出异常的位置
    4. 循环的末尾

    大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和安全点是重合的。

    安全区域又是什么?

    Safe Point 是对正在执行的线程设定的。如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。因此 JVM 引入了 Safe Region。Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP