JVM之垃圾收集器【四】

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。Java虚拟机规范中没有规定垃圾收集器应该怎么实现。本文讨论的是JDK1.7之后的HotSpot虚拟机(Update 14 之后提供了G1收集器)。

HotSpot虚拟机包含的垃圾收集器:在这里插入图片描述
其中有线相连的可以搭配使用,垃圾收集器所在区域表示属于新生代收集器或老年代收集器。

Serial收集器

Serial收集器是最基本、最古老的收集器,曾经(JDK1.3.1之前)是虚拟机新生代收集器的唯一选择。看名字就知道是一个单线程的收集器,但这里的“单线程”不是仅仅说它只会使用一条收集线程完成垃圾收集工作,重要的是它在进行垃圾收集时,必须暂停其它的所有工作线程,直到收集结束。

Serial/Serial Old收集器运行示意图:在这里插入图片描述
就算到现在,它依然是虚拟机运行在Client模式下的默认新生代收集器。它的优点:简单而高效(如果是单CPU的环境,由于没有线程交互的开销)。适用于桌面应用。

ParNew 收集器

ParNew是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余包括Serial收集器使用的控制参数(-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure…)、收集算法、回收策略等都一样,实现上复用了很多Serial的代码。
它是Server模式下虚拟机的首选新生代收集器,另外只有它和Serial能与CMS收集器配合使用。如果JVM参数设置了-XX:UseConcMarkSweepGC,那么它就是默认的新生代收集器,此外也可以显示指定(-XX:UseParNewGC)。
它默认开启的线程数和CPU数量相同,当CPU多的时候,可以使用-XX:ParallelGCThreads指定。
在这里插入图片描述

Parallel Scavenge 收集器

与CMS关注尽可能缩短垃圾收集时用户线程的停顿时间相比,Parallel Scavenge收集器的特点是它注重吞吐量的控制。停顿时间不用解释,高吞吐量可以高效利用CPU,尽快完成运算任务,适合后台运算的任务。另外Paraller Scavenge收集器和ParNew收集器的一个重要区别是它有自适应调节策略。

它提供了两个精确控制吞吐量的参数,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis 和 直接设置吞吐量大小的 -XX:GCTimeRatio。-XX:MaxGCPauseMillis允许设置大于0的毫秒数,收集器会尽可能保证内存回收花费时间不超过该值,如果不了解系统的话不建议设置,因为GC停顿时间以牺牲吞吐量和新生代空间为代价,设置不好可能会适得其反;-XX:GCTimeRatio是一个0到100的整数,表示垃圾收集时间占总时间的比率,相当于吞吐量的倒数,默认99。

另外,还有一个开关参数-XX:UseAdaptiveSizePolicy,打开后不需要手工指定新生代大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象年龄大小(-XX:PretenureSizeThreshold)等参数,虚拟机会根据系统运行情况动态调整这些参数来提供最合适停顿时间或最大吞吐量,这种调节方式称为“GC自适应调节策略”。通常可以设置基本参数(-Xmx最大堆)和MaxGCPauseMillis(更关注最大停顿时间)或GCTimeRatio(更关注最大吞吐量)来给虚拟机设立一个优化目标,虚拟机会自动完成。

Serial Old 收集器

Serial Old 是Serial收集器的老年代版本,使用“标记-整理”算法。主要也是在于Client模式下给虚拟机使用;在Server模式下,有两个用途:1、JDK1.5之前和Parallel Scavenge搭配使用,2、CMS收集器的备选方案。

Parallel Old 收集器

Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本,使用“标记-整理”算法,JDK1.6才有。在这之前,如果新生代收集器Parallel Scavenge只可以和Serial Old搭配使用,Server模式下,CPU多的时候性能很低。所以其实基本没什么用。
在注重吞吐量以及CPU资源敏感的情况下,可以使用Parallel Scavenge和Parallel Old组合。
在这里插入图片描述

CMS收集器

CMS(Concurrent Mark Sweep) 收集器是以获取最短回收停顿时间为目标的,从名字可以看出,采用“标记-清除”算法,它的运作过程包含4个步骤:

  1. 初始标记:标记一下GC Roots能关联到的对象,速度很快。
  2. 并发标记:进行GC Roots Tracing 的过程,很慢。
  3. 重新标记:修正并发标记阶段因为用户线程继续执行而导致标记产生变动的标记记录,速度也很快,比初始标记慢一点。
  4. 并发清除
    其中初始标记和重新标记需要STW。由于耗时最长的并发标记和并发清除过程可以用户线程一起工作,所以总体来看:CMS的收集过程和用户线程是一起并发执行的。
    在这里插入图片描述
    优点:
    并发收集,低停顿。
    缺点:
    对CPU资源很敏感,虽然不会导致用户线程停顿,但会占用线程,总吞吐量会降低,CMS默认启动回收线程数是(CPU数+3)/4;
    无法处理浮动垃圾,浮动垃圾就是清除的时候用户线程执行产生的垃圾;
    大量空间碎片,CMS参数-XX:UseCMSCompactAtFullCollection开关参数(默认开启),Full GC前合并整理空间碎片,内存整理过程不是并发的,停顿时间会变长。

为什么CMS要使用“标记-清除”算法?因为如果换成“标记-整理”算法,把垃圾清理后,剩下的对象也顺便整理,会导致这些对象的内存地址发生变化,此时其它线程还在工作,如果引用的对象地址变了,后果是。。。

G1 收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是以后可以替换掉JDK1.5发布的CMS收集器。在JDK1.9 中, G1成为了Server模式下默认的垃圾收集器,取代了(Parallel Scavenge和Parallel Old组合)。它有如下特点:
并发与并行:
充分利用多CPU、多核的硬件优势,可以通过并发的方式让java程序继续执行。
分代收集:
保留了分代概念。
空间整合:
从整体看,采用“标记-整理”算法,从局部看(两个Region之间),采用“复制”算法,所以不会产生内存碎片。
可预测的停顿:
可以建立可预测停顿时间模型。

G1把Java堆划分为多个大小相等区域(Region),虽然保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离,他们都是一部分Region的集合(也就是某个Region可能属于新生代的Eden或Survivor区,也可能属于老年代)。每一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围是1MB~32MB,且为2的N次幂。

Region中有一类特殊的Humongous区,专门存储大对象。G1认为只要超过Region容量一半的对象就是大对象。如果一个对象大小超过了整个Region容量,会存放在N个连续的Humongous区中,G1的大多数行为会把Humongous区当做老年代。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小和回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,有效避免进行全堆范围的垃圾收集。这样其实有利于程序长时间的运行(解决了CMS会产生内存碎片的问题)。

思考:一个对象和它内部所引用的对象可能不在同一个Region中,那么垃圾回收的时候会扫描整个堆来完成可达性分析吗?
答案是不需要的,因为每个Region都有一个Remembered Set(记忆集), 用来记录本Region中所有对象引用的对象所在的Region, 进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止遍历整个堆内存。

它的运作过程分为:
1.初始标记:和CMS相似
2.并发标记:和CMS相似
3.最终标记:和CMS重新标记相似
4.筛选回收:回收废弃对象,会触发STW, 并且会对各个Region的回收价值和成本进行排序。
除了并发标记,其余阶段全会触发STW。G1并不是纯粹的追求低延迟,官方的设定标准是在延迟可控的情况下提高吞吐量

总结:

常用垃圾收集器组合:
Serial + Serial Old —> -XX:+UseSerialGC
ParaNew + CMS —> -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
Parallel Scavenge + Parallel Old —> -XX:+UseParallelGC -XX:+UseParallelOldGC
G1 -XX:+UseG1GC

发布了11 篇原创文章 · 获赞 0 · 访问量 612

猜你喜欢

转载自blog.csdn.net/fei1234456/article/details/104962384