当JVM在运行时没有足够的内存进行分配时会发生什么?

22

思考了很长时间,试图找到一种通用的方式来提出这个问题(但是没有成功),我现在打算举一个具体的例子:

假设我有一台Linux机器,其中有1GB内存可以分配给进程(物理内存和交换空间总计1GB)。

我在机器上安装了标准的Oracle Hotspot JVM版本7。如果在某个时刻,已经运行的程序占用了其中400MB,只有剩余的600MB可用,我使用以下JVM标志启动一个Java程序:

java -Xms256m -Xmx512m -jar myJar.jar

发生了什么? :

A. JVM是否因为尝试分配所有512 MB内存而失败(因为当前可用内存不足)而无法立即启动?

如果JVM启动:

如果在某个时刻,运行中的Java进程需要超过400 MB的内存(并且除了当前Java进程已经使用的内存之外,仍然只有400 MB的空闲内存),会发生什么情况:

B. Java进程是否会因为OutOfMemoryError而失败?

C. 它是否会失败并显示其他(标准)错误信息?

D. 这是未定义的行为吗?


1
我相信选项(B),但如果只有200 Mb(小于Xms)可用,则选择A。 - Scorpion
6个回答

11

-Xmx仅仅定义了堆的最大大小,它不保证有足够的内存可用。它只确保堆永远不会超过给定值。话虽如此,选项B)将发生,将抛出OutOfMemoryError。


谢谢。但是,我可以假设如果我传递一个比当前可用内存大小更大的值(例如,在我的例子中,如果我将Xms更改为500m),那么JVM将无法启动吗? - Shivan Dragon
1
请点击这里查看:https://dev59.com/LHA65IYBdhLWcg3wxBir - Polygnome
谢谢提供链接,那个问题解决了我的一些疑惑,但它只涉及到Xmx超过系统实际总内存的情况。我想知道的是当Xmx值超过当前可用(空闲)内存(物理和交换)时会发生什么(但不会超过系统已安装的物理内存总量,也不会违反32位内存限制)。 - Shivan Dragon

6
假设我有一台Linux机器,它有1GB的内存可以分配给进程(物理和交换总共1GB)。
我的第一反应是,除非你在谈论手机,否则我会增加更多的内存。你可以花不到100美元买16GB(b = bit,B = byte)。
如果您的系统没有512MB(加上一些开销),它在启动时分配用于堆的连续虚拟内存时可能会失败,因此可能无法立即启动JVM。
即使您有550MB可用,程序也可能无法启动,因为它需要加载的内容不仅仅是堆。
如果您的程序在运行时使用了512MB,无论您的机器有多少内存,都可能会发生OutOfMemoryError。这个错误只会在JVM启动后发生。如果不能启动,您不会收到此错误。
它会因为其他标准错误而失败吗?
如果程序开始后你的交换空间用尽了,那么这是有可能发生的。这种情况很少见,只会在负载严重的机器上发生。我所见过的情况是由于低级操作系统无法分配内存而导致JVM崩溃。 Java 6 Update 25 VM crash: insufficient memory

特别是关于未定义行为的最后一条评论,给你点赞+1。 - Shivan Dragon
1
@Peter - 我不是完全确定,但我认为如果系统的内存少于-xmx指定的内存大小,JVM将被设置为最大可用内存,并不会立即崩溃。--- 不知道-xms是否也适用。可能不是。--- 我还认为答案应该更加概括,而不是围绕着512 MB的例子,以便更清晰地向将来可能发现这个问题的其他社区成员解释。 --- 对出色的回答点赞。特别是最后一部分。 - CosmicGiant
1
-xms设置对启动程序没有任何影响,它告诉垃圾回收器轻松增长到这个大小。注意:即使您将其设置为最小值,"Hello World"程序也不会使用最小大小。 - Peter Lawrey
所以,如果您将-xmx-xms设置为2 GB,并且系统可用空间大于此值,则在运行时不会发生崩溃,直到触发OutOfMemoryError,但是如果系统的可用空间小于指定值,则-xmx将设置为小于2 GB,从而导致比-xms更小,从而触发启动崩溃。 --- 据我所理解的。 - CosmicGiant
我有一个进程,其中xms和xmx相同,但是我的进程有时会在测试环境中崩溃,因为内存不足,我认为低内存会导致我的进程崩溃,但是我找不到OOM错误或OOM堆转储文件(我已经打开了选项)。如果我是正确的,为什么没有OOM文件? - JaskeyLam
显示剩余3条评论

2

OutOfMemoryError是一种错误类型,当Java虚拟机因为内存不足无法分配对象并且垃圾回收器无法提供更多内存时,将会抛出该错误。

因此,简而言之,“B. Java进程将会因为OutOfMemoryError而失败”


谢谢你的回答。我可以假设这对于所有热点实现都是正确的吗(即对于所有可用热点JVM的操作系统)? - Shivan Dragon

2
如果你所占用的内存太多,以至于剩余空间无法维持一个空闲的JVM,你将会得到一些错误提示,例如程序没有足够的内存,或者JVM会崩溃。
如果你可以运行JVM,你可以使用-Xmx指定堆空间的限制。这并不意味着JVM在启动时会分配所有的堆空间,它只是一个内部限制。如果JVM想要增加堆空间,但是没有足够的内存,或者需要的堆比-Xmx指定的还要大,那么在当前运行的Java程序中,你将会遇到OutOfMemoryError。
在极端情况下,当JVM正在运行时,你可能会耗尽剩余内存,同时JVM需要更多的内存进行其内部操作(而不是堆空间)- 这时JVM会告诉你它需要更多的内存,但是无法获取任何内存,然后终止或直接崩溃。

我有一个进程,其中xms和xmx相同,但是我的进程在测试环境中有时会崩溃,因为内存不足,我认为低内存会杀死我的进程,但我找不到OOM错误或OOM堆转储文件(我打开了选项)。如果我是正确的,为什么没有OOM文件? - JaskeyLam

1

JVM进程将在虚拟内存中运行,因此其他正在运行的进程的分配问题是相关的,但并不完全决定性。

当JVM无法分配更多内存(无论出于何种原因)时,进程本身不会终止,而是开始在JVM内部抛出OutOfMemoryError,但不会在JVM外部抛出。换句话说,JVM继续运行,但在JVM内运行的程序通常会失败,因为大多数程序不能充分处理低内存条件。在这种相当普遍的情况下,当程序没有采取任何措施来处理错误时,JVM将终止程序并退出。最终,这是由内存分配引起的,但并非直接如此。在低内存条件下,代码片段可以自我缩放并继续运行。

还有其他人指出,有时JVM本身无法很好地处理低内存,但这是一个相当极端的情况。


0

尽管这个问题已经有10年了,但我今天也在思考这个问题,并决定实际尝试一下。 :-)

  • Linux版本 Linux version 5.10.0-0.bpo.9-amd64
  • JVM版本 OpenJDK Runtime Environment (build 11.0.14+9-post-Debian-1deb10u1)

使用这个小测试程序:

import java.util.*;

public class OOMTest {
    public static void main(String... atgs){
        var list = new ArrayList<String>();
        while(true){
            list.add(new String("abc"));
        }
    }
}

在一台拥有4G RAM和4G交换空间的机器上(这只是我的NAS :-)):

tomi@unyanas:~/workspace$ free -h
              total        used        free      shared  buff/cache   available
Mem:          3.7Gi       980Mi       2.4Gi        27Mi       355Mi       2.4Gi
Swap:         3.7Gi       2.2Gi       1.5Gi
  • 当运行时允许1G堆时,进程会因OOM而死亡:
tomi@unyanas:~/workspace$ java -Xmx1G OOMTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at OOMTest.main(OOMTest.java:9)
  • 当允许使用10G堆时,那么:
    • 进程会按照上述方式启动,尽管机器上没有那么多RAM+swap
    • 但输出明显不是OOM,进程只是被内核杀死:
tomi@unyanas:~/workspace$ java -Xmx10G OOMTest
Killed

总结一下:

  • 当主机内存不足时,会出现OOM,但至少不是必然的(我尝试了3次,其中有3次出现了这种情况)
  • 使用-Xmx超过机器容量是允许的
  • 似乎只有在进程的“cap”为-Xmx值(默认或明确指定)时才能保证出现OOM,否则将有更多的空闲内存(RAM+swap)留给操作系统

对于上述情况,可以说这种情况是由于测试代码具有无限的内存占用,因此我还想尝试一下是否可以通过设置过高的-Xmx值使具有高生成率但内存占用有限的进程失败。因此,如果GC被愚弄以相信实际可用的内存比实际上多得多,并最终被杀死,或者如果它将被内核通知有关失败的OS级内存分配并因此限制堆大小。答案是可以愚弄它。

我已经像这样修改了上面的代码:

import java.util.*;

public class OOMTest {
    public static void main(String... atgs){
        var list = new ArrayList<String>();
        while(true){
            list.add(new String("abc"));
            if (list.size() > 50000000){
                list.remove(list.size() - 1);
            }
        }
    }
}

当指定一个机器可以处理的-Xmx值时,程序可以运行任何长时间(好吧,我真的让它运行了很长时间,直到我吃晚饭,但你懂我的意思 :-))

因此,这永远不会退出(启用GC日志记录后,一旦达到2G堆大小,就可以观察到一个漂亮的重复模式):

java -Xmx2G OOMTest

但是当使用Xmx10G运行时,进程再次被杀死,没有OOM:

tomi@unyanas:~/workspace$ java -Xmx10G OOMTest
Killed

这表明,当JVM尝试分配比当前可用的RAM+swap更多的内存时,它得到的唯一“建设性反馈”就是类似于kill -9的东西。因此,通过使用过高的-Xmx值,本来可以正常运行的进程可能会失败。这绝不意味着在所有操作系统、JVM实现甚至仅仅是GC算法(我使用的是默认的G1)上都会发生这种情况,但在上述设置中肯定是这样的。

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