在寫「垃圾回收-實戰篇」時,按書中的一個例子作了一次實驗,我以爲涉及的知識點挺多的,因此單獨拎出來與你們共享一下,相信你們看完確定有收穫。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 等文章,歡迎關注個人微信公衆號哦。