前言
上文已经讲解垃圾收集的各种算法,算法可以理解为方法,如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
正文
目录
面试官:你认识到的收集器都有哪些啊?
答:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
面试官:为什么HotSpot虚拟机需要这么多收集器?
答:HotsSpot垃圾是分代收集的,所以不用的分代收集器也不同,即使是同一年代里收集器也会不同,因为每个收集器特点和性能不同也就有了收集器的多样性,所以各个收集器互相组合使用才能适应不同场景。
面试官:那你说一下每个收集器都有什么特点吧!
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、Parallel Old、CMS;
整堆收集器:G1;
开始之前我们了解一个概念
Minor GC
又称新生代GC,指发生在新生代的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
Full GC
又称Major GC或老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
Major GC速度一般比Minor GC慢10倍以上;
Serial收集器
这个收集器是最基本的,也是历史最悠久的收集器,目前基本不会用了,想当年那也是新生代的唯一选择,但是这么多年过去了,HotSpot也没有说过河拆桥把它废了,“老而无用、食之无味弃之可惜”。
他是一个单线程收集器,他在工作的时候,必须暂停其他所有的工作线程,直到收集结束。这里要知道一个很严重的问题就是,暂停一切线程的结果就是当前运行在这个JDK的所有程序里的用户线程全部暂停,也就是说这一瞬间都是死掉的,用户看到的现象就是页面无任何响应,如果这种现象出现的时间长且频繁用户就崩溃了。
这里埋下了一个伏笔,越优秀的收集器,他的停顿时间一定越短,这也是所有收集器共同追求的目标。
ParNew收集器
他是Serial收集器多线程版本,其所有控制参数、收集算法、对象分配规则、回收策略等都与Serial完全一样。下面是ParNew收集器工作的过程。
他的重要之处在于,除了多线程提高了性能之外,他还可以与CMS收集器(下面介绍)搭配使用的原因。
在单CPU环境下ParNew的性能没办法超过Serial,但是随着CPU数量增多他的优势就会越来越明显。
Parallel Scavenge收集器
他也是一款新生代收集器,使用的是复制算法,并且是并行对线程收集器。可以看到收集器的进步都是保留上一代之长,弥补上一代之短。
很多收集器关注用户线程的停顿时间,但是Parallel Scavenge则关注吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),例:虚拟机运行100分钟,其中垃圾收集时间用了1分钟,那吞吐量就是99%。
他是怎样控制吞吐量呢?
使用参数控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ,以及直接设置吞吐量大小 -XX:GCTimeRatio参数。
MaxGCPauseMillis参数是一个大于0的毫秒数,收集器一次工作尽可能不超过设定的这个值,但是设置太小GC停顿时间缩短,造成了垃圾收集频率变快。如果你设定停顿100毫秒,10秒收集一次的频率,改成70毫秒的停顿时间,那么频率就可能变成5秒一次。停顿时间下降,吞吐量也会下降,GC还会变得更频繁。
XX:GCTimeRatio参数设置垃圾收集时间占总时间的比率,0<n<100的整数;
GCTimeRatio相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n)
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19);
默认值是1%--1/(1+99),即n=99;
看来找准最优的临界点真的是Parallel Scavenge收集器比较配置的。
不要担心,HotSpot又提供了一个参数 XX:+UseAdptiveSizePolicy帮助我们实现GC自适应的调节策略,他会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量。
这个参数开启,JVM就可以动态分配新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;
这里插入一个笔者的经历:当时面试有人问我,Eden与Survivor区的比例可以变化吗?
看到这里童鞋们就可以回答:Parallel Scavenge收集器开启 XX:+UseAdptiveSizePolicy可以动态分配。
这也是Parallel Scavenge收集器优越于ParNew收集器一个重要点。
Serial Old收集器
Serial Old是 Serial收集器的老年代版本,也是继承Serial收集器单线程的特点。
工作模型图在Serial收集器中展示了。
Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本,继承了Parallel New多线程对特点,在JDK1.6及之后用来代替老年代的Serial Old收集器;
参数"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;
工作模型图在Parallel New收集器中展示了。
接下来讲解CMS于G1收集器,在将之前要理解一个概念
可能前面也提到过,不过在这里了解以下也不晚
并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
CMS收集器
并发标记清理收集器也称为并发低停顿收集器或低延迟垃圾收集器;他的宗旨是:低停顿。
HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;
他采用的是“标记-清除算法",因此会生大量的空间碎片。为了解决这个问题,CMS可以通过配置以下两种参数解决:
-
-XX:+UseCMSCompactAtFullCollection:参数,强制JVM在FGC完成后対老年代迸行圧縮,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置。
-
-XX:+CMSFullGCsBeforeCompaction=n :参数,在执行了n次FGC后, JVM再在老年代执行空间碎片整理
初始标记 (Initial Mark)
停止一切用户线程,仅使用一条初始标记线程对所有与GC Roots直接相关联的 老年代对象进行标记,速度很快
并发标记 (Concurrent Marking Phase)
使用多条并发标记线程并行执行,并与用户线程并发执行.此过程进行可达性分析,标记所有这些对象可达的存货对象,速度很慢
重新标记 ( Remark)
因为并发标记时有用户线程在执行,标记结果可能有变化
停止一切用户线程,并使用多条重新标记线程并行执行,重新遍历所有在并发标记期间有变化的对象进行最后的标记.这个过程的运行时间介于初始标记和并发标记之间
并发清除 (Concurrent Sweeping)
只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象
这个过程非常耗时
G1收集器
G1(Garbage-First)是JDK7-u4才推出商用的收集器;他比CMS更高级了,他是并行与并发,能充分利用多CPU、多核环境下的硬件优势;
G1以下特点
并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短"Stop The World"停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种类似火车算法的实现;
都不会产生内存碎片,有利于长时间运行;
可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能建立可预测的停顿时间模型;
可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
用来替换掉JDK1.5中的CMS收集器;
上面提到Region 概念,肯定都不会理解,让我们看一下G1的内存模型
G1将Java堆空间分割成了若干相同大小的区域,即region
Humongous是特殊的Old类型,专门放置大型对象
在JDK11中,已经将G1设为默认垃圾回收器
G1垃圾收集过程
初始标记
标记与GC Roots直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快。
并发标记
进行全面的可达性分析,开启一条并发标记线程与用户线程并行执行.这个过程比较长。
最终标记
标记出并发标记过程中用户线程新产生的垃圾.停止所有用户线程,并使用多条最终标记线程并行执行。
筛选回收
回收废弃的对象.此时也需要停止一切用户线程,并使用多条筛选回收线程并行执行。
题外话:我们知道目前为止JDK8是应用最广泛对版本,并且JDK9、10是一个过度版,企业应用效果不好,JDK11是一个里程碑版本,重要程度相当于现在JDK8,并且JDK11默认使用G1收集器,G1的性能在早些年就突出于CMS并且官方对性能测试结果也是这样说明的。