《深入理解Java虚拟机》阅读——垃圾回收机制

深渊向深渊呼唤

《深入理解Java虚拟机》阅读——垃圾回收机制

前言 why——为什么需要垃圾回收 what——垃圾回收做些什么 where——去哪里回收垃圾 how——垃圾回收是怎么做的 垃圾是否要回收 引用计数法 可达性分析算法 方法区判断是否可回收 垃圾回收的方式 方法论 标记-清除算法 复制算法 标记-整理算法 分代收集算法 实现(HotSpot) Serial收集器 ParNew收集器 Parallel Scavenge收集器 Serial Old收集器 Parallel Old收集器 CMS收集器 G1收集器

前言

从小老师就告诉我们学习有3个w和1个h,分别是what做什么、where在哪里、why为什么和how怎么做,于是我们今天也从这四个角度出发,跟着《深入理解Java虚拟机》学习一下jvm的垃圾回收机制。

why——为什么需要垃圾回收

对于我们Java程序员来说,一开始对于内存其实是很不敏感的(至少我是这样的,因为所有的工作都交给jvm来完成了,我们可以通过一个变量来创建数组,不需要通过malloc函数来动态分配内存;在我们使用完这个数组之后,不需要通过free函数来手动释放内存。所以为了让我们变得对计算机不了解一些(不是 所以其实是为了让我们编程更方便一点,jvm帮我们完成了动态分配内存和对内存进行垃圾回收。
那么出现了另一个why,为什么我们还要学习内存动态分配和垃圾回收呢?

答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节 ——《深入理解Java虚拟机》

在上一篇关于Java运行时内存的博客中,我们在说到栈和堆时,发现栈和堆在申请不到足够的所需的内存时就会抛出OutOfMemoryError的异常,这也说明jvm并不是万能的,也会出错,因为内存是物理设备的,不够就是不够,就像裤兜里的钱一样,不够就是不够!虽然我们钱不够的话我们可以走出店门不买了,但是内存不够程序可就崩溃了,可见我们必须要了解底层的内存分配和垃圾回收(这里就先说说垃圾回收。

what——垃圾回收做些什么

有这样三件事情需要我们思考:

哪些内存需要回收? 什么时候回收? 如何回收?

我们思考完之后就是要让垃圾回收要做的事情了:判断哪些内存区域需要回收、判断什么时候进行回收、用什么样的方式进行回收。
判断哪些内存需要回收就是where了,判断什么时候进行回收和用什么样的方式回收就是how了。

where——去哪里回收垃圾

首先我们明确垃圾是什么,这里所说的垃圾可不是我们使用电脑时将一个文件拖到回收站那个垃圾桶里的垃圾,这里的垃圾是指我们在运行Java程序时,new出了一大堆对象,而这堆对象在我们不需要使用的时候它就成了垃圾,因为它“占着茅坑不拉屎”,你都没有用了还占着内存干啥对吧,所以我们就需要去回收它们来释放掉这些内存。
那我们现在要追究去哪里回收内存,我们肯定要知道jvm占了哪些内存。在上篇博客中我们知道了jvm运行时有堆、虚拟机栈、本地方法栈、方法区、程序计数器这五个区域。虚拟机栈和本地方法栈以及程序计数器这三个部分都是在编译期间可以知道,等于说这三个区域的分配和回收是可以确定的,当方法或者线程结束时,内存也就跟着回收了,所以我们不用管这三个区域。而堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,所以我们只有在程序处于运行期间才能知道会创建出哪些对象,于是垃圾回收重点关注堆和方法区这两个区域。

how——垃圾回收是怎么做的

这里的怎么做其实有两层含义,一方面是人如何实现垃圾回收的,另一方面是垃圾回收是如何回收垃圾的。其实也就是前面说的判断什么时候进行回收和用什么样的方式进行回收了。

垃圾是否要回收

进行垃圾回收的第一步就是我们要知晓某对象是否还存活着,也就是它是否还有用。这里我们会学习到两种方法进行对象死活的判断,分别是引用计数法和可达性分析算法。

引用计数法

引用计数法的概念是这样的:给对象中添加一个引用计数器,每当有一个地方引用这个对象,计数器就加1,当引用失效时就给计数器减1,如果一个对象的计数器的值为0就代表这个对象没有用了需要回收。
是不是很简单,事实上这个算法不仅实现起来简单,效率还高,但是几乎当前主流的Java虚拟机都没用使用这个算法来进行垃圾的判断,这是为什么呢?我们思考到如果两个对象互相引用,那岂不是哪怕没有其他对象对它们进行引用,它们的计数器的值永远都是1,永远不会被回收,这样就会产生内存泄漏了。
于是我们再瞅瞅另一个算法是怎样的。

可达性分析算法

这个算法的思路是这样的:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是没用了的。
在这里插入图片描述
看上图,根据可达性分析算法可知,以GC Roots为起始点向下搜索,搜索到的Object 1、Object 2、Object 3、Object 4这四个对象都是仍然存活着的,而Object 5、Object 6、Object 7这三个对象没有被搜索到,所以它们是被判定可以被回收的。
那么什么对象可以被当作GC Roots呢?
在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。 方法区中类静态属性引用的对象。 方法区中常量引用的对象。 本地方法栈中JNI(即一般说的Native方法)引用的对象。

被上述GC Roots对象直接引用间接引用的对象就被认为是可达的,而不可达的对象就被认为是可以被回收的对象。


所以你们注意到没有,无论是引用计数法还是可达性分析算法,都与对象之间的引用有关,可见引用是个重点来的,具体内容请看之后的博客,这里只引用一段书中的内容。

在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。
在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。 ——《深入理解Java虚拟机》

另外,其实即使在可达性分析种被判定为可回收的对象也不是一定会被回收的。要真正宣布一个对象死亡,至少要经历两次标记过程:在对象经过了可达性分析后发现没有与GC Roots相连接的引用链,那么这个对象会被第一次标记并且进行一次筛选,筛选是看这个对象有没有必要进行finalize()方法,如果该对象没有重写finalize()方法或者finalize()方法已经被调用过一次了,就说明该对象没有必要进行finalize()方法了,也就是说它死亡了。
而如果该对象被认为有必要进行finalize()方法,也就是说这个对象重写了finalize()方法且还没有调用过,那么如果在重写的finalize()方法里进行了“自救”也就是重新被GC Roots直接或间接引用上,那么在第二次进行标记时这个对象就不会被认为要被回收了。具体过程引用书上的内容:

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。 ——《深入理解Java虚拟机》

注意:通过在finalize()方法里进行自救或者做些什么善后工作的方式并不推荐,可以使用try-finally的方式或者其他的方式来做会更好。《深入理解Java虚拟机》的作者建议大家忘记有finalize()这个方法。

方法区判断是否可回收

引用计数法和可达性分析算法是用来判断new出来的对象是否可以回收的,对象都是在堆里的,前面也说了垃圾回收关注堆和方法区这两块区域,那么接下来我们说说方法区里是如何判断是否有垃圾要回收的。
方法区在各个Java虚拟机中的实现不尽相同,但是主要回收的还是废弃常量和无用的类。判断废弃常量跟判断废弃对象很像,如果没有引用指向该常量,那么就说明该常量是废弃常量。而判断一个类是否是无用的类的条件就会严苛许多,类需要同时满足下面三个条件才能够算是一个无用的类:

该类所有的实例都已经被回收。也就是说堆里没有任何该类的实例对象。 加载该类的ClassLoader已经被回收。 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

即使一个类满足了上述三个条件,也不一定会被回收,虚拟机提供了一些参数对其进行控制,例如-verbose:class、-XX:+TraceClassLoading等等。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


到这里说完了如何判断是否可回收,接下来就是说说回收的方式了。

垃圾回收的方式

方法论

标记-清除算法

标记-清除算法是最简单基础的算法,后面的算法都是基于它的。它的主要思路是先标记出所有需要回收的对象,在标记完候对所有被标记的对象进行统一回收。如下图:
标记-清除算法
该算法有两个不足,分别是效率问题和会产生大量内存碎片。
标记和清除的效率都不高,内存碎片太多会导致以后在分配较大对象时因为找不到足够的连续内存而不得不提前触发另一次垃圾回收。

复制算法

复制算法的基本思路是:划分出两个相同的区域,每次只在其中一个区域里存放对象,在进行一次gc后将依然存活的对象复制到另一个区域,然后将要回收的对象一次性回收掉。如下图:
复制算法
很容易发现这种算法的优缺点。这种算法虽然避免了出现大量内存碎片而且只要移动堆顶指针来分配内存就可以,但是将原本的内存空间缩小了一半属实有点亏。那既然缩小一半有点太亏了,那我们可以少缩小一些。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 ——《深入理解Java虚拟机》

所以实际上,我们在使用到复制算法的时候,是在Eden区和一块Survivor区中分配内存,然后在经过一次gc后将幸存对象复制到另一块Survivor区中,再一次性回收掉要回收的无用对象。HotSpot虚拟机默认Eden区和Survivor区的大小比例为8:1,也就是说如果是100MB的内存,Eden区占80MB,两块Survivor分别占10MB,每次能使用的内存空间是90%,只缩小了10%的内存空间,这就比之前损失一半的内存空间划算多了。
但是虽然研究表明每次可能会回收掉98%的对象,但是也可能会出现存活对象大于10%的情况对吧,所以在一块Survivor区放不下存活对象的时候,就需要放在其他内存(老年代)中进行分配担保。

标记-整理算法

标记-整理算法其实是针对老年代对象存活率高的特点对复制算法进行的改造,它是这样的:将依然存活的对象向一侧移动,然后回收掉端边界以外的无用对象。如下图:
标记-整理算法

分代收集算法

这个算法没有什么好说的,就是将前面三种算法进行一个结合。直接看书中的内容。

当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一半是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。 ——《深入理解Java虚拟机》

实现(HotSpot)

注:这里有一部分是如何进入到垃圾回收的实现,因为我暂时还没有搞懂,就先不写了,等以后搞懂了再来补充。

首先我们要明确一个概念:垃圾回收器通常是组合起来使用的。也就是说有些垃圾回收器负责回收新生代,有些垃圾回收器负责回收老年代,也有的垃圾回收器都可以回收。我们先来看看JDK1.7的HotSpot虚拟机有哪些垃圾回收器(JDK1.8跟1.7是一样的)。
HotSpot虚拟机的垃圾回收器
两个垃圾回收器之间存在连线的表示它们可以搭配使用。在上面的区域的垃圾回收器表示它们负责新生代的垃圾回收,而在下面的区域的垃圾回收器表示它们负责老年代的垃圾回收,而在中间的表示既可以进行新生代的垃圾回收也可以进行老年代的垃圾回收。
接下来我们就分别学习一下这几个垃圾回收器的特性、基本原理和使用场景。

虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,所以我们选择的只是对具体应用最合适的收集器。 ——《深入理解Java虚拟机》

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器。 ——《深入理解Java虚拟机》

在jdk1.3之前,Serial收集器是新生代垃圾回收的唯一选择,由此可见它的历史真的十分悠久了,看它的名字serial串行就可以见名知义了,它在执行垃圾回收的时候是单线程执行的嘛,但是这里的单线程不止是说它使用一个CPU或一条收集线程去进行垃圾回收,这里要重点强调它在执行的时候只有它能执行!也就是会产生“Stop The World”,别的工作线程都必须停下来等Serial收集器工作完了才能继续工作。Serial收集器和Serial Old收集器合作运行的运行过程如下图:
Serial/Serial Old
从图中可以看出,Serial负责收集新生代的垃圾使用的是复制算法,Serial Old负责收集老年代的垃圾使用的是标记-整理算法。这两个垃圾收集器在运行的时候都需要将用户正在工作的进程暂停,所以这就是它们串行运行的缺点,因为如果用户在使用计算机的时候,突然后台就来了一下垃圾回收,就啥都暂停了,多难受是不,所以后面就出现了很多并行和并发的垃圾回收器。但是它也有它的优点:简单且高效。

在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接收的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。 ——《深入理解Java虚拟机》

注:上面提到了并行和并发,,在垃圾回收器的上下文语境中这两个词语与我们平时理解的会有些许的不同,这里解释一下。并行(Parallel)是指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。并发(Concurrent)是指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。


ParNew收集器

ParNew其实是Serial收集器的多线程版本,也是上面说的并行垃圾收集器,它与Serial的差别只是Serial是单线程而ParNew是多线程,其余的比如jvm控制参数(如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold)、收集算法(复制)、会发生STW、对象分配规则、回收策略等等都是一样的。所以它和Serial Old垃圾回收器配合使用的运行过程是这样的:
ParNew/Serial Old
由此图可看出ParNew垃圾收集器相对Serial收集器并没有做出多大的创新,而且ParNew也不见得就一定比Serial的效率要高,在单CPU的场景下,Serial的效率肯定是要高于ParNew的,因为ParNew还有线程切换带来的性能消耗,但是如果在多物理CPU或者多逻辑CPU(超线程)的情况下ParNew的优势就体现出来了。ParNew默认开启的收集线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads参数来进行控制。

ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前jdk7刚出来的时候 只有它能与CMS收集器配合工作。 ——《深入理解Java虚拟机》

CMS收集器是工作在老年代的垃圾回收器,是JDK1.5的时候推出的,它是第一款真正意义上的并发收集器,CMS之后再学习。另外新生代还有一个垃圾收集器也就是Parallel Scavenge收集器,它是JDK1.4推出的,因为它没用使用传统的GC收集器代码框架,是另外独立实现的,所以在老年代使用CMS收集器的时候,新生代只能选择ParNew或者Serial收集器,ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。


Parallel Scavenge收集器

Parallel Scavenge收集器和ParNew收集器一样是运行在新生代的、并行的、使用复制算法的,但是它们之间有一个很大的不同,就是它们的目的或者说关注点不一样,像ParNew或者CMS这样的收集器主要关注的是垃圾回收导致用户线程停顿的时间长短,而Parallel Scavenge收集器关注的是吞吐量,它的目标是达到一个可控制的吞吐量(Throughput)。什么是吞吐量呢?直白一点,吞吐量 = 用户线程运行时间 / (用户线程运行时间 + 垃圾回收花费的时间),也就是说如果jvm运行了100分组,而垃圾回收花费了1分钟,用户线程运行了99分钟,那么吞吐量就等于99%。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 ——《深入理解Java虚拟机》

Parallel Scavenge收集器提供两个参数用来精准的控制吞吐量,分别是-XX:MaxGCPauseMillis和-XX:GCTimeRatio。-XX:MaxGCPauseMillis参数见名知义能看出来这是用来控制垃圾回收停顿时间的,设置这个参数的时候要传入一个大于0的值,收集器将尽可能的保证垃圾回收产生的停顿时间不超过该值,但是这个值不是越小越好的,这个值设置的越小,那么垃圾回收的次数必然会增多。-XX:GCTimeRatio参数也能看出来是设置垃圾回收时间比例的,事实上它是用来设置允许的垃圾回收的时间占总时间的最大比例,即1 - 吞吐量,但是它不是直接设置的,它需要传入一个大于0小于100的整数,假设传入的是9,那么允许的最大GC时间就占总时间的10%(1/(1 + 9) = 0.1),这个参数的默认值是99,也就是说默认允许最大的垃圾回收时间占总时间的1%。
除了上述两个参数之外,Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy,这个参数很厉害的,看到有+就知道这是个开关参数,当作这个参数被打开之后,就不需要我们去手动的设置新生代的大小、Eden区与Survivor区的比例以及晋升老年代对象年龄这些细节参数了,虚拟机能够根据当前系统的运行情况收集性能监控信息,去动态的进行调整以提供最合适的停顿时间或者最大的吞吐量,这种调节方式被称为GC自适应的调节策略。所以对于我们这种菜鸟来说使用这个Parallel Scavenge收集器配合GC自适应再好不过了,把调优任务交给虚拟机,我们只要设置好最大堆和最大停顿时间或者吞吐量就好了。


Serial Old收集器

Serial Old收集器在前面就有提到,它是单线程的,使用的是标记-整理算法,运行的时候会产生STW现象,而且我们在之前的垃圾回收器图中可以看到,它可以和新生代的Serial收集器、ParNew收集器、Parallel Scavenge收集器配合使用,也可以和老年代的CMS收集器配合使用。按道理它是单线程的垃圾回收器,主要意义就是运行在Client模式下了,但是在Server模式下,它可以作为CMS收集器的后备预案,在并发收集发生Concurrent Mode的时候,也就是说新产生的垃圾大于回收的速度了,CMS无法承受压力了就会启动Serial Old来进行一次串行的垃圾回收。
另外在Server模式下,在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用。

注:需要说明一下,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非直接使用了Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现非常接近,所以在官方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解。 ——《深入理解Java虚拟机》

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用的标记-整理算法,在JDK1.6的时候开始提供,我个人认为Parallel Old收集器是来拯救Parallel Scavenge的,不然Parallel Scavenge在老年代垃圾回收这一块可太尴尬了,不能和CMS搭配使用,Serial Old收集器又太慢了,白瞎了Parallel Scavenge在新生代的高吞吐量了,所以在Parallel Old收集器出现了才让“吞吐量优先”收集器有了比较名副其实的应用组合。Parallel Old和Parallel Scavenge搭配使用的运行过程如下图:
Parallel Old/Parallel Scavenge

CMS收集器

CMS全称Concurrent Mark Sweep,即并发标记清除,所以从名字中我们能获取到两个信息,CMS是并发执行的,这个之前也说过,还有就是它使用的标记-清除算法。前面也提到过CMS追求的是最短回收停顿时间,所以在互联网站或者B/S系统的服务端的场景中,CMS就很符合这类应用的需求。
CMS的工作过程分四个阶段:

初始标记(CMS initial mark):标记一下GC Roots能直接关联到的对象,单线程会产生STW,但速度很快。 并发标记(CMS concurrent mark):进行GC Roots Tracing,trace是追溯的意思,就是说完善GC Roots引用链,并发执行的不会产生STW,需要比较长的时间。 重新标记(CMS remark):修正并发标记的时候因为用户程序在继续运行而产生的变动,多线程会产生STW,时间比初始标记长但远小于并发标记。 并发清除(CMS mark sweep):并发地清除被标记的对象。

因为并发标记和并发清除的时间远大于初始标记和重新标记的时间,而且初始标记和重新标记的时间非常短,所以总体上来说CMS收集器回收过程是和用户线程一起并发执行的。根据上面四个步骤我们也很容易能够画出CMS的运行流程图:
CMS

CMS的优缺点也十分明显,优点就是它是并发收集的,停顿时间非常短,缺点就要分三个点细细说了:

面向并发设计的程序都对CPU资源非常敏感,CMS也不例外。在并发阶段,CMS虽然不会导致用户线程停顿,但是也会因为占用了一部分CPU资源而导致用户线程变慢。CMS默认启动的回收线程数是 (CPU数量 + 3) / 4,所以说当CPU在4个以上时比如5个,CMS会占用 2 / 5 = 40%的CPU资源,并且会随着CPU个数的增加而降低占用资源比例,但是如果CPU少于4个,比如2个,那么就会占用 1 / 2 = 50%也就是一半的CPU资源,会严重影响用户线程的执行速度。 CMS收集无法处理浮动垃圾,这个在上面聊Serial Old收集器的时候就说到了,并且说到了jvm会将CMS作为CMS的后备预案,但是注意是在出现了“Concurrent Mode Failure”之后才会启动Serial Old收集器的,那么怎样会出现这样的失败呢?其实是因为CMS是并发执行的嘛,在并发的过程中会放入新的对象到老年代,所以CMS必须要预留一部分内存来存放可能会新增的对象,在JDK1.5的默认设置中,CMS收集器当老年代使用了68%的空间后就会被激活,也可以使用-XX:CMSInitiatingOccupancyFraction来控制。很明显,如果这个百分比越高的话,CMS被触发的次数就越少,在JDK1.6中,CMS收集器的启动阈值已经提升到92%,但是预留内存越少就越容易出现“Concurrent Mode Failure”,所以我觉得这个预留内存就需要我们自己去根据实际场景进行衡量了。 标记-清除算法会产生大量的内存碎片。针对这个缺点,CMS提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,是说在CMS撑不住要进行Full GC的时候进行一次内存整理,但是这个整理过程是没有办法并发进行的,所以会产生停顿时间,另外这个参数默认是开启的。然后虚拟机设计者还提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是设置CMS在经历过多少次不整理内存的Full GC后来一次整理内存的Full GC,默认是0,表示每次进入Full GC都进行内存整理。

然后这里补充一下CMS第一个缺点的一个过时的解决方案。当时虚拟机提供了一种名叫增量式并发收集器(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,它就是在并发标记和并发清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源时间,这样整个垃圾收集的过程就会更长,但对用户程序的影响就会显得少一些。实践证明,这个i-CMS的效果很一般,所以在JDK1.7的时候它就被声明为“deprecated”过时的了。

G1收集器

G1(Garbage-First)收集器是当今收集器当时jdk1.7是最新的jdk 技术发展的最前沿成果之一,早在JDK1.7刚刚确立项目目标,Sun公司给出的JDK1.7RoadMap里面,它就被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。从JDK 6u14中开始就有Early Access版本的G1收集器供开发人员实验、试用,由此开始G1收集器的“Experimental(试验性的)”状态持续了数年时间,直至JDK 7u4,Sun公司才认为它达到足够成熟的商用程度,移除了“Experimental”的标识。 ——《深入理解Java虚拟机》

而在JDK1.9的时候,jvm就将G1收集器作为默认的收集器了。
由于G1的内容比较多,而且实现与前面学习的收集器有很大区别,所以我就放在后续博客中详细聊了。

如有错误,欢迎指正!!

栏目