垃圾收集器(Garbage Collector(GC))是一直伴随着 Java 程序员的最有争议的问题之一。我们接受了独立的收集器的原则,但是控制该收集器的迫切需要常常被证明是不可抗拒的。典型情况下,您用好了一个或多个资源,并指望它们会被回收。但它们并没有被回收。一定出现了问题!IBM 为我们提供了一种强行执行 GC 的方法 — 调用 System.gc(),那么使用这一调用就一定没有问题,是这样吗? 错。几乎总是错的。GC 称为“内存管理器”会更好,因为这是它真正做的事情。同 C/C++ 一样,它根据应用程序的请求分配内存。但是与 C/C++ 不同的是,内存释放是 GC 单独执行的。因为内存管理的复杂性中 99% 与未使用内存的自动定位及释放有关。一般我们提到“GC”就是指 Java 内存管理。Java 虚拟机(Java virtual machine(JVM))有它自己的内存池。我们不使用一般的本地方法,如 malloc。当然,这个内存池只是从本机 OS 中分配的一大块。Java 内存被称作堆。当 Java 对象需要内存时,就从堆中进行内存分配。通常,JVM 在被要求时才对内存进行分配。当且仅当内存分配发生错误(内存溢出)时才执行 GC。是的,是的,我知道 Java 1.1.8 有“异步 GC”操作。在 IBM Java SDK 中,这个循环实际上从未被激活。因为性能的缘故,异步 GC 操作被配置成只有在您的应用程序确实什么事也不做的时候才会运行。即使您的应用程序静止,也要经过相当长时间的超时,异步 GC 才会运行。在 Java 2 中已经没有异步操作了。因此,对于 IBM Java SDK 来说,GC 完全是一个同步操作,而且仅在内存分配发生错误时才发生。GC 的工作方式
我们当然不会完整地讨论这些内容;关于这一主题已经有整本专著问世了。我们只要讨论基础知识。GC 通过扫描 JVM 里运行的所有线程的堆栈和寄存器来执行。如果它发现有些东西似乎是对 Java 堆内的引用,GC 将会继续查下去。如果该引用确实是对对象的引用,那么 GC 将跟随该对象内的所有引用。被引用的对象以及该对象所引用的所有对象都将被“标记”(或标志)。显然,只有在线程被中止的情况下它才能起作用,因此,GC 要停止 JVM 里的所有线程才能开始执行,当然,其中不包括它正在其上运行的那个线程。标记阶段结束时,GC 会扫描堆,并对照在标记阶段标志的那些对象检查堆里的所有对象。任何没有被标记的对象都是没有被任何线程引用的对象,因而,这些对象将会被回收并被放回到空闲池。此处需要指出的重点是:GC 是一个停止一切的操作。
GC 是一个同步操作。
GC 需要扫描每个线程的堆栈。
GC 需要扫描整个 Java 堆。
就 CPU 使用和时间而言,GC 操作代价高昂。这就是我们要尽可能减少它的执行的原因。虽然它效率极高,但在一个有许多线程并且堆的大小达多兆字节的环境中运行需要时间。这解释了相当常见的说法 -“我的对象在应该被回收的时候并没有被回收”。它们符合被回收的条件;可是没有必要运行 GC,所以它并没有执行。System.gc()
System.gc() 是“内存分配错误是运行 GC 的唯一原因”这一规则的一个例外。虽然这可能是一种例外情况,但并不是一个好主意。请相信我。文档说明该调用设置了一个标志,该标志表明在 JVM 非常想要时可以运行 GC。System.gc() 调用实际上做的事情是:如果调用它的时候有一个 GC 循环正在运行,那么就怱略这次调用;否则,就开始一次完整的 GC 循环。这就是说,每次(或 99.9% 次)调用 System.gc() 的时候,您都会开始一个完整的 GC 循环。也许根本没有必要运行 GC,但是您还是强行让它执行完整个标记/扫描过程。您完全可以把 System.gc() 看作是 System.StopEverythingForAWhile() 调用或 System.SlugMyPerfomance() 调用。对于这个调用,有一个可能的正当理由。因为如果没有这个调用,直到堆用完 GC 才会运行。如果您有一个千兆字节的堆,在 GC 清理全部垃圾时,您将遭到相当大的打击,然后您可能会考虑以固定间隔强行执行一个 GC 循环以达到“少量多次”的效果。但是,在这种情况下,您考虑调整堆的大小会更好些。我们为什么要强行执行 GC?
到处散布 System.gc() 调用的诱惑非常大。在某一阶段,我们总是遇到“内存溢出”的问题,第一反应是认为 GC 出了问题,并强行执行一个循环。尽管这常常能改善局面(至少是暂时能),并且似乎验证了“GC 出了问题”这一想法,但事实也许并非如此。请记住,GC 处于食物链的底部。由于 GC 的常规操作是彻底检查系统运行时的状态,所以代码中其它地方所犯的错误常常会被 GC 暴露出来。这样,别处所犯的错误导致 GC 没有响应,而程序员认为 JVM 是在 GC 处异常结束的。因此,应该是 GC 的问题。追查要从定位问题开始,因此,我们强行让 GC 在代码中的不同地方执行。不幸的是,一旦了解了 GC 查出的问题的真相,我们总是会忘记删除所有那些 System.gc() 调用。在一天结束时,最终在您的代码中会有成十、成百或者上千个强行执行 GC 的调用(您有多少个呢?)。除了缓慢的性能和在强制执行的非必要 GC 循环上花费过多的时间之外,这使您一无所获。当允许 GC 按您预想的那样运行时,您可能会感到吃惊,一个劣等系统的响应速度也如此之快。

解决方案 »

  1.   

    垃圾收集器(Garbage Collector,GC)对Java程序员来说,基本 上是透明的,但是一个优秀的Java程序员必须了解GC的工作原理、如何优化GC的性能、如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统、实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能。本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能。
    一GC的基本原理Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的"。GC将负责回收所有"不可达"对象的内存空间。对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象(详见参考资料1 )。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往往有不同的实现算法。这也给Java程序员的开发带来行多不确定性。本文研究了几个与GC工作相关的问题,努力减少这种不确定性给Java程序带来的负面影响。二 增量式GC( Incremental GC )GC在JVM中通常是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CPU。当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Java程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在停顿时间和回收率之间进行权衡。一个好的GC实现允许用户定义自己所需要的设置,例如有些内存有限有设备,对内存的使用量非常敏感,希望GC能够准确的回收内存,它并不在意程序速度的放慢。另外一些实时网络游戏,就不能够允许程序有长时间的中断。增量式GC就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在整体性能上可能不如普通GC的效率高,但是它能够减少程序的最长停顿时间。下图就表示了,增量式GC和普通GC的比较。其中灰色部分表示线程占用CPU的时间。
    Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在运行Java程序时增加-Xincgc的参数。HotSpot JVM增量式GC的实现是采用Train GC算法。它的基本想法就是,将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁高和具有相关性的对象放在一队中,随着程序的运行,不断对组进行调整。当GC运行时,它总是先回收最老的(最近很少访问的)的对象,如果整组都为可回收对象,GC将整组回收。这样,每次GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。Train GC算法是一个非常好的算法,具体算法见参考资料4。三 详解finalize函数finalize是位于Object类的一个方法,该方法的访问修饰符为protected,由于所有类为Object的子类,因此用户类很容易访问到这个方法。由于,finalize函数没有自动实现链式调用,我们必须手动的实现,因此finalize函数的最后一个语句通常是super.finalize()。通过这种方式,我们可以实现从下到上实现finalize的调用,即先释放自己的资源,然后再释放父类的资源。根据Java语言规范,JVM保证调用finalize函数之前,这个对象是不可达的,但是JVM不保证这个函数一定会被调用。另外,规范还保证finalize函数最多运行一次。很多Java初学者会认为这个方法类似与C++中的析构函数,将很多对象、资源的释放都放在这一函数里面。其实,这不是一种很好的方式。原因有三,其一,GC为了能够支持finalize函数,要对覆盖这个函数的对象作很多附加的工作。其二,在finalize运行完成之后,该对象可能变成可达的,GC还要再检查一次该对象是否是可达的。因此,使用finalize会降低GC的运行性能。其三,由于GC调用finalize的时间是不确定的,因此通过这种方式释放资源也是不确定的。通常,finalize用于一些不容易控制、并且非常重要资源的释放,例如一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。在这种情况下,程序员应该以通过程序本身管理(包括释放)这些资源为主,以finalize函数释放资源方式为辅,形成一种双保险的管理机制,而不应该仅仅依靠finalize来释放资源。下面给出一个例子说明,finalize函数被调用以后,仍然可能是可达的,同时也可说明一个对象的finalize只可能运行一次。
    class MyObject{
        Test main; //记录Test对象,在finalize中时用于恢复可达性
        public MyObject(Test t)
        {  
    main=t; //保存Test 对象
        }
        protected void finalize()
        {
    main.ref=this;// 恢复本对象,让本对象可达
    System.out.println("This is finalize");//用于测试finalize只运行一次
        }
    }class Test {
    MyObject ref;
      public static void main(String[] args) {
      Test test=new Test();
      test.ref=new MyObject(test);
      test.ref=null; //MyObject对象为不可达对象,finalize将被调用
      System.gc(); 
      if (test.ref!=null) System.out.println("My Object还活着");
    }

    运行结果:
    This is finalize
    MyObject还活着此例子中,需要注意的是虽然MyObject对象在finalize中变成可达对象,但是下次回收时候,finalize却不再被调用,因为finalize函数最多只调用一次。四 程序如何与GC进行交互Java2增强了内存管理功能, 增加了一个java.lang.ref包,其中定义了三种引用类。这三种引用类分别为SoftReference、WeakReference和PhantomReference。通过使用这些引用类,程序员可以在一定程度与GC进行交互,以便改善GC的工作效率。这些引用类的引用强度介于可达对象和不可达对象之间。它们的引用强度如下图所示:
    创建一个引用对象也非常容易,例如如果你需要创建一个Soft Reference对象,那么首先创建一个对象,并采用普通引用方式(可达对象);然后再创建一个SoftReference引用该对象;最后将普通引用设置为null。通过这种方式,这个对象就只有一个Soft Reference引用。同时,我们称这个对象为Soft Reference 对象。Soft Reference的主要特点是据有较强的引用功能。只有当内存不够的时候,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。以下给出这种引用类型的使用伪代码;
    //申请一个图像对象
    Image image=new Image();//创建Image对象

    //使用 image

    //使用完了image,将它设置为soft 引用类型,并且释放强引用;
    SoftReference sr=new SoftReference(image);
    image=null;

    //下次使用时
    if (sr!=null) image=sr.get();
    else{
    //由于GC由于低内存,已释放image,因此需要重新装载;
    image=new Image();
    sr=new SoftReference(image);

    Weak引用对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象,GC总是进行回收。Weak引用对象更容易、更快被GC回收。虽然,GC在运行时一定回收Weak对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map结构中,引用数据量较大的对象,一旦该对象的强引用为null时,GC能够快速地回收该对象空间。该例子见参考资料4;Phantom引用的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些对象,它们执行完了finalize函数,并为不可达对象,但是它们还没有被GC回收。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。五一些Java编码的建议根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议。最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null。我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。 
    尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。 
    如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory。 
    注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。 
    当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。