Java有析构函数吗?

664

Java有析构函数吗?我似乎找不到任何相关文档。如果没有,我该如何实现相同的效果?

为了让我的问题更具体,我正在编写一个处理数据的应用程序,并且规范要求有一个“重置”按钮,可以将应用程序恢复到刚启动时的状态。但是,所有数据都必须“实时”存在,除非应用程序关闭或按下重置按钮。

通常作为C/C++程序员,我认为这很容易实现。(因此我计划最后再实现它。)我将所有可“重置”的对象结构化到相同的类中,以便在按下重置按钮时可以销毁所有“实时”对象。

我在想,如果我所做的只是取消引用数据并等待垃圾收集器收集它们,那么如果我的用户反复输入数据并按下重置按钮,会不会出现内存泄漏?我也在思考,既然Java作为一种成熟的语言,应该有一种方法来避免这种情况发生或优雅地解决它。


9
只有在保留了不需要的对象引用时才会出现内存泄漏,也就是说,你的程序存在缺陷。垃圾回收器将根据需要运行(有时更早)。 - Peter Lawrey
22
如果你正在通过对象快速处理数据,虚拟机可能不会及时运行垃圾回收。认为垃圾回收器总能跟上进度或做出正确决策是一种谬论。 - Kieveli
2
@Kieveli 在出现错误之前,JVM不会运行GC吗? - WVrock
4
如果Java有一个能够将其一次性销毁的析构函数,那就太好了。 - Tomáš Zato
@WVrock - 有趣的问题。答案是“不行”(至少对于某些类型的“通过对象快速处理数据”),但原因很微妙。实际上,当您在垃圾回收中花费大约97%的时间,并且只有3%的时间用于实际程序逻辑时,您会遇到实际错误,因为大多数引用仍然具有指向它们的指针。如果“快速处理”使用少量指针,则不会出现问题。 - sf_jeff
显示剩余2条评论
24个回答

584

由于Java是一种垃圾收集语言,您无法预测对象何时(甚至是否)被销毁。因此,不存在直接等价的析构函数。

有一个继承的方法叫做finalize,但是这完全取决于垃圾回收器。因此,对于需要显式清理的类,惯例是定义一个close方法,并仅在进行检查时使用finalize(即如果close没有调用,则现在执行并记录错误)。

最近有一个产生了深入讨论的finalize问题,如果需要的话可以提供更多深度...


8
在这个上下文中,"close()" 是指 Java 中 java.lang.AutoCloseable 接口中的方法吗? - Sridhar Sarnobat
25
不,AutoCloseable接口是在Java 7中引入的,但“close()”约定已经存在很长时间了。 - Jon Onstott
1
@dctremblay 对象的销毁是由垃圾回收器完成的,而垃圾回收器可能在应用程序的生命周期内永远不会运行。 - Piro
9
请注意,Java 9 已经弃用finalize 方法。 - Lii
我们还可以使用 WeakReference - 存储在其中的对象将在垃圾回收器开始搜索垃圾之前被删除。 - neoexpert
显示剩余2条评论

146

请查看 try-with-resources 语句。例如:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
  System.out.println(br.readLine());
} catch (Exception e) {
  ...
} finally {
  ...
}

BufferedReader.close()方法中释放不再需要的资源。您可以创建自己的实现AutoCloseable接口的类,并以类似的方式使用它。

finalize相比,这种方法在代码结构方面更为有限,但同时使得代码更加易于理解和维护。此外,在应用程序的生命周期内不能保证finalize方法会被调用。


14
我很惊讶这个回答的得票数这么少。它是真正的答案。 - nurettin
32
我不同意这就是确切的答案。如果一个实例在多次方法调用之间处理一个资源,并且处理时间跨度较长,则使用“try-with-resources”并不能解决问题。除非可以接受按照方法被调用的频率关闭和重新打开该资源,但这并不是普遍的情况。 - Eric
18
确实,这并不是实际答案。除非对象的构建和使用完全被 try 封装,且 finally 用于强制调用 obj.finalize() 以管理对象的销毁过程。即使如此,这种设置也无法解决 OP 提出的问题:通过“重置”按钮触发的程序中途对象销毁。 - 7yl4r
2
其他用户已经在您的应用程序入口点中展示了这一点。全局定义您的变量。在入口函数中使用try进行初始化。在finally(当您的应用程序关闭时)进行去初始化。这是完全可能的。 - TamusJRoyce
1
@nurettin 当时提出这个问题时,Java 7才发布了3个月,如果这有助于更好地理解的话。 - corsiKa
显示剩余2条评论

115

不,这里没有析构函数。原因是所有的Java对象都是堆分配并且由垃圾回收器管理。没有显式释放(比如C++中的delete运算符),就没有合理的方式来实现真正的析构函数。

Java确实支持finalizer,但它们只应该被用作保护对象的安全网,当对象持有指向本地资源的句柄时,例如:套接字、文件句柄、窗口句柄等等。当垃圾回收器回收一个没有finalizer的对象时,它仅仅将内存区域标记为可用而已。当对象拥有一个finalizer时,它首先被复制到一个临时位置(还记得吗?我们正在进行垃圾回收),然后被排队进入等待最终处理的队列,接着一个Finalizer线程以非常低的优先级轮询队列并执行finalizer。

当应用程序退出时,JVM会停止而不等待未完成的对象被最终处理,所以你几乎没有任何保证你的finalizer会被执行。


4
谢谢您提及本地资源 - 这是一个需要“析构函数”式方法的领域。 - Nathan Osman
是的,我现在也面临着同样的问题,需要释放通过对C++进行本地调用分配的资源/句柄。 - nikk
@ddimitrov,理论上Java能够实现显式释放内存吗?还是这是一个逻辑矛盾? - mils
2
@mils天真地实现显式释放将会破坏Java的假设,即任何引用都指向一个活动对象。您可以遍历所有指针并将别名设置为null,但这比GC更昂贵。或者您可以尝试使用一些线性类型系统(请参见Rust中的“所有权”),但这是一项重大的语言更改。还有其他选项(请参见JavaRT作用域内存等),但总的来说,显式释放与Java语言不太匹配。 - ddimitrov

32
避免使用finalize()方法。它们不是一种可靠的资源清理机制,滥用它们可能会导致垃圾回收器出现问题。 如果您的对象需要解除分配调用以释放资源,请使用显式方法调用。这种约定可以在现有API中看到(例如CloseableGraphics.dispose()Widget.dispose()),通常通过try/finally调用。
Resource r = new Resource();
try {
    //work
} finally {
    r.dispose();
}

试图使用已释放的对象应该引发运行时异常(请参见IllegalStateException)。


编辑:

我在想,如果我所做的只是取消引用数据并等待垃圾回收器收集它们, 那么如果我的用户重复输入数据并按下重置按钮,会不会有内存泄漏?

通常,你所需要做的就是取消对对象的引用-至少应该是这样工作的。如果你担心垃圾回收,可以查看Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning(或适用于你JVM版本的等效文档)。


1
这不是dereference的意思。它不是“将对象的最后一个引用设置为null”,而是获取(读取)引用的值,以便您可以在随后的操作中使用它。 - user146043
1
try..finally仍然是一种有效且推荐的方法吗?假设我之前在finalize()中调用了本地方法,我能否将该调用移动到finally子句中? finalize() { destroy(); } protected native void destroy(); } class Alt_Resource { try (Resource r = new Resource()) { // use r } finally { r.destroy(); } - aashima
r不会被限定在finally块中。因此,在那个点上你不能调用destroy。现在,如果你更正作用域,使对象创建在try块之前,你将得到丑陋的“try-with-resources之前”的情况。 - userAsh

25

随着Java 1.7的发布,现在您还可以使用try-with-resources块作为附加选项。例如,

public class Closeable implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("closing..."); 
    }
    public static void main(String[] args) {
        try (Closeable c = new Closeable()) {
            System.out.println("trying..."); 
            throw new Exception("throwing..."); 
        }
        catch (Exception e) {
            System.out.println("catching..."); 
        }
        finally {
            System.out.println("finalizing..."); 
        } 
    }
}
如果你执行这个类,c.close() 会在 try 块结束时执行,在 catchfinally 块执行之前。与 finalize() 方法不同,close() 是有保证被执行的。但是,在 finally 子句中没有必要显式地执行它。

如果我们不使用try-with-resources块会怎样呢?我认为我们可以在finalize()中调用close(),以确保close已被调用。 - shintoZ
4
根据我阅读的回答,无法保证finalize()方法会被执行。 - Asif Mushtaq

15

我完全同意其他答案中说的,不要依赖于finalize的执行。

除了try-catch-finally块之外,您可以使用Runtime#addShutdownHook(在Java 1.3中引入)来执行程序的最终清理。

这与析构函数不同,但是可以实现一个关闭钩子,在其中注册有监听器对象,可以调用清理方法(关闭持久数据库连接、删除文件锁等)-通常情况下会在析构函数中完成这些操作。 再次强调-这并不能取代析构函数,但在某些情况下,可以使用此方法实现所需的功能。

这样做的好处是使销毁行为松散地耦合于程序的其他部分。


addShutdownHook 显然是在 Java 1.3 中引入的。无论如何,它在我的 1.5 版本中都可用。 :) 参见:http://stackoverflow.com/questions/727151/how-do-i-cleanup-an-opened-process-in-java - skiphoppy
2
据我的经验,如果您在Eclipse中使用红色的“终止”按钮,关闭挂钩将不会被调用 - 整个JVM会立即被销毁,关闭挂钩不会被优雅地调用。这意味着,如果您使用Eclipse进行开发,则在开发和生产过程中可能会看到不同的行为。 - Hamy

13

7
我认为,可能会被调用的方法在我的眼里基本上是无用的。最好不要用一个无用的特殊方法来污染语言,最多只能提供虚假的安全感。我永远不理解Java语言的开发人员为什么认为finalize是一个好主意。 - antred
@antred Java语言的开发者们同意。我猜,当时对于他们中的一些人来说,这是他们第一次设计带有垃圾回收功能的编程语言和运行环境。更不可理解的是,为什么那个其他的托管语言在已经明白这个概念是一个坏主意的时候,还抄袭了这个概念 - Holger

9

我同意大部分答案。

你不应该完全依赖finalizeShutdownHook

finalize

  1. JVM不能保证何时调用finalize()方法。

  2. finalize()只会被GC线程调用一次。如果一个对象在finalizing方法中复活,那么finalize将不会再次被调用。

  3. 在你的应用程序中,你可能有一些活动对象,垃圾回收从未被调用。

  4. 任何由finalizing方法抛出的异常都会被GC线程忽略

  5. System.runFinalization(true)Runtime.getRuntime().runFinalization(true)方法增加了调用finalize()方法的可能性,但现在这两个方法已经被弃用。由于缺乏线程安全性和可能导致死锁,这些方法非常危险。

shutdownHooks

public void addShutdownHook(Thread hook)

注册一个新的虚拟机关闭钩子。Java虚拟机响应两种类型的事件而关闭:
1.程序正常退出,即最后一个非守护线程退出或调用exit(等效于System.exit)方法时;
2.响应用户中断(例如键入^C)或系统范围事件(例如用户注销或系统关闭)而终止虚拟机。
关闭钩子只是一个已初始化但未启动的线程。当虚拟机开始其关闭序列时,它将以某种未指定的顺序启动所有注册的关闭钩子,并让它们并发运行。当所有钩子都完成后,如果启用了退出时终结,则会运行所有未调用的finalizer。
最后,虚拟机将停止。请注意,在关闭序列期间,守护线程将继续运行,如果通过调用exit方法来启动关闭,则非守护线程也将继续运行。
关闭钩子也应该快速完成它们的工作。当程序调用exit时,期望虚拟机会迅速关闭和退出。
但是即使Oracle文档引用了以下内容:
在极少数情况下,虚拟机可能会中止,即在没有清理关闭的情况下停止运行。
这种情况发生在虚拟机在外部被终止,例如在Unix上使用SIGKILL信号或在Microsoft Windows上使用TerminateProcess调用时。如果本机方法出现问题,例如损坏内部数据结构或尝试访问不存在的内存,则虚拟机也可能会中止。如果虚拟机中止,则无法保证是否运行任何关闭钩子。
结论:适当使用try{} catch{} finally{}块并在finally{}块中释放关键资源。在释放资源时,捕获Exception和Throwable。

8
首先需要注意的是,由于Java是垃圾回收的,因此很少需要处理对象销毁的问题。首先,因为您通常没有任何要释放的托管资源,其次,因为您无法预测它何时或是否会发生,所以对于您需要在“没有人再使用我的对象时立即发生”的事情来说是不合适的。
您可以使用java.lang.ref.PhantomReference在对象被销毁后得到通知(实际上,说它已经被销毁可能略有不准确,但如果将其虚引用排队,则不再可恢复,这通常相当于相同的结果)。一种常见用法是:
将需要被销毁的类中的资源分离到另一个辅助对象中(请注意,如果您要做的只是关闭连接,这是一种常见情况,您不需要编写新类:在这种情况下,要关闭的连接将是“辅助对象”)。
创建主对象时,同时创建一个PhantomReference。将其引用到新的辅助对象上,或者设置从PhantomReference对象到其对应的辅助对象的映射。
在主对象被收集后,PhantomReference将被排队(或者它可能会被排队——就像最终器一样,没有保证它会被排队,例如如果VM退出,则不会等待)。确保您正在处理其队列(从特殊线程或定期处理)。由于对辅助对象的硬引用,辅助对象尚未被收集。因此,可以在辅助对象上执行任何清理操作,然后丢弃PhantomReference,辅助对象最终也将被收集。
还有finalize()方法,看起来像一个析构函数,但不像一个析构函数。通常情况下,这不是一个好的选择。

为什么要使用PhantomReference而不是WeakReference? - uckelman
2
@uckelman:如果你只需要通知,那么PhantomReference就可以胜任这项工作,这基本上是它的设计目的。弱引用的附加语义在这里是不需要的,在你的ReferenceQueue被通知的时候,你不能再通过WeakReference恢复对象,所以使用它的唯一原因是为了避免记住PhantomReference的存在。弱引用所做的任何额外工作可能是微不足道的,但为什么要费心呢? - Steve Jessop
谢谢你提到PhantomReference。虽然不完美,但总比没有好。 - foo
@SteveJessop,您认为弱引用相对于虚引用有什么“额外工作”? - Holger

6
finalize()函数是析构函数。
然而,通常不应该使用它,因为它在垃圾回收之后被调用,而你无法确定何时会发生(如果有的话)。
此外,需要多次GC才能释放具有finalize()的对象。
你应该尝试在代码的逻辑位置使用try{...} finally{...}语句进行清理!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接