垃圾回收器
本文转载自Java虚拟机(二)-垃圾收集器 by 你好生活
并行&并发
我们先理解并行和并发
并行是和串行是相对应的,指多条垃圾收集线程并行工作,此时用户线程处于等待状态。
并发指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
Serial 和 Serial Old 收集器
Serial收集器这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。用于新生代,采用复制算法
Serial Old收集器是它的老年代版本,使用标记-整理算法
ParNew 收集器
ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但是很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器。
Parallel Scavenge 收集器 和 Parallel Old 收集器
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。 同样Parallel Scavenge 使用复制算法,Parallel Old使用标记-整理算法
注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Scavenge 加Parallel Old 收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,系统系统停顿时间最短,以给用户带来较好的体验。CMS收集器就是非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的
运作步骤
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC RootsTracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
cms的优点
并发收集,低停顿
cms的缺点
-
回收时间长,吞吐量不如Parallel
由于我们在执行CMS垃圾回收器的过程中有一部分资源让给了用户线程,那就会导致回收时间长,内存空间没有被及时释放掉也就会导致吞吐量不如Paralle -
无法处理浮动垃圾
由于CMS并发清理阶段用户线程还在运行着,自然会有新的垃圾产生,这部分垃圾只能留待下一次GC时再清理掉。这一部分垃圾成为“浮动垃圾” -
会产生大量的空间碎片
为什么这么说呢?因为我们的CMS垃圾回收器的算法采用的是标记清除算法,这个算法的弊端就是会产生空间随便,造成空间不连续。但是没关系CMS提供了两个参数,解决了这个问题-XX:+UseCMSCompactAtFullCollection
这个参数可以开启我们的内存空间整理,使我们的空间整齐划一。通常情况下我们会配合另一个参数来使用
-XX:CMSFullGCsBeforeCompaction
这个参数的意思是,在CMS执行多少次full gc之后进行空间整理,这个参数不填也没关系,他默认的是0次,也就是说,在每一次垃圾回收之后,都会整理一次内存空间。
-
并发模式失败:Concurrent model failure
什么意思呢?大家思考一下,当我们的老年代内存空间满了之后,虽然这时候在做FULL GC,但是在FULL GC的过程中有一个新的对象进来了怎么办?
此时会进入STW状态,并且CMS会自动切换到用Serial old垃圾收集器来回收。Serial我们都知道,它是一个单线程的垃圾回收器。那这种情况出现是不是会严重降低我们的执行效率?
那么我们为了解决这个问题,可以通过调整老年代空间被占满了多少之后触发FULL GC
-XX:CMSInitiatingOccupancyFraction
参数如上,通过这个参数可以调整触发full gc的百分比,java6之后默认是92%。
G1收集器
G1堆内存分配
为了实现STW的时间可预测,G1将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region可能是Eden,也有可能是Survivor,也有可能是Old,另外Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的进行回收大多数情况下都把Humongous Region作为老年代的一部分来进行看待。
G1回收算法
G1追求低停顿,采用标记-整理+复制算法
G1运行过程
G1的运行过程与CMS大体一致,分为以下四个步骤:
初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记( Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫 描完成以后,并发时有引用变动的对象会产生漏标问题,G1中会使用SATB(snapshot-at-the-beginning)算法来解决,后面会详细介绍。
最终标记(Final Marking):对用户线程做一个短暂的暂停,用于处理并发标记阶段仍遗留下来的最后那少量的SATB记录(漏标对象)。
筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。
G1 优点
停顿时间短;
用户可以指定最大停顿时间;
不会产生内存碎片:G1 的内存布局并不是固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域 (Region),G1 从整体来看是基于“标记-整理”算法实现的收集器,但从局部 (两个Region 之间)上看又是基于“标记-复制”算法实现,不会像 CMS (“标记-清除”算法) 那样产生内存碎片。
G1 缺点
G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
ZGC收集器
在JDK11中,加入了实验性质的ZGC。是一款低停顿高并发的收集器。它增加了染色指针(Colored Pointer)、读屏障(Load Barrier)等多种技术来降低回收时长。 主要为了满足如下目标进行设计:
- 停顿时间不会超过10ms
- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下)
- 可支持几百M,甚至几T的堆大小(最大支持4T)
ZGC内存布局
和G1一样,ZGC也采取基于Region的堆内存布局,但与他们不同的是,ZGC的Region具有动态性(动态的创建和销毁,以及动态的区域容量大小)。
ZGC的Region可以分为三类:
- 小型Region:容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region:容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region:容量不固定,可以动态变化,但必须为2MB的整数倍,用于存放4MB或以上的大对象。并且每个大型Region只会存放一个对象。
ZGC的运作过程
ZGC的运作过程大致可划分为以下四个大的阶段。四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段。
运作过程如下:
- 并发标记(Concurrent Mark): 与G1一样,并发标记是遍历对象图做可达性分析的阶段,前后也要经过类似于G1的初始标记、最终标记的短暂停顿,而且这些停顿阶段所做的事情在目标上也是相类似的。
- 并发预备重分配( Concurrent Prepare for Relocate): 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。
- 并发重分配(Concurrent Relocate): 重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。
- 并发重映射(Concurrent Remap): 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,ZGC的并发映射并不是以一个必须要“迫切”去完成的任务。ZGC很巧妙地把并发重映射阶段要做的工作,合并到下一次垃圾收集循环中的并发标记阶段里去完成,反正他们都是要遍历所有对象的,这样合并节省了一次遍历的开销。
优点
高吞吐量、低延迟
ZGC是支持“NUMA-Aware”的内存分配。MUMA(Non-Uniform Memory Access,非统一内存访问架构)是一种多处理器或多核处理器计算机所设计的内存架构。
现在多CPU插槽的服务器都是Numa架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
缺点
ZGC最大的问题是浮动垃圾
ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。
ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。
解决方案
目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

浙公网安备 33010602011771号