Java是否有析构函数?


594

Java是否有析构函数?我似乎无法在此找到任何文档。如果没有,我怎么能达到同样的效果?

为了使我的问题更具体,我正在编写一个处理数据的应用程序,规范中说应该有一个“重置”按钮,以将应用程序恢复为最初的启动状态。但是,除非关闭应用程序或按下重置按钮,否则所有数据都必须处于“活动”状态。

我通常是C / C ++程序员,所以我认为这是微不足道的。(因此,我计划最后实现它。)我对程序进行了结构设计,以使所有“可重置”对象都位于同一类中,以便在按下重置按钮时可以销毁所有“活动”对象。

我在想,如果我只是想取消引用数据并等待垃圾收集器来收集数据,如果我的用户重复输入数据并按下“重置”按钮,会不会发生内存泄漏?我也在想,因为Java作为一种语言已经相当成熟,所以应该有一种方法可以防止这种情况的发生或妥善解决。


7
如果您持有对不需要的对象的引用,则只会发生内存泄漏。即您的程序中有一个错误。GC将根据需要运行(有时会更快)
彼得·劳瑞

17
如果您通过对象快速处理数据,则虚拟机将无法足够快地运行GC。GC可以始终跟上或做出正确决策的想法是一个谬论。
Kieveli 2011年

1
@Kieveli在给出错误之前,JVM不能运行GC吗?
WVrock 2014年

4
是的,如果有Java的析构函数将其彻底销毁一次,那将是很好的。
托马什Zato -恢复莫妮卡

Answers:


526

因为Java是一种垃圾收集语言,所以您无法预测何时(甚至不然)一个对象将被销毁。因此,没有析构函数的直接等效项。

有一个称为的继承方法finalize,但这完全由垃圾收集器决定调用。因此,对于需要显式整理的类,约定是定义一个close方法并仅将finalize用于完整性检查(即,如果尚未调用close,请立即进行操作并记录错误)。

最近有一个问题引起了对定稿的深入讨论,因此如果需要的话应该提供更多的深度...


5
在这种情况下,“ close()”是否引用java.lang.Autocloseable中的方法?
Sridhar Sarnobat 2013年

21
不,AutoCloseable是Java 7中引入的,但是'close()'约定已经存在了很长时间。
Jon Onstott 2014年

为什么您无法预测何时(或什至)一个对象将被销毁。有什么近似的其他方式可以预测呢?
dctremblay '16

@dctremblay对象破坏由垃圾收集器完成,垃圾收集器可能永远不会在应用程序的生存期内运行。
Piro说恢复莫妮卡的时间为

4
请注意,该finalize方法在Java 9中弃用
。– Lii

124

看一下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在应用程序的运行期间根本不会调用任何方法。


12
我很惊讶这票数很少。这是实际答案。
nurettin

20
我不同意这是实际答案。如果实例有一个资源,它在多个方法调用中需要处理更长的时间,那么try-with-resources将无济于事。除非可以按上述方法调用的速率关闭并重新打开该资源,否则这不是普遍现象。
埃里克

14
确实,这不是实际的答案。除非对象的构造和使用完全由封装,try并且finally用来强制调用,否则无法使用此结构来管理对象的销毁obj.finalize()。而且,即使这种设置也不能解决OP带来的问题:“复位”按钮触发的对象销毁中间程序。
7yl4r '16

1
其他用户已经在您的应用程序入口点显示了此操作。全局定义变量。尝试使用输入功能对其进行初始化。最后(在应用程序关闭时)取消初始化。这完全有可能。
TamusJRoyce

1
@nurettin当问到这个问题时,Java 7才问世了3个月,是否有帮助。
corsiKa

110

不,这里没有破坏者。原因是所有Java对象都是堆分配和垃圾回收。没有显式的释放(即C ++的delete运算符),就没有明智的方法来实现真正的析构函数。

Java确实支持终结器,但它们仅用作保护持有本地资源的句柄(例如套接字,文件句柄,窗口句柄等)的对象的保障。当垃圾收集器在没有终结器的情况下收集对象时,它仅标记内存地区是免费的,仅此而已。当对象具有终结器时,首先将其复制到一个临时位置(请记住,我们在此处进行垃圾收集),然后将其放入等待等待终结的队列中,然后终结器线程以非常低的优先级轮询该队列。并运行终结器。

当应用程序退出时,JVM停止运行而无需等待未完成的对象完成,因此实际上无法保证您的终结器将运行。


4
感谢您提到本地资源-这是“类析构函数”方法有用的领域。
内森·奥斯曼

是的,我现在通过释放对C ++的本机调用分配的资源/句柄面临着同样的问题。
尼克,

@ddimitrov,理论上Java可以实现显式释放吗?还是这是逻辑上的矛盾?

1
@mils天真地实现显式释放会破坏Java的假设,即任何引用都指向活动对象。您可以遍历所有指针并使别名空值化,但这比GC昂贵。或者,您可以尝试使用一些线性类型系统(请参阅Rust中的“所有权”),但这是主要的语言更改。也有其他选项(请参阅JavaRT范围内存等),但是通常,显式释放并不适合Java语言。
ddimitrov

31

应该避免使用finalize()方法。它们不是可靠的资源清理机制,并且有可能通过滥用它们在垃圾收集器中引起问题。

如果需要在对象中进行释放调用,例如要释放资源,请使用显式方法调用。可以在现有的API(例如CloseableGraphics.dispose()Widget.dispose())中并且通常通过try / finally进行调用。

Resource r = new Resource();
try {
    //work
} finally {
    r.dispose();
}

尝试使用已处置的对象应引发运行时异常(请参见 IllegalStateException)。


编辑:

我当时在想,如果我只是想取消引用数据并等待垃圾收集器来收集数据,那么如果我的用户反复输入数据并按下“重置”按钮,会不会发生内存泄漏?

通常,您需要做的就是解除对对象的引用-至少,这就是它应该起作用的方式。如果您担心垃圾收集,请查看Java SE 6 HotSpot [tm]虚拟机垃圾收集优化(或JVM版本的等效文档)。


1
那不是取消引用的意思。它不是“将对象的最后一个引用设置为null”,而是从引用中获取(读取)值,以便您可以将其用于后续操作。

1
try..final仍然是有效和推荐的方法吗?假设我以前在finalize()中调用本机方法,可以将调用移到finally子句吗? class Resource { finalize() { destroy(); } protected native void destroy(); } class Alt_Resource { try (Resource r = new Resource()) { // use r } finalize { r.destroy(); }
aashima

r不会限制在finally块内。因此,您此时无法调用销毁。现在,如果您将作用域更正为在try块之前创建对象,那么最终将遇到“在try-with-resources之前”的丑陋案例。
userAsh

21

随着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 2014年

3
我在上述答案中读到的@shintoZ无法保证finalize()执行
Asif Mushtaq 2015年

14

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

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

这与析构函数不同,但是可以实现一个关闭钩子,该钩子具有注册了侦听器对象的侦听器对象,可以在该侦听器对象上调用清除方法(关闭持久性数据库连接,删除文件锁等),这些操作通常可以在破坏者。同样,这不是销毁器的替代品,但是在某些情况下,您可以使用此方法获得所需的功能。

这样做的好处是使解构行为程序的其余部分松散耦合


addShutdownHook显然是在Java 1.3中引入的。无论如何,我可以在1.5中使用它。:):参见:stackoverflow.com/questions/727151/…–
skiphoppy

1
仅供参考,以我的经验,如果您在Eclipse中使用红色的“终止”按钮,则不会调用关闭挂钩-整个JVM立即被销毁,关闭挂钩不会被正常调用。这意味着如果使用Eclipse开发,则在开发和生产过程中可能会看到不同的行为
Hamy 2015年

11

没有, java.lang.Object#finalize是您能得到的最接近的东西。

但是,不能保证何时(如果)调用它。
看到:java.lang.Runtime#runFinalizersOnExit(boolean)


5
在我的书中,可能会或可能不会被调用的方法基本上是没有用的。最好不要使用无用的特殊方法来污染语言,这种方法充其量只会给人以错误的安全感。我永远不会理解为什么Java语言的开发人员认为finalize是个好主意。
antant

@antred Java语言的开发人员同意。我想,对于当时的某些人来说,这是他们第一次设计带有垃圾回收的编程语言和运行时环境。较难理解的是,为什么其他托管语言一次复制了该概念,而这已经被理解为一个坏主意。
Holger

7

首先,请注意,由于Java是垃圾回收的,因此很少需要做任何关于对象销毁的事情。首先,因为您通常没有任何可释放的托管资源,其次,因为您无法预测何时或是否会发生这种情况,因此对于需要“在没有人再使用我的对象的情况下立即发生的事情”是不合适的”。

在使用java.lang.ref.PhantomReference销毁对象之后,您会收到通知(实际上,说它已被销毁可能有点不准确,但是如果对它的幻像引用已排队,则它不再可恢复,通常等于同样的事情)。常见用法是:

  • 在您的类中分离出需要分解为另一个帮助器对象的资源(请注意,如果您要做的只是关闭连接,这是很常见的情况,则无需编写新的类:在这种情况下,要关闭的连接将是“帮助对象”。
  • 创建主对象时,还要为其创建PhantomReference。让它引用新的帮助器对象,或者建立从PhantomReference对象到其对应的帮助器对象的映射。
  • 收集主要对象后,会将PhantomReference排队(或者可以将其排队-像终结器一样,无法保证它会一直存在,例如,如果VM退出则它不会等待)。确保正在处理其队列(在特殊线程中或不时进行处理)。由于硬引用了辅助对象,因此尚未收集辅助对象。因此,您需要对辅助对象进行任何清理,然后丢弃PhantomReference,最终也会收集辅助对象。

还有finalize(),它看起来像一个析构函数,但行为却不一样。通常这不是一个好选择。


为什么用PhantomReference而不是WeakReference?
uckelman 2011年

2
@uckelman:如果您只想通知,那么PhantomReference可以完成工作,这几乎就是它的设计目的。这里不需要WeakReference的其他语义,并且在通知ReferenceQueue时,您无法再通过WeakReference恢复对象,因此使用它的唯一原因是不必记住必须存在PhantomReference。WeakReference所做的任何额外工作都可以忽略不计,但是为什么要打扰呢?
Steve Jessop

感谢您对PhantomReference的提示。它不是完美的,但总比没有好。
foo 2012年

@SteveJessop,您认为什么是“额外的工作”?
Holger

6

finalize()函数是析构函数。

但是,通常不应使用它,因为它是在GC之后调用,您无法确定何时会发生(如果有的话)。

此外,要取消分配具有 finalize()

您应该尝试使用try{...} finally{...}语句清除代码中逻辑上的位置!


5

我同意大多数答案。

您不应完全依赖finalizeShutdownHook

完成

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

  2. finalize()仅由GC线程调用一次。如果对象从finalization方法中恢复过来,则finalize不会再次调用。

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

  4. Exception由finalize方法抛出的任何内容都将被GC线程忽略

  5. System.runFinalization(true)Runtime.getRuntime().runFinalization(true)方法增加了调用finalize()方法的可能性,但是现在不赞成使用这两种方法。由于缺乏线程安全性和可能产生死锁,这些方法非常危险。

shutdownHooks

public void addShutdownHook(Thread hook)

注册一个新的虚拟机关闭挂钩。

Java虚拟机将响应以下两种事件而关闭:

  1. 当最后一个非守护线程退出时System.exit,或者调用exit(等效于)方法时,程序将正常退出,或者
  2. 响应于用户中断(例如键入^ C)或系统范围的事件(例如用户注销或系统关闭)来终止虚拟机。
  3. 关闭钩子只是一个已初始化但未启动的线程。当虚拟机开始其关闭序列时,它将以未指定的顺序启动所有已注册的关闭挂钩,并使其同时运行。当所有挂钩完成后,如果启用了退出完成,它将运行所有未调用的终结器。
  4. 最后,虚拟机将停止。请注意,如果关闭是通过调用exit方法启动的,则守护程序线程将在关闭序列期间继续运行,非守护程序线程也将继续运行。
  5. 关机挂钩也应迅速完成工作。程序调用exit时,期望虚拟机将立即关闭并退出。

    但是,即使Oracle文档引用了

在极少数情况下,虚拟机可能会中止,即在不干净关闭的情况下停止运行

当虚拟机在外部终止时会发生这种情况,例如SIGKILL在Unix上使用信号或TerminateProcess在Microsoft Windows上使用调用。如果本机方法出错(例如,破坏内部数据结构或尝试访问不存在的内存),则虚拟机也可能中止。如果虚拟机中止,则无法保证是否将运行任何关闭挂钩。

结论适当地使用try{} catch{} finally{}块并在finally(}块中释放关键资源 。在finally{}块释放资源期间,捕获Exception和捕获Throwable


4

如果只是担心而已,那就不要担心。只要相信GC,它就可以做得不错。我实际上看到了一些东西,它是如此高效,以至于在某些情况下创建微小对象的堆比使用大型数组的性能要好。


3

也许您可以使用try ... finally块在使用对象的控制流中最终确定对象。当然,它不会自动发生,但是在C ++中销毁也不会自动发生。您通常会在finally块中看到资源关闭。


1
当所讨论的资源只有一个所有者并且从未有其他代码“窃取”对它的引用时,这是正确的答案。
史蒂夫·杰索普

3

Lombok中有一个@Cleanup批注,该批注大部分类似于C ++析构函数:

@Cleanup
ResourceClass resource = new ResourceClass();

在处理时(在编译时),Lombok插入适当的try-finally块,以便resource.close()在执行离开变量的范围时调用该块。您还可以显式指定另一种释放资源的方法,例如resource.dispose()

@Cleanup("dispose")
ResourceClass resource = new ResourceClass();

2
我看到的好处是嵌套将更少(如果您有很多需要“销毁”的对象,这可能很重要)。
阿列克谢

一个有资源的尝试块可以一次包含多个资源
亚历山大-恢复莫妮卡

1
但是它们之间没有指令。
阿列克谢

公平。我认为,那么建议是即使对于多个资源也要使用try-with-resource,除非它们之间需要指示,迫使您制作新的try-with-resource块(增加嵌套),然后使用@Cleanup
Alexander-Reinstate莫妮卡

2

与Java中的析构函数最接近的等效项是finalize()方法。与传统析构函数的最大区别在于,您不能确定何时调用它,因为这是垃圾收集器的责任。我强烈建议您在使用它之前仔细阅读此内容,因为您用于文件句柄等的典型RAIA模式无法与finalize()一起可靠地工作。


2

这里有很多不错的答案,但是还有一些其他信息说明您为什么应该避免使用 finalize()

如果JVM由于System.exit()或而退出Runtime.getRuntime().exit(),则默认情况下不会运行终结器。从Javadoc中获取Runtime.exit()

虚拟机的关闭序列包括两个阶段。在第一阶段,所有已注册的关闭挂钩(如果有)以某种未指定的顺序启动,并允许它们并发运行直到它们完成。在第二阶段,如果启用了退出完成,则所有未调用的终结器都将运行。完成此操作后,虚拟机将停止。

你可以打电话 System.runFinalization()但它只是“尽一切努力完成所有出色的定稿”,而不是保证。

有一种System.runFinalizersOnExit()方法,但是不要使用-它是不安全的,很久以前已过时。


1

如果要编写Java Applet,则可以重写Applet的“ destroy()”方法。它是...

 * Called by the browser or applet viewer to inform
 * this applet that it is being reclaimed and that it should destroy
 * any resources that it has allocated. The stop() method
 * will always be called before destroy().

显然,这不是想要的,而是其他人正在寻找的东西。


1

仅考虑原始问题...,我认为我们可以从所有其他学到的答案中得出结论,也可以从Bloch的基本有效Java第 7项“避免终结器”中得出结论,以某种方式寻求合法问题的解决方案不适合Java语言...:

...并不是OP真正想要的解决方案,而是将所有需要重置的对象保留在某种“游戏围栏”中的解决方案,所有其他不可重置的对象只能通过某种引用对其进行引用访问对象的...

然后,当您需要“重置”时,断开现有的游戏围栏,然后重新制作一个:游戏围栏中的所有对象被抛弃,永不返回,并且有一天将由GC收集。

如果这些对象中的任何一个是Closeable(或没有,但是有一个close方法),您可以在Bag创建(并可能打开)它们时将它们放在游戏围栏的a 中,并且访问者在切断游戏围栏之前的最后动作是通过所有Closeables关闭...?

该代码可能看起来像这样:

accessor.getPlaypen().closeCloseables();
accessor.setPlaypen( new Playpen() );

closeCloseables可能是一种阻塞方法,可能涉及一个闩锁(例如CountdownLatch),以处理(并酌情等待)特定于结束的线程中的任何Runnables/ ,尤其是在JavaFX线程中。CallablesPlaypen


0

尽管Java的GC技术已经取得了相当大的进步,但是您仍然需要注意参考。人们想到的是看似琐碎的参考模式的许多情况,这些模式实际上是引擎盖下的老鼠巢。

从您的帖子中看来,您似乎并没有为实现对象重用而尝试实现reset方法(是吗?)。您的对象是否持有需要清除的其他任何类型的资源(即必须关闭的流,必须返回的任何合并或借用的对象)?如果您唯一担心的是内存释放,那么我将重新考虑对象结构,并尝试验证我的对象是否为自包含结构,这些结构将在GC时清除。



0

没有Java没有析构函数.Java背后的主要原因是垃圾收集器始终在后台被动工作,并且所有对象都在堆内存中生成,这就是GC工作的地方。在c ++中,我们由于没有类似垃圾收集器的东西,因此必须显式调用delete函数。


-3

我以前主要处理C ++,这也是导致我寻求析构函数的原因。我现在经常使用JAVA。我做了什么,但这可能并不是每个人的最佳情况,但是我通过将所有值重置为0或通过函数将其默认值实现了自己的析构函数。

例:

public myDestructor() {

variableA = 0; //INT
variableB = 0.0; //DOUBLE & FLOAT
variableC = "NO NAME ENTERED"; //TEXT & STRING
variableD = false; //BOOL

}

理想情况下,这并非在所有情况下都适用,但是在存在全局变量的情况下,只要您没有很多变量,它将起作用。

我知道我不是最好的Java程序员,但似乎对我有用。


尝试更多地使用不可变的对象,在您获得它之后,这将更有意义:)
R. van Twisk

5
这并没有多大意义,因为它毫无意义-即一无所获。如果您的程序需要重置原始类型才能正常工作,则您的类实例的作用域设置不正确,这意味着您可能在不创建新Object()的情况下将现有对象的属性重新分配给新对象的属性。
simo.3792 2014年

1
除了有很多重置变量的需求。我选择名称析构函数是因为它适合我的工作。它确实成就了什么,您几乎什么都不懂
ChristianGong
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.