刨根问底---一次OOM试验造成的电脑雪崩引发的思考

简介: 刨根问底---一次OOM试验造成的电脑雪崩引发的思考

问题初现----电脑雪崩



在写「垃圾回收-实战篇」时,按书中的一个例子做了一次实验,我觉得涉及的知识点挺多的,所以单独拎出来与大家共享一下,相信大家看完肯定有收获。


画外音:尽信书不如无书,对每一个例子我们最好亲自试试,说不定有新的发现

实验是这样的:想测试在指定的栈大小(160k)下通过不断创建多线程观察其造成的 OOM 类型


画外音:造成 OOM 的原因有很多,将在本周的 「垃圾回收-实战篇」一文中做详细描述,这里不再赘述


实验的代码如下:


public class Test {
  private void dontStop() {
    while(true) {
    }
  }
  public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static  void main(String[] args) {
    Test oom = new Test();
    oom.stackLeakByThread();
    }
}
复制代码


过了一会儿风扇狂转,不久就发生了 OOM,然后程序没有终止,用 Ctrl + C 也无法终止,会提示「the VM may need to be forcibly terminated」,这是什么鬼,如图示



电脑卡死了,鼠标键盘完全没法响应! 只好重启了电脑,然后我先在终端输入 top 命令,再执行以上的程序, 发现 CPU的负载达到了 800%!



在以上对问题的描述中至少有三个问题值得我们去思考


  1. 以上 while (true) 为啥会造成 cpu 负载 800%
  2. 在主线程发生 OOM 后我在终端用 Ctrl +  C 试图终止 Java 进程的执行,但没成功,为啥中止信号不生效呢
  3. 主线程发生 OOM 后 Java 进程为啥不会停止运行

一个个来看


while (true) 与 cpu 负载的关系



首先我们要明白 **%CPU ** 代表的含义,它指的是进程占用一个核的百分比,如果进程启动了多个线程,多线程就会占用多个核,是可能超过 100% 的,但最多不超过 CPU核数 * 100%, 怎么查看逻辑 CPU 的个数


  • Linux 下可以用


cat /proc/cpuinfo| grep "processor"| wc -l
复制代码


  • Mac 可以用


sysctl hw.logicalcpu
复制代码


我的电脑是 Mac 的,用以上命令查了一下逻辑核心发现是 8 个, 而实验看到的 CPU 占有率是 800%,也就是说我们的实验程序打满了 8 个逻辑 CPU!有人说那是因为你在源源不断地创建线程啊,当然就打满了逻辑 CPU 了,那我们再来试验一下,只创建 7 个线程,加个主线程共 8 个,这 8 个主线程内部都只执行一个 while(true) {} ,如下


public class Test {
        private int threadCount = 0;
  private void dontStop() {
    while(true) {
    }
  }
  public void stackLeakByThread() {
        while (true) {
                        // 只创建 7 个线程, 加上主线程共 8 个线程
            if (threadCount > 7) {
                continue;
            }
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static  void main(String[] args) {
    Test oom = new Test();
    oom.stackLeakByThread();
    }
}
复制代码


执行之后 %CPU 还是接近 800%(大家可以试验一下,这里不贴图了), 也就是说 8 个 while(true) 把 8 个核全部打满了,平均一个 while(true) 打满一个核 ,那么问题来了, 单个线程执行 while(true) 为啥会打满一个核呢,CPU 不是按时间片来分配各个进程的吗



如图示:操作系统按时间片的调度算法来给不同的进程分配 CPU 时间,如果某个进程时间片用完了,会让出 CPU 的控制权给其他的进程执行


首先,需要指明的是:CPU 确实是按时间片来给不同的进程分配它的控制权的


但 CPU 对时间片的分配策略是动态的, 具有偏向性的,简单理解如下: Java 中的线程执行完系统分配的时间片后确实是会让出 CPU 的执行权,但别的进程会告诉系统自己没什么事情要做,不需要那么多的时间,这个时候系统就会切换到下一个进程,直到回到这个死循环的进程上,而 Java 进程无论什么时候都再循环,都会一直会报告有事情要做,系统就会把尽可能多的时间分给它(正所谓会哭的小孩有奶吃),系统会不断调高 while(true) 线程的优先级,提升它的 CPU 占用时间片,也就是说 while(true)  这个死循环用光了别的进程省下的时间,不让 CPU 有片刻休息的时间,导致 CPU 负载过高,这就像马太效应,勤奋的线程执行的越努力,其他懒惰的线程就越会被缩短时间片,越得不到机会!画外音: Windows 系统中就存在一个称为「优先级推进器」(Priority Boosting,可以关闭)的功能,大致作用就是当系统发现一个线程执行得特别勤奋努力的话,可能会越过线程优先级优先为此线程分配执行时间


发生 OOM 后 Ctrl+C 为啥无法中止 Java 进程



上文提到,发生 OOM 后, 由于已经观察到 OOM 的现象,所以想把 Java 进程通过 Ctrl+C 杀死,但发现不起作用,如图示



为啥 Ctrl + C 这种通用的 kill 掉进程的方式不起作用呢,我在 Oracle 的论坛(见文末参考链接)找到了 Oracle 工程师的回答


The message "Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal UNKNOWN to handler- the VM may need to be forcibly terminated" is getting printed by the JVM's native signal handling code. The signal handler itself encountered OOM while making a Java up-call and that's why the JVM didn't get terminated with ctrl+c.


简单地说就是 JVM 中的信号处理器确实收到了终端发出的 Ctrl + C 的终止信号,但当它调用 Java 进程想中止时发生了 OOM 导致中断失败, 那为啥调用会发生 OOM 呢,我猜是因为信号处理器要启动一个线程来做这种终止通知的操作,而我们知道,当前已经无法再创建线程了(已经发生 unable to create new native thread 的错误了)


主线程发生 OOM 后 Java 进程为啥不会停止运行



最后一个问题,主线程发生 OOM  后居然 Java 进程没终止,这个该怎么解释

Main 主线程与其他的子线程并不是父子关系,而是平等的关系,所以主线程虽然因为 OOM 挂了,但其他子线程并不会停止运行,由于它们执行的 while(true),所以子线程会一直存在,既然它们一直存在,那对应的 Java 进程就会一直运行着。


那怎么让主线程终止运行后,其他线程也可立即结束呢,可以把这些子线程设置为守护线程,创建好 Thread thread 后,可以用 thread.setDaemon(true) 将其设置成守护线程,这样当主线程挂了,守护线程也会立即停止运行,原因嘛,也很简单,既然是守护线程,那被守护的线程都挂了,那守护线程也没存在的意义了


总结



本文通过一个 OOM 试验引出了三个值得思考的问题,相信大家应该学了不少知识点,这里还是要提醒一下大家,看到书中的 demo 时,最好能亲自去尝试一下,说不定你能有新的发现!纸上得来终觉浅,绝知此事要躬行!碰到问题最好穷追猛打,这样在每次试验中我们都能有收获!


参考

blog.csdn.net/russell_tao…

blog.csdn.net/aitangyong/…

zhuanlan.zhihu.com/p/91573757

community.oracle.com/thread/4088…

相关文章
|
3月前
|
算法 IDE Java
Java 项目实战之实际代码实现与测试调试全过程详解
本文详细讲解了Java项目的实战开发流程,涵盖项目创建、代码实现(如计算器与汉诺塔问题)、单元测试(使用JUnit)及调试技巧(如断点调试与异常排查),帮助开发者掌握从编码到测试调试的完整技能,提升Java开发实战能力。
345 0
|
数据采集 机器学习/深度学习 数据挖掘
python数据分析——数据预处理
数据预处理是数据分析过程中不可或缺的一环,它的目的是为了使原始数据更加规整、清晰,以便于后续的数据分析和建模工作。在Python数据分析中,数据预处理通常包括数据清洗、数据转换和数据特征工程等步骤。
565 0
|
存储 缓存 Rust
一文读懂 Deno
一文读懂 Deno
526 0
|
机器学习/深度学习 算法 数据挖掘
【Python机器学习】Mean Shift、Kmeans聚类算法在图像分割中实战(附源码和数据集)
【Python机器学习】Mean Shift、Kmeans聚类算法在图像分割中实战(附源码和数据集)
476 0
【Python机器学习】Mean Shift、Kmeans聚类算法在图像分割中实战(附源码和数据集)
|
存储 内存技术
内存条RAM详细指南
内存条(RAM)是电脑中用于临时存储数据和程序的部件,CPU依赖它执行操作。内存条经历了从主内存扩展到读写内存整体的发展,常见类型包括SDRAM和DDR SDRAM。内存容量、存取时间和奇偶校验是衡量其性能的关键指标。在选购时,应考虑类型、容量、速度和品牌,知名品牌的内存条提供更好的可靠性和稳定性。
4311 2
|
11月前
|
设计模式 存储 缓存
前端必须掌握的设计模式——策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,旨在将多分支复杂逻辑解耦。每个分支类只关心自身实现,无需处理策略切换。它避免了大量if-else或switch-case代码,符合开闭原则。常见应用场景包括表单验证、风格切换和缓存调度等。通过定义接口和上下文类,策略模式实现了灵活的逻辑分离与扩展。例如,在国际化需求中,可根据语言切换不同的词条包,使代码更加简洁优雅。总结来说,策略模式简化了多条件判断,提升了代码的可维护性和扩展性。
|
传感器 物联网 5G
5G的三大主要特性:解锁未来无限可能
5G的三大主要特性:解锁未来无限可能
1652 1
|
网络安全 Docker 容器
WSL2 固定IP与局域网访问
该文档介绍了如何在新版WSL2中配置镜像模式网络,以实现WSL2 IP与主机相同的固定设置。然而,启用此模式后,Docker服务在本机上无法访问。作者分享了针对这个问题的解决方案,包括编辑`.wslconfig`文件开启镜像网络和调整Docker设置。具体步骤涉及更新WSL和Docker(docker-ce)的安装,以及修改`daemon.json`文件以允许本机和局域网访问Docker服务。
1123 2
|
存储 安全 物联网
Web3如何重塑物联网的未来
Web3技术的核心在于去中心化,这意味着数据和操作不再依赖于单一的中心化实体,而是分布式地存储和管理。
206 2