2 JVM中对象的生命周期
在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段 (Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)、释放阶段 (Free)。上面的这7个阶段构成了JVM中对象的完整的生命周期。下面分别介绍这7个阶段。
2.1 创建阶段
在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:
(1) 为对象分配存储空间。
(2) 开始构造对象。
(3) 递归调用其超类的构造方法。
(4) 进行对象实例初始化与变量初始化。
(5) 执行构造方法体。
上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个 规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在 Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:
public class A { ... }
这个声明等同于下面的声明:
public class A extends java.lang.Object { ... }
下面是在创建对象时的几个关键应用规则:
(1) 避免在循环体中创建对象,即使该对象占用内存空间不大。
(2) 尽量及时使对象符合垃圾回收标准。
(3) 不要采用过深的继承层次。
(4) 访问本地变量优于访问类中的变量。
关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到而且我们很容易犯类似的错误,例如下面的代码:
... ... for (int i = 0; i < 10000; ++i) { Object obj = new Object(); System.out.println("obj= " + obj); } ... ...
上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:
... ... Object obj = null; for (int i = 0; i < 10000; ++i) { obj = new Object(); System.out.println("obj= " + obj); } ... ...
采用下面的编写方式,仅在内存中保存一份对该对象的引用,而不像上面的代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。
另外,不要对一个对象初始化多次,这同样会带来较大的内存开销,降低系统性能,如:
public class A { private Hashtable table = new Hashtable(); public A() { // 将Hashtable对象table初始化了两次 table = new Hashtable(); } }
正确的方式为:
public class B { private Hashtable table = new Hashtable(); public B() { } }
不要小看这个差别,它却使应用软件的性能相差甚远,如图所示。
看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则开发出来的应用也是个低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。
2.2 应用阶段
在对象的应用阶段,对象具备下列特性:
可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。
对象引用的结构层次示意如图所示。
强引用
强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。
软引用
软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用 对象还能保证在Java抛出OutOfMemory异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度 的使用内存而不引起OutOfMemory。下面是软引用的实现代码:
... ... import java.lang.ref.SoftReference; ... A a = new A(); ... // 使用a ... // 使用完了a,将它设置为soft引用类型,并且释放强引用; SoftReference sr = new SoftReference(a); a = null; ... // 下次使用时 if (sr != null) { a = sr.get(); } else { // GC由于低内存,以释放a,因此需要重新装载; a = new A(); sr = new SoftReference(a); } ... ...
软引用技术的引进使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期 较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建,提高应用对内存的使用效率,提高系统稳定性。但 事物总是带有两面性的,有利亦有弊,在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态 在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。
弱引用
弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对 象,GC总是进行回收。Weak引用对象更容易、更块被GC回收。虽然,GC在运行时一定回收Weak对象,但是复杂关系的Weak对象群常常需要好几次 GC的运行才能完成。Weak引用对象常常用于Map结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在 了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:
... ... import java.lang.ref.WeakReference; ... A a = new A(); ... // 使用a ... // 使用完了a,将它设置为weak引用类型,并且释放强引用; WeakReference wr = new WeakReference(a); a = null; ... // 下次使用时 if (wr != null) { a = wr.get(); } else { a = new A(); wr = new WeakReference(a); } ... ...
虚引用
虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。
注意 在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
2.3 不可视阶段
当一个对象处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。
... ... public void process() { try { Object obj = new Object(); obj.doSomething(); } catch (Exception e) { e.printStackTrace(); } while (isLoop) { // ... loops forever // 这个区域对于obj对象来说已经是不可视的了 // 因此下面的代码在编译时会引发错误 obj.doSomething(); } } ... ...
如果一个对象在使用完之后,而且在其可视区域不再使用,此时应该主动将其设置为空。可以在上面的代码行obj.doSomething();下添加代码行obj = null;这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。
2.4 不可到达阶段
处于不可到达阶段的对象在虚拟机的对象引用根集合中再也找不到直接或间接的强引用,这些对象一般是所有线程栈中的临时变量,所有已装载的类的静态变量或者是对本地代码接口(JNI)的引用。
2.5 可收集阶段、终结阶段与释放阶段
当一个对象处于可收集阶段、终结阶段与释放阶段时,该对象可能处于下面三种情况:
(1) 回收器发现该对象已经不可到达。
(2) finalize 方法已经被执行。
(3) 对象空间已被重用。
当对象处于上面三种情况下,该对象就处于可收集阶段、终结阶段与释放阶段了。