目录
1、垃圾收集相关基本概念
1.1 根可达算法
- 可达性分析是Java垃圾回收机制中的一个重要概念,用于确定哪些对象可以被回收,哪些对象应该保留在内存中。
- 根可达算法的基本思想是从一组称为“GCRoot”的对象开始,查找从这些根对象开始可以到达的所有对象。这些可以到达的对象被称为“存活对象”,而不能到达的对象被称为“垃圾对象”。
- 作为GC Roots的对象主要包括下面4种:
- 1.虚拟机栈(栈中的本地变量表): 各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 2.方法区中类静态变量: java类的引用类型静态变量
- 3.方法区中常量:比如:字符串常量池里的引用。
- 4.本地方法栈中JNI指针: (即一般说的Native方法)
1.2 常见垃圾回收算法
1.2.1 标记-复制
- 核心思想是将内存分为两个区域,分别为活动区和空闲区
- 优点是可以快速回收内存中的垃圾对象
- 缺点是需要额外的内存空间和复制操作
1.2.2 标记-清除
- 只需要遍历一遍堆,不需要进行额外的内存拷贝操作,因此不会产生复制算法的内存开销。
- 缺点:清除操作可能会在堆中留下大量的不连续的空间,导致内存碎片化问题;
1.2.3 标记-整理
- 核心思想是先标记出所有活动对象,然后将所有活动对象压缩到一端
- 算法会将所有标记为“活动对象”的对象移动到堆的一端,因此可以解决内存碎片化问题,并且不需要额外的内存空间。
- 缺点:标记-整理算法需要在移动对象时修改指针,可能会对性能产生一定的影响。
2、ZGC介绍
2.1 ZGC的概述
ZGC是一种可伸缩、低延迟、高吞吐量的垃圾回收器,是JDK 11版本中引入的新特性。它的设计目标是为了能够在非常大的Java堆上实现高效的并发垃圾回收,支持4TB级别的堆(JAVA13已支持到了16TB),同时最小化应用程序的暂停时间。
2.1 ZGC特点
2.2.1 分区模型
与G1类似,ZGC同样采取了分区模型:
与G1不同的是,ZGC的分区大小并不相同,ZGC是由三种页面(多个) 组成一个堆空间:
- 小页面: 2 M
- 中页面: 32 M
- 大页面: 2n M(>32M)
新建对象时:
- 当对象大小小于等于256KB时,对象分配在小页面
- 当对象大小在256KB和4M之间,对象分配在中页面
- 当对象大于4M,对象分配在大页面
- ZGC对于不同页面回收的策略也不同。简单地说,小页面优先回收,中页面和大页面则尽量不回收。
2.2.2 低停顿时间
- ZGC可以将停顿时间降至10毫秒以内,并且随着Java堆大小的增加,垃圾回收的暂停时间不会显著增加。这使得应用程序不会因为长时间的垃圾回收而导致用户等待。
- ZGC使用了各种技术来实现低延迟,例如并发标记、并发回收等
- 总之,ZGC是一种非常强大、灵活和高效的垃圾回收器,可以满足各种Java应用程序的需求,特别是那些需要高吞吐量和低延迟的应用程序。
3. ZGC的工作原理
3.1 染色指针
传统垃圾回收器在回收过程中如何标记对象状态?
传统的垃圾回收器在回收过程中通常使用对象头标记来标记对象状态。
在传统垃圾回收算法中,垃圾回收器通过扫描所有对象的对象头,判断对象的存活状态,并将存活对象从垃圾对象中区分出来,这种标记方法需要扫描所有对象的对象头,耗费时间和资源。
ZGC使用染色指针进行对象状态标记:
不改变对象体,只改变指针
染色指针标记原理:
- ZGC只支持64位系统(使用64位指针)
- ZGC中低42位表示使用中的堆空间(2^42 = 4T)
- ZGC借几位高位来做GC相关的事情(快速实现垃圾回收中的并发标记、转移和重定位等)
- 42位:绿色标识,用来辅助GC
- 43位:红色标识,用来辅助GC
- 44位:蓝色标识,初始化状态
-
为什么有2个mark标记?
每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
GC周期1:使用mark0,则周期结束所有引用mark标记都会成为01。
GC周期2:使用mark1,则期待的mark标记10,所有引用都能被重新标记。
3.2 ZGC工作流程
ZGC采取了基础的标记-复制算法。
3.2.1 初始状态
- 在ZGC初始化之后,此时所有对象的指针为Remapped(蓝色),程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动。
3.2.2 初始标记
- 这个阶段需要暂停(STW),初始标记只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,停顿时间不会随着堆的大小或者活跃对象的大小而增加。
- 初始标记只需要扫描所有GC Roots将其标为绿色
3.2.3 并发标记
- 这个阶段不需要暂停(没有STW),扫描剩余的所有对象,这个处理时间比较长,所以走并发,业务线程和GC线程同时运行
- 并发标记将与GC Root连接的所有对象的指针染成绿色
3.2.3 再标记
三色标记法:
- 白色(未被扫描)
- 灰色(本对象被扫描,但是还没扫描完该对象的子对象)
- 黑色(根对象,或者该对象与它的子对象都被扫描过)
三色标记的漏标问题:
- (GC)线程1完成所有的标记
- (GC)线程2还处于半完成状态
- 引用发生以下改变(业务线程):
- A.c=C(读引用)
- B.c=null
- 线程1、2完成所有的标记C对象是白色,被漏标,被错误的回收
解决方案:读屏障
与内存的读写屏障不同,这里的读屏障可以理解为一个AOP
遇到读引用的代码,插入一个aop before 的操作,把C的引用关系存下来。
比如:
出现代码:A.c=C时,先把C记录下来
以上其实是出现在并发标记阶段的操作,
在再标记阶段,主要做的就是处理读屏障记录下来的漏标对象,这个阶段需要暂停(STW)
3.2.4 并发转移准备
这个阶段统计计算哪些Region需要回收,哪些不需要回收,以及计算出可以优先回收的区域。
3.2.5 初始转移
转移初始标记的存活对象同时做对象重定位(此过程有STW)
3.2.6 并发转移
- 并发转移是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forwerd Table),记录从旧对象到新对象的转向关系。
- ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的读屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,GC将这种行为称为指针的“自愈”(Self-Healing)能力。
- 转发表类似于hashmap ,在转移对象时同时插入转发表数据,该操作为原子操作
转移对象并且插入转发表数据:
至此,GC的一个周期就结束了,但是目前还有点问题,并发转移过程中出现的野指针还没处理(指针自愈只能解决一小部分),染色指针的红色指针也还没出现呢。
由于指针自愈的机制,ZGC对野指针的处理并不需要太紧迫,所以ZGC将这一操作放在了下一个周期的并发标记阶段。
3.2.6 下一次GC
初始标记:与上文中的初始标记一致,只不过时绿色指针换为了红色指针
并发标记:修正指针地址并且删除转发表修改染色指针为红色
其他阶段除了颜色,基本一致
4. ZGC的使用
- JDK要求11及以上
- 添加以下JVM参数启用ZGC:
-
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
-
- 特有参数:
- -XX:ZAllocationSpikeTolerance 修正系数,数值越大,越早触发GC (default = 2.000000)
- -XX:ZCollectionInterval ZGC发生的最小时间间隔 ,秒 (default = 0.000000)
- -XX:ZFragmentationLimit relocation时,当前region碎片化大于此值,则回收region (default = 25.000000)
- -XX:ZMarkStackSpaceLimit 指定为标记堆栈分配的最大字节数 (default = 8589934592 = 8096M)
- -XX:ZProactive 是否启用主动回收 (default true)
- -XX:ZUncommit 是否归还不使用的内存给OS(default true)
- -XX:ZUncommitDelay 不再使用的内存最多延迟多久会归还给OS (default = 300 s)
4. 总结
相比于传统的垃圾回收算法,ZGC的主要特点是低延迟、大内存,做到了最小化垃圾收集对应用程序的影响,可以在数毫秒甚至更短的时间内完成垃圾回收,此外,ZGC能够处理非常大的堆内存,多达4TB。
具备这些优点的同时,ZGC也有一定的局限性:
- JDK版本限制:目前,ZGC只适用于64位Java虚拟机,并且需要JDK 11或更高版本才能使用。
- 初始内存消耗:与其他GC算法相比,ZGC在启动时需要更多的内存来初始化,这可能会导致启动时间更长或需要更多的内存
如何选择合适的垃圾收集器需要从具体情况考虑,需要考虑多个因素,如响应时间要求、吞吐量以及内存大小等