尽管我已经给Java足够的内存,但它仍然会耗尽内存!

5

我正在运行一个java服务器(具体是Winstone:http://winstone.sourceforge.net/)。

命令如下: java -server -Xmx12288M -jar /usr/share/java/winstone-0.9.10.jar --useSavedSessions=false --webappsDir=/var/servlets --commonLibFolder=/usr/share/java

它之前一直很好用,但现在需要加载更多的内容到内存中。

奇怪的是,根据“top”显示,它有15.0g的VIRT(虚拟内存),而RES(实际使用内存)为8.4g。一旦达到8.4g,即使从磁盘加载,CPU也会停留在100%(这时Java正在进行垃圾回收),最终我会收到Java的OutOfMemoryError错误。 预计,CPU卡在100%是由于Java在进行垃圾回收。

那么,我的问题是什么? 我已经给了它12G的内存! 它只使用了8.2G的内存就挂掉了。我做错了什么吗?

哦,我使用的是 java version "1.6.0_07" Java(TM) SE Runtime Environment (build 1.6.0_07-b06) Java HotSpot(TM) 64-Bit Server VM (build 10.0-b23, mixed mode) 在Linux上。

谢谢, Matt


2
如果这8.4GB被放置在这12GB的“中间”,并且程序试图一次性分配3GB的块,那么可能不存在这样一个连续的、空闲的内存块。这会导致GC启动以尝试释放一些东西,但如果它无法释放足够的内存来找到该块,则即使它在技术上应该是空闲的,它也会崩溃。 - Michael Madsen
它添加的东西并不是特别大。它是一堆文本文件(一堆约为25-50,000个)。所以它并不像试图分配一个大块内存。虽然也许它遇到了一些碎片化问题...?但是GC应该可以清理这个问题,对吧? - spitzanator
从下面的评论中可以看出:在这个进程开始运行之前,你可以尝试运行另一个占用大量内存的进程。如果你的服务器以相同的方式崩溃,但是在较低的内存大小下(我认为会这样),那至少能表明操作系统内存不足可能导致你所看到的相同问题。(当然,这并不能证明这就是真正的问题。) - Dan Breslau
那是非常多的内存!!! 你确定所有东西都被优化得尽可能好了吗? - Ricket
6个回答

8
奇怪的是,根据“top”命令,它有15.0g的虚拟内存(VIRT),而其常驻内存(RES)为8.4g。一旦达到8.4g,即使从磁盘加载,CPU也会挂起100%,最终导致Java的OutOfMemoryError错误。
我认为你误解了事情。“-Xmx12288M”选项并不保留物理内存。它设置了Java堆大小的上限。Java还需要内存来存储非堆对象,例如permgen空间、代码空间、内存映射文件等等。一个12g的堆加上JVM使用/共享的非堆内存,总和可能达到15g。
“top”命令报告的8.4g RES是当前用于运行JVM的物理内存量。它与Java堆的大小没有直接关系。实际上,您可以预期RES数字会随着操作系统虚拟内存系统交换不同进程的页面而上下移动。这完全超出了JVM的控制范围。
很可能,CPU挂起100%是Java在进行垃圾回收。
是的,通常会发生这种情况。
我能想到三种可能的解释:
1.最有可能的是,操作系统无法给JVM所请求的内存,因为没有足够的交换磁盘空间。例如,如果您有两个使用15g虚拟内存的进程,那么就是30gb。鉴于您有24g的物理内存,您将需要至少8g(可能更多)的交换空间。如果可分配给用户进程的物理内存量+交换空间量小于进程使用的总虚拟空间,则操作系统将开始拒绝JVM扩展堆的请求。您可以运行“swapon -s”查看可用/正在使用的交换空间量。
2.您的应用程序可能真的使用了您所说的12g堆的所有内存,并且这不够用。(也许您有一个存储泄漏。也许它确实需要更多内存。)
3.也有可能(但极不可能)有人设置了进程限制。您可以使用shell内置的'ulimit'命令查看是否已经设置了此项操作; 有关详细信息,请参阅“man ulimit”。
  • 如果您使用-verbose:gc-XX:+PrintGCDetails选项,GC可能会给您更多关于发生了什么的线索。特别是,它将告诉您在内存耗尽时Java堆实际有多大。

  • 您可以编写一个简单的Java应用程序,只分配而不释放大量内存,然后查看在使用与当前选项相同的方式运行时它能够分配多少内存,然后在OOM错误时崩溃。(我认为这不会告诉您任何新信息。编辑2实际上,如果按照@Dan的建议运行它,它将告诉您一些信息!)

  • 如果(似乎很可能)真正的问题是您没有足够的交换空间,则无法通过Java端来解决此问题。您需要重新配置系统以拥有更多的交换空间。请参阅Linux系统管理文档,swaponmkswap等的man页面。


嗯,我怀疑 JVM 使用了 8.4 GB 的内存。随着我们打开文件,它会线性扩展,并存储在堆中。虽然我没有设置交换空间,但我想这可能是原因之一。另外,绝对不会有进程限制。有没有其他好办法可以传给 JVM(类似于“-Xmx12288M”),让它按照我希望的方式运行(或者至少给我一个更好的失败指示)? - spitzanator
请记住,像 str += otherString; 这样的简单操作可能需要至少 (str + otherString)*2 的内存长度,如果无法分配,则会出现 oom 异常。如果 str 很大,这可能很重要。如果您在移动数组以扩展内容或使用库来执行此操作,则同样如此,而这些操作都是在您看不到的情况下进行的。 - nos
@spitzanator:我已经在你上面的问题中回复了。 - Dan Breslau

1

有时候OutOfMemoryError并不意味着对象堆已经用完,而是其他原因。错误堆栈中是否有其他信息?

特别地,JVM需要一堆内存/地址空间与堆空间分开。在某些情况下,给进程更多的对象堆可能会减少这个其他池的空间,反而使得使用更大的“-Xmx”设置更容易出现OOME!

内存映射文件可能会占用大量的地址空间;未正确关闭文件可能会导致该空间保留直到GC/finalization,在不可预测的时间发生。此外,具有更大的堆会推迟GC/finalization——这意味着这个本地地址空间可能会保留更长时间,因此:更大的堆可能意味着由于其他内存耗尽而更频繁地出现OOME。

您还可以使用相同值的“-Xms”来强制立即在启动时获取所有堆地址空间,从而在更方便/可理解/可重现的阶段触发失败。

最后,即使没有内存请求失败,也存在一个阈值,当GC花费的时间“太多”时会抛出OOME,这表明垃圾正在被创建得和收集一样快,可能是在一个紧密的循环中。一个从磁盘加载大量低效数据,为少量保留数据创建大量垃圾的情况可能会导致这种情况。查找[GC overhead limit]以获取详细信息。

0

你能升级到最新的Java版本(1.6.0_17)吗?


好的调用。我升级了,但似乎没有帮助。 - spitzanator

0
也许这是一个愚蠢的问题,但你确定程序启动时你的交换分区至少有15GB的可用空间吗?

啊,抱歉。机器有24个G的内存。我分配了12给这个进程,另外12给另一个进程(它们都需要12)。这不是一个愚蠢的问题。 - spitzanator
但是交换空间呢?你有配置任何交换空间吗,还是完全从RAM运行?如果你完全从RAM运行,那么除非Linux知道它应该将12GB分配给进程(我不确定如何做到这一点),否则你可能会因为其他进程的争用而耗尽物理内存。 - Dan Breslau
机器的RAM没有争用。我已经查看过了,当Java进程崩溃时仍然有可用的空闲RAM。 - spitzanator

0

只是为了确认,它不是PermGen空间用尽了,对吗?


错误不在PermGen空间上,至少异常信息没有这么说... - spitzanator

0
奇怪的是,根据“top”的显示,它有15.0g的虚拟内存,而其常驻内存集大小为8.4g。一旦达到8.4g,即使从磁盘加载,CPU也会挂起在100%,最终导致Java的OutOfMemoryError。可以推测,CPU挂起在100%时,Java正在进行垃圾回收。
我猜Java必须使用那么多内存,以至于它决定(或被迫)使用磁盘空间来存储数据。现在,下一次它必须收集垃圾时,就必须使用IO来完成此操作。在磁盘上进行垃圾回收肯定是相当昂贵的。由于您有这么多对象,垃圾收集器可能会一遍又一遍地介入。这可能是为什么您的CPU处于100%状态...永远进行垃圾回收。
正如Stephen所说的那样,-verbose:gc和-XX:+PrintGCDetails将提供更多提示。
但除此之外,也许值得投资于不必一次性加载所有文件的实现?在内存极限工作似乎不是一个成功的策略。

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