G1收集器和ZGC收集器

前置#

  1. 不要把垃圾收集器的功能看的太单一,除了回收垃圾之外,内存的组织、分配、管理等操作也是垃圾收集器的工作。有的垃圾收集器选择分代设计,有的选择不分代。
  2. 垃圾收集器的性能主要从三方面考虑:停顿时间、吞吐量和内存占用

对于第二点,这三种目标无法同时满足,一个垃圾回收器基本只能满足其中的一到两点。比如这篇笔记中介绍的G1和ZGC都在内存占用方面颇高,而G1更加关注吞吐量,ZGC更加关注停顿时间。

吞吐量即程序运行时间在总运行时间中所占的比例,总运行时间=程序运行时间+垃圾回收时间。

开始之前可以扫一眼之前的一篇文章:JVM 二 垃圾清理

G1收集器#

貌似在JDK9到JDK17中默认的垃圾收集器都是G1。

G1收集器的特征:

  1. 主要关注吞吐量,即满足垃圾清理占用的时间在程序总运行时间中所占的比例足够小。
  2. 使用Region作为内存管理的单元
  3. 分代,但使用Mixed GC,即并非一次清理只面向某一个分代
  4. 基于标记整理算法,不会产生内存碎片

内存布局#

G1中的Region都一样大,可以使用-XX:G1HeapRegionSize设置,取值范围为1~32MB

难免有大于最大Region容量的大对象,只要一个对象超过Region容量的一半,G1就认为它是一个大对象,存放大对象的区域称作Humongous区域,大对象存在于N个连续的Humongous Region区域中,G1对待它们的行为类似老年代。

回收收益比#

G1会权衡回收一个Region的收益,即可以获得的空间和回收所需的时间,然后参考用户设置的-XX:MaxGCPauseMillis(最大GC暂停时间),回收在该时间内可以完成那些的Region中,回收收益最大的那些。

虽然用户可以设置一个它期待的GC暂停时间,但这个值应该是合理的,不能异想天开。如果GC暂停时间设置的太小,会导致内存回收的时间赶不上内存分配时间从而引发频繁的FullGC

记忆集#

记忆集是为了解决标记过程中发现系统中存在的跨代引用,在传统的新生代——老年代模型中,只有两个分区,所以只需要维护一个简单的记忆集,而G1将内存区分为很多Region,它的每一个Region都需要维护一个自己的记忆集,所以它占用的内存也会较高。

垃圾回收步骤#

  1. 初始标记:简单的标记GC Roots能直接关联到的对象,必须停顿用户线程,但时间很短。
  2. 并发标记:从初始标记得到的内容,对堆中对象进行扫描标记,该阶段与用户线程并发
  3. 最终标记:重新检查并标记那些并发标记阶段由于用户操作而修改的引用关系
  4. 筛选回收:回收,选出指定停顿时间内回收价值最高的那些Region,复制其中的存活对象到新Region,清理旧Region。由于出现移动操作,需要停止用户线程。

G1回收器只有并发标记阶段与用户线程并发

总结#

G1收集器的优点在于它会收集那些成本最高的Region,设置一个期待的最大的停止时间。

对于当前系统,内存不再紧张,G1包括后面的ZGC收集器为了获得更短的停止时间都做了一些妥协,比如划分Region,一次不完成整个堆(新生代或老年代)的清理,只清理其中一部分。这种思想说成白话就是:如果当前内存空间不算紧张,那就一次清除少点儿,缓缓再清

ZGC#

  1. 也基于Region,但是Region是动态的。可以动态创建,具有动态大小,可以动态变化。
  2. (暂时)不分代
  3. 使用染色指针
  4. 极低的停顿时间

Region分类#

  1. 小型Region:2MB,防置小于256KB的对象
  2. 中型Region:32MB,放置大于等于256KB,小于4MB的对象
  3. 大型Region:容量不固定,可以动态变化,放置大于等于4MB的对象

染色指针(Colored Pointer)#

简单来说,就是现在的操作系统由于物理硬件大小限制,并没有用上地址空间全部位,ZGC使用这些空余的位来存储GC需要的额外信息。

有些垃圾收集器把额外信息放置在对象头上,有些放在其它与对象不相关的数据结构中,如Bitmap,ZGC放在对象指针中没被用到的位上,这样不需要额外的空间。当然也有缺点,这导致了系统可用的寻址空间变小,也就是系统可安装的内存变小(4TB,目前来说是足够使用还有不小的富余,而且从目前的发展方向来看,大部分系统不再纵向扩展系统的性能)。

并发的清除过程#

ZGC和之前的一款并不在JDK中的垃圾收集器Shenandoah都支持并发清除过程。

使用复制算法举例,在之前的垃圾收集器中,该操作必须停止用户线程,这是因为用户可能继续读写因为本次无需被清除而被复制到其它位置的对象,此时对该对象的修改可能体会不到新对象上。

Shenandoash使用Brooks Pointer技术解决这一问题(ZGC应该也是),核心思想即转发指针,即在对象前添加一个新的引用字段,正常情况下该引用字段指向对象自己,当产生复制,该指针可以指向复制后的对象,然后任何对旧对象的访问都可以直接转发到新对象上。

问题就是对象访问多了一层成本。

运作过程#

  1. 并发标记:从图中可以看到,并发标记前后也需要停止用户线程,和其它收集器一样,进行初始标记和最终标记过程。
  2. 并发预备重分配:因为没有类似记忆集的结构,所以ZGC扫描所有Region,决定清除哪些Region,这些被清除的Region会被放到重分配集中,被清除的Region中的部分无需清除的内容后面需要复制到其它位置
  3. 并发重分配:执行复制,为每个重分配集中的Region维护一个转发表,记录从旧对象到新对象的转换关系。当此时有用户线程访问了这些对象中的一个,ZGC会查询转发表,找到复制后的对象地址,并且在此次就把该对象的引用更新成新的,下次就不用再多一次的转发了。
  4. 并发重映射:将之前的旧引用更新成新引用。

对比#

posted @   yudoge  阅读(796)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
历史上的今天:
2020-03-14 四个基本空间
2020-03-14 基、维数
2020-03-14 向量空间、列空间、零空间、可解性
点击右上角即可分享
微信分享提示
主题色彩