一、GC分类与性能指标
1、垃圾回收器概述
1、垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
2、由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
3、从不同角度分析垃圾收集器,可以将GC分为不同的类型。
4、垃圾收集器分类:
-
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器
和并行垃圾回收器。
-
按照工作模式分,可以分为并发式垃圾回收器
和独占式垃圾回收器。
-
按碎片处理方式分,可分为压缩式垃圾回收器
和非压缩式垃圾回收器
。
-
按工作的内存区间分,可分为年轻代垃圾回收器
和老年代垃圾回收器
。
5、Java不同版本新特性
-
语法层面:Lambda表达式、switch、自动拆箱装箱、enum。
-
API层面:Stream API、新的日期时间、Optional、String、集合框架。
-
底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化。
2、按线程数分类
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器
和并行垃圾回收器。
-
串行回收
指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作
,此时应用程序线程被暂停,直至垃圾收集工作结束
。
-
并行回收
与串行回收相反,并行回收
可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量
,不过并行回收和串行回收一样,都是采用独占式,使用了Stop The World机制
。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0745286f9d49da3050b628f2e702f6e3.png)
3、串行与并行垃圾回收器的区别
1、相同点
-
都是独占式
的,用户线程都必须等到垃圾回收器执行完才能执行,造成了Stop The World
。
2、不同点
-
串行垃圾回收器:同一时间段只能有一个CPU执行
垃圾回收。
-
并行垃圾回收器:同一时间可以有多个CPU同时执行
垃圾回收,吞吐量提高了。
4、串行与并行垃圾回收器的应用场景
1、在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合
,串行回收器的性能超过并行回收器和并发回收器
。因此串行回收默认被应用在客户端的Client模式下的JVM中。
2、在并发能力比较强的CPU上
,并行回收器产生的停顿时间要短于串行回收器
。
5、按工作模式分类
按照工作模式分,可以分为并发式垃圾回收器
和独占式垃圾回收器。(串行/并行垃圾回收器)
-
并发式垃圾回收器
与应用程序线程交替执行
,以尽可能减少应用程序停顿时间(即STW)
。
-
独占式垃圾回收器(Stop The World)
一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收完全结束
。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/c6b7ff8b35716c0be41f80d8de7f5227.png)
6、按碎片处理方式分类
按碎片处理方式分,可分为压缩式垃圾回收器
和非压缩式垃圾回收器
。
-
压缩式垃圾回收器
会在垃圾回收完成后,对存活对象进行压缩整理,消除回收后的碎片
。再分配对象空间使用指针碰撞的方式。
-
非压缩式垃圾回收器
不会对存活对象进行压缩处理。再分配对象空间使用空闲列表的方式。
7、按工作的内存区间分类
按工作的内存区间分,可分为年轻代垃圾回收器
和老年代垃圾回收器
。
-
年轻代垃圾回收算法:一般使用复制算法
,在幸存者
区就使用的是复制算法
。
-
老年代垃圾回收算法:一般用标记清除和标记压缩
算法。
8、评估GC的性能指标
1、吞吐量
:运行用户代码的时间占总运行时间的比例
(总运行时间=程序的运行时间+内存回收的时间)(越高越好)
。
2、垃圾收集开销
:吞吐量的补数,垃圾收集所用时间与总运行时间的比例(越低越好)
。
3、暂停时间
:执行垃圾收集时,程序的工作线程被暂停的时间(STW的时间,越低越好)
。
4、收集频率
:相对于应用程序的执行,收集操作发生的频率。
5、内存占用
:Java堆区所占用的内存大小
。
6、快速
:一个对象从诞生到被回收所经历的时间。
小总结
:
-
吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”,总体表现会随着技术进步越来越好。一款优秀的收集器通常最多同时满足其中的两项。
-
这三者里,暂停时间
的重要性日益凸显。因为随着硬件发展,内存占用多些越来越容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。
-
由于内存不值钱了,主要抓住两点:提高吞吐量、减少用户线程暂停时间(STW)
。
9、评估GC的性能指标之吞吐量(throughput)
1、吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
。
2、比如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
3、这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。
4、下图垃圾回收的频率只有两次
,吞吐量就高了。吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/be84c0e9c32f00c72c6f0156238e6378.png)
10、评估GC的性能指标之暂停时间(pause time)
1、暂停时间
是指一个时间段内应用程序线程暂停,让GC线程执行的状态。如:GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。
2、下图提高了垃圾收集的次数
,每次垃圾收集的时间很短,如果在并发垃圾回收器
下,CPU不断的进行上下文切换(垃圾回收线程与用户程序线程切换),此时就会减少用户线程的STW,给用户低延迟
的感觉如此的话,吞吐量就降低了
。暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/2b4e9afef611a6f7532be5d7021bb2aa.png)
11、吞吐量与暂停时间对比
1、高吞吐量
较好因为会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快
。
2、低暂停时间(低延迟)
较好是因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型。有时候甚至短暂的200ms暂停都可能打断终端用户体验
。因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序
。
3、不幸的是高吞吐量
和低暂停时间
是一对相互竞争的目标。
-
如果追求高吞吐量
,那么必然需要降低内存回收的执行频率
,但是这样会导致GC需要更长的暂停时间来执行内存回收
。
-
如果追求低延迟
,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收
,但是这样会导致年轻代内存的缩减和程序吞吐量的下降
。
4、在设计(或使用)GC算法时,必须确定目标:一个GC算法只可能针对两个目标之一
(即只专注于较大吞吐量
或最小暂停时间
),或尝试找到一个折中方案。
5、现在标准:在最大吞吐量优先的情况下,降低停顿时间
。
二、不同的垃圾回收器概述
1、垃圾收集器发展史
1、垃圾收集机制是Java的招牌能力,极大地提高了开发效率。GC垃圾收集器是和JVM一脉相承的,它是和JVM进行搭配使用,在不同的使用场景对应的收集器也是有区别。
2、有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector
-
1999年随JDK1.3.1一起来的是串行方式的Serial GC
,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
。
-
2002年2月26日,Parallel GC
和Concurrent Mark Sweep GC(CMS GC)
跟随JDK1.4.2一起发布。
-
Parallel GC在JDK6之后成为HotSpot默认GC
。
-
2012年,在JDK1.7u4版本中,G1
可用。
-
2017年,JDK9中G1变成默认的垃圾收集器
,以替代CMS
。
-
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
-
2018年9月,JDK11发布。引入Epsilon垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
。
-
2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。
-
2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
-
2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应用。
2、七种经典的垃圾收集器
1、串行回收器:Serial(年轻代)
、Serial Old(老年代)
2、并行回收器:ParNew(年轻代)
、Parallel Scavenge(年轻代)
、Parallel Old(老年代)
3、并发回收器:CMS(老年代)
、G1(年轻代/老年代)
3、七种经典收集器与垃圾分代之间的关系
1、新生代收集器:Serial
、ParNew
、Parallel Scavenge
2、老年代收集器:Serial Old
、Parallel Old
、CMS
3、整堆收集器:G1
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/d212616dce187f8fd178f86228379321.png)
4、垃圾收集器的组合关系
1、两个收集器之间存在连线,就说明它们可以搭配使用:
-
Serial/Serial old
-
Serial/CMS (JDK8废弃、JDK9被移除)
-
ParNew/Serial Old (JDK8废弃、JDK9被移除)
-
ParNew/CMS
-
Parallel Scavenge/Serial Old (JDK14中废弃)
-
Parallel Scavenge / Parallel Old
(JDK8 默认GC)
2、其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(失败会进行Serial Old)。
3、(红色虚线)由于维护和兼容性测试的成本,在JDK8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
4、(绿色虚线)JDK14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)。
5、(青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/5fd55aaba778a9c539aa9d4341c5769d.png)
5、为什么需要那么多垃圾收集器
1、因为Java的使用场景很多,移动端,服务器等。所以就需要针对不同的场景,提供不同的垃圾收集器,提高垃圾收集的性能。
2、虽然会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种在任何场景下都适用的完美收集器存在,更加没有万能的收集器。因此选择的只是针对具体应用最合适的收集器。
6、查看默认垃圾收集器
1、设置JVM启动参数查看使用的默认收集器:-XX:+PrintCommandLineFlags
程序打印输出:-XX:+UseParallelGC
表示使用 ParallelGC ,ParallelGC默认和Parallel Old绑定使用
。
-XX:InitialHeapSize=264726528 -XX:MaxHeapSize=4235624448
-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2、命令行方式查看使用的默认收集器:jinfo -flag 相关垃圾回收器参数 进程ID
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/76f620b82e0e00182972d65f7c293cc2.png)
三、Serial回收器(串行回收)
1、Serial回收器概述
1、Serial收集器是最基础的、历史最悠久的收集器。在JDK1.3.1之前是HotSpot虚拟机新生代收集器的唯一选择。
2、Serial收集器采用复制算法
、串行回收和STW机制的方式执行内存回收。
3、除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。Serial Old收集器同样也采用了串行回收和STW机制,只不过内存回收算法使用的是标记-压缩算法
。
4、Serial Old GC是运行在Client模式下默认的老年代的垃圾回收器,Serial Old在Server模式下主要有两个用途:
-
与新生代的Parallel Scavenge配合使用
。
-
作为老年代CMS收集器的后备垃圾收集方案
。
5、Serial收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作
,更重要的是强调在它进行垃圾收集时,必须暂停其他所有的工作线程
,直到它收集结束。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/7b6ce2818fc38648545f0a793cb965f2.png)
2、Serial收集器的优势
1、单线程情况下与其他收集器相比,Serial简单而高效
,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销
,专心做垃圾收集自然可以获得最高的单线程收集效率。
2、运行在Client模式下的虚拟机是个很好的选择。
3、在用户的桌面应用场景中,可用内存一般不大(几十到一两百MB),可以在较短时间内完成垃圾收集(几十到一百多MS),只要不频繁发生,使用串行回收器是可以接受的。
3、设置Serial垃圾回收器
1、设置JVM启动参数指定新生代和老年代都使用串行收集器
:-XX:+UseSerialGC
2、等价于新生代用Serial GC
,且老年代用Serial Old GC
。
4、Serial收集器现状
1、这种垃圾收集器了解就行,现在已经不用串行的了。而且在限定单核CPU才能使用,现在CPU都不是单核了。
2、对于交互较强的应用来说,这种垃圾收集器是不能接受的。一般在Java Web应用程序中是不会采用串行垃圾收集器的。
四、ParNew回收器(并行回收)
1、ParNew回收器概述
1、如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程并行
版本。
2、Par是Parallel的缩写,New说明只能处理新生代的垃圾收集。
3、ParNew收集器除了采用并行回收(同时多条线程)
的方式执行内存回收外,其余的行为(所有控制参数、收集算法、STW、对象分配规则、回收策略)与Serial收集器完全一致。ParNew收集年轻代中同样采用复制算法、STW机制
。
4、ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f24e25c31511375129c7b88abd4a2075.png)
5、并行还是串行:
-
对于新生代,回收次数频繁,使用并行方式高效
。
-
对于老年代,回收次数少,使用串行方式节省资源(CPU并行需要切换线程,串行可以省去切换线程的资源)。
2、ParNew与Serial对比
1、由于ParNew收集器是基于并行回收,是否可以断定ParNew收集器的回收效率在任何场景下都比Serial高,要从以下两点出发:
-
ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量
。
-
但是在单个CPU的环境下,ParNew收集器不比Serial收集器更高效
。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销
。
3、设置ParNew垃圾回收器
1、设置JVM启动参数指定新生代使用ParNew收集器,不影响老年代
:-XX:+UseParNewGC
2、设置JVM启动参数限制ParNew收集器回收的线程数,默认开启和CPU数据相同的线程数
:-XX:ParallelGCThreads
五、Parallel回收器(吞吐量优先)
1、Parallel Scavenge回收器概述
1、HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的之外,Parallel Scavenge收集器同样也是基于标记-复制算法、并行回收和STW机制
实现的收集器。
2、Parallel Scavenge与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(ThroughPut)
,也被称为吞吐量优先的垃圾收集器
。
3、自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特征
。
4、高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
。因此,常见在服务器环境中使用。如:那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
5、Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器
。
6、Parallel Old收集器采用了标记-压缩算法
,但同样也是基于并行回收和STW机制
。
7、在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很好,Java8中默认是此垃圾收集器
。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f2b68dcaad8d4584e7747c44f6bf372c.png)
2、设置Parallel垃圾回收器
1、设置JVM启动参数指定年轻代使用Parallel并行收集器执行内存回收任务
:-XX:+UseParallelGC
。
2、设置JVM启动参数指定老年代使用ParallelOld并行收集器执行内存回收任务
:-XX:+UseParallelOldGC
。
3、默认JDK8是开启的,默认设置一个,另一个也会被自动开启(互相激活)
。
3、设置年轻代并行收集器的线程数
1、设置JVM启动参数指定年轻代并行收集器的线程数
:-XX:ParallelGCThreads
。
2、注意事项:
-
一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
-
在默认情况下,当CPU数量小于8个,它的值等于CPU数量。
-
当CPU数量大于8个,它的值等于3+[5 * CPU_Count] / 8
。
4、设置垃圾收集器最大停顿时间
1、设置JVM启动参数设置垃圾收集器最大停顿时间(即STW的时间,单位是毫秒)
:-XX:MaxGCPauseMillis
。
2、收集器将尽力保证内存回收花费的时间不超过设定值。收集器在工作时会调整Java堆大小或者其它一些参数
。
3、不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的
。
4、假设系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。该参数谨慎使用
。
5、设置吞吐量大小
1、设置JVM启动参数设置垃圾收集时间占总时间的比例(即等于1/(n+1))
:-XX:GCTimeRatio
。
2、说明:
-
取值范围(0,100)。默认值99,也就是垃圾回收时间不超过1%。
-
垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如把此参数设置为19,那允许最大垃圾收集时间就占总时间的5%(即1 / (19 + 1))。
-
与-XX:MaxGCPauseMillis
参数有一定矛盾性。STW时间越长,Radio参数就容易超过设定的比例
。
6、设置Parallel收集器自适应调节策略
1、设置JVM启动参数开启Parallel收集器的自适应调节策略
:-XX:+UseAdaptiveSizePolicy
。
2、说明:
-
当这个参数被激活之后,就不需要指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
。
-
如果手动调优存在困难的话,使用Parallel Scavenge收集器配合自适应调节策略
,把内存管理的调优任务交给虚拟机去完成。只需要把基本的内存数据设置好(如-Xmx
设置最大堆),然后使用-XX:MaxGCPauseMillis
参数(更关注最大停顿时间)或-XX:GCTimeRatio
参数(更关注吞吐量)给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。