基础知识

性能指标

在调优Java应用程序时,重点通常放在两个主要目标上:响应性吞吐量

 响应性Responsiveness 是指应用程序对请求的数据做出响应的速度:

  • 桌面用户界面对事件的响应速度
  • 网站返回页面的速度
  • 数据库查询的返回速度

 吞吐量Throughput 专注于最大程度地提高应用程序在特定时间段内的工作量:

  • 在给定时间内完成的事务次数
  • 批处理程序在一小时内可以完成的作业数
  • 一小时内可以完成的数据库查询数

较长的暂停时间Pause Time对于注重响应性的应用程序是不可接受的,但对于注重吞吐量的应用程序来说可以接受的。前者重点是在短时间内做出响应,后者则侧重与长时间运行的处理效率。

GC 基础

GC Root

可达性分析是 Java GC 算法的基础,基本思路就是以一系列名为 GC Roots 对象作为起始点,通过引用关系遍历对象图,如果一个对象到 GC Roots 间没有任何可达路径相连时,则说明此对象可以被回收。

可以作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中JNI(即一般说的native方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

三色标记

可达性分析中重要的一环就是遍历整个堆,并标记其中的存活对象。一种常用的标记算法是 三色标记法tri-color marking

每个对象可能为以下 3 种颜色之一:

  • white — 未被标记
  • gray — 本身已标记,但部分引用的对象完成标记(动图的黄色对象)
  • black — 本身已标记,且所有引用的对象完成标记(动图的蓝色对象)

标记算法从 GC Roots 出发遍历堆,可达对象先标记 gray,然后再标记 为 black。

遍历完成之后所有可达对象都是 black 的,此时所有标记为 white 的对象都是可以回收的。

当实现并发标记算法时,必须防止 white 对象被漏标,否则可能导致不该回收的对象被回收。

分代收集

传统垃圾收集器将堆分成三个部分:年轻代YoungGen = Eden + Survivor,老年代OldGen和永久代PermGen,每个区域内存连续且大小固定。

  • 年轻代:一次性使用的临时对象(例如:方法中构造的临时对象)
  • 老年代:被长期引用的常驻对象(例如:缓存对象、单例对象)
  • 永久代:JVM 运行过程中一直存在的对象(例如:字符串常量、类信息)

将堆内存进行划分后,可以按照对象生命周期长短,在不同区域使用不同的回收算法,提高 GC 的效率。

算法分类

Mark and Sweep标记-清除

 用一个空闲列表free-list记录失效对象占用的内存区域,方便后续重新分配给新对象。

  • 回收原理简单,GC 停顿时间短
  • 维护空闲列表需要一定的空间开销
  • 内存碎片较多,可能导致内存分配失败

Mark-Sweep-Compact标记-整理

 将所有存活对象移动到内存区域的开头,剩余的连续内存区域都是可用的空闲空间。

  • 通过指针碰撞查找空闲空间,分配速度快
  • 内存碎片少,内存分配失败概率低
  • 复制对象会导致较长时间的 GC 停顿

Mark and Copy标记-复制

 将内存划分为活动区间空闲区间,前者用于动态分配对象,后者用于容纳 GC 存活对象。

 GC 时只需将存活对象从前者复制到后者,然后交换两者的角色即可。

  • 标记和复制在同一阶段同时进行,当存活对象少时回收效率极高
  • 需要预留一个空闲空间用于容纳存活对象,造成内存浪费

CMS 回顾

CMS Concurrent Mark-Sweep 是一个采用 标记-清除 算法的老年代收集器。

它通过与应用程序线程并发执行大多数垃圾回收工作,来最大程度地减少由于 GC 导致的暂停。

通常情况下,CMS 收集器不会复制或压缩活动对象,这意味着无需移动活动对象即可完成垃圾回收。

然而过多的内存碎片可能造成分配失败,最终导致 FullGC。可以通过分配更大的堆来规避这一问题。

CMS 对老年代的回收可以分为以下几个步骤:

  • Initial Mark (STW) 初始标记

    • 标记 GC Roots 直接可达的老年代对象
    • 遍历新生代存活对象,标记直接可达的老年代对象
  • Concurrent Mark 并发标记

    GC 线程遍历 Initial Mark 阶段标记出来存活的老年代对象,然后递归标记这些可达的对象。

    该阶段与应用线程并发运行,期间会发生新生代对象晋升、老年代对象引用关系更新,需要对这些对象进行重新标记,避免发生遗漏。

    CMS 用一个card-table管理老年代,并发标记过程中,某个对象的引用关系发生了变化,则将对象所在的内存块标记为 Dirty Card

    CMS 使用增量更新incremental update解决并发修改导致的漏标问题:把 black 对象重新标记为 grey,下次重新扫描其引用。

  • Preclean 预清理

    这一阶段主要是处理 Concurrent Mark 阶段中引用关系改变,导致没有标记到的存活对象的。通过并发地重新扫描这些对象,预清理阶段可以减少 Remark 阶段的 STW。

    这个阶段会处理前一个阶段被标记为 Dirty Card 的部分,将其中变化了的对象作为 GC Root 再进行扫描并重新标记。

  • Abortable Preclean 可终止的预清理

    这个阶段作用与 Preclean 类似,但可以通过设置 扫描时长(默认5秒)或 Eden 区使用占比(默认50%)控制本阶段的结束时机。

    增加这一阶段的原因,是期待这期间能发生一次 YoungGC 清理无效的年轻代对象,减少 Remark 阶段扫描年轻代的时间。

  • Remark (STW) 重新标记:

    这个阶段同时扫描 YoungGen 与 OldGen,重新标记整个老年代中所有存活对象。

    由于之前的 Concurrent MarkPreclean 阶段是与用户线程并发执行的,年轻代对老年代的引用可能已经发生了改变,Remark 要花很多时间处理这些改变,会导致长时间的 STW。

    此外,即使新生代的对象已经不可达了,CMS 也会使用这些不可达的对象当做的 GC Roots 来扫描老年代,导致部分失效的老年代对象无法被及时回收。

    可以加入参数 -XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次 YoungGC,回收掉年轻代的对象无用的对象。这样进行年轻代扫描时,只需要扫描 Survivor 区的对象即可,一般 Survivor 区非常小,这大大减少了扫描时间。

  • Concurrent Sweep 并发清理

    至此,老年代所有存活的对象已经被标记完成。这个阶段主要是清除那些没有标记的对象并且回收空间。

    被回收的空间会被添加到 空闲列表中,以供以后分配。这一过程可能会对空闲空间进行合并,但是不会移动存活对象。

    由于该阶段是与应用线程并发运行的,自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,无法在当次收集中处理掉它们。只好留待下一次GC时再清理掉。这一部分垃圾就称为 浮动垃圾

  • Resetting 重置

    清除数据结构,并重置定时器,为下一轮 GC 做准备。

G1 算法

设计目的

G1 Garbage-First 是一种服务器端的垃圾收集器:

  • 可以与应用程序线程并行运行,减少 STW
  • 整理空闲空间减少内存碎片,但不引入较长的 GC 暂停时间
  • 提供可预测的GC暂停时间,无需牺牲很多吞吐量

G1 能够在大内存的多处理器计算机上,保证 GC 暂停时间可控,并实现高吞吐量。

其最终目的是取代 CMS 成为服务端 GC 更好的解决方案:

  • 采用 标记-整理 算法,可以避免使用细粒度的空闲列表进行分配。简化了收集器设计并消除了潜在的碎片问题。
  • 使用 增量回收incremental collecting 算法,其 GC 暂停时间比 CMS 更具可预测性,并允许用户指定期望的暂停时间。

基本概念

G1 将堆划分为一组大小相等的且连续的堆区域Region

G1 中新生代与老年代不再连续,每个区域可以在 EdenSurvivorOld 之间切换角色。此外,还有一类被称为 Humongous 的巨型区域,用于容纳体积 ≥ 标准区域大小的50%的对象。JVM 通常会将内存划分为 2000个区域,每个大小从 1 到 32Mb 不等,由 JVM 在启动时通过 -XX:G1HeapRegionSize 指定。

每个区域会被进一步细分成多个卡片Card,每个大小为 512Kb,用于实现细粒度的引用统计。

分区设计可以避免一次收集整个堆,每次 GC 只收集区域的一个子集 CSetcollection set,其中必然包含所有 Young 区域,同时可能包括部分 Old 区域:

根据回收区域的不同,可以将 GC 分为:

  • YoungGCCSet 只包含 Young 区域
  • MixedGCCSet 同时包含 YoungOld 区域
  • FullGC: 回收整个堆(可用空间耗尽时触发,单线程执行)

G1 根据存活对象的字节数统计每个区域的 活跃度liveness,然后根据期望停顿时间来确定该 CSet 的大小,并保证那些垃圾多(活跃度低)的区域会被优先回收,故此得名 垃圾优先

G1 的执行过程可以表示为由 3 个阶段组成的循环:

Young GC

堆中一开始只有 YoungGen,因此只会触发 YoungGC,将 EdenSurvivor 区域中的活动对象复制到另一个空闲的 Survivor 区域。



G1 中将 将存活对象复制到其他区域 的过程称为 疏散Evacuation。为了减少停顿时间,疏散工作由多个 GC 线程并行完成。

YoungGC 过程中会根据预期目标停顿时间 -XX:MaxGCPauseMillis 动态调整新生代的大小,通过 -XX:G1NewSizePercent 参数可以人为干预这一过程,但会让预期停顿时间参数失效。

当堆的整体占用空间足够大时(超过45%),就会进入 Concurrent Marking 阶段。通过 -XX:InitiatingHeapOccupancyPercent 选项可以配置这一行为。

Concurrent Marking

与 CMS 类似,G1 中的并发标记包括多个阶段,其中一些阶段是并发的,另一些阶段则会 STW。

  • Initial Mark (STW) 初始标记

    扫描并标记 GC Root 对象直接可达的老年代存活对象。

    Initial Mark 并没有独立的执行阶段,而是嵌入 YoungGC 中执行的,其停顿时间会被分摊,因此实际的开销非常低。

  • Root Region Scan 扫描根区域

    扫描 Root Region 并标记所有可达的老年代存活对象。

    此处的 Root Region 就是先前 YoungGC 中生成的 Survivor 区域,其包含的对象都会被视为 GC Root

    为了避免移动对象对标记产生影响,该过程必须在下次 YongGC 启动前完成。

  • Concurrent Mark 并发标记

    启动并发标记线程,扫描并标记整个堆中的存活对象(线程数可以通过 -XX:ConcGCThread 进行配置)。

    为了避免重复标记,G1 使用 SATBsnapshot-at-the-beginning算法解决漏标问题:

    应用线程对在 Concurrent Mark 执行期间进行的所有并发更新,都应保留先前的已知标记信息。

    该约束是通过预写屏障pre-write barrier实现:

    Concurrent Mark 扫描过程中,当应用线程修改某个字段时,会将先前的引用对象存储在日志缓冲区log buffers中,然后交由并发标记线程处理。

    为了避免移动对象对标记产生影响,该过程必须在下次 YoungGC 启动前完成。所有的标记任务必须在堆满前完成,如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行 FullGC

  • Remark (STW) 重新标记

    启动并行标记线程,完成对整个堆中存活对象的标记(线程数可以通过 -XX:ParallelGCThread 进行配置)。

    该阶段会暂停所有应用线程,避免发生引用更新,并完成对SATB 日志缓冲区中剩余对象的标记,找出所有未被访问的存活对象。

    该阶段还执行一些额外的清理操作,例如:

    • 卸载不可达的类(通过 -XX:+ClassUnloadingWithConcurrentMark 开启)
    • 处理引用对象(弱引用、软引用、虚引用、最终引用)
  • Cleanup 清理垃圾

    整理统计信息并识别出高收益的老年代分区,为 MixedGC 做准备。

    主要工作有:

    • RSet 梳理(后续说明)
    • 识别回收收益高的老年代分区 (基于释放空间和暂停目标)
    • 直接回收的没有活跃对象的空闲分区

    此外还会执行一些清理工作,为下一次 Concurrent Marking 做好准备。

Mixed GC

MixedGC 主要流程与 YoungGC 类似,不同的地方在于 CSet 中包含了 Old 区域。

需要注意的是,Concurrent Marking 结束后,并不一定会立即触发 MixedGC,中间可能会穿插多次的 YoungGC

当收集某个区域时,我们必须知道是否有来自非收集区域引用,来确定它们的活动性:

  • 从非收集区域到收集区域的 incoming reference 是重要的(被非收集区引用的对象必须存活)
  • 从收集区域到非收集区域的 outgoing reference 是可忽略的(非收集区域不参与GC)

但查找整个堆非常耗时,同时也失去了增量收集的优势。为了解决这一问题,G1 为每个区域维护了一个 RSetremembered set,用于记忆从其他区域指向自己的引用。

收集过程

在执行收集时,RSet 中引用信息会扮演局部 GC Roots 的角色,避免耗时的引用查找,保证每个区域的 GC 能够独立进行:

注意,象如果 Old 区域中对在 Concurrent Marking 阶段被确定为垃圾,即使有外部引用,该对象也会被作为垃圾回收。

接下来发生的事情与其他收集器所做的相同:多个并行GC线程找出哪些对象是活动的,哪些对象是垃圾:

最后,释放空闲区域,将活动对象移到 Survivor 区域,并在必要时创建新对象:

RSet 维护

为了维护 RSet,在应用线程对字段执行写操作时,会触发写后屏障post-write barrier

如果更新后的引用是跨区域的(即从一个区域指向另一个区域),则对应的条目将出现在目标区域的 RSet 中。

为了减少写屏障带来的开销,该过程是异步的:

应用线程只负责把更新字段所在的 Card 信息插入一个DCQDirty Card Queue,然后由 Refine 线程将其拾取并将信息传播到被引用区域的 RSet。

如果应用线程插入速度过快,会导致 Refine 线程来不及处理,那么应用线程将接管 RSet 更新的任务,从而导致性能下降。

总结

并发标记增量收集 是 G1 实现高性能与可预测回收的关键。

对于 CPU 资源充足且对延迟敏感的服务端应用来说,G1 算法能够在大堆上提供良好的响应速度。

作为代价,额外的写屏障与更活跃GC线程,会对应用的吞吐量产生负面影响。

参考资料

G1 收集器的更多相关文章

  1. JAVA G1收集器 第11节

    JAVA G1收集器 第11节 上两章我们讲了新生代和年老代的收集器,那么这一章的话我们就要讲一个收集范围涵盖整个堆的收集器——G1收集器. 先讲讲G1收集器的特点,他也是个多线程的收集器,能够充分利 ...

  2. G1收集器-原创译文[未完成]

    G1收集器-原创译文 原文地址 Getting Started with the G1 Garbage Collector 目的 本文介绍了如何使用G1垃圾收集器以及如何与Hotspot JVM一起使 ...

  3. CMS收集器和G1收集器优缺点

    首先要知道 Stop the world的含义(网易面试):不管选择哪种GC算法,stop-the-world都是不可避免的.Stop-the-world意味着从应用中停下来并进入到GC执行过程中去. ...

  4. JVM垃圾收集器-G1收集器

    G1收集器是当前收集器技术发展的最前沿成果,在JDK1.6_Updata14中提供了Early Access版本的G1收集器以供适用.G1收集器是垃圾收集器理论进一步发展的产物,它与前面的CMS收集器 ...

  5. JVM-如何判断对象存活与否与CMS收集器和G1收集器的区别

    JVM如何判断对象存活? 1.计数器 2.可达性分析   (很多主流语言采用这种方法来判断对象是否存活) 计数器:每当有一个地方引用该对象时,计数器 +1:引用失效则 -1: 优点:实现简单,判定效率 ...

  6. G1收集器的收集原理

    G1收集器的收集原理 来源 http://blog.jobbole.com/109170/ JVM 8 内存模型 原文:https://blog.csdn.net/bruce128/article/d ...

  7. CMS垃圾收集器与G1收集器

    1.CMS收集器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器.基于“标记-清除”算法实现,它的运作过程如下: 1)初始标记 2)并发标记 3)重新标记 4)并发清除 初始标记.从新标记这两 ...

  8. G1收集器

    转载:https://blog.csdn.net/zhou2s_101216/article/details/79202893 http://blog.jobbole.com/109170/ http ...

  9. 垃圾收集器之:G1收集器

    G1垃圾收集器是一种工作在堆内不同分区上的并发收集器.分区既可以归属于老年代,也可以归属新生代,同一个代的分区不需要保持连续.为老年代设计分区的初衷是我们发现并发后台线程在回收老年代中没有引用的对象时 ...

  10. CMS收集器和G1收集器

    CMS收集器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器.基于"标记-清除"算法实现,它的运作过程如下: 初始标记 并发标记 重新标记 并发清除 初始标记.从新标记这两 ...

随机推荐

  1. PHP的命令执行漏洞学习

    首先我们来了解基础 基础知识来源于:<web安全攻防>徐焱 命令执行漏洞 应用程序有时需要调用一些执行系统命令的函数,如在PHP中,使用system.exec.shell_exec.pas ...

  2. 详细!Mybatis-plus常用API全套教程,我就不信你看完还不懂!

    前言 官网:Mybatis-plus官方文档 简化 MyBatis ! 创建数据库 数据库名为mybatis_plus 创建表 创建user表 DROP TABLE IF EXISTS user; C ...

  3. FL Studio水果音乐制作入门教程

    "没有早期音乐教育,干什么事我都会一事无成".这并非某位音乐家精心熬制的心灵鸡汤,而是出自物理学家爱因斯坦之口,朋友们没有看错,就是那个被称为二十世纪伟大科学家的爱因斯坦,所以,别 ...

  4. 找回消失的IDM嗅探下载浮动条的方法

    我们之前讲了IDM资源嗅探的下载浮动条的设置方法,然而在有些时候,这个下载浮动条无法正常显示出来,影响了下载体验,这个问题该如何解决呢? 1.安装IDM扩展程序 一般来说,在IDM安装完成后,会在浏览 ...

  5. SQL server分页的四种方法(算很全面了)

      这篇博客讲的是SQL server的分页方法,用的SQL server 2012版本.下面都用pageIndex表示页数,pageSize表示一页包含的记录.并且下面涉及到具体例子的,设定查询第2 ...

  6. cocoslua3.17 android机器上播放音效不全

    开发过程中遇到一个问题,一个8秒的音效,在android机器上播放不完就结束了:网上说是由于android播放音效的内存限制的:原因知道了,那怎么解决呢? 通过各种搜索查找发现还是解决不了问题,然后自 ...

  7. HMM、CTC、RNN-T训练是所有alignment的寻找方法

    1.1 LAS产生label的计算   LAS是可以看做能够直接计算给定一段acoustic feature时输出token sequences的概率,即\(p(Y|X)\),LAS每次给定一个aco ...

  8. ubuntu解决安装速度问题

    速度慢得原因:linux系统很多的软件源链接都是在国外服务器上,由于国内防火墙影响导致下载速度极慢,甚至超时. 解决办法一:购买梯子 这样你就可以快速的下载国外服务器的软件包了,但是你得有个可靠得梯子 ...

  9. 浅谈 STL

    简介 STL是Standard Template Library的简称,中文名标准模板库,从根本上说,STL是一些"容器"的集合,这些"容器"有list,vec ...

  10. C语言常用的一些转换工具函数!

    1.字符串转十六进制 代码实现: 2.十六进制转字符串 代码实现: 或者 效果:十六进制:0x13 0xAA 0x02转为字符串:"13AAA2" 3.字符串转十进制 代码实现: ...