sxyyu 2019-11-18
垃圾回收算法有很多种,目前商业虚拟机常用的是分代回收算法,但最初并不是用这个算法的
我们来看一下垃圾收集算法的背景知识
标记-清除算法
最基础的垃圾回收算法,顾名思义,整个回收过程分两步:
1.逐个标记
2.统一回收
该算法可以算是后来所有垃圾回收算法的基石(后续所有算法都有标记和清除这两步,只不过策略上有了一些优化)
这里值得一说的是这个标记 虚拟机是如何判断一个对象是“活”还是“死”?
因此又引出两种标记算法:
1.引用计数算法
引用计数算法非常简单且高效,当一个对象被引用一次则+1不再被引用则-1,当计数为0就是不可能在被使用的对象了,但是这种算法存在一个致命的缺陷:两个对象相互引用对方呢?所以,这种算法肯定不能用,pass掉
2.可达性分析算法
目前的标记算法主流实现都是用的可达性分析算法。就是以一个叫GC Roots的对象为起点,通过引用链向下搜索,如果一个对象通过引用链无法与GC Roots对象链接,就视为可回收对象,上面说的那种相互引用的情况自然也解决了。
扩展:即使是可达性分析中不可达的对象也并不是非死不可,只是暂处‘缓刑’,真正宣告一个对象死亡至少还要经历两次标记过程:当被判定不可达之后那么他被第一次标记并进行筛选,若对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过就‘放生’,如果被判定需要执行finalize()方法就会被放到一个叫F-Queue的队列中进行第二次标记对象被再次被引用就会放生,否则就会被回收。
finalize()方法
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)
说到这里敏锐的小伙伴可能以及察觉到了,上面都在说引用所以引用的定义就显得尤为关键了
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当到达一定年龄就会前往老年代。
扩展01:JVM何时会进行全局GC
01.手动调用System.GC 但也不是立即调用 02.老年代空间不足 03.永生代空间不足 04.计算得知新生代前往老年代平均值大于老年代剩余空间
扩展02:在压力测试时,发现FullGC频率很高,如何解决
01.观察GC日志,判断是否有内存泄漏,或者存在内部不合理点 02.调整JVM参数,如新生代、老年代大小 S0+S1大小比例,选用不同的立即回收器 03.Dump内存,做进一步的对象分析 04.压测脚本的编写,性能问题解决前可以发现问题,并对解决方案进行验证