摘要:簡單地說就是 JVM 中的信號處理器確實收到了終端發出的 Ctrl + C 的終止信號,但當它調用 Java 進程想中止時發生了 OOM 導致中斷失敗, 那爲啥調用會發生 OOM 呢, 我猜 是因爲信號處理器要啓動一個線程來做這種終止通知的操作,而我們知道,當前已經無法再創建線程了(已經發生 unable to create new native thread 的錯誤了)。Main 主線程與其他的子線程並不是父子關係,而是平等的關係,所以主線程雖然因爲 OOM 掛了,但其他子線程並不會停止運行,由於子線程們執行的 while(true),所以子線程會一直存在,既然它們一直存在,那對應的 Java 進程就會一直運行着。

問題初現----電腦雪崩

在寫「垃圾回收-實戰篇」時,按書中的一個例子做了一次實驗,我覺得涉及的知識點挺多的,所以單獨拎出來與大家共享一下,相信大家看完肯定有收穫。

畫外音: 盡信書不如無書,對每一個例子我們最好親自試試,說不定有新的發現

實驗是這樣的:想測試在指定的棧大小(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」,這是什麼鬼,如圖示

電腦卡死了,鼠標鍵盤完全沒法響應!只好重啓了電腦,然後我先在終端輸入 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) ,如下

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 不是按時間片來分配各個進程的嗎

如圖示: 操作系統按時間片的調度算法來給不同的進程分配 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 時,最好能親自去嘗試一下,說不定你能有新的發現!紙上得來終覺淺,絕知此事要躬行!碰到問題最好窮追猛打,這樣我們纔會收穫很多,進步很快!

參考

https://blog.csdn.net/russell_tao/article/details/7103012

https://blog.csdn.net/aitangyong/article/details/16858273

https://zhuanlan.zhihu.com/p/91573757

https://community.oracle.com/thread/4088001

更多算法 + 計算機基礎知識 + Java 等文章,歡迎關注我的微信公衆號哦。

相關文章