如何设计:在随机访问文件时避免资源泄漏

3
我有一个客户端/服务器应用程序,其中客户端应用程序将打开文件。这些文件被分成块,并发送到服务器。
客户端不仅发送文件块,还发送其他数据。每个消息(数据或文件块)都有优先级级别。
所有消息都会根据其优先级缓冲并写入TCP流中。当前输入和输出流在发送或接收完文件后(客户端/服务器两侧)都会关闭。这意味着流会保持打开状态,直到发送文件完成为止。
TCP连接允许失败,因为它会重新连接并恢复消息发送,因此流将在某个时候关闭。
但是,如果例如JVM被终止,则流将不会被清除。
我解决这个问题的第一思路是在finalizer中添加清理代码,但我知道当JVM被终止时(或者调用System.exit时),finalizer可能不会运行。
我的第二个想法是重写应用程序的一部分,并仅在读/写一个块所需时间内使用流。因此,我最终会为每个块打开/关闭文件。这种方法的优点是允许我使用try-catch-finally结构,但我有一种打开和关闭这么多文件会带来相当大的开销的感觉。
那么,在设计不允许finally{}块的情况下,如何清理资源?或者应该避免这样的设计,也许以我描述的方式?
我还担心可能会打开与优先级数量相同的文件(理论上无限制)。
大多数文件通常只有几KB大小,但在某些情况下,它们可能会变得非常大,达到几GB。
感谢您的帮助。

也许我没有表达清楚:我所指的流是FileInputStream/FileOutputStreams。另一方面,TCP连接将永久保持在线(如果失败,则重新连接)。 - Charles V.G.
我不确定我理解了你的问题。通过清理流,你是指删除部分传输的文件吗? - WilQu
不,我的意思是在不同文件的FileInputStream / FileOutputStream上调用close()。我会尝试画些东西,让这个问题不那么神秘。我将在几分钟内回来进行编辑。 - Charles V.G.
2个回答

1

请看一下java.lang.Runtime.addShutdownHook()。我建议添加一个关闭所有打开的流的钩子,你需要维护这种情况下的列表。不过,注意以下内容:

在极少数情况下,虚拟机可能会中止,在没有清理关闭的情况下停止运行。这种情况发生在虚拟机被外部终止时,例如在Unix上使用SIGKILL信号或在Microsoft Windows中使用TerminateProcess调用。如果本地方法出现问题(例如,损坏内部数据结构或尝试访问不存在的内存),虚拟机也可能会中止。如果虚拟机中止,则不能保证是否会运行任何关闭挂钩。


谢谢,我会看一下的! - Charles V.G.

1

如果我理解问题正确,您担心在JVM以不受控制的方式终止时没有正确清理。

虽然这是一个普遍性的问题,但在您的特定情况下,我认为没有什么需要担心的。您只打开文件进行读取,因此没有持久状态会被您的应用程序破坏(据我了解,TCP连接的另一端可以优雅地处理断开连接)。打开的文件是应用程序内部的一种状态。如果应用程序被杀死,操作系统会负责清理所有锁定或任何数据结构,以便内部处理该文件上的操作。这意味着在OS内核中不会留下任何“垃圾”,虽然这既不是一种优雅的方式也不是一种推荐的清理方式,但它确实有效(当然,只在紧急情况下使用,而不是作为处理事物的正常方式)。关闭您的应用程序将自动关闭打开的文件,并且不应使文件处于任何不一致的状态。

当应用程序被杀死时,它将无法完成正在执行的任何操作。这可能会导致应用程序级状态不一致或其他某些应用程序逻辑问题,但仍然不会损坏任何操作系统级别的结构(假设操作系统没有漏洞)。您可能会丢失数据或破坏应用程序的状态,但不应该能够破坏文件系统或内核中的数据。

因此,如果您在具有事务样式操作的应用程序中杀死应用程序(其中您希望完全执行或根本不执行操作,但永远不应该使任何中间状态对外部世界可见),则可能会遇到问题。例如,如果您有一个文件,并且需要将其替换为更新版本,则首先截断旧文件,然后写入新内容(这是显而易见的方法)。如果在截断文件但在写入新内容之前杀死应用程序,则会出现问题。但是,为了引发这种风险,您需要可变状态,即编写某些内容。如果您只读取内容,则几乎可以肯定是安全的。

如果你遇到这种情况,可以采取几种方法。其中之一就是试图使应用程序变得牢固,并确保始终进行良好的清理。实际操作中,这非常困难。您可以将关闭挂钩添加到Java应用程序中,当应用程序关闭时执行。但它仅适用于受控制的关闭(如Linux上的常规 kill (SIGTERM))。但是,这不能保护您免受管理员(Linux上的 kill -9 ),OOM-killer(同样是Linux)等强制杀死应用程序的影响。当然,其他操作系统也有这些情况或其他情况,在这些情况下,应用程序被关闭而无法控制。如果您不编写在受控硬件和软件环境中运行的实时应用程序,则几乎不可能防止所有可能导致应用程序被强制终止并阻止其运行清理过程的方式。
因此,通常的妥协是在应用程序中只采取简单的预防措施(如关闭挂钩),但要记住无法防止一切,因此要使手动清理变得可能和容易。例如,对于覆盖文件的情况,解决方案是将操作分成两个步骤:首先将旧文件移动到新名称以备份(此操作通常在操作系统级别上是原子的,因此安全),然后将文件的新内容写入旧名称,最后在检查新文件已正确写入后删除备份。这样,如果应用程序在操作之间被终止,就存在一个简单的清理过程:将备份文件移回原始名称,从而恢复到较旧但一致的状态。这可以手动完成(但应记录),或者您可以向应用程序添加清理脚本或特殊的“清理”命令,以使此操作变得简单、可见并消除过程中的人为错误可能性。假设您的应用程序很少被杀死,这通常比花费大量时间尝试使应用程序防弹更有效。接受失败通常比试图防止它更好(不仅在编程中)。

在操作系统和硬件层面上仍然可能会出现问题,但是对于普通硬件和操作系统来说,这真的很难避免。即使您的应用程序在正确的位置看到了新文件,它可能并没有被物理写入磁盘,如果它只驻留在缓存中,那么如果您的应用程序被杀死,它不会丢失,但如果有人拔掉机器的电源,它就会丢失。但是处理这种故障是完全不同的故事。

长话短说,在您只读取文件的特定情况下,如果您的应用程序被杀死,操作系统应该执行所有清理工作,对于其他情况,上面提到了一些提示。


你所说的话充满了智慧。在应用程序/业务逻辑层面上,一切都可以失败,就像你建议的那样。 - Charles V.G.
我会研究添加关闭挂钩,谢谢你的提示!有人确认我不会损坏操作系统及其文件描述符/句柄,这让我感到宽慰。但是我必须提到,我确实有文件正在写入(通信是双向的),我知道这在我的帖子中并不清楚,所以我接受这个答案是正确的。你知道长时间打开许多文件句柄会导致性能下降吗?我相信文件句柄的数量在操作系统级别是有限制的。 - Charles V.G.
为了完整起见,我应该补充说明我正在研究这个问题,因为我曾经在Win2008R2上遇到了这个描述在这里(https://dev59.com/mXE85IYBdhLWcg3wx2n2)的错误。那里建议的解决方案没有起作用。 - Charles V.G.
@CharlesV.G. 是的,保持过多的文件描述符打开可能本身就是一个问题,尽管它与原帖中描述的问题有所不同。在Linux/UNIX上,你有ulimit,出于安全考虑,它限制了打开的文件描述符数量,这些文件描述符被文件和网络连接使用(默认通常为1024个)。我不清楚Windows是否有类似的机制。如果你遇到此限制,你会收到适当消息的IOException,但通常很难以更聪明的方式处理这个问题,最好的办法是关闭不再需要的句柄。 - Michał Kosmulski

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