java虚拟机02-垃圾收集与对象的引用
1.概述
1.1垃圾回收需要完成的三件事
Lisp语言是第一门真正使用内存分配和动态垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就开始思考GC需要完成的三件事:
(1) 哪些内存需要回收
(2) 什么时候回收
(3) 如何回收
1.2 为什么要了解GC和内存分配
虽然经过半个多世纪的发展,目前的动态分配与内存回收技术已经相当成熟,但是当需要排查各种内存溢出、内存泄漏问题时、当垃圾收集成为系统达到更高并发量的瓶颈时,
我们就需要对这些“自动化”的技术实施必要的监控和调节。
2.判断对象存活的依据
2.1 引用计数法
原理:在对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不再使用的。
优点:实现简单,判定效率高,在大部分情况下是一个不错的算法。
缺点:他很难解决对象之间相互循环引用的问题。
//如下图所示,当一个对象中有一个引用指向另一个对象,而另一个对象中有一个引用指向这个对象,就造成计数器不可能为0并且两个对象都不再使用的问题 class Test{ public static void main(String[] args){ Obj o1 = new Obj(); Obj o2 = new Obj(); o1.reference = o2; o2.reference = o1; } }
2.2 可达性分析算法
原理:通过一系列的GC Roots对象作为起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可用的。
如图所示,object5、object6、object7虽然相互关联,但是没有与GC Roots相连,所以他们会被判定为可回收的对象
Java中可以作为GC Roots 的对象:
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法中JNI(即一般说的native方法)引用的对象
优点:避免了对象之间循环引用无法回收的问题,回收准确度增加
缺点:需要进行遍历,开销比引用计数法大
3.再谈引用
3.1 概述
在JDK1.2之前 java对引用的定义很传统,如果reference类型的数据中存储的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很存粹,太过于狭隘。
我们还希望有这样一类对象存在:当内存空间还足够时,还能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景
3.2 引用概念扩充
在JDK1.2之后 java对引用的概念进行了扩充,将引用分为 强引用(Stong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种引用的强度依次减弱
3.3 强引用
强引用就是指程序代码中普遍存在的 Object obj = new Object();这类的引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象
3.4 软引用 SoftReference
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出内存溢出异常
3.5 弱引用 WeakReference
弱引用也是用来描述非必须对象的,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否充足,都会回收掉只被弱引用回收的对象。
3.6虚引用 PhatomReference
虚引用也成为幽灵引用或幻影引用,他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得对象的实例。
为一个对象设置一个虚引用的唯一目的就是能在这个对象被垃圾收集器回收时得到一个系统通知。
4. 对象生存还是死亡
即使在可达性分析算法中不可达的对象(没有GC Roots与其相连),也并非是“非死不可的”,这时候他们暂时处于“缓刑”阶段。要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有与
GC Roots相连的引用链,那他将会被第一次标记并且筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果一个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做 F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的,低优先级的Finalizer线程去执行它,这里所谓的执行是指虚拟机会触发这个方法,
但是无法保证它运行结束(因为如果一个对象的finalize()方法执行的缓慢或者有死循环存在,可能会导致整个队列永久等待,甚至导致整个回收系统崩溃)。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己(只需要重新与引用链上任何一个对象关联即可,
即使是把this关键字赋值给某个对象的成员变量也可以),那样在第二次进行标记的时候它将会被移出“即将被回收”的集合。如果对象这时还没有逃脱,那他基本上就真的被回收了。
注意:对象的finalize()方法只能被调用一次,如果对象再一次被回收,他的finalize()方法也不会被再次执行。
1 public class TestFinalize { 2 //用于自救的引用 3 public static TestFinalize SAVE_HIMSELF = null; 4 5 6 public void isAlive() { 7 System.out.println("yes i alive!!"); 8 } 9 10 11 @Override 12 protected void finalize() throws Throwable { 13 super.finalize(); 14 System.out.println("finalize was executed !"); 15 //将自己赋值给引用链其他对象 16 TestFinalize.SAVE_HIMSELF = this; 17 } 18 19 20 public static void main(String[] args) throws Exception{ 21 22 SAVE_HIMSELF = new TestFinalize(); //创建对象 23 24 System.out.println("------------first--------------"); 25 SAVE_HIMSELF = null; //释放对象 26 System.gc(); //调用垃圾回收器进行回收 27 Thread.sleep(500); //由于finalize方法优先级低 所以等待0.5秒 等待finalize执行 28 if(null != SAVE_HIMSELF) { 29 //若对象自救成功 则可以调用对象的方法 30 SAVE_HIMSELF.isAlive(); 31 }else { 32 //若对象自救失败 33 System.out.println("dead !"); 34 } 35 36 System.out.println("-------------second--------------"); 37 //由于finalize只能被调用一次 所以下面这一次一定是失败的 38 SAVE_HIMSELF = null; //释放对象 39 System.gc(); //调用垃圾回收器进行回收 40 Thread.sleep(500); //由于finalize方法优先级低 所以等待0.5秒 等待finalize执行 41 if(null != SAVE_HIMSELF) { 42 //若对象自救成功 则可以调用对象的方法 43 SAVE_HIMSELF.isAlive(); 44 }else { 45 //若对象自救失败 46 System.out.println("dead !"); 47 } 48 49 } 50 51 }
运行结果:
注意:建议大家尽量不要使用finalize() 方法,她并不像C/C++中的析构方法那样。他的运行代价昂高,不确定性大,无法保证各个对象的调用顺序。
5.回收方法区
很多人认为方法区(或者HotPost虚拟机中的永久代)是没有垃圾收集的,java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集。而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是新生代中,常规应用
进行一次垃圾收集一般可以回收70%-90%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收java堆中的对象非常的类似。以常量池中字面量的回收为例:例如一个字符串 abc 已经进入了常量池中,但是当前系统没有任何一个String对象是叫做
abc的。换句话说,就是没有任何String对象引用常量池中的abc对象。如果这个时候发生内存回收,而且必要的话,这个 abc 常量就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用也与此类似。
判断一个常量是废弃常量比较简单,但是判断一个类是 无用的类 的条件相对苛刻很多。需要同时满足下面三个条件才是算是 无用的类
1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
2.加载该类的ClassLoader 已经被回收。
3.该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类方法。
虚拟机可以对满足上述三个条件的无用类进行回收,这里说的仅仅是可以,而不是和对象一样,不使用了就必然被回收。是否对类进行回收 HotSpot虚拟机提供了 -Xnoclassgc参数进行控制。
还可以使用 -verbese:class 以及 -XX:+TraceClassLoading 、-XX:+TraceClassUnLoading 查看类的加载、卸载信息。其他虚拟机可能有些出入。
在大量使用反射、动态代理、CGLib、等ByteCode 框架、动态生成jsp以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。