在写「垃圾回收-实战篇」时,按书中的一个例子作了一次实验,我以为涉及的知识点挺多的,因此单独拎出来与你们共享一下,相信你们看完确定有收获。java
画外音:尽信书不如无书,对每个例子咱们最好亲自试试,说不定有新的发现算法
实验是这样的:想测试在指定的栈大小(160k)下经过不断建立多线程观察其形成的 OOM 类型shell
画外音:形成 OOM 的缘由有不少,将在本周的 「垃圾回收-实战篇」一文中作详细描述,这里再也不赘述segmentfault
实验的代码以下:微信
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%!oracle
在以上对问题的描述中至少有三个问题值得咱们去思考ide
一个个来看测试
首先咱们要明白 %CPU 表明的含义,它指的是进程占用一个核的百分比,若是进程启动了多个线程,多线程就会占用多个核,是可能超过 100% 的,但最多不超过 CPU核数 * 100%, 怎么查看逻辑 CPU 的个数ui
cat /proc/cpuinfo| grep "processor"| wc -l
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 后, 因为已经观察到 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 进程没终止,这个该怎么解释
Main 主线程与其余的子线程并非父子关系,而是平等的关系,因此主线程虽然由于 OOM 挂了,但其余子线程并不会中止运行,因为它们执行的 while(true),因此子线程会一直存在,既然它们一直存在,那对应的 Java 进程就会一直运行着。
那怎么让主线程终止运行后,其余线程也可当即结束呢,能够把这些子线程设置为守护线程,建立好 Thread thread 后,能够用 thread.setDaemon(true) 将其设置成守护线程,这样当主线程挂了,守护线程也会当即中止运行,缘由嘛,也很简单,既然是守护线程,那被守护的线程都挂了,那守护线程也没存在的意义了
本文经过一个 OOM 试验引出了三个值得思考的问题,相信你们应该学了很多知识点,这里仍是要提醒一下你们,看到书中的 demo 时,最好能亲自去尝试一下,说不定你能有新的发现!纸上得来终觉浅,绝知此事要躬行!碰到问题最好穷追猛打,这样在每次试验中咱们都能有收获!
参考
https://blog.csdn.net/russell...
https://blog.csdn.net/aitangy...
https://zhuanlan.zhihu.com/p/...
https://community.oracle.com/...
更多算法 + 计算机基础知识 + Java 等文章,欢迎关注个人微信公众号哦。