日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

大家好,我是坤哥

網(wǎng)上看到一個很有意思的美團(tuán)面試題:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰,這個問題我看了不少回答,但發(fā)現(xiàn)都沒答到根上,所以決定答一答,相信大家看完肯定會有收獲,本文分以下幾節(jié)來探討

  1. 線程崩潰,進(jìn)程一定會崩潰嗎
  2. 進(jìn)程是如何崩潰的-信號機(jī)制簡介
  3. 為什么在 JVM 中線程崩潰不會導(dǎo)致 JVM 進(jìn)程崩潰
  4. openJDK 源碼解析

線程崩潰,進(jìn)程一定會崩潰嗎

一般來說如果線程是因?yàn)榉欠ㄔL問內(nèi)存引起的崩潰,那么進(jìn)程肯定會崩潰,為什么系統(tǒng)要讓進(jìn)程崩潰呢,這主要是因?yàn)樵谶M(jìn)程中,各個線程的地址空間是共享的,既然是共享,那么某個線程對地址的非法訪問就會導(dǎo)致內(nèi)存的不確定性,進(jìn)而可能會影響到其他線程,這種操作是危險的,操作系統(tǒng)會認(rèn)為這很可能導(dǎo)致一系列嚴(yán)重的后果,于是干脆讓整個進(jìn)程崩潰

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

線程共享代碼段,數(shù)據(jù)段,地址空間,文件

非法訪問內(nèi)存有以下幾種情況,我們以 C 語言舉例來看看

  1. 針對只讀內(nèi)存寫入數(shù)據(jù)
  2. #include <stdio.h>
    #include <stdlib.h>
    int main() {
    char *s = "hello world";// 向只讀內(nèi)存寫入數(shù)據(jù),崩潰
    s[1] = 'H';
    }
  3. 訪問了進(jìn)程沒有權(quán)限訪問的地址空間(比如內(nèi)核空間)
  4. #include <stdio.h>
    #include <stdlib.h>
    int main() {
    int *p = (int *)0xC0000fff; // 針對進(jìn)程的內(nèi)核空間寫入數(shù)據(jù),崩潰
    *p = 10;
    }
  5. 在 32 位虛擬地址空間中,p 指向的是內(nèi)核空間,顯然不具有寫入權(quán)限,所以上述賦值操作會導(dǎo)致崩潰
  6. 訪問了不存在的內(nèi)存,比如
  7. #include <stdio.h>
    #include <stdlib.h>
    int main() {
    int *a = NULL;
    *a = 1;
    }

以上錯誤都是訪問內(nèi)存時的錯誤,所以統(tǒng)一會報 Segment Fault 錯誤(即段錯誤),這些都會導(dǎo)致進(jìn)程崩潰

進(jìn)程是如何崩潰的-信號機(jī)制簡介

那么線程崩潰后,進(jìn)程是如何崩潰的呢,這背后的機(jī)制到底是怎樣的,答案是信號,大家想想要干掉一個正在運(yùn)行的進(jìn)程是不是經(jīng)常用 kill -9 pid 這樣的命令,這里的 kill 其實(shí)就是給指定 pid 發(fā)送終止信號的意思,其中的 9 就是信號,其實(shí)信號有很多類型的,在 linux 中可以通過 kill -l查看所有可用的信號

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

當(dāng)然了發(fā) kill 信號必須具有一定的權(quán)限,否則任意進(jìn)程都可以通過發(fā)信號來終止其他進(jìn)程,那顯然是不合理的,實(shí)際上 kill 執(zhí)行的是系統(tǒng)調(diào)用,將控制權(quán)轉(zhuǎn)移給了內(nèi)核(操作系統(tǒng)),由內(nèi)核來給指定的進(jìn)程發(fā)送信號

那么發(fā)個信號進(jìn)程怎么就崩潰了呢,這背后的原理到底是怎樣的?

其背后的機(jī)制如下

  1. CPU 執(zhí)行正常的進(jìn)程指令
  2. 調(diào)用 kill 系統(tǒng)調(diào)用向進(jìn)程發(fā)送信號
  3. 進(jìn)程收到操作系統(tǒng)發(fā)的信號,CPU 暫停當(dāng)前程序運(yùn)行,并將控制權(quán)轉(zhuǎn)交給操作系統(tǒng)
  4. 調(diào)用 kill 系統(tǒng)調(diào)用向進(jìn)程發(fā)送信號(假設(shè)為 11,即 SIGSEGV,一般非法訪問內(nèi)存報的都是這個錯誤)
  5. 操作系統(tǒng)根據(jù)情況執(zhí)行相應(yīng)的信號處理程序(函數(shù)),一般執(zhí)行完信號處理程序邏輯后會讓進(jìn)程退出

注意上面的第五步,如果進(jìn)程沒有注冊自己的信號處理函數(shù),那么操作系統(tǒng)會執(zhí)行默認(rèn)的信號處理程序(一般最后會讓進(jìn)程退出),但如果注冊了,則會執(zhí)行自己的信號處理函數(shù),這樣的話就給了進(jìn)程一個垂死掙扎的機(jī)會,它收到 kill 信號后,可以調(diào)用 exit() 來退出,但也可以使用 sigsetjmp,siglongjmp 這兩個函數(shù)來恢復(fù)進(jìn)程的執(zhí)行

// 自定義信號處理函數(shù)示例

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
// 自定義信號處理函數(shù),處理自定義邏輯后再調(diào)用 exit 退出
void sigHandler(int sig) {
  printf("Signal %d catched!n", sig);
  exit(sig);
}
int main(void) {
  signal(SIGSEGV, sigHandler);
  int *p = (int *)0xC0000fff;
  *p = 10; // 針對不屬于進(jìn)程的內(nèi)核空間寫入數(shù)據(jù),崩潰
}

// 以上結(jié)果輸出: Signal 11 catched!

如代碼所示:注冊信號處理函數(shù)后,當(dāng)收到 SIGSEGV 信號后,先執(zhí)行相關(guān)的邏輯再退出

另外當(dāng)進(jìn)程接收信號之后也可以不定義自己的信號處理函數(shù),而是選擇忽略信號,如下

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

int main(void) {
  // 忽略信號
  signal(SIGSEGV, SIG_IGN);

  // 產(chǎn)生一個 SIGSEGV 信號
  raise(SIGSEGV);

  printf("正常結(jié)束");
}

也就是說雖然給進(jìn)程發(fā)送了 kill 信號,但如果進(jìn)程自己定義了信號處理函數(shù)或者無視信號就有機(jī)會逃出生天,當(dāng)然了 kill -9 命令例外,不管進(jìn)程是否定義了信號處理函數(shù),都會馬上被干掉

說到這大家是否想起了一道經(jīng)典面試題:如何讓正在運(yùn)行的 JAVA 工程的優(yōu)雅停機(jī),通過上面的介紹大家不難發(fā)現(xiàn),其實(shí)是 JVM 自己定義了信號處理函數(shù),這樣當(dāng)發(fā)送 kill pid 命令(默認(rèn)會傳 15 也就是 SIGTERM)后,JVM 就可以在信號處理函數(shù)中執(zhí)行一些資源清理之后再調(diào)用 exit 退出。這種場景顯然不能用 kill -9,不然一下把進(jìn)程干掉了資源就來不及清除了

為什么線程崩潰不會導(dǎo)致 JVM 進(jìn)程崩潰

現(xiàn)在我們再來看看開頭這個問題,相信你多少會心中有數(shù),想想看在 Java 中有哪些是常見的由于非法訪問內(nèi)存而產(chǎn)生的 Exception 或 error 呢,常見的是大家熟悉的 StackoverflowError 或者 NPE(NullPointerException),NPE 我們都了解,屬于是訪問了不存在的內(nèi)存

但為什么棧溢出(Stackoverflow)也屬于非法訪問內(nèi)存呢,這得簡單聊一下進(jìn)程的虛擬空間,也就是前面提到的共享地址空間

現(xiàn)代操作系統(tǒng)為了保護(hù)進(jìn)程之間不受影響,所以使用了虛擬地址空間來隔離進(jìn)程,進(jìn)程的尋址都是針對虛擬地址,每個進(jìn)程的虛擬空間都是一樣的,而線程會共用進(jìn)程的地址空間,以 32 位虛擬空間,進(jìn)程的虛擬空間分布如下

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

那么 stackoverflow 是怎么發(fā)生的呢,進(jìn)程每調(diào)用一個函數(shù),都會分配一個棧楨,然后在棧楨里會分配函數(shù)里定義的各種局部變量,假設(shè)現(xiàn)在調(diào)用了一個無限遞歸的函數(shù),那就會持續(xù)分配棧幀,但 stack 的大小是有限的(Linux 中默認(rèn)為 8 M,可以通過 ulimit -a 查看),如果無限遞歸很快棧就會分配完了,此時再調(diào)用函數(shù)試圖分配超出棧的大小內(nèi)存,就會發(fā)生段錯誤,也就是 stackoverflowError

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

好了,現(xiàn)在我們知道了 StackoverflowError 怎么產(chǎn)生的,那問題來了,既然 StackoverflowError 或者 NPE 都屬于非法訪問內(nèi)存, JVM 為什么不會崩潰呢,有了上一節(jié)的鋪墊,相信你不難回答,其實(shí)就是因?yàn)?JVM 自定義了自己的信號處理函數(shù),攔截了 SIGSEGV 信號,針對這兩者不讓它們崩潰,怎么證明這個推測呢,我們來看下 JVM 的源碼來一探究竟

openJDK 源碼解析

HotSpot 虛擬機(jī)目前使用范圍最廣的 Java 虛擬機(jī),據(jù) R 大所述, Oracle JDK 與 OpenJDK 里的 JVM 都是 HotSpot VM,從源碼層面說,兩者基本上是同一個東西,OpenJDK 是開源的,所以我們主要研究下 Java 8 的 OpenJDK 即可,地址如下:
https://github.com/AdoptOpenJDK/openjdk-jdk8u,有興趣的可以下載來看看

我們只要研究 Linux 下的 JVM,為了便于說明,也方便大家查閱,我把其中關(guān)于信號處理的關(guān)鍵流程整理了下(忽略其中的次要代碼)

美團(tuán)一面:為什么線程崩潰崩潰不會導(dǎo)致 JVM 崩潰

 

可以看到,在啟動 JVM 的時候,也設(shè)置了信號處理函數(shù),收到 SIGSEGV,SIGPIPE 等信號后最終會調(diào)用 JVM_handle_linux_signal 這個自定義信號處理函數(shù),再來看下這個函數(shù)的主要邏輯

JVM_handle_linux_signal(int sig,
                        siginfo_t* info,
                        void* ucVoid,
                        int abort_if_unrecognized) {

   // Must do this before SignalHandlerMark, if crash protection installed we will longjmp away
  // 這段代碼里會調(diào)用 siglongjmp,主要做線程恢復(fù)之用
  os::ThreadCrashProtection::check_crash_protection(sig, t);

  if (info != NULL && uc != NULL && thread != NULL) {
    pc = (address) os::Linux::ucontext_get_pc(uc);

    // Handle ALL stack overflow variations here
    if (sig == SIGSEGV) {
      // Si_addr may not be valid due to a bug in the linux-ppc64 kernel (see
      // comment below). Use get_stack_bang_address instead of si_addr.
      address addr = ((NativeInstruction*)pc)->get_stack_bang_address(uc);

      // 判斷是否棧溢出了
      if (addr < thread->stack_base() &&
          addr >= thread->stack_base() - thread->stack_size()) {
        if (thread->thread_state() == _thread_in_Java) {
            // 針對棧溢出 JVM 的內(nèi)部處理
            stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);
        }
      }
    }
  }

  if (sig == SIGSEGV &&
               !macroAssembler::needs_explicit_null_check((intptr_t)info->si_addr)) {
         // 此處會做空指針檢查
      stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
  }


  // 如果是棧溢出或者空指針最終會返回 true,不會走最后的 report_and_die,所以 JVM 不會退出
  if (stub != NULL) {
    // save all thread context in case we need to restore it
    if (thread != NULL) thread->set_saved_exception_pc(pc);

    uc->uc_mcontext.gregs[REG_PC] = (greg_t)stub;
    // 返回 true 代表 JVM 進(jìn)程不會退出
    return true;
  }

  VMError err(t, sig, pc, info, ucVoid);
  // 生成 hs_err_pid_xxx.log 文件并退出
  err.report_and_die();

  ShouldNotReachHere();
  return true; // Mute compiler

}

從以上代碼(注意看加粗的紅線字體部分)我們可以知道以下信息

  1. 發(fā)生 stackoverflow 還有空指針錯誤,確實(shí)都發(fā)送了 SIGSEGV,只是虛擬機(jī)不選擇退出,而是自己內(nèi)部作了額外的處理,其實(shí)是恢復(fù)了線程的進(jìn)程,并拋出 StackoverflowError 和 NPE,這就是為什么 JVM 不會崩潰且我們能捕獲這兩個錯誤/異常的原因
  2. 如果針對 SIGSEGV 等信號,在以上的函數(shù)中 JVM 沒有做額外的處理,那么最終會走到 report_and_die 這個方法,這個方法主要做的事情是生成 hs_err_pid_xxx.log crash 文件(記錄了一些堆棧信息或錯誤),然后退出

至此我相信大家明白了為什么發(fā)生了 StackoverflowError 和 NPE 這兩個非法訪問內(nèi)存的錯誤,JVM 卻沒有崩潰。原因其實(shí)就是虛擬機(jī)內(nèi)部定義了信號處理函數(shù),而在信號處理函數(shù)中對這兩者做了額外的處理以讓 JVM 不崩潰,另一方面也可以看出如果 JVM 不對信號做額外的處理,最后會自己退出并產(chǎn)生 crash 文件 hs_err_pid_xxx.log(可以通過 -XX:ErrorFile=/var/log/hs_err.log 這樣的方式指定),這個文件記錄了虛擬機(jī)崩潰的重要原因,所以也可以說,虛擬機(jī)是否崩潰只要看它是否會產(chǎn)生此崩潰日志文件

總結(jié)

正常情況下,操作系統(tǒng)為了保證系統(tǒng)安全,所以針對非法內(nèi)存訪問會發(fā)送一個 SIGSEGV 信號,而操作系統(tǒng)一般會調(diào)用默認(rèn)的信號處理函數(shù)(一般會讓相關(guān)的進(jìn)程崩潰),但如果進(jìn)程覺得"罪不致死",那么它也可以選擇自定義一個信號處理函數(shù),這樣的話它就可以做一些自定義的邏輯,比如記錄 crash 信息等有意義的事,回過頭來看為什么虛擬機(jī)會針對 StackoverflowError 和 NullPointerException 做額外處理讓線程恢復(fù)呢,針對 stackoverflow 其實(shí)它采用了一種?;厮莸姆椒ūWC線程可以一直執(zhí)行下去,而捕獲空指針錯誤主要是這個錯誤實(shí)在太普遍了,為了這一個很常見的錯誤而讓 JVM 崩潰那線上的 JVM 要宕機(jī)多少次,所以出于工程健壯性的考慮,與其直接讓 JVM 崩潰倒不如讓線程起死回生,并且將這兩個錯誤/異常拋給用戶來處理

 

原文鏈接:
https://mp.weixin.qq.com/s/JnlTdUk8Jvao8L6FAtKqhQ

分享到:
標(biāo)簽:JVM
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定