5、JVM~分代收集理论和垃圾回收算法
5.1 分代收集理论
当前虚拟机的垃圾收集大多都采用分代收集算法,这种算法只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
5.2 垃圾回收算法
5.2.1 标记-清除算法
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单,但是会带来两个明显的问题:
效率问题(如果需要标记的对象太多,效率不高)
空间问题(标记清除后会产生大量不连续的碎片)
5.2.2 标记-复制算法
为了解决标记-清理算法的问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。这种算法的缺点是比较浪费内存空间,每次只有1/2的空间可以被使用。
5.2.3 标记-整理算法
根据老年代的特点推出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
5.3 如何判断是否垃圾对象
5.3.1 引用计数器法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,主要的原因是它很难解决对象之间相互循环引用的问题。比如,对象A和对象B之前相互引用,除此之外没有其他引用,但是他们因为互相引用对方,导致它们的引用计数器都不是0,导致GC无法回收。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
5.3.2 可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等。
5.3.3 如何判断对象之间的引用关系
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用;
强引用:普通的变量引用
public static User user = new User();
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。
弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用;
5.3.4 finalize()方法
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
- 第一次标记并进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。 - 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:工作中不要用finalLize方法,垃圾回收前如果对象实现啦finalLize方法,会回调用finalize方法进行一次拯救,而且只会执行一次,因为finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间如果比较长,可能出现耗尽资源之前,gc却仍未触发。
5.4 如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类呢?类需要同时满足下面3个条件才能算是 “无用的类” :
- 该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。