性能文章>刨根问底——记一次 OOM 试验造成的电脑雪崩引发的思考>

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

2年前
749501

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

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

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

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

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

实验的代码如下:

publicclass 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」,这是什么鬼,如图示

image.png

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

image.png

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

  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) ,如下

publicclass Test {
        privateint 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();threadCount++;
        }
    }

    public static  void main(String[] args) {
		Test oom = new Test();
		oom.stackLeakByThread();
    }
}

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

image.png

如图示:操作系统按时间片的调度算法来给不同的进程分配 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 杀死,但发现不起作用,如图示

image.png

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

The message “Java HotSpot™ 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 时,最好能亲自去尝试一下,说不定你能有新的发现!纸上得来终觉浅,绝知此事要躬行!碰到问题最好穷追猛打,这样我们才会收获很多,进步很快!

本文来自公众号:码海

点赞收藏
geekoftaste
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

【全网首发】一次想不到的 Bootstrap 类加载器带来的 Native 内存泄露分析

记一次线上RPC超时故障排查及后续GC调优思路

记一次线上RPC超时故障排查及后续GC调优思路

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

解读JVM级别本地缓存Caffeine青出于蓝的要诀 —— 缘何会更强、如何去上手

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

【全网首发】一次疑似 JVM Native 内存泄露的问题分析

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

解读JVM级别本地缓存Caffeine青出于蓝的要诀2 —— 弄清楚Caffeine的同步、异步回源方式

【全网首发】从源码角度分析一次诡异的类被加载问题

【全网首发】从源码角度分析一次诡异的类被加载问题

1
0