本篇尽量体系的介绍各种垃圾回收器和垃圾回收器的算法
对象的死或生
To be , or not to be , that is the question
一个对象的死或生是一个非常重要的问题,在很多面向对象的语言中有自己对于对象回收的方式
引用计数器
这种方式的实现很简单,只需要在对象中维护一个引用计数器,只要这个对象中的引用计数器归零,那么就代表这个对象没有被引用,需要被回收
这种引用计数器的方式非常简单,效率也不低,在Python、部分游戏脚本中都有使用
引用计数器的缺点我们大家都很清楚,就是无法解决循环引用问题,因为这个问题,所以市面上主流的垃圾JVM都没有使用引用计数器的方式
根可达算法
根可达算法非常容易,我们的虚拟机中有被称之为“根”的对象,只要一个对象可以通过根顺藤摸瓜地访问到,那么这个对象就还被引用,没有死亡
以下是根的类型
- 方法区中的类对象的静态引用对象(一个类的static对象)
- 方法区中常量引用的对象,例如字符串常量池中引用的对象(也就是字符串对象,1.8之后字符串被存在堆里)
- 本地方法栈中引用的对象(Native Stacks)
- 被同步锁持有的对象(synchronized锁住的对象)
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
在Java1.2之后,引用就引用了强软弱虚的概念,这里不多做赘述
一个对象的自我拯救
- 当一个对象被判定为垃圾的时候其实它不会立刻就被回收,其实它还有一个拯救自己的机会
- 当一个对象在经历了根可达算法后发现这个对象不可达之后会第二次对这些对象进行一次筛选,如果这个对象没有重写过finalize()方法或者执行过finalize()方法,那么这个对象就会被回收;如果这个对象有必要执行finalize()方法,那么这个对象就会被放到一个队列F-Queue,在这个队列里面的对象都会被挨着执行finalize()方法,在这个finalize()方法里面就有机会让这个对象脱离无引用的状态——将this赋予给某个对象的静态引用对象。在执行了所有finalize()方法之后就会在做最后一次引用检测,如果检测通过,那么这个对象还可以继续存活
- 这个方法强烈不建议使用,最好直接将这个方法忘了
方法区与对象回收
- 一个对象的常量的是否回收判定很简单,就是通过根可达算法来判定,但是方法区中对象的回收与否非常的麻烦,要满足下面所有的条件
- 这个类的实例已经全部被回收,也就是说这个类对象的派生实例全部被回收
- 加载这个类的类加载器已经被回收
- 该类对应的class对象没有在其他地方被引用,构造,简单地说就是没有地方通过反射得到对象实例
- 这里我们也可以发现,想要在方法区中进行回收带来的收益极其低微,这同时也是Java1.7版本的永久带回收效益低带来方法区内存溢出的危险之一
垃圾回收算法
分代收集理论
在谈垃圾回收算法之前,我们需要了解垃圾回收的一些重要理论,很多市面上的虚拟机都是按照这么一套理论进行设计的,这个理论包括下面三点
弱分代假说:大多数的对象生命周期都很短
强分代假说:经历了越多次垃圾回收的对象,生命力越顽强
跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
跨代引用假说很有意思,因为如果我们只按照上面的两条假说来设计虚拟机的话,那么原本被分离的对象有可能存在跨代应用。如果我们的虚拟机准备在新生代里面进行垃圾回收,在垃圾回收的时候我们的年轻代中的GC Root有可能无法达到某些对象,但是这些对象有可能被老年代里面的对象引用,所有我们为了完整找到所有垃圾,还需要将老年代里面的对象进行一遍根可达计算,这样非常浪费资源,于是虚拟机会维护一个容器,里面直接存放了跨代引用的对象,这样在进行根可达算法的时候直接就可以遍历这个容器中的对象,不再需要将整个老年代遍历一遍
这个理论指导了虚拟机将堆内存进行分代,将不同年龄带的对象存储在不同区域
Mark-Sweep(标记清除)
- 标记清除算法一共有两个部分
- 标记:在这个阶段会对对象进行标记,可以标记垃圾,也可以标记非垃圾
- 清楚:这个阶段就会将找到的所有垃圾一起清楚
- 标记清楚一共有两个主要缺点
- 执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低
- 内存空间的碎片化问题,标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作
Mark-Compact(标记压缩/标记整理)
标记压缩算法也有两个部分
标记:对对象进行标记
整理:在这个阶段,会将对象给移动到空间的前面,将整个空间整理的齐整
这个算法的优缺点非常明显
- 优点:没有碎片空间
- 缺点:效率比之Mark-Sweep来说更加的低,在进行整理的阶段会将所有的工作线程都停止,然后专心地整理空间,这个时间被称为stw;对象需要移动,那么对象的地址空间就会发生改变,那么这些对象的引用也需要进行改变
Copying(复制算法)
复制算法非常的简单
将要回收的空间分成两个想等大小的空间,每一次只使用一个部分
当一个部分需要进行垃圾回收的时候,会将其中使用的空间进行标记,记录出所有的存活对象,然后将这些存活的对象移动到另外一片空间
这个算法的优劣非常明显
- 优点:这个算法可以让空间中没有碎片
- 缺点:这个算法需要浪费一半的空间;当对象需要被移动的时候,对象的引用还是需要进行改动
关于这个算法,有个非常有意思的事情。一个人借鉴了Copying算法,设计了虚拟机的新生代规则,以至于这个新生代的规则到了如今还在沿用。这个人根据弱引用假说实验得出98%的对象都会在第一次回收的时候被回收,于是他就将堆空间的新生代划分成了3个部分,分别是eden、survivor from、survivor to,这三个空间之间的比例是8 : 1 : 1,每一次垃圾回收都会回收eden区域和survivor区域中的一个区域,然后将这两个区域中存活的对象移动到另外一个survivor区域
经典垃圾回收器
Serial
- 这是一个最古老的垃圾回收器,从名字上我们就可以看得出,这个垃圾回收器是一个单线程的垃圾回收器,这个垃圾回收器分为两类,一个是年轻代的就叫做serial,一个是老年代的叫做serial old;
- serial使用的算法是copying;serial old使用的算法是mark compact
- serial算法的历史虽然非常古老,但是这个垃圾回收器并不是一无是处,这个垃圾回收器可以使用在单线程、内存几十兆的机器上
Parallel
- 这个垃圾回收器其实就是Serial垃圾回收器的多线程版本,区别并不大;这个垃圾回收器的年轻代就叫做 Parallel;老年代就叫做 Parallel Old
ParNew+CMS
- ParNew其实和Parallel垃圾回收器没有太大的区别,他们之间主要的区别是ParNew可以在CMS的某个运行期间运行,可以说ParNew是为了CMS设计的,同时CMS唯一可以配合的年轻代垃圾回收器就是ParNew
- CMS垃圾回收器虽然有非常多的问题,但是这个垃圾回收器可以算得上是里程碑式的设计,这个垃圾回收器开启了并发回收的时代,顺带一提,CMS垃圾回收器是唯一一个可以老年代单独进行垃圾回收的垃圾回收器,其他的垃圾回收器如果老年代开始垃圾回收的话,那么也会开启年轻代的垃圾回收
- CMS的优点缺点在以前的文章中有详细的解释,这里不多做赘述
文档信息
- 本文作者:JunHua yin
- 本文链接:https://yin-JH.github.io/2021/04/06/%E9%98%85%E8%AF%BB-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA-%E8%A1%A5%E5%85%85JVM%E7%9F%A5%E8%AF%86%E4%BD%93%E7%B3%BB%E7%B3%BB%E5%88%97(%E4%BA%8C)%E4%B9%8B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E5%92%8C%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E5%99%A8%E7%AE%97%E6%B3%95/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)