内存回收

 

 


Java解释器很清楚地知道它取得了哪些对象与数组,它也知道各个局部变量调用了哪些对象和数组以及那些对象或数组又调用了哪些其他的对象和数组。因此,解释器当然会知道对象何时已不再被其他的变量或对象所使用。当解释器找到这样的对象时,它知道可以安全地回收该对象所占用的内存。内存回收也可以侦测并撤销对象之间的循环(cycle)。循环是指某些对象之间有相互的引用,但并没有被其他操作中的对象所引用。任何这样的循环都会被回收。

 

不同的VM在处理内存回收的方式上本来就会有所不同。这是很合理的,内存回收是以一个低优先权的线程来执行的,所以在没有其他的事情要做时才会执行它大部分的操作,例如在等待用户输入数据的空闲时间。内存回收被当成高优先权来执行的唯一情况(也就是实际上真的会使系统崩溃的时候),就是可用的内存变得非常非常少的时候。但这并不会时常发生,因为这个低优先权的线程会在后台不停地清除不需要的对象。

 

JAVA中的内存泄漏

 

Java所支持的内存回收可大幅降低内存泄漏(memory leak)的发生率。内存泄漏会发生在内存已经被分配但却没有被回收时。刚开始时,你也会认为内存回收可以预防内存泄漏的原因,是因为它会回收所有不再被使用到的对象。然而,在JAVA里,如果一个对不再被使用的对象的有效引用被闲置时,内存泄漏的情形仍然会发生。例如,当一个method执行了很久(甚至会一直执行下去),在那个method中的局部变量所保留的对象引用可能会比实际所需要的时间还长。以下的程序代码可作说明:

 

public class MemoryLeak {   
    public static void main(String[] args) {   
        int big_array[] = new int[100000];   
  
        // 使用big_array[]做一些运算并取得结果   
        int result = compute(big_array);   
           
           
        //我们不再需要big_array了。当没有其他的引用指向它时,它便会被回收。   
        //因为big_array是一个局部变量,它会引用该数组,直到此method结束为止。   
        //但这个method不会结束,因此,我们自己必须显示撤销此引用,   
        //让内存回收程序直到可以回收该数组   
        big_array = null;   
           
        //此为无穷循环,用来处理用户的输入   
        for(;;)handle_input(result);   
    }   
    }

 

  

内存泄漏也会发生在你使用哈希表(hash table)或其他类似的数据结构来将某个对象关联到另外一个对象时。即使这两个对象已不再被需要了,但它们之间的关联性仍会被保留在哈希表里,直到该哈希表被回收时,该对象才会跟着被回收。如果哈希表的生命周期比该对象长的话,内存泄漏的情况就会发生。

 

要避免内存泄漏最重要的就是,当拥有这些引用的对象一直都是存在的情况下且该对象不再不需要时,就需要将对该对象的引用设为null值。有个常见的内存泄漏来源,就是在Object数组被用来代表由对象组成的collection数据结构里。一般会使用独立的size字段来记录目前数组的哪些元素是合法的。当从集合里移走一个对象时,只递减size字段是不够的,你必须要将适当的数组元素值设定为null,以便过时的对象引用不再存在。

 

隐含超类字段

 

考虑这个例子,假设我们的PlaneCircle类需要知道圆心到坐标原点(0,0)的距离,我们可以加入另外的实例字段来记录这个值:

 

public double r;

  

为构造函数加入以下的程序代码来计算字段的值:


this.r = Math.sqrt(cx*cx + cy*cy); //勾股定理



 

但请稍等,这个新字段r与Circle超类的半径字段r的名称相同。当这样的情形发生时,我们说PlaneCircle类的字段r隐含了Circle的字段r(当然,这是个人为的例子:新字段因该被称为distanceFromOrigin.虽然你因该试着要去鞭面这种情况发生,但子类字段有时候确实会将超类的字段给隐藏起来。)

 

使用此PlaneCircle的新定义时,表达式r与this.r都是指PlaneCircle中的字段。

那我们要如何才能涉及到Circle的r字段呢?可以使用super关键字的特殊语法:

 

r                        //指的是PlaneCircle中的字段    
this.r                  //指的是PlaneCircle中的字段    
super.r               //指的是Circle中的字段

 

 

另一个用来引用隐含字段的方法是将this(或该类的任何实例)强制转换成适当的超类,然后再访问其字段:

 


((Circle)this).r      //引用Circle类的字段r


 

 当你需要引用一个隐含字段,而此字段并不是直接在超类中被定义时,强制转换的技术就特别有用。例如,假设A、B与C类都定义了一个相同名称的字段x,且C是B的子类,B是A的子类,然后,在类C的方法里,你可以用以下的方式来引用这些不同的字段:

 

x                            //类C中的字段x 
this.x                     //类C中的字段x 
super.x                   //类B中的字段x 
((B)this).x               //类B中的字段x 
((A)this).x               //类A中的字段x 
super.super.x         //不合法,无法引用类A中的字段x

 

 

 你无法使用super.super.x来引用某个超类的超类中的隐含字段x.这是不合法的语法。

 

同样的,如果类C有一个实例C,则你可以用以下的方式来引用这三个名为X的字段:

 

c.x                        //类C中的字段x 
((B)c).x                 //类B中的字段x 
((A)c).x                 //类A中的字段x

   

到目前为止,我已经讨论过实例字段了。类字段也是可以隐含的,你可以使用相同的super语法来引用字段的隐含值,但你并不需要这么做,因为你一定可以使用该字段的名称来引用类字段的值,但你并不需要这么做,因为你一定可以使用该字段的名称来引用类字段的值。假设PlaneCircle的实现者决定要更改Circle.PI的值,因为它觉得原先的设定值不够精确,他可以定义他自己的类字段PI:

 

public static final double PI = 3.14159265258979323846

 

 

现在,在PlaneCircle中的程序代码可以通过PlaneCircle.PI或表达式PI来使用这个更为精确的数字了,他也可以通过表达式super.PI与Circle.PI来引用旧的数值。请注意,PlaneCircle所继承的area()与circumference()方法是定义于Circle类,所以它们要使用Circle.PI,即使该值已经被PlaneCircle.PI隐含了。