谈谈你熟悉的垃圾回收器
JVM的垃圾回收器,似乎是每次面试都必问的,对垃圾回收器的掌握程度、可以区分一个一年和三年工作经验的程序员。
你对垃圾回收器熟悉,说明你知道:
- 项目使用的垃圾回收器
- 这种垃圾回收器的特点
- JVM的参数
- GC的情况
常见的垃圾回收器如下:(重要)
收集器 | 串行/并行/并发 | 收集目标 | 算法 | 优点 | 缺点 | 特点 |
---|---|---|---|---|---|---|
Serial New | 串行 | 新生代 | 复制 | 单CPU下效率高 | 产生较长时间的停顿 | 简单高效,没有现存交互开销,适用单CPU、Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 同上 | 同上 | 同上 |
Parallel New | 并行 | 新生代 | 复制 | 单CPU下效率高,可设置可控参数 | 产生较长时间的停顿 | 简单高效,没有现存交互开销,适用多CPU、Server模式 |
Parallel Scavenge | 并行 | 新生代 | 复制 | 最高效率的利用CPU,高吞吐 | 产生较长时间的停顿 | 吞吐量控制,client、server模式均可以 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 多CPU下效率高 | 产生较长时间的停顿 | |
CMS | 并发 | 老年代 | 标记-清除 | 最小的停顿时间,低停顿 | CPU资源敏感,无法处理浮动垃圾,产生大量内存碎片 | 适合互联网B/S系统服务端 |
G1 | 并发 | 新生代+老年代 | 标记-整理 | 分代收集,空间整合(标记整理算法),可预测停顿 | 面向服务端应用 |
一般说的 Serial收集器 都是指 Serial New
# 1、并发垃圾收集和并行垃圾收集的区别
(A)、并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
如 ParNew、Parallel Scavenge、Parallel Old;
(B)、并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
如CMS、G1(也有并行);
# 2、Minor GC和Full GC的区别
(A)、Minor GC
又称新生代GC,指发生在新生代(包括 Eden 和 Survivor 区域)的垃圾收集动作;
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
(B)、Full GC
又称老年代GC,指发生在老年代的GC;
出现Full GC经常会伴随至少一次的Minor GC;
Full GC速度一般比Minor GC慢10倍以上;
# 3、Full GC触发机制是什么
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小 大于To Space可用内存,则把该对象转存到老年代,且该对象大于老年代的可用内存时。
# 4、聊聊你熟悉的或者项目中用的垃圾回收器
我列举平时自己用的比较多的垃圾回收器,其中重要的是CMS和G1
有连线的表示可以搭配使用:
# 1、ParNew
Parallel 是并行的意思,New 是新生代的意思,所以 ParNew 的意思是新生代采用了多线程回收
新生代收集器,其实就是Serial的多线程版本。收集算法、Stop the World、对象分配规则、回收策略等都与Serial收集器完全一样。
特点:
使用复制算法,关注缩短垃圾收集时间,可以和CMS收集器配合工作。
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;
因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
参数:
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;
"-XX:+UseParNewGC":强制指定使用ParNew;
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
# 2、Parallel Scavenge
复制算法,新生代的收集器。
特点:
高效率的利用CPU,高吞吐,适合后台运算而不需要太多交互的任务。
参数:
-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间(这个参数只对Parallel Scavenge有效)
-XX:GCRatio 直接设置吞吐量的大小。
# 3、CMS
标记-清除算法,老年代收集器。
特点:
等待时间很少(这也是使用标记-清除法的原因),适合用户交互,提高用户体验
收集过程分为如下四步:(重要)
(1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。
初始标记的时候是一个 STW (stop the world)
的过程,所有的用户线程都会停止,这个时候只是标记一下 GC Roots
能直接达到的对象,由于只是标记一层所以整个速度相对会比较快。
(2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
并发标记是一个 GC Roots
扫描的过程,会扫描整个链路标记可以回收的对象;由于整个的链路会比较长,所以相对会耗时久一点,不过由于这个过程是并发的,所以对用户线程运行是没有影响的,不会STW
(3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
再次标记,会 STW
不过速度也较快。
(4). 并发清除,回收内存空间,时间很长。
清除之前扫描的所有垃圾对象,所以会相对比较耗时,不过这个阶段是可以并发进行的所以对用户线程的运行不会有影响,不会STW
。
总结一下CMS:
整个 CMS
垃圾回收器是基于标记-清除算法的,先通过三个过程标记出需要清理的对象,然后再进行清理。整个过程中初始标记和重新标记会触发 STW
,其他两个阶段是并发进行的。
标记-清除算法会产生内存碎片,所以不适合需要频繁回收的年轻代,所以只适合老年代。产生碎片是 CMS
的缺点,并发是 CMS
的优点,毕竟任何一个收集器都会有优缺点。
参数:
"-XX:+UseCMS-CompactAtFullCollection" 用于指定在执行完FullGC 之后 是否对内存空间进行压缩整理,
"-XX:+CMSFullGCs-BeforeCompaction" 设定在执行多少次FullGC 之后对内存空间进行压缩整理
-XX:+CMSInitiatingOccupanyFraction" 设置老年代中的内存使用率达到多少百分比的时候执行内存回收
"-XX:UseConMarkSweepGC" 表示年轻代使用并行收集器,老年代使用CMS
"-XX:ParallelGCThreads" 年轻代并行收集器工作时的线程数量可以使用,一般最好与CPU的数量相当.
# 4、G1
复制算法、标记-整理算法,新生代、老年代 都回收,而不需要与其他收集器搭配;
特点:
能充分利用多CPU、多核环境下的硬件优势;可以并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行; 能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;这样隔离的目的是为了不需要进行整个堆空间的扫描,G1
会将每个 Region
的回收成本进行量化,从而达到一个成本控制,可以在限定的停顿时间内完成回收。
结合多种垃圾收集算法,空间整合,不产生碎片(一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。)
G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒,可以并发进行,降低停顿时间,并增加吞吐量;
G1能实现可预测的停顿是因为它可以避免对堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾的价值(回收的内存大小和时间的比值)大小,在后台维护一个优先列表,每次优先回收价值最大的Region,这也是可预测停顿的实现的原理。
可以分为4个步骤(与CMS较为相似):
(1).、初始标记(Initial Marking)
和CMS的初始标记一样,仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要STW
,但速度很快;
(2).、并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行,并发执行,不会STW;
并不能保证可以标记出所有的存活对象;
(3).、最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;这个过程需要停顿线程,所以需要STW
,且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;
(4).、筛选回收(Live Data Counting and Evacuation)
这一步是和CMS区别最大的地方。
首先排序各个Region的回收价值和成本,然后根据用户期望的GC停顿时间(-XX:MaxGCPauseMillis
)来制定回收计划,最后按计划回收一些价值高的Region中垃圾对象;
这个阶段也可以做到与用户程序一起并发执行(不会STW
),但是因为只回收一部分 Region
,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
参数:
"-XX:+UseG1GC":指定使用G1收集器;
-XX:G1NewSizePercent Java 堆初始化大小 ,默认是整个Java堆大小的5%,
"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;
"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;
应用场景:
面向服务端应用,针对具有大内存、多处理器的机器;
最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;
如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
目前4h8g的服务器,使用的是G1
CMS和G1的总结:
同:
都是并发和分代的垃圾回收器,并且都是低延迟的;
异:
CMS 是基于标记-清除算法
的,吞吐相对G1低一点,只适合在老年代使用,同时年轻代和老年代是物理隔离的,不可预测停顿时间,但是停顿时间短。
G1 是总体来说是基于 标记-整理
(可以合理使用其他),吞吐相对高一点,可预测停顿时间的垃圾回收器,可以同时使用在年轻代和老年代,同时年轻代和老年代是逻辑隔离的。
以上这么多垃圾回收器的个人总结:
serial 是单线程回收新生代和老年代的,后来 parallel New 出现了新生代并行,parallel New 改善后还有Parallel Scavenge,它可以高吞吐,新生代基本就优化了,那年老代怎么办,于是 Parallel Old 出现了,老年代也进行并行回收
一般 Parallel Scavenge收集器+Parallel Old收集器 就可以解决大部分的痛点了
但是为了回收停顿时间 (但是还是不能预测)更好,CMS收集器出现了,并发清理老年代,但是它用的是标记-清除(所以会有内存碎片,如果用标记-整理,就会慢一点)
这样一来,似乎很完美了,但是HotSpot开发团队 又搞了个 G1,使新生代和老年代不再是物理隔离的了,基于复制算法、标记 - 整理多种算法,可预测的停顿时间,一起把新生代、老年代都回收了。
# 5、如何选择垃圾回收器
# 1、单CPU或内存较小,使用Serial收集器
HotSpot虚拟机运行在客户端模式下的默认新生代收集器。
啥叫客户端模式?比如说运行在桌面的应用程序,这种程序分配到的内存一般不会很大,,垃圾收集的停顿时间完全可以控制在十几、几十毫秒,只要不是频繁GC,停顿时间是可以接受的。
# 2、多CUP,关注吞吐量,后台运算而不需要太多交互的分析任务,使用Parallel Scavenge收集器
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;
而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
# 3、多CPU,关注服务的响应速度,希望系统停顿时间尽可能短,使用ParNew+CMS收集器组合
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。 CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的增加而下降。
# 4、服务期内存较大,可以使用G1收集器。
用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。
以上大部分来自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
有空需要再看看《深入理解Java虚拟机》,补充一下各种垃圾回收器的优缺点和使用场景
参考:
- https://www.cnblogs.com/cxxjohnson/p/8625713.html
- 介绍CMS垃圾回收器的好文:https://blog.csdn.net/zqz_zqz/article/details/70568819