如何判断一个对象是垃圾
我们都知道了当堆中的区域没有足够内存去存放对象时就会触发垃圾回收,那么如何来判断一个对象是不是垃圾呢?
1.
引用计数法:
一旦相互持有引用,就导致对象永远没法被回收
例如:
给对象定义一个引用计数器,当该对象被引用时该计数器就++,引用结束时就–。那么当垃圾回收时就会回收掉计数器为0的对象。
那么就有个问题,如果两个本身是无用的对象相互引用,瞒天过海,瞒过JVM就会导致这两个对象永远不会被回收掉。
2.
可达性分析
由GC Root出发,开始寻找,看看某个对象是否可达
GC Root:可以是 类加载器、Thread、本地变量表、static成员、常用引用、本地方法栈中的变量等
例如:从GC root开始,被引用的就是可达,即不是无用对象不会被回收,反之就是垃圾对象,可以被回收。
垃圾回收算法
判断一个对象已经是垃圾了,那么它是怎么被回收的呢?
1.
标记清除算法:
分为标记和清除两阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象
缺点:
GC后会产生很多不连续的空间
如下图:浅蓝色就是存活的对象,灰色就是要被回收的对象,白色就是没被使用的空间
垃圾回收之后,即变成如下图所示:
显然会产生很多不连续的空间,假设一个格子时1MB,新建一个对象为4MB,按理说还有15MB的空间,但是没有连续的4MB的空间,所以这个4MB的对象放不下就会提前触发GC了,我们要知道GC次数是越少越好的。
2.
复制算法:
将可用内存按容量划分为大小相等的两块,每次只用其中一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
相比于标记清除算法,解决了不连续空间碎片的问题
缺点:
将空间一分为二,浪费一半的空间
如下图:将空间一分为二,绿色部分为保留的部分,用来复制GC后存活的对象
GC后即变成如下图所示:
存活的对象全部复制到右边,左边就变成了保留的部分,用来存放下次GC后存活的对象
3.
标记整理算法:
标记过程仍然与标记清除算法
一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
相比于上边两种算法,既解决了不连续空间碎片的问题,也解决了浪费一半空间的问题
GC后即变成如下图所示:
分代收集算法
我们知道,堆内存中分为不同的区域即,那么针对不同的区域是不是要去使用不同的垃圾回收算法呢?肯定是的。
Young区:
采用复制算法
原因:
Young区的大部分对象都是朝生夕死的,只有少部分存活。那么复制这个少部分对象的成本就会比较低。Young区中的Eden区和Survivor区都是采用复制算法。
old区:
采用标记清除或标记整理
原因:
因为old区的对象相对于存活时间比较长了,如果用复制算法的话,这时候会有大量的对象存活得不到回收,是不是就意味着要复制大量的对象从而复制的成本会很高的。所以采用标记清除或标记整理的算法更合适。
垃圾收集器
有了上边的垃圾回收算法了,是不是就要对其实现,那么不同的垃圾收集器就是对不同垃圾回收算法的实现
(1)Serial
(2)Serial Old
(3)ParNew
(4)Parallel Scavenge
(5)Parallel Old
(6)CMS
(7)G1
垃圾收集器搭配使用
,如下图:
上边是适用于Young区,下边是适用于old区,G1收集器比较特殊。
Serial垃圾收集器
实现了复制算法。如下图,单线程,GC的时候应用程序会暂停,在jdk1.3之前是主流的收集器。与其搭配使用的可以是Serial Old、CMS。Serial Old与 Serial相似,都是单线程收集器
ParNew垃圾收集器
用于Young区,实现了复制算法。如下图, 多线程,也会停止用户应用程序的线程,对于Serial不同的是ParNew用多线程的方式来进行垃圾收集。与其搭配使用的可以是Serial Old、CMS。
Parallel Scavenge垃圾收集器
用于Young区,实现了复制算法。和ParNew垃圾收集器一样,都是多线程的方式来进行垃圾收集,只不过Parallel Scavenge相比ParNew,更加关注吞吐量
。与其搭配使用的可以是Serial Old、Parallel Old 。
CMS垃圾收集器
用于old区,全称Concurrent Mark Sweep
,一个并发收集器
,更加关注于停顿时间
。上边所说的垃圾收集器,进行垃圾收集的时候,不管是单线程还是多线程,都需要停止用户应用程序的线程来进行垃圾收集。而CMS可以与用户线程并行来执行垃圾收集,如下图:CMS的垃圾收集分为四个阶段:
1.初始标记
:单线程来进行初始标记,这个过程会非常非常的快所以采用单线程,通过初始标记来找到GCROOT能够关联到的对象。
2.并发标记
:多线程来进行标记,防止初始标记不完整,对GCROOT关联进一步的追踪。可以和用户线程并发执行。
3.重新标记
:单线程来进行标记,为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4.并发清理
:与用户线程并发进行,减少停顿时间。
G1垃圾收集器
适用于Young区和old区,这个收集器比较厉害了,是目前比较主流的收集器,这里就作为了解。如下图:
它和CMS相似,更加关注停顿时间,与CMS不同的是,G1收集器用户可以自定义设置一个预期的停顿时间,在进行筛选回收的时候,可以根据用户设置的停顿时间来进行合理的回收。
相关知识总结
垃圾收集器分类
①串行收集器
->Serial和Serial Old->只能有一个垃圾回收线程执行,用户线程停止。适用于内存比较小的嵌入式设备
。
②并行收集器[吞吐量优先]
->parallel Scanvenge和parallel Old->多条垃圾回收线程并行工作,用户线程停止。适用于科学计算、后台处理等交互场景。
③并发收集器[停顿时间优先]
->CMS、G1->用户线程和垃圾收集线程同时进行(不一定是并行的,可能是交替执行),垃圾收集线程在执行的时候不会停止用户线程。适用于相对时间有要求的场景,比如web。
吞吐量和停顿时间
这两个指标也是评价垃圾收集器的标准,其实JVM调优也就是观察这两个变量。
吞吐量:
运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
停顿时间:垃圾收集器进行垃圾回收时用户应用程序的停顿时间
注意:
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效的利用cpu时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多的交互任务。
如何选择合适的收集器
1.优先调整堆的大小,让服务器自己选择
2.如果内存小于100M,使用串行收集器
3.如果是单核,并且没有停顿时间的要求,使用串行或者让JVM自己选
4.如果允许停顿时间超过1秒,选择并行或者JVM自己选
5.如果响应时间最重要,并且停顿时间不能超过1秒,使用并发收集器
如何开启收集器
1.串行收集器
-xx:+UseSelialGC
-xx:+UseSelialOldGC
2.并行(吞吐量优先)
-xx:+UseParallelGC
-xx:+UseParallelOldGC
3.并发收集器(响应时间优先)
-xx:+UseConcMarkSweepGC
-xx:+UseG1GC