Java虚拟机突然退出,没有明显的原因。

3
我有一个问题,我的Java程序突然退出,没有抛出任何异常或正常结束程序。
我正在编写一个程序来解决Project Euler第14个问题。这是我得到的内容:
private static final int INITIAL_CACHE_SIZE = 30000;
private static Map<Long, Integer> cache = new HashMap<Long, Integer>(INITIAL_CACHE_SIZE);

public void main(String... args) {
    long number = 0;
    int maxSize = 0;

    for (long i = 1; i <= TARGET; i++) {
        int size = size(i);
        if (size > maxSize) {
            maxSize = size;
            number = i;
        }
    }
}
private static int size(long i) {
    if (i == 1L) {
        return 1;
    }
    final int size = size(process(i)) + 1;
    return size;
}

private static long process(long n) {
    return n % 2 == 0 ? n/2 : 3*n + 1;
}

这个代码可以正常运行,使用 1 000 000 的 TARGET 大约需要 5 秒完成。
我想通过添加缓存来进行优化,所以我将 size 方法更改为以下内容:
private static int size(long i) {
    if (i == 1L) {
        return 1;
    }
    if (cache.containsKey(i)) {
        return cache.get(i);
    }
    final int size = size(process(i)) + 1;
    cache.put(i, size);
    return size;
}

现在当我运行它时,每次到达555144时程序就会停止(进程退出)。没有抛出任何异常、错误、Java VM崩溃或其他任何东西。
更改缓存大小似乎也没有任何效果,那么缓存引入如何导致此错误?
如果我强制缓存大小不仅是初始值,而且是永久的,像这样:
    if (i < CACHE_SIZE) {
        cache.put(i, size);
    }

这个错误不再出现了。 编辑:当我将缓存大小设置为2M时,该错误再次出现。

有人能够重现这个问题,并且提供一些可能发生的原因吗?


我正在运行Windows Vista Business和JDK 1.6.0_03。 - Jorn
你可能想尝试更新JDK并查看是否出现相同的行为。它们现在已经更新到16至1.6。 - digitaljoel
我还想知道您使用的JDK是什么...供应商、版本和架构。甚至语言级别也可能是一个因素。当我开始学习Java时,我真的很喜欢“一次编写,到处运行”的理念,而且我不明白为什么有人会说“一次编写,到处调试”。现在我想我知道他们的意思了... - weiji
7个回答

12

这只是一个没有被打印出来的OutOfMemoryError。如果我设置了很高的堆大小,程序就可以正常运行,否则会因为未记录的OutOfMemoryError而退出(在调试器中很容易看到)。

您可以通过传递以下JVM参数并重新运行程序来验证此问题并获取堆转储(以及打印OutOfMemoryError的输出):

-XX:+HeapDumpOnOutOfMemoryError

这样将会打印出以下内容:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid4192.hprof ...
Heap dump file created [91901809 bytes in 4.464 secs]

通过使用 -Xmx200m 等参数增加堆大小,您将不会遇到任何问题——至少对于TARGET=1000000而言。


是的,你说得对。那个命令行参数确实显示了OutOfMemoryError。 - Jorn

3
听起来像是JVM本身崩溃了(当程序没有任何异常提示时,这是第一个想法)。在这种问题中的第一步是升级到您平台的最新版本。假设您的用户级别具有访问该目录的权限,JVM应将堆转储到启动JVM的目录中的.log文件中。
话虽如此,有些OutOfMemory错误不会在主线程中报告,因此除非您使用try/catch(Throwable t)并查看是否收到其中一个,否则很难确定您是否真正只是用完了内存。它仅使用100MB可能意味着JVM未配置为使用更多内存。可以通过将JVM的启动选项更改为-Xmx1024m来更改,以获得1 GB的内存,以查看问题是否有所改善。
进行try catch的代码应如下所示:
public static void main(String[] args) {
     try {
         MyObject o = new MyObject();
         o.process();
     } catch (Throwable t) {
         t.printStackTrace();
     }
 }

在处理过程中一定要将所有操作都放在方法中,不要将缓存存储在静态变量中。这样,在catch语句中如果发生错误,对象就会超出范围并可以被垃圾回收,释放足够的内存以允许打印堆栈跟踪信息。不能保证这种方法一定有效,但至少能提高成功率。


问题是一个OutOfMemoryError,可能是在一个线程中重新哈希hashmap时,它的大小超过了某个限制。 - Martin OConnor

1

size(long i) 的两种实现之间的一个显著区别在于你创建的对象数量。

在第一种实现中,没有创建任何 Objects。而在第二种实现中,你正在进行大量的自动装箱,为每个访问缓存创建一个新的 Long,并在每次修改时放入新的 Long 和新的 Integer

这可以解释内存使用量的增加,但不能解释为什么没有出现 OutOfMemoryError。增加堆大小可以让我完成操作。

来自this Sun aritcle

性能...可能很差,因为它在每个 get 或 set 操作上都会装箱或拆箱。它对于偶尔使用来说足够快,但在性能关键的内部循环中使用它是愚蠢的。


增加的内存使用可能是导致OOM的原因 - 我猜快速生成对象在垃圾收集器释放未使用的对象之前消耗了内存。 - weiji

0

递归的size()方法可能不是缓存的好地方。我在main()的for循环中放置了一个cache.put(i, size)的调用,它的速度更快。否则,我也会得到OOM错误(没有更多的堆空间)。

编辑:这里是源代码 - 缓存检索在size()中进行,但存储是在main()中完成的。

public static void main(String[] args) {
    long num = 0;
    int  maxSize = 0;

    long start = new Date().getTime();
    for (long i = 1; i <= TARGET; i++) {
        int size = size(i);
        if (size >= maxSize) {
            maxSize = size;
            num = i;
        }
        cache.put(i, size);
    }

    long computeTime = new Date().getTime() - start;
    System.out.println(String.format("maxSize: %4d on initial starting number %6d", maxSize, num));
    System.out.println("compute time in milliseconds: " + computeTime);
}

private static int size(long i) {
    if (i == 1l) {
        return 1;
    }

    if (cache.containsKey(i)) {
        return cache.get(i);
    }

    return size(process(i)) + 1;
}

请注意,从size()函数中移除cache.put()调用不仅会避免缓存已计算的大小,还可以避免重新缓存先前计算的大小。这不会影响哈希映射操作,但是就像akf指出的那样,它避免了自动装箱/拆箱操作,这也是您的堆内存消耗的来源。我在size()函数中也尝试过一个"if (!containsKey(i)) { cache.put()等等",但不幸的是,它也会耗尽内存。

0
如果您的Java进程突然崩溃,可能是某些资源达到了最大值,例如内存。您可以尝试设置更高的最大堆大小。

我认为不是这个问题。它没有抛出OutOfMemoryExceptions异常。此外,Windows任务管理器告诉我它没有使用超过100 MB的内存。 - Jorn
它可能不会使用超过100MB的内存,但是当它尝试分配一些非常巨大的内存块时就会崩溃(这在任务管理器中不会显示,因为它从未被分配)。然而,你提到缺乏OutOfMemoryExceptions并不完全符合这种情况... - rmeador
1
我已经尝试过了,可以重现此问题。Java exe进程向操作系统返回错误代码1。我怀疑哈希图的重新哈希中存在 bug。我使用的是Sun JDK 1.6.0_16。 - Martin OConnor

0

在崩溃后,您是否看到生成了堆转储文件?该文件应位于JVM的当前目录中,这是我寻找更多信息的地方。


退出时不会自动生成堆转储文件,除非传递参数。可能有其他方法,但在这种特殊情况下,请传递:-XX:+HeapDumpOnOutOfMemoryError。 - Joshua McKinnon
堆转储会自动生成(虚拟机实现之间可能会有所不同),但并非仅在出现OutOfMemoryErrors时,而是仅在真正的虚拟机崩溃时才会生成。 - Yishai

0

我在cache.put(i, size)这行代码上遇到了OutOfMemory错误。

要查看错误,请在Eclipse中使用调试模式运行程序,它将出现在调试窗口中。它不会在控制台中生成堆栈跟踪。


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