垃圾回收与终结处理
程序员都了解初始化的重要性,但是常常会忘记同样也重要的清理工作。毕竟。谁需要清理一个int呢?但是我们知道把一个对象用完了以后“不管不顾”总是不安全的。因为很容易导致内存泄漏。当然了,你可能会说,我们有Java内存回收器啊。需要说明白的是,Java内存回收器只知道释放哪些由new
创建的对象。假如你的代码没有通过new
创建对象而是通过别的途径(比如说,调用了JNI本地方法,C++里面通过malloc
方法开辟的一片空间)获得一块内存空间,你如何释放?针对这种情况,Java允许在对象中添加一个finalize()
方法。
finalize()
工作原理
一旦垃圾回收器准备好回收某一个对象的时候,它将首先调用这个对象的finalize()
方法,并且在下一次执行垃圾回收的动作时才会真正回收对象占用的内存,这里实际上就是说finalize()
会做一些垃圾回收前的重要的垃圾清理工作。
析构函数 == finalize()?
学过C++的同学都知道C++里面有一个析构函数(C++中销毁对象必须使用这个函数),很多人常常会将finalize()
方法类比为Java中的析构函数。这样想对么?这里需要明确地区分一下啊,C++里面,对象一定会被回收;但是在Java中,对象不一定会被回收。或者可以这么说:对象是有可能不被垃圾回收器回收的,垃圾回收并不等于“析构”。
Java中并没有提供“析构函数”或者相似的概念,如果必须执行某些清理前的动作,你必须自己手动地去编写一个执行清理动作的方法。比如(这个例子摘自ThinkingInJava),假设某个对象在创建的时候会将自己绘制在屏幕上,如果不是明确地从屏幕上将其擦除,它可能永远也得不到清理,如果在finalize()
里加入某种擦除的功能,当“垃圾回收”发生时(不一定保证会发生),finalize()
得到了调用,图像就会被擦除。要是“垃圾回收”没有发生,图像就会一直保存下去。什么时候会发生“垃圾回收”,这个又是另一个话题了,我们在另一篇博客里面会仔细讲解JVM垃圾回收的相关细节,在本文中仅仅介绍一下相关的理论。一般来说,有一个大的原则就是,内存不够用的时候就会发生GC(包括Minor GC 或 Full GC)。
finalize()有何用途
我们可能已经有所体会就是,我们不能太相信或者说太依赖finalize()
方法了,因为它不一定会发生。这里说一说垃圾回收的目标是什么,那就是内存,垃圾回收只与内存相关。也就是说,使用垃圾回收器的唯一原因就是为了回收程序不再使用的内存。所以对于垃圾回收有关的任何行为来说(尤其是finalize()
方法),它们也必须同内存及其回收有关。但这是否意味着要是对象中含有其它对象,finalize()
方法就应该明确释放那些呢?不是的,无论这个对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这个就将finalize()
方法的使用先知道了一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。不过,大家也都知道,Java中一切皆为对象,那这种特殊的情况是什么情况呢?
看来之所以要使用finalize()
方法,是因为我们在分配内存的时候可能会使用到类似C语言中的做法,而非Java中的通常做法。这种情况主要发生在使用“本地方法”的情况下。什么是“本地方法”?“本地方法”是一种在Java程序中使用非Java代码的方式。本地方法目前只支持C和C++,也就是说,我们通过调用C的malloc()
方法分配了一块空间,这个空间保存在哪儿呢?在线程栈中。关于线程栈的内容,我们在后面的JMM博客中会给大家仔细介绍。如果我们的Java代码没有及时地释放掉这部分“特殊”的内存空间,很容易会出现内存泄漏的情况。当然啦,free()
方法是C和C++中的函数,所以,我们要在finalize()
中调用本地方法free()
来释放掉这些特殊的内存。
一定要记得清理
Java中不允许创建局部对象,必须使用new
创建对象。在Java中,也没有用于释放对象的delete
,因为垃圾回收器会帮助你释放存储空间。甚至可以很肤浅地认为,正是由于垃圾回收机制的存在,是的Java没有析构函数。然而随着时间的深入,我们就会明白垃圾回收器的存在并不能完全替代析构函数。而且绝对不能直接调用finalize()
方法,所以这也不是一种解决的方法。不管是“垃圾回收”还是“终结”,都不一定保证会发生。如果Java虚拟机(JVM)并没有面临内存耗尽的情况,它是不会浪费时间去执行垃圾回收以恢复内存的。要知道,一次stop-the-world
会给应用程序带来多大的损失!