无论你是跟同事、同学、上下级、同行、或者面试官讨论技术问题的时候,很容易卷入JVM大型撕逼现场。为了能够让大家从大型撕逼现场中脱颖而出,最近我苦思冥想如何把知识点尽可能呈现的容易理解,方便记忆。于是就开启了这一系列文章的编写。为了让JVM相关知识点能够形成一个体系,arthinking将编写整理一系列的专题,以尽量以图片的方式描述相关知识点,并且最终把所有相关知识点串成了一张图。持续更新中,欢迎大家阅读。有任何错落之处也请您高抬贵手帮忙指正,感谢!
导读
- 一个对象的一生经历了什么?
- 如何判断对象是否可用?
- 引用计数法和可达性分析算法各有什么优缺点?
- 哪些对象可以作为GC ROOT?
- 垃圾回收的时候如何快速寻找到根节点?(安全点和OopMap)
- 垃圾回收算法有哪些?各有什么优缺点?
- 有哪些垃圾回收器?各有什么优缺点?适用什么场景
1、对象回收处理过程
2、判断用户是否可用算法
2.1、引用计数算法
如上图,给对象一个引用计数器refCount。每有一个对象引用它,计数器加1,当refCount=0的时候,表示对象不再可用。
缺点
很难解决循环引用的问题:
objA.instance = objB;
objB.instance = objA;
如上,即使 objA 和 objB 都不再被访问之后,他们依旧互相引用这,所以计数器不为0。
2.2、可达性分析算法
如上图,从GC Roots开始向下搜索,连接的路径为引用链;
GC Roots不可达的对象被判为不可用;
可作为 GC Root的对象
如上图,虚拟机栈帧中本地变量表引用的对象,本地方法栈中,JNI引用的对象,方法区中的类静态属性引入的对象和常量引用对象都可以作为 GC Root。
关于引用类型
强引用:类似Object a = new Object();
软引用:SoftReference<String> ref = new SoftReference<String>("Hello world");
,OOM前,JVM会把这些对象列入回收范围进行二次回收,如果回收后内存还是不做,则OOM。软引用SoftReference介绍以及简单用法cache
弱引用:WeakReference<Car> weakCar = new WeakReference<Car>(car);
,每次垃圾收集,弱引用的对象就会被清理。
虚引用:PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>());
,幽灵引用,不能用来取得一个对象的实例,唯一用途:当一个虚引用引用的对象被回收,系统会受到这个对象被回收了的通知。利用虚引用PhantomReference实现对象被回收时收到一个系统通知
3、HotSpot中如何实现判断是否存在与GC Roots相连接的引用链
第一小节流程图里面的是否存在与GC Roots相连接的引用链
这个判断子流程是怎么实现的呢,这节我们来仔细探讨下。
一般的,我们都是选取可达性分析算法,这里主要阐述怎么寻找GC Root以及如何检查引用链。
3.1、枚举根节点
如上图,在一个调用关系为:
ClassA.invokeA() --> ClassB.invokeB() --> doinvokeB() -->ClassC.execute()
的情况下,每个调用对应一个栈帧
,栈帧里面的本地变量表存储了GC Roots的引用。
如果直接遍历所有的栈去查找GC Roots,效率太低了。为此我们引入了OopMap和安全点的概念。
安全点和OopMap
如上图,在源代码编译的时候,会在特定位置下记录安全点,一般为:
- 循环的末尾;
- 方法返回前或者调用方法的call指令后;
- 可能抛出异常的位置。
通过安全点把代码分成几段,每段代码一个OopMap。
OopMap记录栈上本地变量到堆上对象的引用关系,每当触发GC的时候,程序都都先跑到最近的安全点然后自动挂起,然后再触发更新OopMap
,然后进行枚举GC ROOT,进行垃圾回收:
安全区域
:在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。如处于Sleep或者Blocked状态的线程。
为了在枚举GC Roots的过程中,对象的引用关系不会变更,所以需要一个GC停顿。
还有一种抢先式中断的方式,几乎没有虚拟机采用:先中断所有线程,发现线程没中断在安全点,恢复它,继续执行到安全点。
找到了该回收的对象,下一步就是清掉这些对象了,HotSpot将去交给CG收集器,详细见后续小节说明。
4、垃圾回收算法
概览图
4.1、标记-清除算法
4.1.1、算法描述
- 标记阶段:标记处所有需要回收的对象;
- 清除阶段:标记完成后,统一回收所有被标记的对象;
4.1.1、优点
还没想到
4.1.2、不足
- 效率不高:标记和清除两个过程效率都不高;
- 空间问题:产生大量不连续的内存碎片,进而无法容纳大对象提早触发另一次GC。
4.2、复制算法
4.2.1、算法描述
- 将可用内存分为容量大小相等的两块,每次只使用其中一块;
- 当一块用完,就将存活着的对象复制到另一块,然后将这块全部内存清理掉;
4.2.2、优点
- 不会产生不连续的内存碎片;
- 提高效率:
- 回收:每次都是对整个半区进行回收;
- 分配:分配时也不用考虑内存碎片问题,只要移动堆顶指针,按顺序分配内存即可。
4.2.3、缺点
- 可用内存缩小为原来的一半了,适合GC过后只有少量存活的
新生代
,可以根据实际情况,将内存块大小比例适当调整; - 如果存活对象数量比较大,复制性能会变得很差。
4.2.4、JVM中新生代的垃圾回收
如下图,分为新生代和老年代。其中新生代又分为一个Eden区和两个Survivor去(from区和to区),默认Eden : from : to 比例为8:1:1
。
可通过JVM参数:-XX:SurvivorRatio
配置比例,-XX:SurvivorRatio=8
表示 Eden区大小 / 1块Survivor区大小 = 8
。
第一次Young GC
当Eden区满的时候,触发第一次Young GC,把存活对象拷贝到Survivor的from区,清空Eden区。
第二次Young GC
再次触发Young GC,扫描Eden区和from区,把存活的对象复制到To区,清空Eden区和from区。如果此时Survivor区的空间不够了,就会提前把对象放入老年代。
默认的,这样来回交换15次后,如果对象最终还是存活,就放入老年代。
交换次数可以通过JVM参数
MaxTenuringThreshold
进行设置。
4.2.5、JVM内存模型
JDK8 之前
JDK8
如上图,JDK8的方法区实现变成了元空间,元空间在本地内存中。
JVM内存相关参数:
内存分配如何保证并发?
4.3、标记-整理算法
4.3.1、算法描述
- 标记过程与标记-清楚算法一样;
- 标记完成后,将存活对象向一端移动,然后直接清理掉边界以外的内存。
4.3.2、优点
- 不会产生内存碎片;
- 不需要浪费额外的空间进行分配担保;
4.3.3、不足
- 整理阶段存在效率问题,适合老年代这种垃圾回收频率不是很高的场景;
4.4、分代收集算法
当前商业虚拟机都采用该算法。
-
新生代
:复制算法(CG后只有少量的对象存活) -
老年代
:标记-整理算法 或者 标记-清理算法(GC后对象存活率高)
5、垃圾回收器
这一步就是我们真正进行垃圾回收的过程了。
本节概念约定:
并发
:用户线程与垃圾收集线程同时执行,但不一定是并行,可能交替执行;
并行
:多条垃圾收集线程并行工作,单用户线程仍处于等待状态。
以下是垃圾收集器概览图
5.1、Serial收集器
5.1.1、特点
串行化
:在垃圾回收时,必须赞同其他所有工作线程,知道收集结束,Stop The World
;
在单CPU模式下无线程交互开销,专心做垃圾收集,简单高效。
5.1.2、适用场景
- 特别适合限定单CPU的环境;
-
Client模式
下的默认新生代收集器,用户桌面应用场景分配给虚拟机的内存一般不会很大,所以停顿时间也是在一百多毫秒以内,影响不大。
5.2、ParNew收集器
Parallel New?
5.2.1、特点
- Serial收集器的
多线程版本
;
5.2.2、适用场景
- 许多运行在
Server模式
下的虚拟机中的首选新生代收集器; - 除了Serial收集器外,只有它能和
CMS收集器
搭配使用。
-XX:+UseConcMarkSweepGC
选型默认使用ParNew收集器。也可以使用-XX:+UseParNewGC
选项强制指定它。ParNew收集器在单CPU环境比Serial收集器效果差(存在线程交互开销)。
CPU数量越多,ParNew效果越好,默认开启收集线程数=CPU数量。可以使用
-XX:ParallelGCThreads
参数限制垃圾收集器的线程数。
5.3、Parallel Scavenge收集器
5.3.1、特点
- 新生代收集器,使用复制算法,并行多线程;
-
吞吐量优先收集器
:CMS等收集器会关注如何缩短停顿时间,而这个收集器是为了吞吐量而设计的。
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )
也就是说整体垃圾收集时间越短,吞吐量越高。
5.3.2、适用场景
- 可以高效利用CPU时间,尽快完成程序的运算任务,适合后台运算不需要太多交互的任务;
5.3.3、相关参数
-
-XXMaxGCPauseMillis
:设置最大垃圾收集停顿时间,大于0的毫秒数;缩短GC停顿时间会牺牲吞吐量和新生代空间。新生代空间小,GC回收就快,但是同时会导致GC更加频繁,整体垃圾回收时间更长。
-
-XX:GCTimeRatio
:设置吞吞量大小。0~100的整数,垃圾收集时间占总时间的比率,相当于吞吐量的倒数。- 19: 1/(1+19)= 5%,即最大GC时间占比5%;
- 99: 1/(1+99)=1%,即最大GC时间占比1%;
-XX:+UseAdaptiveSizePolicy
:GC自适应调节策略开关,打开开关,无需手工指定-Xmn
(新生代大小)、-XX:SurvivorRatio
(Eden与Survivor区比例)、-XX:PretenureSizeThreshold
(晋升老年代对象年龄)等参数,虚拟机会收集性能监控信息,动态调整这些参数,确保提供最合适的 停顿时间或者最大吞吐量。
5.4、Serial Old收集器
5.4.1、特点
Serial收集器的老年代版本。使用单线程,标记-整理算法。
5.4.2、适用场景
- 主要给Client模式下的虚拟机使用;
- Server模式下,量大用途:
- JDK1.5版本之前的版本与Parallel Scavenge收集器搭配使用;
- 作为CMS收集器的后备预案,发生Concurrent Mode Failure时使用。
5.5、Parallel Old收集器
5.5.1、特点
Parallel Scavenge收集器的老年代版本,使用多线程,标记整理算法。
5.5.2、使用场景
- 主要配合Parallel Scavenge使用,提高吞吐量。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑这个组合。
JDK1.6之后提供,之前Parallel Scavenge只能与Serial Old配合使用,老年代Serial Old无法充分利用服务器多CPU处理器能力,拖累了实际的吞吐量,效果不如ParNew+CMS组合;
5.6、CMS收集器
Concurrent Mark Sweep
5.6.1、特点
- 设计目标:获得最短回收停顿时间;
- 注重服务响应速度;
- 标记-清除算法;
5.6.2、缺点
- 对CPU资源敏感,虽然不会导致用户线程停顿,但是会占用一部分线程(CPU资源)而导致应用程序变慢,吞吐量降低;
- CMS收集器无法处理
浮动垃圾
。在CMS并发清理阶段,用户线程会产生垃圾。如果出现Concurrent Mode Failure失败,会启动后备预案:临时启动Serial Old收集器重新进行老年代垃圾收集,停顿时间更长了。-XX:CM SInitiatingOccupancyFraction
设置的太高容易导致这个问题; - 基于标记-清除算法,会产生大量空间碎片。
5.6.3、使用场景
- 互联网网站或者B/S系统的服务器;
5.6.4、相关参数
-
-XX:+UseCMSCompactAtFullCollection
:在CMS要进行Full GC时进行内存碎片整理(默认开启)。内存整理过程无法并发,会增加停顿时间; -
-XX:CMSFullGCsBeforeCompaction
:在多少次 Full GC 后进行一次空间整理(默认0,即每一次 Full GC 后都进行一次空间整理); -
-XX:CM SInitiatingOccupancyFraction
:触发GC的内存百分比,设置的太高容易导致Concurrent Mode Failure失败(GC过程中,用户线程新增的浮动垃圾,导致触发另一个Full GC)。
CMS为什么要采用
标记-清除算法
?CMS主要关注低延迟,所以采用并发方式清理垃圾,此时程序还在运行,如果采用压缩算法,则会涉及到移动应用程序的存活对象,这种场景下不做停顿是很难处理的,一般需要停顿下来移动存活对象,再让应用程序继续运行,但是这样停顿时间就边长了,延迟变长。CMS是容忍了空间碎片来换取回收的低延迟。
5.7、G1收集器
G1:Garbage-First,即优先回收价值最大的Region(注1)。
注1:G1与收集器将整个Java堆换分为多个代销相等的独立区域,跟踪各个Region里面的垃圾堆积的价值大小,优先回收价值最大的Region。
如上图,G1收集器分为四个阶段:
-
初始标记
:只标记GC Roots能直接关联到的对象,速度很快。并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能够在正确可用的Region中创建新对象,这阶段需要停顿线程; -
并发标记
:GC RootsTracing过程。该阶段对象变化记录在线程Remembered Set Logs中。 -
最终标记
:修正并发期间因用户程序运作而导致标记产生变动的部分对象的标记记录。把Remembered Set Logs数据合并到Remembered Set中。这个阶段需要停顿,但是可并行执行; -
筛选回收
:对各个Region回收价值和成本进行排序,根据用户期望Gc停顿时间制定回收计划。与CMS不一样,这里不用和用户线程并发执行,提高收集效率,使用标记-整理算法,不产生空间碎片。
5.7.1、特点
-
并行与并发
:并发标记,并行最终标记与筛选回收; 分代收集
-
空间整合
:基于标记-整理算法,不会产生碎片。 -
可预测的停顿
:G与收集器将整个Java堆换分为多个代销相等的独立区域,避免在整个Java堆中进行全区域的垃圾回收,跟踪各个Region里面垃圾堆积的价值大小,后台维护一个优先列表,每次根据运行的收集时间,优先回收价值最大的Region。
本文为arthinking
基于相关技术资料和官方文档撰写而成,确保内容的准确性,如果你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。
大家可以关注我的博客:itzhai.com
获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。
如果您觉得读完本文有所收获的话,可以关注
我的账号,或者点赞
啥的。关注我的公众号,及时获取最新的文章。
References
How Does Reference Counting Garbage Collection Work
利用虚引用PhantomReference实现对象被回收时收到一个系统通知
请问 JVM线程的栈在64位Linux操作系统上的默认大小是多少?
Understanding Java Garbage Collection Algorithms
Mark-and-Sweep: Garbage Collection Algorithm
本文为arthinking
基于相关技术资料和官方文档撰写而成,确保内容的准确性,如果你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。
大家可以关注我的博客:itzhai.com
获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。
如果您觉得读完本文有所收获的话,可以关注
我的账号,或者点赞
啥的。关注我的公众号,及时获取最新的文章。
本文作者: arthinking
博客链接: https://www.itzhai.com/jvm/java-garbage-collection-theory.html
版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。