垃圾回收堆内存

文章来源: http://book.csdn.net/bookfiles/435/index.html

内存管理的话题在C或C++程序设计中讨论得相对较多,因为在C与C++程序设计中需要开发人员自己申请并管理内存,开发人员可以申请/借用(Apply)系统内存并且负责释放/归还(Release)系统内存,如果“只借不还”就会造成系统内存泄露的问题。在Java程序设计中,这些工作由Java虚拟机(JVM)负责处理。所有内存的申请、分配、释放都由JVM负责完成。因此,开发人员就省去了这部分工作,不过这并不意味着开发人员可以完全依赖于JVM的内存管理功能,如果你这样想并在实际的应用开发中也这样做,你所开发的应用的性能,就有可能不是最优的。这是因为,无论配置多么优良的硬件环境其自身资源都是有限的,由于没有合理、科学的使用内存资源,即使是Java应用也会出现内存枯竭的现象。例如,我们经常会遇到的OutOfMemoryException。再者Java语言(其实不只Java语言)的性能极大程度上依赖于其运行的硬件环境资源,而内存又是硬件环境资源中重要的一部分,因此说,如果开发人员开发的Java应用没能有效、合理地使用系统内存,那么这个应用就不可能具备较高的性能,甚至会导致整个系统在运行一段时间后崩溃。本章将对Java应用开发中与内存管理相关的技术做详细的讲解。

2.1  垃圾回收

谈到Java内存管理的话题,就必然会提到垃圾回收的概念,垃圾回收的英文名称为Garbage Collection,简称GC,它是Java程序设计中有关内存管理的核心概念,Java虚拟机(JVM)的内存管理机制被称为垃圾回收机制。因此,要想掌握在开发Java应用时怎样才能合理地管理内存,首先应该了解Java虚拟机的内存管理机制——垃圾回收机制。否则,在不了解垃圾回收具体实现机制的情况下讨论Java程序设计中的内存管理,优化Java应用性能,就有些纸上谈兵,舍本逐末了。

上面我们提到Java程序设计中的内存管理机制是通过垃圾回收来完成的,那么在JVM运行环境中什么样的对象是垃圾呢?下面我们给出了在JVM运行环境中垃圾对象的定义:

一个对象创建后被放置在JVM的堆内存(heap)中,当永远不再引用这个对象时,它将被JVM在堆内存(heap)中回收。被创建的对象不能再生,同时也没有办法通过程序语句释放它们。

我们也可以这样给JVM中的垃圾对象下定义:

当对象在JVM运行空间中无法通过根集合(root set)到达(找到)时,这个对象就被称为垃圾对象。根集合是由类中的静态引用域与本地引用域组成的。JVM中通过根集合索引对象如图2-1所示。

图2-1  JVM中通过根集合索引对象

&注意  图2-1中打了X标记的对象就是不可到达的对象,这些对象就被JVM视为垃圾对象并被JVM回收。JVM将给这些对象打上相应的标记,然后清扫回收这些对象,并将散碎的内存单元收集整合。

这里提到了堆内存的概念,它是JVM管理的一种内存类型,在做Java应用开发时经常会用到由JVM管理的两种类型的内存:堆内存(heap)与栈内存(stack)。有关堆内存的概念,在前面的相关章节中,已经做过相应的介绍。简单地讲,堆内存主要用来存储程序在运行时创建或实例化的对象与变量,例如:我们通过new MyClass()创建的类MyClass的对象。而栈内存(stack)则是用来存储程序代码中声明为静态(static)(或非静态)的方法,JVM、堆内存(heap)与栈内存(stack)三者的关系如图2-2所示。

图2-2  JVM、堆内存与栈内存的关系

下面通过一个实例来看一下堆内存(heap)与栈内存(stack)中所存储对象的类型有哪些不同。

… …

public class BirdTest {

        static Vector birdList = new Vector();   

        static void makeBird () {

              Object bird= new Bird ();

              birdList.addElement(bird);

            }

          public static void main(String[] arg) {

              makeBird ();

          …

    }

}

… …

在上面的代码中声明了一些静态的变量与方法,同时也通过关键字new创建了一些对象实例,下面给出这个简单的类在运行时JVM中堆内存(heap)与栈内存(stack)中所存储的对象情况,如图2-3所示。

图2-3  JVM中堆内存与栈内存中所存储的对象情况

在图2-3中,可以看到我们在类BirdTest中声明的Vector 类的birdList对象,以及在运行时创建的Bird对象都被放在了堆内存(heap)中,而把两个静态方法main()与makeBird()放在了栈内存(stack)中。这说明birdList对象占用了堆内存,静态方法main()与makeBird()则占用了栈内存。在对Java程序设计中内存管理技术做更为深入的讨论之前,有必要再详细地讲一下堆内存(heap)的相关知识。

堆内存

堆内存(heap)在JVM启动的时候就被创建,它是JVM中非常关键的一个内存管理区域。堆内存中所存储的对象可以被JVM自动回收,不能通过其他外部手段回收,也就是说开发人员无法通过添加相关代码的手段,回收位于堆内存中的对象。堆内存(heap)通常情况下被分为两个区域:新对象(new object)区域与老对象(old object)区域。这里又引入了两个有关JVM内存管理的新概念:新对象(new object)区域与老对象(old object)区域。下面分别对这两个概念做一下介绍。

新对象(new object)区域。又可以细分为三个小区域:伊甸园(Eden)区域、From区域与To区域。伊甸园区域用来保存新创建的对象,它就像一个堆栈,新的对象被创建,就像指向该栈的指针(如果你熟悉C语言,应该非常熟悉指针的概念)在不断增长一样,当伊甸园区域中的对象满了之后,JVM系统将要做可到达性测试,主要任务是检测有哪些对象由根集合出发是不可到达的,这些对象就可以被JVM回收,并且将所有的活动对象从伊甸园区域拷到To区域,此时一些对象将发生状态交换,有的对象就从To区域被转移到From区域,此时From区域就有了对象。上面对象迁移的整个过程,都是由JVM控制完成的。当我们在使用一些Java应用服务器软件时,通过其所提供的内存与性能监控界面,会看到这一过程引起的系统内存的变化。在这个过程执行期间,Java虚拟机的性能是非常低下的,这个过程会严重影响正在运行的应用的性能。

老对象(old object)区域。在老对象区域中的对象仍然会有一个较长的生命周期,大多数JVM系统中的垃圾对象,都来源于“短命”对象,经过一段时间后,被转入老对象区域的对象,就变成了垃圾对象。此时,它们都被打上相应的标记,JVM系统将会自动回收这些垃圾对象,建议你不要频繁地强制系统做垃圾回收,这是因为JVM会利用有限的系统资源,优先完成垃圾回收工作,致使应用无法快速地响应来自用户端的请求,这样会影响系统的整体性能,这也正是我们不建议读者自己频繁强制做垃圾回收的原因。

为了使读者能够更清楚地了解垃圾回收的过程,根据上面的讲解,给出了JVM做垃圾回收的过程示意图,如图2-4所示。

图2-4  JVM做垃圾回收的过程示意

通过上面的学习,我们知道垃圾回收与对象的生命周期是紧紧联系在一起的,那么JVM中的对象生命周期是怎样的呢?下面就讲解一下JVM中对象的生命周期的相关知识。

2.2  JVM中对象的生命周期

在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。

2.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-5所示。

图2-5  初始化对象多次所带来的性能差别

看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。

2.2.2  应用阶段

当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:

— 系统至少维护着对象的一个强引用(Strong Reference);

— 所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。

上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。

Java对象引用的结构层次示意如图2-6所示。

图2-6  对象引用的结构层次示意

由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。

1.强引用

强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。

2.软引用

软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:

… …

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)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。

3.弱引用

弱引用(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);

}

… …

弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。

… …

WeakReference wr = new WeakReference(obj);

WeakReference wr = new WeakReference(obj, false);

… …

4.虚引用

虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 Java 终结机制更灵活的方式调度 pre-mortem 清除操作。

&注意  在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

2.2.3  不可视阶段

在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。

… …

public void process () {

    try {

         Object obj = new Object();

         obj.doSomething();

     } catch (Exception e) {

     e.printStackTrace();

     }

     while (isLoop) { // ... loops forever

      // 这个区域对于obj对象来说已经是不可视的了

         // 因此下面的代码在编译时会引发错误

         obj.doSomething(); 

     }

}

… …

如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。

2.2.4  不可到达阶段

处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。

2.2.5  可收集阶段、终结阶段与释放阶段

对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:

(1)垃圾回收器发现该对象已经不可到达。

(2)finalize方法已经被执行。

(3)对象空间已被重用。

当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。

2.3  Java中的析构方法finalize

在C++程序设计中有构造函数与析构函数的概念,并且是内存管理技术中相当重要的一部分,而在Java语言中只有构造器(也可以称为构造函数)的概念,却没有析构器或析构函数的概念。这是因为,理论上JVM负责对象的析构(销毁与回收)工作。也就是上面讲到的垃圾回收的概念。那么Java语言中是否真的不存在与C++中析构函数职能类似的方法?其实Java语言中的finalize 方法与C++语言中的析构函数的职能就极为类似。finalize方法是Java语言根基类Object类中所包含的一个方法,这个方法是保护类型的方法(protected),由于在Java应用中开发的所有类都为Object的子类,因此用户类都从Object对象中隐式地继承了该方法。因此,我们在Java类中可以调用其父类的finalize方法,并且可以覆盖自身继承来的finalize方法。虽然我们可以在一个Java类中调用其父类的finalize方法,但是由于finalize方法没有自动实现递归调用,我们必须手动实现,因此finalize函数的最后一个语句通常是super.finalize()语句。通过这种方式,我们可以实现从下到上finalize的迭代调用,即先释放用户类自身的资源,然后再释放父类的资源。通常我们可以在finalize方法中释放一些不容易控制,并且非常重要的资源,例如:一些I/O的操作,数据的连接。这些资源的释放对整个应用程序是非常关键的。

finalize方法最终是由JVM中的垃圾回收器调用的,由于垃圾回收器调用finalize的时间是不确定或者不及时的,调用时机对我们来说是不可控的,因此,有时我们需要通过其他的手段来释放程序中所占用的系统资源,比如自己在类中声明一个destroy()方法,在这个方法中添加释放系统资源的处理代码,当你使用完该对象后可以通过调用这个destroy()方法来释放该对象内部成员占用的系统资源。虽然我们可以通过调用自己定义的destroy()方法释放系统资源,但是还是建议你最好将对destroy()方法的调用放入当前类的finalize()方法体中,因为这样做更保险,更安全。在类深度继承的情况下,这种方法就显得更为有效了,我们可以通过递归调用destroy的方法在子类被销毁的时候释放父类所占用的资源,例如下面的代码:

1.原始基类A

public class A {

      Object a = null;

      public A() {

          a = new Object();

          System.out.println("创建a对象");

      }

      protected void destroy() {

         System.out.println("释放a对象");

         a = null;

         // 释放自身所占用的资源

         …

      }

      protected void finalize() throws java.lang.Throwable {

         destroy();

         // 递归调用超类中的finalize方法

         super.finalize(); 

      }

  

}

2.一级子类B

public  class B extends A {

    Object b = null;

    public B() {

          b = new Object();

          System.out.println("创建b对象");

    }

    protected void destroy() {

         b = null;

         // 释放自身所占用的资源

         System.out.println("释放b对象");

         super.destroy();

    }

    protected void finalize() throws java.lang.Throwable {

         destroy();

         // 递归调用超类中的finalize方法

         super.finalize(); 

    }

  

}

3.二级子类C

public class C extends B {

    Object c = null;

    public C() {

     c = new Object();

         System.out.println("创建c对象");

    }

    protected void destroy() {

         c = null;

         // 释放自身所占用的资源

         System.out.println("释放c对象");

         super.destroy();

    }

    protected void finalize()throws java.lang.Throwable {

         destroy();

         // 递归调用超类中的finalize方法

         super.finalize(); 

    }

  

}

上面的三个类的继承关系是非常明晰的:A->B->C,类A是原始基类(这是一种习惯叫法),类B继承了类A,类C又继承了类B。其实类A并不是真正意义上的原始基类,上面我们已经提到过Java语言中的原始基类是Object类,尽管我们并没有显式的声明,但这已经是系统约定俗成的了。

为了简单清楚地说明问题,我们在这三个类中分别声明了3个方法,用来论证上面所讲解的知识点,在类A的构造器中我们初始化了一个对象a,在destroy方法中通过a = null;释放其自身所占用的资源。并且在finalize方法中,我们调用了destroy方法用来释放其自身所占用的资源,然后调用其超类Object的finalize方法,这是我们以上所提到的“双保险”的内存释放方法;类B与类C的结构与类A极为相似,它们除了释放自身所占用的资源外,它们还在其对应的方法中调用其超类的destroy方法与finalize方法,用来释放超类所占用的资源。如在类B中调用其超类A的destroy方法与finalize方法与在类C中调用其超类B的destroy方法与finalize方法。但是类A与类B、类C有一点不同,那就是在其destroy方法中没有super.destroy()语句,这是因为其超类Object并没有destroy方法。下面看一下当我们调用初始化与销毁类C时,会有什么样的情况发生。以下是调用完成这个过程的测试类Test的源代码:

public class Test {

    c = null;

    public Test () {

         c = new C();

    }

    public static void main(String args[]) {

         MyClass me = new MyClass();

         me.destroy();

         }

    protected void destroy () {

         if (c != null) {

              c.destroy();

         }else {

             System.out.println("c对象已被释放");

         }  

    }  

}

编译执行Test.java:

> javac Test.java

> java Test

下面是这个程序的运行结果:

创建a对象

创建b对象

创建c对象

释放c对象

释放b对象

释放a对象

我们注意到当在Test类中初始化类C的对象时,其构造器产生了递归调用,并且是由基类开始依次调用、初始化成员对象的,而当调用C类对象的destroy方法时系统同样产生了递归调用,但调用的顺序却与初始化调用的顺序完全相反,释放资源的调用顺序是由子类开始的,依次调用其超类的资源释放方法destroy()。由此可见,我们在设计类时应尽可能地避免在类的默认构造器中创建、初始化大量的对象。一个原因是在实例化自身的情况下,造成较大的资源开销;另一个原因是其子类在被实例化时,也同样会带来较大的系统资源开销。因为即使我们没有想调用父类的构造器创建大量无用的对象(至少有时候这些对象对我们是没有意义的),但是系统会自动创建它们,而这些操作与过程对于我们来说是隐含的。为了防止上述情况的发生,造成不必要的内存资源浪费,我们应当尽量不在类的构造器中创建、初始化大量的对象或执行某种复杂、耗时的运算逻辑。

2.4  数组的创建

数组空间的申请也是一个与内存管理关系密切的技术话题。数组空间的申请分为显式申请与隐式申请两种。显式申请是指在程序中直接给出数组的类型与长度,例如下面的代码:

… …

int[]  intArray = new int[1024];

… …

上面的这行代码的意义是,显式地向系统一次性申请了大小为1KB的整数类型的内存空间,这样的声明方式一般出现在对文件或网络资源数据读取的处理代码中,往往用数组来作为数据读取的缓冲区,以提高读取效率。由于我们不知道具体读取的内容的长度,因此,我们只能通过这种方式来读取相关资源,这样做显然有些弊端。例如,文件、网络资源的长度小于你所申请的数组的长度,这就造成了系统内存资源浪费。隐式申请是在声明数组对象时不知道将要得到的数组的具体长度,例如下面的代码:

… …

int[]  intArray = obj.getIntArray();

System.out.println("整型数组长度:"+intArray.length());

… …

在这行代码中我们事先并不知道obj.getIntArray()到底返回的数组长度是多少,这是在程序运行时才能确定的,因此,这里不存在上面的显式申请数组浪费内存的问题,因为数组的长度是由系统决定的,因此,这种方法是值得提倡使用的。但是这种隐式申请的方法只适用于接收某个方法返回值为数组的情况。

如果遇到数组中所保存的元素占用内存空间较大或数组本身长度较大的情况,我们可以采用上面所讲到的软引用的技术来引用数组,以“提醒”JVM及时回收垃圾内存,维护系统的稳定性。例如下面的代码:

… …

Object obj = new char[1000000];

SoftReference ref = new SoftReference(obj);

… …

由于数组对象长度较长,占用了较大的内存空间,因此我们对obj采用了软引用的处理方式,由JVM根据运行时的内存资源的使用情况,来把握是否回收该对象,释放该内存。虽然这会对应用程序产生一些影响(如当我们想使用该数组对象的时候,该对象被回收了)。但是这样做却能保证应用整体的稳健性,达到合理使用系统内存的目的。

2.5  共享静态变量存储空间

我们知道类中的静态变量(Static Variable)在程序运行期间,其内存空间对所有该类的对象实例而言是共享的,因此在某些时候为了节省系统内存开销、共享资源,将类中的一些变量声明为静态变量,通过下面的例子,你可以发现合理应用静态变量带来的好处:

public class WeekA{

       static class Data {

             private int week;

             private String name;

             Data(int i, String s) {

                   month = i;

                   name = s;

             }

       }

       Data weeks[] = {

             new Data(1, "Monday"),

             new Data(2, "Tuesay"),

             new Data(3, "Wednesday"),

             new Data(4, "Thursday"),

             new Data(5, "Friday"),

             new Data(6, "Saturday")

             new Data(7, "Sunday")

       };

       public static void main(String args[]) {

             final int N = 200000;

             WeekA weekinstance;

             for (int i = 1; i <= N; i++){

                   weekinstance = new WeekA ();

             }

       }

}

在上面这段代码中,没有将Data weeks声明为静态变量,因此当创建WeekA对象时将会得到200 000个weeks对象的副本,这些对象被保存在内存中,但是weeks对象中的数据却从来没有被更改过,而且十分稳定。因此,如果能使所有对象共享该数据对象是个不错的解决办法,请看下面的代码:

public class WeekB{

      static class Data {

             private int week;

             private String name;

             Data(int i, String s) {

                   month = i;

                   name = s;

             }

      }

      static Data weeks[] = {

             new Data(1, "Monday"),

             new Data(2, "Tuesay"),

             new Data(3, "Wednesday"),

             new Data(4, "Thursday"),

             new Data(5, "Friday"),

             new Data(6, "Saturday")

             new Data(7, "Sunday")

      };

      public static void main(String args[]) {

             final int N = 200000;

             WeekB weekinstance;

             for (int i = 1; i <= N; i++){

                   weekinstance = new WeekB ();

             }

     }

}   

请注意在类WeekB中,在Data weeks[]之前添加了static关键字,将该对象变量声明为静态的,因此当你创建200 000个WeekB对象时系统中只保存着该对象的一份拷贝,而且该类的所有对象实例共享这份拷贝,这无疑节约了大量的不必要的内存开销,同时实现了要完成的系统功能。

那么是不是我们应该尽量地多使用静态变量呢?其实不是这样的,因为静态变量生命周期较长,而且不易被系统回收,因此如果不能合理地使用静态变量,就会适得其反,造成大量的内存浪费,所谓过犹不及。因此,建议在具备下列全部条件的情况下,尽量使用静态变量:

(1)变量所包含的对象体积较大,占用内存较多。

(2)变量所包含的对象生命周期较长。

(3)变量所包含的对象数据稳定。

(4)该类的对象实例有对该变量所包含的对象的共享需求。

如果变量不具备上述特点建议你不要轻易地使用静态变量,以免弄巧成拙。

2.6  对象重用与GC

有时候我们为了提高系统的性能,避免重复耗时的操作,希望能够重用某些创建完成的对象。既然是重用(reuse)现有对象,就会涉及对象保存的问题,通常我们把用来缓存对象的容器对象称为对象池(ObjectPool)。通过对象池技术,我们可以大大地提高应用的性能,减少内存需求,例如,我们经常用到的JDBC连接池与EJB实例池等技术,都属于对象池技术的范畴。

通过使用对象池的办法来提高系统性能,节约系统内存开销是一个非常简易、高效的方法,对象池通过对其所保存对象的共享与重用,缩减了应用线程反复重建、装载对象的过程所需要的时间,并且也有效地避免了频繁垃圾回收带来的巨大系统开销。

正是考虑到对象池存在的巨大意义以及能够为应用带来卓越的性能优势,下面我们给出对象池的代码框架,以帮助你理解,对象池是如何避免系统频繁的垃圾回收所带来的巨大系统开销的。下面是一个对象池的抽象类,也是应用对象池的基类:

public abstract class ObjectPool

{

     private Hashtable locked, unlocked;

     private long expirationTime;

     abstract Object create();

     abstract void expire( Object o );

    abstract void validate( Object o );

    synchronized Object getObject(){...}

     synchronized void freeObject( Object o ){...}

}

在这个类中声明了5个重要方法:对象创建方法create()、对象过期方法expire()、获取对象方法getObject()、对象有效性验证方法validate()与对象释放方法freeObject()。我们可以通过create()方法创建新的对象实例,并且将这个对象实例保存到哈希表(Hashtable)对象中,当其他应用请求对象实例时,可以通过调用getObject()方法获取哈希表中的对象,并检测其有效性是否过期,如果一切正常则将该对象传递给调用者,调用者使用完对象实例后可以通过调用方法freeObject()将该对象实例释放(归还)给对象池。

既然对象实例被保存,当应用试图重用该对象时就不需要重新创建新的对象,避免大量垃圾对象的产生。即使你所使用的对象的生命周期较短,可以被系统及时回收,也会引发JVM频繁GC的危机,导致系统性能下降。

但是如果长时间地将对象保存在对象池中,也就是驻留在内存中,而这些对象又不被经常地使用,无疑也会造成不必要的内存资源浪费,或者该对象在对象池中遭到破坏,如果不能将该对象及时清除而继续占用系统的内存资源,也是非常麻烦的事情。因此在应用对象池技术重用对象时,应该考虑其必要性并权衡利弊做出最优的选择,如果决定使用对象池技术,需要采取相应的手段清除遭到破坏的对象,甚至在某些情况下需要清除对象池中所有的对象。或者你可以为对象池中的每个对象分配一个时间戳,设定对象的过期时间,当对象过期后及时在内存中将其清除。下面以JDBC连接池为例,说明如何通过对象池技术重用对象中的技术要点,帮助你理解怎样才能更好地提高系统性能,降低系统内存的开销。

在上面的对象池类中,我们声明了一个对象创建方法abstract Object create() throws Exception。在JDBC连接池中也需要创建一个该抽象方法的实现方法Object create() throws SQLException,这个方法抛出了SQLException,在这个方法中通过对方法Driver- Manger.getConnection()的调用获取一个JDBC数据库连接对象,例如下面的代码:

… …

Object create() throws SQLException

     return( DriverManager.getConnection(dsurl, usr, pwd ) );

}

… …

JDBC数据库连接池在接收到外部请求获取连接对象的请求之后,要在getConnection()方法中,调用创建连接对象方法create()。调用create()方法的前提是,连接池要确认连接池中的连接对象数量是否达到极限值与当前池中对象的状态。当连接池中的对象实例数没有达到对象池实例的最大值,并且连接池中所有已存在的连接都处于被占用状态,也就是说,此时连接池中没有空闲连接对象。当具备了上述条件后,才可以通过调用create()方法,创建新的连接对象,响应外部获取连接的请求,然后将创建的对象传递给getObject()方法的调用者。为了同步多线程对资源的访问,通常getObject()方法的声明如下:

synchronized Object checkOut() throws Exception

在getConnection()方法处理过程中有可能抛出SQL异常,因此其声明如下:

public Connection getConnection() throws SQLException

{

    try

    {

        return( ( Connection ) super.getObject () );

    }

    catch( Exception ex )

    {

        throw( (SQLException) ex );

    }

}

为了防止已损坏连接对象残存在连接池中而不能被及时清除,浪费系统内存资源,可以通过一个专门的线程来清除这些连接对象,减少系统内存开销。我们可以通过创建一个线程及时检测连接池中的对象是否有效,如果无效则主动清除,如下所示。

class ConnectionCleanUpThread extends Thread

{

    private ObjectPool pool;

    private long sleepTime;

    ConnectionCleanUpThread ( ObjectPool pool, long sleepTime )

    {

        this.pool = pool;

        this.sleepTime = sleepTime;

    }

    public void run()

    {

        while( true )

        {

            try

            {

                 sleep( sleepTime );

            }

            catch( InterruptedException ex )

            {

                 // 做相应处理

                 …      

            }        

            pool.cleanUp();

        }

    }

}

通过这个线程,就可以完成上面所提到的无效连接对象的清除工作,这个线程是在ObjectPool类的构造器中被初始化并启动的。

… …

Public ObjectPool () {

    cleaner = new CleanUpThread( this, expirationTime );

    cleaner.start();

    …

}

… …

cleanUp()方法在清除所有无效Connection对象的同时,还会要求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值