前段时间由于工作原因一直很忙,上周项目验收后时间终于空闲下来,博客也有好几个月没有更新了,趁着还有几天放假,借这个机会写点东西;网上也有很多人写过Java垃圾收集器,特别现在主流比较火的CMS和G1算法,但是我发现很多的博客作者自己都没搞懂,理解的内容都是错误的,反倒误解了很多读者,所以我整理了下网上资料加上自己理解,来写一写CMS、G1算法,并做下简单总结。
从方法论上讲,程序语言的回收算法主要分为
一、引用计数算法(Reference Counting):给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。
二、根搜索算法(GC Root Tracing):通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。
Java采用了根搜索算法,基本原理根据上面解释应该都能理解,基于根搜索方法,又有具体实现算法
一、标记-清除算法(Mark-Sweep)
最基础的垃圾收集器算法,分为“标记”和“清除”两个阶段,先标记处所需要回收的对象,标记完成后,统一回收掉所有被标记的对象。
缺点:1)效率问题,标记和清除的效率不高。
2)清除后会产生大量的不连续的内存碎片,可能会导致在程序需要为较大对象分配内存时无法找到足够连续的内存,不得不提前触发垃圾收集动作。
二、复制算法(Copying)
将内存容量分成大小相等的两块,每次只使用其中一块,当一块用完时,将还存活的对象复制到另一块去,然后把之前使用满的那块空间一次性清理掉,如此反复。
优点:内存分配的时候不用考虑内存碎片问题,只移动堆顶指针,按顺序分配即可,简单高效。
缺点:内存空间浪费大,每次只能使用当前 能够使用 内存空间的一半;当对象存活率较高时,需要有大量的复制操作,效率低。
三、标记-整理算法(Mark-Compact)
标记整理是在标记-清算上改进得来,前面说到标记-清算内存碎片的问题,在标记-整理中有解决。同样有标记阶段,标记出所有需要回收的对象,但是不会直接清理,而是将存活的对象向一端移动,在移动过程中清理掉可回收对象。
优点:解决了之前内存碎片的问题,特别是在存活率高的时候,效率远高于复制算法。
四、分代收集算法(Generational Collection)
根据内存对象的存活周期不同,将内存划分成几块,java虚拟机中一般将内存划分成新生代和老年代,当新建对象时一般在新生代中分配内存,在新生代垃圾收集器回收几次后仍然存活的对象,将被移动到老年代,或者当大的对象在新生代中无法分配到足够连续的内存空间时也会直接分配到老年代。
上面四种算法JVM在回收内存时都有采用,大多都是复合运用多种算法一起实现垃圾回收,具体细节每个算法都可以写很多内容,为了不偏题,我们这里只写CMS、G1,其他的有兴趣可以自己查询资料。CMS算法主要是应用在分代收集算法的老年代里,是从JDK8开始采用, 当然默认没有启用,如果在开发或生产环境想采用CMS,可以修改JVM配置-XX:+UseConcMarkSweepGC : 手动指定老年代使用CMS收集器。
下面进入正题
CMS定义:英文全称Concurrent Mark Sweep,可以翻译为 并发标记清除,是一种以获取最短回收停顿时间为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以并发
执行,以此来达到降低收集停顿时间的目的。
CMS收集器仅作用于老年代的收集,是基于标记-清除算法
的,它的运作过程分为4个步骤:
其中,初始标记
、重新标记
这两个步骤仍然需要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。
CMS以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需STW才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。
CMS收集器优点:并发收集、低停顿。
CMS收集器缺点:
什么情况下CMS比较适合:
(1)响应时间优先,能接受牺牲一定的吞吐量,如果需要高响应时间和高吞吐量,推荐使用G1。后续文章再继续介绍G1。G1也是JDK9默认的垃圾收集器;
(2)硬件配置较高,即CPU和内存资源充足。CMS由于需要与用户线程并发执行,所以可能会竞争CPU资源。同时CMS并发标记阶段,用户线程同时执行时会新建对象,故内存占用会比较高;
(3)堆大小在3G到8G之间,同时存活时间较长的对象比较多
下面再简单讲讲G1算法,
G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。
G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region。其实这个数字既可以手动调整,G1也会根据堆大小自动进行调整。
G1收集的运作过程大致如下:
停顿线程
,但耗时很短。停顿线程
,但是可并行执行。随着JVM的发展,ORACLE官方推出的JDK11又有了新算法ZGC,它对内存碎片的整理更加优化,回收暂停时间也更加缩短,具体细节本人还没有深入研究,后面有机会可以写文章专门介绍它。
最后,我把目前主流JDK使用到的JVM垃圾收集器采用的算法做下简单总结,方便大家对比参考,
2. 老年代垃圾收集器
原文:https://www.cnblogs.com/kakatadage/p/12213233.html