0%

Java-对象生命周期

阅读更多

1 前言

本篇博客主要介绍JVM如何判断一个对象是否存活,以及JVM何时会对对象进行回收

2 引用计数算法

很多教科书判断对象是否存活的算法:给对象添加一个引用计数器

  • 每当有一个地方引用它,计数器就加1
  • 当引用失效时,计数器值减1
  • 任何时刻计数器为0的对象就是不可能再被使用的

客观地说,引用计数算法的实现简单,判定效率也很高,在大部分情况下都是一个不错的算法,著名案例有

  • 微软的COM(Component Object Model)技术
  • 使用ActionScript 3的FlashPlayer
  • Python语言
  • 在游戏脚本领域被广泛应用的Squirrel

但是Java虚拟机没有选用引用计数来管理内存,主要原因是它难以解决对象之间相互循环引用的问题

3 可达性分析算法

在主流的商用程序语言(Java、C#,包括古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活。这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链项链时,证明对象不可用

在Java语言中,GC Roots的对象包括以下几种

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

4 再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与"引用"有关

JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表一个引用。在这种定义下,只有被引用或没有被引用两种状态

JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为

  • 强引用(String Reference):程序代码中普遍存在,类似Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用(Soft Reference):描述一些还有用但并非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收返回之中进行第二次回收,如果这次回收还没有足够的内存,将抛出内存溢出的异常
  • 弱引用(Weak Reference):描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,即无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用(Phantom Reference)
    • 这四种引用强度依次减弱:一个对象是否有虚引用的存在,完全不会对其生存事件构成影响,也无法通过虚引用来取得一个对象实例,设置虚引用关联的唯一目的:在这个对象被收集器回收时收到一个系统通知
    • "虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动
    • 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中
引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 gc运行后终止
虚引用 ? ? ?

5 生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是"非死不可",这时候它们暂时处于"缓刑"阶段

要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为"没有必要执行"
  2. 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立、低优先级的Finalizer线程去执行它
  • 这里的执行指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是:如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,这可能导致F-Queue队列中其他对象永久处于等待,导致整个内存回收系统崩溃

finalize()方法是对象逃脱死亡命运的最后一次机会。稍后GC将对F-Queue中的对象进行第二次小规模的标记。如果对象要在finalize()中成功拯救自己–只要重新与引用链上的任何一个对象建立关联,那么在第二次标记时将它移出"即将回收"的集合

  • 比如一个静态域赋值为该对象的this
  • 譬如把自己(this)赋值给某个类变量或者对象的成员变量

并不鼓励使用finalize()来拯救对象

  • 因为finalize()并不等同于C++中的析构函数
  • finalize()运行的代价高昂,不确定性大
  • 对于finalize()能做的工作,使用try-finally语句会更好、更及时
  • 甚至可以忘掉有finalize()这种语法

6 回收方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的

  • Java虚拟机规范不要求虚拟机在方法区实现垃圾收集
  • 而且在方法区中进行垃圾收集"性价比"一般比较低:在堆中
  • 在新生代中,常规应用进行一次垃圾收集一般可回收70%-90%的空间,永久代的垃圾收集效率远低于此

永久代的垃圾收集主要回收两部分内容:废弃常量无用的类

  • 回收废弃常量与回收Java堆中的对象非常类似,判定一个常量是否废弃很简单,即判断该常量是否被引用
  • 判定一个类是否无用较为苛刻,需要满足3个条件:
    • 该类所有的实例都已经被回收
    • 加载该类的ClassLoader(类加载器)已经被回收(因此只有通过自定义类加载加载的类才有可能被卸载,否则都是通过Bootstrap ClassLoader\Extension ClassLoader\App ClassLoader来进行加载)
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    • 虚拟机可以对满足上述3个条件的无用类进行回收,仅仅是可以,并不像对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class、-XX:+TraceClassLoading、-XX:TraceClassUnLoading查看类加载和卸载信息,在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出

7 参考

  • 《深入理解Java虚拟机》