第五章 初始化与清理(二)
5.5 清理:终结处理和垃圾回收
清理的工作常常被忽略,Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊情况:假定对象(并非使用new)获得了一块”特殊”的内存区域,由于垃圾回收器只知道释放那些由new分配的内存,所以不知道如何释放特殊内存。Java允许在类中定义一个名为finalize()的方法,工作原理”假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
Java的finalize()和C++的析构函数有所不同,C++中对象一定会被销毁(如果程序中没有缺陷的话),而Java中对象并非总是被垃圾回收。即:1.对象可能不被垃圾回收。2.垃圾回收并不等于”析构”
Java并未提供”析构函数”或类似的概念,要做的类似的清理工作,必须自己动手创建一个执行清理工作的普通方法。当”垃圾回收”发生时(不能保证一定会发生),finalize()得到了调用,相应的工作就会进行,如果垃圾回收没有发生,就不会被调用。
只要程序没有濒临存储空间用完的那一刻,对象占用的控件就总也得不到释放,如果程序执行结束,并且垃圾回收器一直都没有释放你创建的任何对象的存储空间,随着程序的退出,资源会全部还给操作系统。这个策略是恰当的,因为垃圾回收本身也占用内存控件,如果不使用,内存开销会变小。
5.5.1 finalize()用途何在
垃圾回收有关的任何行为(尤其是finalize()方法),它们也必须同内存及回收有关。finalize()方法存在的意义是为了回收那些用new创建出来的对象之外的特殊存储空间,是由于在分配内存时,可能采用了类似C语言中的做法,而非Java中的通常做法(new)。这种情况主要发生在使用”本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但它们可以调用其他语言写的代码。
在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用free()函数,否则存储空间将得不到释放,从而造成内存泄漏。free()是C和C++中的函数,所以需要在finalize()中用本地方法调用它。
5.5.2 你必须实施清理
要清理一个对象,用户必须在需要清理的时刻调用执行清理动作的方法。在C++中,所有对象都会被销毁,都应该被销毁。如果在C++中创建了一个局部对象(也就是在堆栈上创建,这在Java中行不通),此时的销毁动作发生在以”右花括号”为边界的、此对象作用域的末尾处。如果对象是用new创建的,那么当程序员调用C++的delete操作符(Java没有这个命令),就会调用相应的析构函数。如果没有调用delete,那永远不会调用析构函数,这样会出现内存泄漏。
Java中不允许创建局部对象,必须使用new创建对象。在Java中,也没有释放对象的delete,垃圾回收器会帮你释放存储空间。
5.5.3 终结条件
通常不能指望finalize(),必须创建其他的”清理”方法,并明确地调用它们。finalize()还有一个又去的用法,并不依赖于每次都要对finalize进行调用,也就是对象终结条件的验证。
当对某个对象不再感兴趣–也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。下面的例子示范了finalize()可能的使用方法:
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error : checked out");
}
}
public class Test {
public static void main(String[] args) {
Book novel = new Book(true);
novel.checkIn();
new Book(true);
System.gc();
}
}
本例的终结条件时:所有Book对象在被当作垃圾回收前都应该被checkIn(),但是在new Book(true)
这个对象没有被checkIn,要是没有finalize()来验证终结条件,很难发现这种缺陷。
System.gc()用于强制进行和终结动作。
5.5.4 垃圾回收器如何工作
垃圾回收器对于提高对象创建速度有明显的效果,Java虚拟机在工作的时候,存储空间的释放会影响存储空间的分配,由于垃圾回收器的存在,Java从堆分配空间的速度可以和其他语言从堆栈上分配控件的速度相媲美。
先了解其他系统中的垃圾回收机制将能帮助我们更好的理解Java中的回收机制,引用记数是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用和对象连接的时候,引用记数加1,当引用离开作用域或被置为null时,引用记数减1。
垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用记数为0时,就释放其占用的空间(但是,引用技术模式经常会在计数值变为0的时候立即释放对象)。如果对象之间存在循环饮用,可能会出现”对象应该被回收,但引用记数却不为0”的情况。引用记数常用来说明垃圾收集的工作方式,但似乎从来未被应用与任何一种Java虚拟机实现中。
在更快的一些模式中,垃圾回收器并非基于引用记数技术,而是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区中的引用。如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有”活”的对象,对于每个引用,必须追踪和它关联的对象,然后是此关联对象的所有引用,反复进行,直到全部被访问。
在上述的方式下,Java虚拟机将采用一种自适应的垃圾回收技术。其中有一种找到存活对象的方法名为停止-复制(stop-and-copy)。显然这意味着先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另外一个堆,没有被复制的都是应当被回收的。
当把对象从一处搬到另外一处时,所有之乡它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到。对于这种复制式回收器而言,效率会降低。1.需要两个堆来回倒腾,某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。2.程序进入稳定状态后,产生少量垃圾,但是复制式回收器还是会不停的复制,对于第二种情况,一些Java虚拟机会进行检查:要是没有新的垃圾产生,就会切换到另一种工作模式(标记-清扫mark-and-sweep),Sun公司早期版本的Java虚拟机使用了这种技术。对一般用途而言,”标记-清扫”方式速度相当慢,但是当只会产生少量垃圾甚至不会产生垃圾的时候,速度就很快了。
标记-清扫 所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当找到一个存活的对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理才会开始。在清理过程中,没有标记的对象将全部被释放,不会有复制动作发生。所剩下的空间是不连续的,垃圾回收器要是希望得到连续的空间的话,就得重新整理剩下的对象。
停止-复制 的意思是这种垃圾回收机制不在后台进行。垃圾回收动作发生的时候,程序会被停止,Sun公司的文档中,许多参考文献将垃圾回收视为低优先级的后台进程,但事实上早起Sun公司Java虚拟机中并非在后台实现垃圾回收,而是当可用内存较少时,Sun版本的垃圾回收器会暂停运行程序,同样的标记-清扫工作也必须在程序暂停的情况下才能进行。
在Java虚拟机中,内存分配以较大的“块”为单位,严格来说,“停止-复制”要求在释放旧对象之前,必须先把所有存活对象从旧堆中复制到新堆,有了“块”之后,垃圾回收器在回收的时候可以将对象拷贝到废弃的块中,每个块都用响应的代数(generation count)来记录它是否还存活。垃圾回收器会定期进行完整的清理动作–大型对象仍然不会被复制(只是其代数会增加),内涵小型对象的块会被复制并整理。Java虚拟机会进行监视,在“标记-清扫”和“停止-复制”之间切换,这就是“自适应”技术。
Java虚拟机中有很多提高速度的附加技术,尤其是与加载器操作有关的,被称为”即时(just-in-time,JIT)”编译器的技术。它可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度得到了提升。当需要装在某个类时(通常是创建该类的第一个对象),编译器会首先找到.class文件,然后将该类的字节码装入内存。接下来有两种方案可供选择:
- 让即时编译器编译所有代码,这种家在动作散落在整个程序声明周期内,累加起来会花费更多的时间,并且会增加可执行代码的长度。
- 另一种成为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候编译代码,这样不会执行的代码不会被JIT编译。