今天简单给大家介绍下Java的垃圾收集器,如果不是做jvm开发,基本上了解其工作原理即可。不需要深入研究,一般面试也不会问的特别深。
不同的垃圾收集器有不同的垃圾回收算法,通过jps -v命令可以显示当前使用的垃圾回收器
当前比较多的JVM收集器有Serial/Serial Old,ParNew,Parallel Scavenge(ParallerGC)/Parallel Old,Concurrent Mark Sweep (CMS),G1垃圾回收器。
一、垃圾收集器的性能指标
垃圾收集器也分为单线程和多线程的,同时也分为并行和并发。垃圾收集器的主要性能指标就是垃圾回收的吞吐量和垃圾收集时间。
- 并行:垃圾收集的多线程的同时进行。
- 并发:垃圾收集的多线程和应用的多线程同时进行。
注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)
垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间
单线程收集过程
多线程收集过程
二、各个垃圾收集器对比
收集器 |
收集对象和算法 |
收集器类型 |
Serial |
新生代,复制算法 |
单线程 |
ParNew |
新生代,复制算法 |
并行的多线程收集器 |
Parallel Scavenge |
新生代,复制算法 |
并行的多线程收集器 |
Serial Old |
老年代,标记整理算法 |
单线程 |
Parallel Old |
老年代,标记整理算法 |
并行的多线程收集器 |
CMS |
老年代,标记清除算法 |
并行与并发收集器 |
G1 |
跨新生代和老年代;标记整理 + 化整为零 |
并行与并发收集器 |
三、垃圾收集器介绍
3.1 Serial/Serial Old
最古老的,单线程,独占式,成熟,适合单CPU 服务器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old
3.2 ParNew
和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
3.3 Parallel Scavenge(ParallerGC)/Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那有吞吐效率就是99%。
3.4 Concurrent Mark Sweep (CMS)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
整个过程分为4个步骤,包括:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。
- 并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除:不需要停顿。
优点:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点:
CPU资源敏感:因为并发阶段多线程占据CPU资源,如果CPU资源不足,效率会明显降低。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
在1.6的版本中老年代空间使用率阈值(92%)
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片
3.5 G1
G1垃圾回收器是主要针对多处理器以及大内存的机器,以极高的概率满足预测GC停顿时间要求的同时,还具备高吞吐量性能特征。是基于标记整理的垃圾回收器。
3.5.1 G1堆管理方式
堆内存被划分为多个大小相等的逻辑heap 区,其中一部分区域被当成老一代收集器相同的角色(eden, survivor, old), 但每个角色的区域个数都不是固定的。
每个堆Region 的大小在JVM启动时就确定了,JVM通常生成2000个Region,每个Region 大小在1M-32M之间。这些Region会被逻辑映射成Eden, Survivor, 和 old generation(老年代)空间。
3.5.2 G1新生代垃圾回收
新生代中存活的对象被转移到一个或者多个Survivor区,如果达到存活阀值则这部分对象就会被迁移到old 老年区。在回收过程中会有一次STW暂停,会计算Eden和Survivor大小给下一次GC使用。
新生代回收之前:
新生代回收结束后:新生代被转移到Survivor区或者old区,并行垃圾收集。
3.5.3 G1老年代垃圾回收
- 初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。 - 根区域扫描(root region scan)
- G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 并发标记(Concurrent Marking)
- G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
- 最终标记(Remark,STW)
- 该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 清除垃圾(Cleanup,STW)
- 在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
3.6 ZGC
Java11已经推出最新垃圾收集器,ZGC主要为了减少JVM停顿时间。
ZGC全称是Z Garbage Collector,是一款可伸缩(scalable)的低延迟(low latency garbage)、并发(concurrent)垃圾回收器,旨在实现以下几个目标:
- 停顿时间不超过10ms
- 停顿时间不随heap大小或存活对象大小增大而增大
- 可以处理从几百兆到几T的内存大小(最大4T)
3.6.1 主要实现技术
指针标记(Pointer tagging Or Colored Pointers )
ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0,以标记该指向内存的存储状态。相当于在对象的引用上标注了对象的信息(不是对象头)。在这个被指向的内存发生变化的时候(内存在Compact整理被移动时),颜色就会发生变化。
- Marked0/marked1: 判断对象是否已标记
- Remapped: 判断应用是否已指向新的地址
- Finalizable: 判断对象是否只能被Finalizer访问
这几个bits在不同的状态也就代表这个引用的不同颜色
为什么有2个mark标记?每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。
- GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。
- GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。
GC屏障 (GC Barriers)
由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障,读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。
与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。
3.6.2 ZGC原理
逻辑上一次ZGC分为Mark(标记)、Relocate(迁移)、Remap(重映射)三个阶段
- Mark: 所有活的对象都被记录在对应Page的Livemap(活对象表,bitmap实现)中,以及对象的Reference(引用)都改成已标记(Marked0或Marked1)状态
- Relocate: 根据页面中活对象占用的大小选出的一组Page,将其中的活对象都复制到新的Page,并在额外的forward table(转移表)中记录对象原地址和新地址对应关系
- Remap: 所有Relocated的活对象的引用都重新指向了新的正确的地址
实现上,由于想要将所有引用都修正过来需要跟Mark阶段一样遍历整个对象图,所以这次的Remap会与下一次的Remark阶段合并。所以在GC的实现上是2个阶段,即Mark&Remap阶段和Relocate阶段
标记
GC循环的第一部分是标记。标记包括查找和标记运行中的应用程序可以访问的所有堆对象,换句话说,查找不是垃圾的对象。
ZGC的标记分为三个阶段。
第一阶段是STW,其中GC roots被标记为活对象。 GC roots类似于局部变量,通过它可以访问堆上其他对象。 如果一个对象不能通过遍历从roots开始的对象图来访问,那么应用程序也就无法访问它,则该对象被认为是垃圾。从roots访问的对象集合称为Live集。GC roots标记步骤非常短,因为roots的总数通常比较小。
该阶段完成后,应用程序恢复执行,ZGC开始下一阶段,该阶段同时遍历对象图并标记所有可访问的对象。 在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。
在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。
重定位
GC循环的下一个主要部分是重定位。重定位涉及移动活动对象以释放部分堆内存。 为什么要移动对象而不是填补空隙? 有些GC实际是这样做的,但是它导致了一个不幸的后果,即分配内存变得更加昂贵,因为当需要分配内存时,内存分配器需要找到可以放置对象的空闲空间。 相比之下,如果可以释放大块内存,那么分配内存就很简单,只需要将指针递增新对象所需的内存大小即可。
ZGC将堆分成许多页面,在此阶段开始时,它同时选择一组需要重定位活动对象的页面。选择重定位集后,会出现一个Stop The World暂停,其中ZGC重定位该集合中root对象,并将他们的引用映射到新位置。与之前的Stop The World步骤一样,此处涉及的暂停时间仅取决于root的数量以及重定位集的大小与对象的总活动集的比率,这通常相当小。所以不像很多收集器那样,暂停时间随堆增加而增加。
移动root后,下一阶段是并发重定位。 在此阶段,GC线程遍历重定位集并重新定位其包含的页中所有对象。 如果应用程序线程试图在GC重新定位对象之前加载它们,那么应用程序线程也可以重定位该对象,这可以通过读屏障(在从堆加载引用时触发)实现,这可确保应用程序看到的所有引用都已更新,并且应用程序不可能同时对重定位的对象进行操作。
GC线程最终将对重定位集中的所有对象重定位,然而可能仍有引用指向这些对象的旧位置。 GC可以遍历对象图并重新映射这些引用到新位置,但是这一步代价很高昂。 因此这一步与下一个标记阶段合并在一起。在下一个GC周期的标记阶段遍历对象对象图的时候,如果发现未重映射的引用,则将其重新映射,然后标记为活动状态。
垃圾收集器暂时讲到这,后期给大家介绍下JVM优化实战