Java: 有足够的空闲堆来创建对象吗?

8

我最近在代码中发现了这样一个问题 - 基本上是有人试图创建一个大对象,但当没有足够的堆来创建它时就会复制:

try {
    // try to perform an operation using a huge in-memory array
    byte[] massiveArray = new byte[BIG_NUMBER];
} catch (OutOfMemoryError oome) {
    // perform the operation in some slower but less
    // memory intensive way...
}

这似乎不正确,因为Sun公司本身建议您不要尝试捕获Error或其子类。我们讨论了这个问题,并提出了另一个想法,即明确检查空闲堆:

if (Runtime.getRuntime().freeMemory() > SOME_MEMORY) {
    // quick memory-intensive approach
} else {
    // slower, less demanding approach
}

再次说明,这似乎不太令人满意 - 特别是在选择SOME_MEMORY的值时,很难将其与所需的工作轻松关联起来:对于某些任意大的对象,我怎么能估计它的实例化可能需要多少内存?

有更好的方法吗?在Java中是否可能进行这样的管理,或者任何管理语言本身抽象级别以下的内存的想法都不可能?

编辑1:在第一个示例中,估计给定长度的byte[]可能占用的内存量实际上是可行的,但是否存在一种更通用的方式,适用于任意大的对象?

编辑2:正如@erickson指出的那样,有办法在创建对象后估算其大小,但(忽略基于先前对象大小的统计方法)是否有一种方法可以为尚未创建的对象执行此操作?

还有一些争论,即捕获OutOfMemoryError是否合理 - 有人了解任何确凿的事情吗?

5个回答

5

freeMemory并不完全准确。您还需要添加maxMemory()-totalMemory()。例如,假设您使用max-memory=100M启动VM,在您的方法调用时,JVM可能仅从OS中使用50M。其中,假设有30M实际上由JVM使用。这意味着您将显示大约20M可用空闲内存(因为我们只讨论堆),但是如果您尝试创建更大的对象,则它将尝试从OS获取其合同允许的另外50M,然后放弃并出错。因此,您实际上(理论上)有70M可用内存。

要使这更加复杂,上面示例中报告为正在使用的30M包括可能有资格进行垃圾回收的内容。因此,如果达到上限,它将尝试运行GC以释放更多内存。

您可以尝试通过手动触发System.GC来解决此问题,但那并不是一件好事情,因为

-不能保证立即运行

-在运行期间会停止所有操作

您的最佳选择(假设无法轻松重写算法以处理较小的内存块,或者写入内存映射文件,或者使用更少的内存资源)可能是对所需内存进行安全粗略估计,并确保在运行函数之前可用。


System.gc()通常不会停止所有东西,这取决于底层垃圾收集器。 - erickson

2
我不相信有一种合理的、通用的方法能够被安全地假定为100%可靠,即使是免费内存的运行时方法也容易受到垃圾收集后实际上可能有足够内存的影响,但你除非强制进行gc,否则你是不知道的。但是没有绝对可靠的方法可以强制进行GC。:)
话虽如此,我猜想如果您确实知道大致需要多少内存,并在之前运行了System.gc(),并且您正在运行简单的单线程应用程序,则使用.freeMemory调用准确性还是相当高的。
然而,如果任何这些约束条件失败,并且您遇到OOM错误,则会回到原点,因此您可能与捕获Error子类无异。虽然这样做存在一些风险(Sun公司的虚拟机对OOM后发生的情况没有提供很多保证……存在一些内部状态损坏的风险),但对于许多应用程序而言,仅仅捕获它并继续前进不会造成严重的伤害。
然而,我认为更有趣的问题是:为什么有些情况下您确实有足够的内存来做这件事,而其他情况下则没有?也许对涉及的性能权衡进行更多的分析才是真正的答案?

你已经恰当地指出了真实的问题 - 实际的解决方法是重写方法,使其一开始就不需要大量内存。这个问题更多是学术性质,但仍然很有趣... - Dan Vinton
@jsight在“为什么有时候会发生这种情况..”上的问题。你能否(我并不是说应该在内存中完成)对文件系统文件进行加密?有时候整个文件只有1mb,而有时却有12gb。这时就无法释放足够的RAM了!! - OscarRyz
@Oscar:我不认为我会尝试去计算可用内存来确定如何处理这两种情况。相比于实时遥测,我更可能根据文件大小并基于系统启动时的内存来确定块大小,从而做出决策。 - jsight

2

有一些技巧可以用来估计现有对象的大小;你可以使用其中一些来预测尚未创建的对象的大小。

然而,在这种情况下,我认为最好捕获错误。首先,请求可用内存并不考虑垃圾回收后的可用空间,而在引发 OOME 前将执行垃圾回收。而且,使用 System.gc() 请求垃圾回收是不可靠的。它经常被显式禁用,因为它会破坏性能,如果没有禁用……那么,当不必要时使用它可能会破坏性能。

大多数错误都无法恢复。然而,可恢复性取决于调用者,而不是被调用者。在这种情况下,如果你有从 OutOfMemoryError 恢复的策略,捕获它并回退是有效的。

我猜实际上这取决于“慢”和“快”的区别。如果“慢”的方法足够快,我会坚持使用它,因为它更安全、更简单。而且,在我看来,允许它作为一个备用选项意味着它已经“足够快”。不要让小的优化影响您应用程序的可靠性。

2
“尝试分配内存并处理错误”的方法非常危险。
  • 如果你几乎用完了内存,那么稍后可能会发生OOM异常,因为你将事情推向了极限。几乎任何库调用都会至少短暂地分配内存。
  • 在你分配内存的同时,另一个线程可能会收到OOM异常,因为它正在尝试分配相对较小的对象。即使你的分配注定会失败。
唯一可行的方法是您的第二种方法,并纠正其他答案中指出的错误。但是当你决定使用内存密集型方法时,必须确保在堆中留有额外的“松弛空间”。

2

绝对捕捉错误是最糟糕的方法。当你无能为力时,就会出现错误。甚至连创建日志都不行,就像“...休斯顿,我们失去了虚拟机”。

我没有完全理解第二个原因。是因为难以将SOME_MEMORY与操作相关联吗?您能为我重新表述一下吗?

我唯一看到的替代方案是将硬盘用作内存(类似于旧日的RAM / ROM)。我想这就是您在“另一种速度较慢、要求较少的方法”中指向的内容。

每个平台都有其限制,Java支持的RAM数量取决于您的硬件愿意提供多少(实际上是通过配置VM来实现)。在Sun JVM实现中,可以使用

-Xmx 

选项

类似于

java -Xmx8g some.name.YourMemConsumingApp

例如,
当然,您可能会尝试执行需要10 GB RAM的操作。
如果是这种情况,那么您肯定应该使用磁盘交换。
另外,使用策略模式可以使代码更美观。虽然在这里看起来有些过度设计。
if (isEnoughMemory(SOME_MEMORY)) {
    strategy = new InMemoryStrategy();
} else {
    strategy = new DiskStrategy();
}

strategy.performTheAction();

但是,如果“else”涉及大量代码并且看起来很糟糕,则可能会有所帮助。此外,如果您可以使用第三种方法(例如使用云进行处理),则可以添加第三种策略。

...
strategy = new ImaginaryCloudComputingStrategy();
...

编辑

在使用第二种方法时遇到问题:如果有时您不知道将消耗多少 RAM,但您确实知道剩余多少 RAM,则可以使用混合方法(当您拥有足够的 RAM 时,使用 RAM;当您没有 RAM 时,使用 ROM[disk])。

假设有这样一个理论问题。

假设您从流中接收文件,并且不知道它有多大。

然后您对该流执行某些操作(例如加密它)。

如果仅使用 RAM,则速度非常快,但是如果文件足够大以至于会消耗所有应用程序内存,则必须在内存中执行一些操作,然后交换到文件并将临时数据保存在那里。

当内存不足时,VM 将进行 GC,您会获得更多内存,然后执行其他块。重复此过程直到处理完成大型流。

while( !isDone() ) {
    if (isMemoryLow()) {
        //Runtime.getRuntime().freeMemory() < SOME_MEMORY + some other validations 
        swapToDisk(); // and make sure resources are GC'able
    }

    byte [] array new byte[PREDEFINED_BUFFER_SIZE];
    process( array );

    process( array );
}

cleanUp();

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