为什么这个小的Java程序会导致MacOS重新启动?

50

代码如下

Set<Thread> threads = new HashSet<>();

Runnable r = () -> {
    try {
        Thread.sleep(Long.MAX_VALUE);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

for (int i = 0; i < 20000; i++) {
    Thread t = new Thread(r);
    threads.add(t);
    t.start();
    if (i % 100 == 0) {
        System.out.println(i);
    }
    Thread.sleep(2);
}

执行时,我开始看到像这样的值

0
100
200
300

正如预期的那样,一直运行直到我看到:
3900
4000
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:717)
    at App.main(scratch.java:24)
Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

但是过了一会儿(大约10-20秒),MacOS便决定重新启动。导致此次重启的原因是什么?主线程抛出异常,但进程中有约4000个线程在睡眠状态,这会引起操作系统中的什么问题?这是内存溢出还是与操作系统的任务调度相关?
MacOS版本:10.14.3(18D109)
Java版本:“1.8.0_202”
Java(TM) SE Runtime Environment(版本1.8.0_202-b08)
Java HotSpot(TM) 64位服务器虚拟机(版本25.202-b08,混合模式)

4
如果应用程序崩溃了,那并不是太大的问题。然而,如果一个程序能够触发系统重新启动,那就是一个严重的安全相关操作系统漏洞(但也可能是硬件问题,比如内存问题——不要因为每次故障都责怪操作系统)。 - kai
1
我的安卓手机上无法重现。我只是收到了一个OOM错误。 - Carcigenicate
6
可以确认在 MacOS Mojave 上会崩溃,使用的是 Java 版本 "1.8.0_202-ea",但在 Windows 10 上似乎没有崩溃(不过我用了另一台机器)。 - Sash Sinha
5
在100,000个线程时,我遇到了蓝屏死机,并重新启动了。也许这算是一个重现吗? - Carcigenicate
1
@KorayTugay 证据。这是一个来自2016年的stackoverflow漏洞示例,可以破坏Linux内核(RIP):https://www.exploit-db.com/exploits/39992 - kai
显示剩余13条评论
6个回答

11

尽管控制台显示程序已经完成,但JVM进程仍在运行,直到释放所有资源。同时,您的操作系统会耗尽线程,变得缓慢和不稳定,导致所有进程都出现延迟,包括JVM的最终化。作为自我保护,操作系统触发了内核恐慌。这就是为什么你的MacOS重启的原因。

*操作系统 - Operating System


4

Java是在90年代构建的,当时只有多核处理器。

虽然Java已经发展了很多,现代处理器也随之变化。如今我们拥有8核处理器,具有大容量缓存(例如:12MB)。

尽管并行处理已经得到了很大的发展,但Java仍然是围绕着单核处理器模型设计的。但是,足够了解历史,让我用非常简单的方式解释一下发生了什么。

仅仅通过在Java中创建一个新线程,我们就浪费了大量内存。

每个线程消耗约 ~ 512KB - 1MB 的内存,这取决于您的JVM版本(请参见Java线程占用多少内存Java线程:保留内存)。记住这一点,在不断创建新线程时,它们最终将消耗掉堆内存的所有空间。

现在,我自己从未尝试过这种情况,但我认为您计算机的操作系统会因为“内存不足”错误而关闭/重启,以进行反制措施。(这很像导致Windows上臭名昭著的“蓝屏死机”三重错误,需要重启以重置CPU状态)

解决此问题的一种可能方法是手动设置JVM使用的最大堆大小。因此,当您的程序完全利用了预分配的堆时,它不会导致关闭。请参考 SO问题了解如何进行设置。


1
操作系统重启的原因是为了应对当操作系统变得CPU繁忙且线程消耗了操作系统资源时的一种对策。具体而言,“无法创建新的本机线程”表示操作系统已经没有线程可用,无法再创建更多的线程。
此外,需要注意的是,JVM在单个进程中运行,JVM中的线程共享该进程所属的堆。如果在多CPU机器上运行,则Java将利用底层操作系统线程在不同的CPU上执行代码。每当启动一个Java线程时,它就会创建一个关联的操作系统线程,而操作系统负责调度等工作。
操作系统足够聪明,可以根据优先级和其他调度算法利用可用核心进行线程执行。
而“java.lang.OutOfMemoryError”表示操作系统已经使用了分配的堆内存或JVM堆大小中指定的内存。
因此,如果堆大小很大且可用内存不足以供操作系统中的其他进程使用,导致滞后,最终为了重置CPU状态,操作系统会重新启动。

1

0

这是一个分叉炸弹变种。它可能会导致严重的减速,但没有用户程序应该能够崩溃操作系统。这可能是操作系统或内存错误的一个漏洞。尝试运行内存检查?


-4
很可能是因为您没有给JVM足够的内存,或者您的计算机硬件和macOS组合不允许同时有那么多线程处于活动状态。这个问题不仅限于macOS,一些Linux发行版(例如Bodhi Linux)也有这个限制。不要被“OutOfMemoryError”所欺骗 - 这通常意味着JVM无法分配本地线程。

6
我认为“不允许同时活跃的线程数量过多”不是重启的原因。 - Koray Tugay
3
如果这是一个“不允许”的情况,那就不会成为这样的问题。但在操作系统的情况下,这种情况并没有发生,“不允许”的限制被某种方式绕过,导致操作系统本身出现故障。这是一个严重的安全问题:一个非特权用户(至少在进程隔离方面)可以触发特权操作。 - kai
公平的观点。我想知道如果以较低的优先级运行程序并在程序中将线程优先级设置为最低,会发生什么。 - Sina Madani

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