本节将对Java编程语言中与内存管理相关的内容进行介绍。
在Java中,妄图自己操作内存,脱离垃圾回收器的管理(例如即使对象已经变成垃圾,但坚持不回收之),绝对是自讨苦吃。但Java仍然提供了相应的机制来 提醒垃圾回收器进行工作,这其中有利有弊,在使用的时候需要特别注意。
## 3.6.1 析构函数从Java 1.0起,每个Java对象都包含一个finalize
方法,用户可以覆盖该方法以完成自定义析构操作。按照javadoc的说明,finalize
方法会在该对象被当作垃圾回收之前调用,单纯这样看的话,将之作为对象的析构函数是个不错的主意。
但事实上没这样简单,由于finalize
方法中可能会包含任何代码,因此有可能有一些错误代码,是finalize
方法处理问题,例如,在finalize
方法中是已经成为垃圾的对象 复活,或者根据垃圾对象克隆了一个新对象,这样,垃圾回收器就不会再将之回收了。此外,如果在finalize
方法中释放系统资源(例如释放文件句柄),则可能会导致资源无法被充分利用,因为无法保证析构函数会在何时被执行。因此,对系统资源的释放应该由程序员显式控制。
进一步讲,finalize
可能会被任意线程在任意时间调用,无论该线程当前持有哪个对象的锁,都可以。这非常危险,可能会导致死锁的出现,还有可能会违反Java自身的语义。
"Java中的析构函数的设计就是一个失误,应避免使用",这不仅仅是本书作者的意见,也是Java社区的一致意见。
## 3.6.2 Java中的引用很多人以为Java中只有一种引用,对象只分为可达和不可达两种,后一种会被垃圾回收器处理掉。但事实上,Java中存在有多种引用,可以认为是对处于不同存活程度的对象的描述。普通对象的引用称为 强引用(strong reference)。
几种对象引用类型位于java.lang.ref
包下,均继承自java.lang.ref.Reference
类。所有的引用类都有一个get
方法用于获取引用所指向的实际对象,如果对象是不可达的,例如已经被回收了,则get
方法返回null
。
当对象的可达性发生变化时会被放入到java.lang.ref.ReferenceQueue
类的实例中,例如引用对象要被回收掉的时候。在创建引用对象时,可以将之与某个java.lang.ref.ReferenceQueue
的实例绑定,通过对ReferenceQueue
实例的轮询,可以得知对象会在何时被回收掉。
Java中有4类主要的引用,即 强引用,弱引用,软引用和虚引用(strong, weak, soft and phantom references)
### 3.6.2.1 弱引用弱引用指向些不足以使自己保留在内存中的引用,实际上,java.lang.ref.WeakReference
类是对强引用的一个包装,将其标明为弱引用:
WeakReference weak = new WeakReference(object);
在上面的示例中,弱引用实际指向的对象可以用weak.get()
得到,由于object
可能会在任意被回收掉,所以weak.get()
方法可能返回null
。
弱引用常作为缓存的key用于java.util.WeakHashMap
实例中。当JVM执行垃圾回收时,弱引用所指向的对象会被释放掉,这样可以防止无意识的内存泄漏。
相对于弱引用来说,软引用所指向的对象,垃圾回收器会尽可能将之保存在内存中,但当内存不足时,会首先将之回收掉。
软引用到底比弱引用"强"多少,取决于JVM的具体实现。理论上,软引用可以实现得与弱引用一样,并不违反Java的语义。
### 3.6.2.3 虚引用虚引用更常用于实现析构函数,用于取代finalize
方法,与弱引用和软引用类似,它也是对普通对象的包装,只不过其get
方法永远都返回null
。
访问虚引用需要通过轮询java.lang.ref.ReferenceQueue
实例,以便得知某个对象是否要被回收掉了。由于虚引用的get
只会返回null
,所以无法获取到其所指向的真正对象,也就不会出现像finalize
方法那样的问题了。
下面的示例代码使用finalize
方法做析构函数:
/**
* Prints the number of finalized objects
*/
public class Finalize {
static class TestObject {
static int nObjectsFinalized = 0;
protected void finalize() throws Throwable {
System.err.println(++nObjectsFinalized);
}
}
public static void main(String[] args) {
for (;;) {
TestObject o = new TestObject();
doStuff(o);
o = null; //clear any remaining refs to "o"
System.gc(); //try to force gc
}
}
}
下面的示例代码使用虚引用实现同样的目标:
/**
* Prints the number of finalized objects using PhantomReferences
*/
import java.lang.ref.*;
public class Finalize {
static class TestObject {
static int nObjectsFinalized = 0;
}
static ReferenceQueue<TestObject> q = new ReferenceQueue<TestObject>();
public static void main(String[] args) {
Thread finalizerThread = new Thread() {
public void run() {
for (;;) {
try {
//block until PhantomReference is available
Reference ref = q.remove();
System.err.println(++TestObject.nObjectsFinalized);
} catch (InterruptedException e) {
}
}
}
};
finalizerThread.start();
for (;;) {
TestObject o = new TestObject();
PhantomReference<TestObject> pr = new PhantomReference<TestObject>(o, q);
doStuff(o);
o = null; //clear any remaining refs to "o"
System.gc(); //try to force GC
}
}
}
对于JVM来说,一定谨记,编程语言只能 提醒垃圾回收器工作。就Java而言,在设计上,它本身并不能精确控制内存系统。例如,假设两个JVM厂商所实现软引用在缓存中具有相同的存活时间,这根本就是不现实的。
另外一个问题就是大量用户对System.gc()
方法的错误使用。System.gc()
方法也是仅仅 提醒运行时"现在可以做垃圾回收了"。在某些JVM实现中,对该方法的频繁调用,导致频繁的垃圾回收操作,而在某些JVM实现中,大部分时间是忽略该调用的。
在以往的工作经验中,多次看到该方法被滥用,很多时候,只是去除该调用就可以大幅提升性能,这也是为什么JRockit中会有-XX:AllowSystemGC=False
这个参数。