JVM垃圾收集子系统(七大垃圾收集器+垃圾收集算法+引用计数/可达性分析+finalize())

Serial收集器(新生代收集器):

出现最早,资格最老的一个垃圾收集器。
单线程的收集器,所有用户线程执行到这里,都会暂停,执行垃圾回收的保护线程。

缺点:会造成卡顿,用户体验效果差,分配内存空间大的场景下回收效率差,卡顿时间长。
优点:单线程省去了切换线程的开销,内存较小的应用回收效率较高。
一般应用于客户端桌面应用的垃圾回收。
回收算法:复制算法 或者称为 标记-复制算法

标记复制算法就是把新生代内存空间划分为两个相等大小的区域,垃圾回收时把正在使用中的带有引用标记的对象复制到另外一片区域,复制完成后把要回收的区域进行GC内存清理。非常暴力,不会出现碎片化问题。缺点就是对内存空间要求较大,要双倍的内存空间。(所以才能在广阔的新生代Eden区使用)

ParNew收集器(新生代收集器)

它其实Serial的多线程版本,它也是会暂停所有的用户线程等待GC,但是因为它是多线程并行执行(注意是并行执行不是并发),所以它的收集效率会比Serial要高,所以造成的卡顿时间会很短。

并发与并行:
并发是指单CPU中,交替轮换着执行多个任务,表面上的同时进行实际上是交替进行的。
并行是指多核CPU中,每个CPU都在运行一个程序,真正意义上的同时执行。

缺点:如果是单核CPU环境下会有线程切换问题,效率反而比Serial要低。
优点:多核CPU环境下充分利用多核优势,缩短了垃圾回收的时间,缩短了卡顿时间。
回收算法(和Serial一样的):复制算法 或者称为 标记-复制算法

标记复制算法就是把新生代内存空间划分为两个相等大小的区域,垃圾回收时把正在使用中的带有引用标记的对象复制到另外一片区域,复制完成后把要回收的区域进行GC内存清理。非常暴力,不会出现碎片化问题。缺点就是对内存空间要求较大,要双倍的内存空间。(所以才能在广阔的新生代Eden区使用)

Parallel Scavenge收集器(新生代收集器):

一样是在新生代内存中工作的垃圾收集器,一样是复制算法,一样是多线程并行收集器,这个可以说是ParNew的升级版。它升级的内容有:
(1)可供我们设置垃圾收集的停顿时间,通过设置-XX:MaxGCPauseMillis (毫秒)来规定工作时间,时间一到,垃圾收集打扫工作没做完也退出,这样一定程度上能够保证用户线程不会被卡顿太久。
(2)可供我们设置垃圾收集器的吞吐量,也就是垃圾回收时间和运行程序总时间的一个比值,通过设置-XX:GCTimeRatio 后,会对原来的回收区域进行划分,通过吞吐量进行多次回收,每次时间间隔都很短(原来是每次停顿三秒钟,一分钟收集一次,现在变成每次停顿0.3秒,一分钟收集十次,因为0.3秒用户不容易察觉停顿了进行垃圾回收),所以同样是优化了用户体验。

Serial Old 收集器(老年代收集器):

这个其实是Serial收集器的老年代版本,也是单线程收集器。
回收算法是:标记整理:

标记整理算法是结合了标记清除和标记复制两个算法的优点,它还是会按照引用来标记存货对象,然后整理就是把存活对象压缩到堆中的一块,并按顺序排放,同时把堆的其它全部对象清除。

为什么不用跟Serial老大哥一样的标记复制呢?就像前面讲的那样,老年代的内存空间小,而且存活对象生命周期长,假如用了标记清除,每一次的GC都会复制一遍,首先就没有那么大的空间,其次大部分对象都是存活甚至是全部对象存活,那么复制就非常浪费时间并且是做无用功。

Parallel Old 收集器(老年代收集器):

很明显是Parallel Scavenge的老年代版本啦,一样的多线程并行,一样的可设置吞吐量和收集工作时间。
算法是标记整理,具体的原因跟上面一样。

CMS收集器(老年代收集器):

它的简写其实是:Concurrent Mark Sweep , 翻译过来就是 并发标记清除。
所以,它就比较清晰了,它是并发的,算法采用的是标记清除算法。

标记清除算法就是从根结点开始标记所有被引用的对象,然后遍历堆,将没有引用标记的对象清除掉。

分为四个步骤进行垃圾回收:

(1)初始标记:利用可达性分析法对可达对象进行标记,这一步是单线程执行,用户线程有停顿。
(2)并发标记:初始标记后,这一步是对已有标记的对象进行并发跟踪,执行时间长,但是用户线程不需要停顿。
(3)重新标记:并发标记跟踪过程中,产生的新垃圾对象,也就是跟踪标记过程中失去了标记引用的对象,都识别为可回收对象,这里进行重新标记,也是单线程的,需要用户线程有停顿。
(4)并发清理:并发清楚标记的垃圾对象,不需要停顿。

优点显而易见就是把标记和清除的耗费时间的过程都做并发处理,靠着多核CPU优势将用户卡顿减至最小。
缺点就是标记清除算法的通病就是内存碎片化问题严重。并且单核环境下的并发会占用大量资源导致性能很低。

G1收集器:

这是一款JDK1.7后引用的一款很强大的垃圾回收,它打破了之前严格划分的新生代老年代的内存空间的严格划分,它是能同时管理全部内存空间的一款垃圾收集器,可以想象成一块画布上无数个颜料块,黑色代表老年代,白色代表新生代,杂乱无章的揉杂再一起,而它为了区分,会有一张remember set的表,记录每一块颜料块里那个对象的引用情况,并且它会预测停顿时间,并且优先去回收效率最高的那一块内存。

它会利用之前CMS收集器一样的执行步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)清除回收(CMS在这里是直接回收,而G1还会进行分析评估,去选择性回收最大收益的一块内存)

所以G1 的优点包括了CMS收集器的优点,并发与并行同在,老年代与新生代没有严格划分,有利于内存空间整合(也就是清除一块原来是老年代对象的内存后,可以放入新生代对象),同时也解决了碎片化问题(回收掉之后,会融入原来的零散的内存,方便下一次大对象的存入),并且在回收时做到可预测的停顿。但是它通过remember set表上的记录信息,对不同生命周期的对象回收还是有不同的回收模式。

最后,分析对象是否要被回收上(也就是判断对象是否存活),有两个比较主要的判断方法:

(1)引用计数法:

很简单就是一个对象有一个引用,每被引用一次就增加一个引用计数,删除一个引用就减少一个引用计数,在GC的时候只回收引用计数为0 的对象。
但是假如有三个类,ABC,A引用B,B引用C,C引用A,那这就是循环引用,这三个对象互相循环,导致引用都不为0;但是对于外界来说它们三个都已经是不可用的了,导致三块内存都无法被回收,这就是引用计数法的缺陷。

(2)可达性分析法:

这是一个从外由内的分析法,有一个概念叫GC root , 它其实说白了就是外界对某个对象的引用,当一个对象设置为null时候也就是断开了引用,引用链断了就证明该对象已经不可达了,即使还有零星几个引用计数,也要被回收。

这就是有的对象看起来还活着,但是其实它已经死了。

GC Root说白了就是引用,那么能引用到对象内存的地方都有哪里?

(1)栈帧,局部变量表中除了基本类型外,还有引用类型。
(2)方法区(元空间),定义为静态的对象在加载时候分配内存,同时也就会有引用指向。
(3)本地方法栈(native等非Java语言的方法)。

笔试最喜欢问的 finalize()方法:

finalize()还有个好兄弟finally,都是笔试故意来为难你的基础题,异常捕获机制中的finally我们用过很多,但是finalize()…说实话…
正常业务开发谁会用?你用吗?我不用,你用吗?呵,正经业务开发谁会用。讲究!
finalize()是Object中的方法,所有对象在被回收临死前,都会调用一次这个方法,处理一些回收内存前必须进行的工作(例如你可以重写finalize方法,在某个对象被回收前打印一句“额我死了!”),你也可以在临死前最后一刻给该对象又加回一个引用,让该对象不被回收,但是这些都是花里胡哨的没什么用。真的要用这个对象,保持住它的外部引用不就可以了吗?所以这基本上算是一个规范约束。没其他特别的含义,就做一个通知,然后就被回收。

以上就是我的个人总结,鞠躬!

猜你喜欢

转载自blog.csdn.net/whiteBearClimb/article/details/104245607