欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

線程崩潰不會(huì)導(dǎo)致?JVM?崩潰的原因解析

 更新時(shí)間:2022年06月16日 09:56:27   作者:allentofight  
網(wǎng)上看到一個(gè)很有意思的據(jù)說是美團(tuán)的面試題:為什么線程崩潰崩潰不會(huì)導(dǎo)致?JVM?崩潰,這個(gè)問題我看了不少回答,但都沒答到根本原因,所以決定答一答,相信大家看完肯定會(huì)有收獲,本文分以下幾節(jié)來探討,需要的朋友可以參考下

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

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

openJDK 源碼解析

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

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

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

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

1.針對只讀內(nèi)存寫入數(shù)據(jù)

#include <stdio.h>
#include <stdlib.h>
int main() {
   char *s = "hello world";
 s[1] = 'H'; // 向只讀內(nèi)存寫入數(shù)據(jù),崩潰
}

2.針對沒有權(quán)限的內(nèi)存非法操作

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

int?main()?{
???int?*p?=?(int?*)0xC0000fff;
?*p?=?10;?//?針對進(jìn)程沒有寫權(quán)限的內(nèi)核空間寫入數(shù)據(jù),崩潰
}

在 32 位虛擬地址空間中,p 指向的是內(nèi)核空間,顯然不具有寫入權(quán)限,所以上述賦值操作會(huì)導(dǎo)致崩潰

3.訪問了不存在的內(nèi)存,比如

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

int?main()?{
???int?*a?=?NULL;
???*a?=?1;?????//?訪問了不存在的內(nèi)存
}

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

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

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

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

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

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

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

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

//?自定義信號(hào)處理函數(shù)示例
#include?<stdio.h>
#include?<signal.h>
#include?<stdlib.h>
//?自定義信號(hào)處理函數(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!

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

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

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

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

??//?產(chǎn)生一個(gè)?SIGSEGV?信號(hào)
??raise(SIGSEGV);

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

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

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

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

現(xiàn)在我們再來看看開頭這個(gè)問題,相信你多少會(huì)心中有數(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)程的尋址都是針對虛擬地址,每個(gè)進(jìn)程的虛擬空間都是一樣的,而線程會(huì)共用進(jìn)程的地址空間,以 32 位虛擬空間,進(jìn)程的虛擬空間分布如下

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

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

openJDK 源碼解析

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

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

可以看到,在啟動(dòng) JVM 的時(shí)候,也設(shè)置了信號(hào)處理函數(shù),收到 SIGSEGV,SIGPIPE 等信號(hào)后最終會(huì)調(diào)用 JVM_handle_linux_signal 這個(gè)自定義信號(hào)處理函數(shù),再來看下這個(gè)函數(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
??//?這段代碼里會(huì)調(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)?{
????????????stub?=?SharedRuntime::continuation_for_implicit_exception(thread,?pc,?SharedRuntime::STACK_OVERFLOW);
????????}
??????}
????}
??}

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


??//?如果是棧溢出或者空指針最終會(huì)返回?true,不會(huì)走最后的?report_and_die,所以?JVM?不會(huì)退出
??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)程不會(huì)退出
????return?true;
??}

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

??ShouldNotReachHere();
??return?true;?//?Mute?compiler

}

從以上代碼我們可以知道以下信息

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

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

總結(jié)

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

參考文章

Segmentation Fault in Linux: https://www.cnblogs.com/kaixin/archive/2010/06/07/1753133.html

linux SIGSEGV 信號(hào)捕捉,保證發(fā)生段錯(cuò)誤后程序不崩潰: https://blog.csdn.net/work_msh/article/details/8470277

到此這篇關(guān)于線程崩潰為什么不會(huì)導(dǎo)致 JVM 崩潰的文章就介紹到這了,更多相關(guān)線程JVM 崩潰內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++實(shí)現(xiàn)哈夫曼編碼

    C++實(shí)現(xiàn)哈夫曼編碼

    這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)哈夫曼編碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-04-04
  • QT .pro文件的實(shí)現(xiàn)

    QT .pro文件的實(shí)現(xiàn)

    本文主要介紹了QT .pro文件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • Opencv開發(fā)實(shí)現(xiàn)拼圖游戲

    Opencv開發(fā)實(shí)現(xiàn)拼圖游戲

    這篇文章主要為大家詳細(xì)介紹了Opencv開發(fā)實(shí)現(xiàn)拼圖游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • 一篇文章帶你掌握C++虛函數(shù)的來龍去脈

    一篇文章帶你掌握C++虛函數(shù)的來龍去脈

    虛函數(shù)主要通過V-Table虛函數(shù)表來實(shí)現(xiàn),該表主要包含一個(gè)類的虛函數(shù)的地址表,可解決繼承、覆蓋的問題,下面這篇文章主要給大家介紹了如何通過一篇文章帶你掌握C++虛函數(shù)的來龍去脈,需要的朋友可以參考下
    2022-10-10
  • C語言之strtol函數(shù)用法詳解

    C語言之strtol函數(shù)用法詳解

    這篇文章主要介紹了C語言之strtol函數(shù)用法詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • C語言圣誕樹的實(shí)現(xiàn)示例

    C語言圣誕樹的實(shí)現(xiàn)示例

    本篇主要介紹了C語言圣誕樹的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-12-12
  • C++實(shí)現(xiàn)大數(shù)相乘的算法

    C++實(shí)現(xiàn)大數(shù)相乘的算法

    這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)大數(shù)相乘的算法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-09-09
  • VsCode安裝和配置c/c++環(huán)境小白教程(圖文)

    VsCode安裝和配置c/c++環(huán)境小白教程(圖文)

    本文主要介紹了VsCode安裝和配置c/c++環(huán)境小白教程,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • 深入C++四種強(qiáng)制類型轉(zhuǎn)換的總結(jié)

    深入C++四種強(qiáng)制類型轉(zhuǎn)換的總結(jié)

    本篇文章是對C++中四種強(qiáng)制類型轉(zhuǎn)換進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • Unreal學(xué)習(xí)之簡單三角形的繪制詳解

    Unreal學(xué)習(xí)之簡單三角形的繪制詳解

    之所以寫這個(gè)繪制簡單三角形的實(shí)例其實(shí)是想知道如何在Unreal中通過代碼繪制自定義Mesh,如果你會(huì)繪制一個(gè)三角形,那么自然就會(huì)繪制復(fù)雜的Mesh了。所以這是很多圖形工作者的第一課,快跟隨小編一起學(xué)習(xí)起來吧
    2023-02-02

最新評論