代码改变世界

.net 垃圾回收学习 The Truth About Garbage Collection [Java][翻译]

2011-08-23 23:17  一一九九  阅读(260)  评论(0编辑  收藏  举报

  From:http://java.sun.com/docs/books/performance/1st_edition/html/JPAppGC.fm.html#998394      

   GC有可能是Java Platform上被最多用户误解的特性。GC的目的以及对外宣称的都是将应用程序开发人员从内存管理的职责上释放出来,然而,这个目的就没有轻易的达到,在另外一方面,有些开发人员经常做一些操作要求进行垃圾回收,做的事情反而比要求的更多一些。对GC有一个比较稳固的理解是编写鲁棒、高性能的JAVA Platform程序的必要条件。

         这个附录提供了对垃圾回收机制的一个概览,这些能够帮助你很好的理解内存管理。这个附录也包含一些能够帮你解决复杂问题(比如垃圾回收)的信息。

Why Should you Care about Garbage Collection?

         在你的软件开发平台中,内存的分配和回收占据着重要的角色。你的软件的整体的内存需求能够对你的程序性能产生重大的影响。当你需要大量的RAM的时候,OS会使用Virtual Memory,这显然会对你的程序性能造成影响。而这种情况多数出现在你的内存分配了,却没有合理的释放。尽管JVM负责释放没有使用的内存,你仍让需要让JVM知道哪些内存是不在使用的。了解GC的机制是编写成功的,可扩展的内存的必要条件。

The Guarantees of GC

      JAVA Platform的Specification没有太多的关于GC实际上如何工作的讲述。下面是JVMS(Java virtual Machine Specification)中关于内存管理的关键点:

        Heap在Virtual Machine启动的时候创建。存储Objects的Heap是被GC 自动回收的,对行永远不需要明确的释放。JVM不明确采用某种特殊的内存管理系统,而且根据不同的系统实现要求而采用合适的内存管理系统的技术。

        看起来有些让人糊涂, 事实上GC模型没有严格的限制采用何种算法和技术是十分重要和有用的,一个严格限定的GC模型是不可能在所有的平台上都能够实现的。类似的,从长远来看,它可能影响特定平台的优化措施从而影响特定平台上的JVM的效率。

        尽管没有一个地方完整的论述所需要的GC算法,大多数的GC模型在Java Language Specification和JVMS中已经隐含的讲述了。尽管没有明确的GC的工作流程,所有的的VM共享的一个大致统一的的对象声明周期如下描述。

The Object Lifecycle

为了讨论GC的机制,有必要先研究一下Object 的声明周期。 一个对象从生到死(被分配内存到其资源被回收利用)大体上经历了如下过程:

  1. Created
  2. In Use(Strongly Reachable)
  3. Invisible
  4. UnReachable
  5. Collected
  6. Finalized
  7. Deallocated

3.1. Created

           当一个对象被创建的时候, 一般会发生如下事情:

  1. 对象所需要的空间被分配
  2. 对象构造函数开始调用或者执行.--开始构造对象.
  3. 基类的构造函数被调用.
  4. 实例初始化器和实例变量初始化器开始运行
  5. 构造函数的剩余部分被执行.

          这些步骤的实际花费和JVM的具体实现有关系, 同时也和Class的构造实现有关系。 需要记住的是这些花费是存在的。一旦对象被创建出来,假设对象被赋值给某个变量,对象的状态立即进入了使用中

      3.2. In Use        

        那些被至少一个强引用持有的对象被认为是处于使用的状态。在JDK1.1.x中,所有的对象都是强引用。JAVA2引入了另外三种形式的应用:Weak, soft, phantom。下面的例子创建了一个对象并且将其赋给了某个变量。

public class CatTest{
   static Vector catList = new Vector();
   static void makeCat()
   {
      Object  cat = new Cat();
      catList.addElement(cat);
   }
   public static void main()
   {
     makeCat();
    //do something else
   }
} 

//创建并且引用一个对象。

下图A-1 显示了在makeCat方法返回之前的时候在VM内部的对象的结构。在这一时刻,有两个强引用指向了cat Object.

%5V%C)ZM~ZS~43A696U~V}M

     当makeCat方法返回的时候,makeCat方法的StackFrame和其上的临时变量被移除了。这样使得Cat对象只有一个来自CatList静态变量的引用(被Vector直接引用)。

3. Invisible

         当一个Object没有任何可以被程序获取的强引用的时候,Object处于invisible状态,即使可能仍然存在一些引用。并不是所有的Object都会经历这一状态,而且这经常让很多程序员糊涂。 下列代码创建了一个不可见的对象。

public void run(){
  try{
    Object foo = new Object();
    foo.doSomething();
}catch(Exception e)
{
//whatever
}
while(true) { //do stuff}//loo forever
}

     在这个例子中,当Try块结束的时候,对象foo超出了try快的范围。看起来此时foo 临时变量将会被弹出stack, 相关联的对象将变成unreachable。 毕竟,一旦try块结束后,这里没有语义定义了程序将会再次引用Object。 然后,一个有效地JVM的实现是不可能当变量foo超出了Scope的时候就将引用置为0的。 被foo引用的对象仍然被强引用的,至少直到run方法返回之前。在这种情况下,看起来不会持续很久。因为不可见得Objects不能被回收,那可能就是内存泄露的一个可能的源头。假如你进入了这种状况,你需要明确的将你的引用置为NULL来触发垃圾回收。

 

3.4 UnReachable

          当没有任何强引用指向一个对象的时候,Object进入了Unreachable状态。当一个对象处于UnReachable状态的时候, 它GC的回收对象列表中.(it is a candidate for collection)。 注意:成为GC的回收的对象的列表不意味着它会被立即回收。 JVM有可能推迟回收直到有对对象的内存空间的理解需要。

          需要注意的在内存中并不仅仅有强引用持有对象,一定有来自GC Root的引用对象。GC是一个特殊的类变量,包含:

  • 在Stack上的临时变量(任何线程)
  • 静态的变量(来自任何类的)
  • 来自JNI内部代码的特殊引用。

         循环强引用并不会一定会导致内存泄露。考虑如下代码: 创建了两个对象,并且让他们互相引用。

public void buildDog(){
   Dog newDog = new Dog();
   Tail newTail = new Tail();
   newDog.tail = newTail;
   newTail.Dog = newDog;
}
 
图A-2显示了在BuildDog返回之前的对象引用图。在方法返回前,有BuildDog方法中来自
Stack上临时变量的强引用指向了Dog和Tail.

R3]3)GYLC(5F94K611D)WNO

图A-3显示方法BuildDog返回时候的对象引用图。在这个时点上,Dog和Tail从Root上来说是Unreachable的,并且可以被回收的(尽管VM不会立即回收这些对象)。

20N)EJAE5O)0EQ[1$D2960L

3.5 Collected

   当GC意识到一个对象处于Unreachable的时候,GC将该对象纳入了即将释放的队列,此时对象处于Collected的状态。 假如一个对象有Finalize方法,那么把GC把该对象标识为需要Finalization的,假如对象没有Finalize方法,那么GC直接把该对象标识为已经Finalized的状态。

   假如一个类定义了一个Finalizer,那么那个类的任何实例都需要在被释放之前调用Finalizer。这意味着对象的释放因为包含finalizer而被推迟了。

3.6 finalized

   当一个对象在被调用了Finalize方法之后仍然是Unreachable的话,对象此时处于Finalized的状态。 一个finilized的对象是需要被释放的。注意VM的实现控制了Finalizer什么时候会被调用。唯一确定的事情是添加一个Finalizer会延长一个对象的生命周期。这意味着你想让对象短暂存活而加入了Finalizer是比较糟糕的注意。你最好自己做一些清理工作而不需要依赖与Finalizer方法。使用Finalizer会导致其后的重要资源不会在一个确定的时间内被释放。假如你考虑通过Finalizer来确保重要的资源在一个明确的时间内被释放掉,那么你需要仔细考虑一下。

    在Swing团队的一个QA发现了finalize方法推迟GC的一种情况。QA团队建立了一个压力测试通过使用一个线程来发送人为的事件到GUI来模拟用户输入。 在Test运行在Toolkit的某一个版本几分钟之后,应用程序报了一个内存溢出的错误。问题出现的原因是:发送事件的线程比Finalizer线程运行在一个更高的等级,程序之所以内存溢出是因为10000个图形对象被finalizer队列持有等待一个运行它们Finalizer方法的机会。最终证明这些Graphic对象持有了大量的内部资源。问题被修复了,修复的方式是确保Swing一旦用完一个Graphic对象的时候,立即调用该对象的Dispose方法来尽快的释放内部资源。

   除了延长对象的生命周期,finalize方法也会增加对象的大小。比如说,某些JVM,经典的JVM实现,会添加额外的字段到有finalize方法的对象以确保它们能够被一个finalization queue 持有。

3.7 Deallocated

    Deallocated状态是GC的最后一个状态。假如一个对象在上述工作之后仍然处于Unreachable的状态,那么它进入了即将被Deallocated的状态。再次说明,什么时候以及什么方式Deallocation会发生由JVM决定。