共计 1446 个字符,预计需要花费 4 分钟才能阅读完成。
垃圾回收算法有很多种,目前商业虚拟机常用的是 分代回收算法 ,但最初并不是用这个算法的
我们来看一下垃圾收集算法的背景知识
标记 - 清除算法
最基础的垃圾回收算法,顾名思义,整个回收过程分两步:
1. 逐个标记
2. 统一回收
该算法可以算是后来所有垃圾回收算法的基石(后续所有算法都有标记和清除这两步,只不过策略上有了一些优化)
这里值得一说的是这个 标记 虚拟机是如何判断一个对象是“活”还是“死”?
因此又引出两种标记算法:
1. 引用计数算法
引用计数算法非常简单且高效,当一个对象被引用一次则 + 1 不再被引用则 -1,当计数为 0 就是不可能在被使用的对象了,但是这种算法存在一个致命的缺陷:两个对象相互引用对方呢?所以,这种算法肯定不能用,pass 掉
2. 可达性分析算法
目前的标记算法主流实现都是用的可达性分析算法。就是以一个叫 GC Roots 的对象为起点,通过引用链向下搜索,如果一个对象通过引用链无法与 GC Roots 对象链接,就视为可回收对象,上面说的那种相互引用的情况自然也解决了。
扩展:即使是可达性分析中不可达的对象也并不是非死不可,只是暂处‘缓刑’,真正宣告一个对象死亡至少还要经历两次标记过程:当被判定不可达之后那么他被第一次标记并进行筛选,若对象没有覆盖 finalize()方法或者 finalize()方法已经被虚拟机调用过就‘放生’,如果被判定需要执行 finalize()方法就会被放到一个叫 F -Queue 的队列中进行第二次标记对象被再次被引用就会放生,否则就会被回收。
说到这里敏锐的小伙伴可能以及察觉到了,上面都在说 引用 所以引用的定义就显得尤为关键了
JDK1.2 后 Java 对引用的概念进行了扩充,将引用分为: 强引用、软引用、弱引用、虚引用 四种
强引用:用处很大,无论如何都不会被 GC 回收
软引用:有一定用处但不大,内存实在不够才会在内存溢出之前回收掉
弱引用:比软引用强度更弱一些,GC 会让它多活一轮,下一轮就回收
虚引用:必回收,唯一作用就是被 GC 回收时会收到一个系统通知
复制算法
前面说的标记 - 清除算法其实两个过程效率都很低,并且回收之后内存被‘抠出很多洞’内存碎片化严重,此时如果过来了一个较大的对象,找不到一整块连续的内存空间就不得不提前触发另外一次 GC 回收。
而复制算法则选择将内存一分为二每次只使用其中一半,满了之后将存活的对象整齐复制到另一块干净的内存上,将剩下的碎片一次性擦除,简单高效。但是也存在一个很大的缺陷,那就是可用内存变为原来的一半了。
分代收集算法
事实上后来 IBM 公司经过研究发现,98% 的对象都是‘朝生夕死’,所以并不需要 1:1 的划分内存,即我们现在常用的分代收集算法:
根据对象的存活周期将内存划分为两块,分别为新生代和老年代,然后对各代采用不同的回收算法,在新生代中大部分是‘朝生夕死’的对象,继续将新生代 8:2 划分为 Eden 区和 survival 区,其中 survival 区 1:1 分成 s0 和 s1 两块,采用之前说的复制算法,减少内存碎片的产生。
新生代满了会进行一次 minor GC,minor GC 存活的对象转移到 survival 区,survival 区满了就会将 survival 区进行回收,存活的 survival 区对象复制到另外一块 survival 区中,并且 survival 区对象每存活一轮年龄 + 1 当到达一定年龄就会前往老年代。
扩展:JVM 何时会进行全局 GC
01. 手动调用 System.GC 但也不是立即调用
02. 老年代空间不足
03. 永生代空间不足
04. 计算得知新生代前往老年代平均值大于老年代剩余空间