垃圾回收机制(GC)
导读
1、一个对象一生经历了什么?
2、如何判断对象是否可用?
3、引用计数法和可达性分析算法各自优缺点?
4、哪些对象可以作为GC ROOT?
5、垃圾回收的时候如何快速寻找根节点?
6、垃圾回收算法有哪些?各自优缺点?
7、有哪些垃圾回收器?各自优缺点?适用什么场景?
1、对象回收处理过程
2、判断用户是否可用计算
2.1、引用计数算法
如上图,给对象一个引用技术refCount。每有一个对象引用它,计时器加1,当它为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。
弱引用:
WeakReference<Car> weakCar = new WeakReference<Car>(car);每次垃圾收集,弱引用的对象就会被清理
虚引用:
幽灵引用,不能用来获取一个对象的实例,唯一用途:当一个虚引用引用的对象被回收,系统会收到这个对象被回收的通知。
3、HotSpot中如何实现判断是否存在与GC Roots相连接的引用链
第一小节流程图里的是否存在与GC Roots相连接的引用链 这个判断子流程是怎么实现的呢,这节我们来仔细探讨下。
一般的,我们都是选取可达性分析算法,这里主要阐述怎么寻找GC Root以及如何检查引用链。
3.1、枚举根节点
如上图,在一个调用关系为:
ClassA.invokeA() --> ClassB.invokeB() -->doinvokeB() -->ClassC.execute()
的情况下,每个调用对应一个栈帧,栈帧里面的本地变量表存储了GC Roots的引用。
如果直接遍历所有的栈去查找GC Roots,效率太低了。为此我们引入了OopMap和安全点的概念。
安全点和OopMap
如上图,在源代码编译的时候,会在特定位置下记录安全点,一般为:
1、循环的末尾
2、方法返回前 或者调用方法的call指令后
3、可能抛出异常的位置
通过安全点把代码分成几段,每段代码一个OopMap。
OopMap记录栈上本地变量到堆上对象的引用关系,每当触发GC的时候,程序都先跑的最近的安全点,然后自动挂起,然后在触发更新OopMap,然后进行枚举类GC ROOT,进行垃圾回收:
安全区域:在一段代码片段之中,引用关系不会发生变化,因此在这个区域中的任意位置开始 GC 都是安全的。如处于Sleep或者Blocked状态的线程。
为了在枚举GC Roots的过程中,对象的引用关系不会变更,所以需要一个GC停顿。
还有一种抢先式中断的方式,几乎没有虚拟机采用:先中断所有线程,发现线程没中断在安全点,恢复它,继续执行到安全点。
找到了该回收的对象,下一步就是清掉这些对象了,HotSpot将去交给CG收集器。
4、垃圾回收算法
概览图
4.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
再次触发Young GC,扫描Eden区和from区,把存活的对象复制到To区,清空Eden区和from区。如果此时Survivor区的空间不够了,就会提前把对象放入老年代。
默认的,这样来回交换15次后,如果对象最终还是存活,就放入老年代。
交换次数可以通过JVM参数MaxTenuringThreshold进行设置。
4.2.5、JVM内存模型
JDK8之前
JDK8
如上图,JDK8的方法区实现变成了元空间,元空间在本地内存中。
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 Olde收集器
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。