最近看了jvm的相关文章,为了避免后续忘记,使用博客记录下学习笔记。本文围绕HotSpot虚拟机讨论几种垃圾收集算法,内容参考周志明老师的《深入理解Java虚拟机》第三版,主要介绍第二版未详细介绍的G1收集器以及现在应用广泛,但可能会被官方淘汰的CMS收集器。下面先简单介绍一下基础概念:
分代垃圾收集集
我们将jvm收集集按收集目标分为:
- 新生代收集集 ,目标只是新生代的垃圾收集集;
- 老年代收集集 ,目标只是老年代的垃圾收集集;
- 混合收集集 ,目标包含新生代+老年代的垃圾收集集;
- 整堆收集集 ,收集整个java堆和方法区的垃圾收集集;
垃圾收集算法–理论
我们将垃圾收集算法,从原理上分为:
- 标记-清除算法 ,分为“标记”和“清除”两个阶段;
- 标记-复制算法 ,将可用内存按分为大小相等的两块,每次只用其中一块,当这块内存用完了,就将还存活的复制到另外一块去,然后将原来的内存清理掉;
- 标记-整理算法 ,将所有存活的对象都移向内存空间的另外一端,直接清理掉边界以外的内存;
经典垃圾收集器–实践
下面将介绍7款垃圾收集器,其中,重点讨论CMS以及G1收集器。7款收集器之间的关系如下图(图片来源–深入理解Java虚拟机第三版):
- serial收集器 ,新生代收集器,单线程收集器,当它在进行垃圾收集时,必须暂停其他所以工作线程,直到它收集结束。是客户端模式下的默认新生代收集器。优点:简单高效,内存消耗小,对于单核系统来说,由于没有线程交互的开销,可以获得最高的单线程收集效率;缺点:暂停期间导致的停顿可能用户无法忍受;
- ParNew收集器 ,新生代收集器,为serial收集器的多线程并行版本,默认开启的收集线程数与处理核心数相同;优点:并行,在多核环境下表现比serial好,对于系统资源的高效利用还是很有好处的。缺点:在单核环境下不会比serial效果更好。
- Parallel Scavenge收集器 ,新生代收集器,基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,目标是达到一个可控的吞吐量。吞吐量=处理器用于运行用户代码的时间/处理器总消耗时间。优点:侧重于关注吞吐量,可动态调整参数以提供最合适的停顿时间或最大的吞吐量(自适应调节)。
- serial Old收集器 ,老年代收集器,顾名思义,是serial老年代版本,也是一个单线程收集器,使用标记-整理算法。供客户端模式下的虚拟机使用。服务端模式有两种应用:1、与Parallel Scavenge搭配使用 2、作为CMS发生失败时的备选预案;
- parallel Old收集器 ,老年代收集器,是Parallel Scavenge的老年代版本,基于标记-整理算法实现。
- CMS收集器 ,(Clean Mark Sweep)老年代收集器,是以获取最短回收停顿时间为目标的收集器。适合对相应速度要求高的应用上面。CMS整个过程分为四部分:
1) 初始标记(Stop The World):标记一下GC Roots能直接关联到的对象。
2) 并发标记:从GC Roots的直接关联对象开始遍历整个对象图。
3) 重新标记(Stop The World):修正并发标记期间,有变动的那一部分对象的标记记录
4) 并发清除:清理删除掉标记阶段的已经死亡的对象。
以打扫教室为例来理解CMS运行过程:首先,全班同学出教室,打扫卫生的两个同学进教室,明确各自分工(第一步),全班同学进教室,打扫卫生的同学开始标记哪个地方有垃圾(第二步),由于不停的还有新的垃圾扔地上,无法完全清理,因此全部同学需再出去一次,打扫卫生的同学将这期间(短时间内新增的垃圾较少)产生的垃圾重新标记出来(第三步),全班同学再进来,这时候将所有标记的垃圾清扫出去。
由于1、3步时间很短,虽然造成短暂的停顿,但是从整体看来,CMS与用户线程是并发执行的。
优点:并发收集、低停顿。
缺点:1、在并发阶段,虽然不会导致用户线程停顿,但是因为占用一部分线程,导致用户应用程序变慢,吞吐量降低。另外,CMS默认启动的回收线程数是(处理器核心数量+3)/4,如果处理核心数在四个或以上,并发回收时的垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着核心数量的下降而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就会很大。
2、由于CMS收集器无法处理“浮动垃圾”,可能导致full GC
3、由于在收集垃圾时用户线程还在并发执行,因此还需要预留足够的内存空间给用户线程使用,因此CMS触发并不是由于老年代快被填满时进行,而是有一个设置的阈值。
4、标记-清除算法,会产生大量空间碎片,由于空间碎片过多导致无法有新空间分配,将引起full GC - G1收集器 ,(Garbage First),跳出分代思想,基于Region的堆内存布局。官方推荐使用,想淘汰掉CMS收集器。
面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收效益最大。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每个区域根据需要,扮演Eden空间,Survivor空间或者老年代空间。收集器能对扮演不同角色的Region采用不同的策略去处理。Region中还有一类特殊的区域,用来存储大对象,G1认为只要大小超过一个Region一半大小的对象即可判定为大对象。
虽然G1还有新生、老年代的概念,但是已经不是固定区域,而是一系列区域的动态集合。
G1收集器的运作过程大致可划分为以下四步骤:
1) 初始标记:仅针对GC Root能直接关联到的对象,需停顿线程,但耗时短,借用Minor GC的时候同步完成,所以G1在这个阶段并没有额外的停顿。
2) 并发标记:可达性分析,找出要回收的对象,与用户线程同步进行。
3) 最终标记:对用户线程停顿,用于处理并发阶段结束后仍遗留下来的少量记录
4) 筛选回收: 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
优点:1、从整体上来看,是标记-整理g算法,从局部上来看,两个Region之间是标记-复制算法,意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
缺点:1、产生的内存占用以及程序运行时的额外负载都要比CMS高。
2、每个Region对应的一份卡表,意味着卡表占用内存空间较高。
CMS与G1选型
根据周志明老师的实践经验,目前在小内存应用上CMS的表现大概率优于G1,而在大内存应用上G1大多能发挥其优势。这个优劣势的java堆容量平衡点在6GB到8GB之间。