成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

美團一面:為什么線程崩潰崩潰不會導致 JVM 崩潰

開發 前端
為了這一個很常見的錯誤而讓 JVM 崩潰那線上的 JVM 要宕機多少次,所以出于工程健壯性的考慮,與其直接讓 JVM 崩潰倒不如讓線程起死回生,并且將這兩個錯誤/異常拋給用戶來處理。

大家好,我是坤哥

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

  • 線程崩潰,進程一定會崩潰嗎?
  • 進程是如何崩潰的-信號機制簡介。
  • 為什么在 JVM 中線程崩潰不會導致 JVM 進程崩潰。
  • openJDK 源碼解析。

線程崩潰,進程一定會崩潰嗎

一般來說如果線程是因為非法訪問內存引起的崩潰,那么進程肯定會崩潰,為什么系統要讓進程崩潰呢,這主要是因為在進程中,各個線程的地址空間是共享的,既然是共享,那么某個線程對地址的非法訪問就會導致內存的不確定性,進而可能會影響到其他線程,這種操作是危險的,操作系統會認為這很可能導致一系列嚴重的后果,于是干脆讓整個進程崩潰。

圖片

線程共享代碼段,數據段,地址空間,文件

非法訪問內存有以下幾種情況,我們以 C 語言舉例來看看:

針對只讀內存寫入數據。

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

int main() {
char *s = "hello world";
// 向只讀內存寫入數據,崩潰
s[1] = 'H';
}

訪問了進程沒有權限訪問的地址空間(比如內核空間)。

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

int main() {
int *p = (int *)0xC0000fff;
// 針對進程的內核空間寫入數據,崩潰
*p = 10;
}

在 32 位虛擬地址空間中,p 指向的是內核空間,顯然不具有寫入權限,所以上述賦值操作會導致崩潰。

訪問了不存在的內存,比如:

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

int main() {
int *a = NULL;
*a = 1;
}

以上錯誤都是訪問內存時的錯誤,所以統一會報 Segment Fault 錯誤(即段錯誤),這些都會導致進程崩潰。

進程是如何崩潰的-信號機制簡介

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

圖片

當然了發 kill 信號必須具有一定的權限,否則任意進程都可以通過發信號來終止其他進程,那顯然是不合理的,實際上 kill 執行的是系統調用,將控制權轉移給了內核(操作系統),由內核來給指定的進程發送信號。

那么發個信號進程怎么就崩潰了呢,這背后的原理到底是怎樣的?

其背后的機制如下:

  • CPU 執行正常的進程指令。
  • 調用 kill 系統調用向進程發送信號。
  • 進程收到操作系統發的信號,CPU 暫停當前程序運行,并將控制權轉交給操作系統。
  • 調用 kill 系統調用向進程發送信號(假設為 11,即 SIGSEGV,一般非法訪問內存報的都是這個錯誤)。
  • 操作系統根據情況執行相應的信號處理程序(函數),一般執行完信號處理程序邏輯后會讓進程退出。

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

// 自定義信號處理函數示例

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
// 自定義信號處理函數,處理自定義邏輯后再調用 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; // 針對不屬于進程的內核空間寫入數據,崩潰
}

// 以上結果輸出: Signal 11 catched!

如代碼所示:注冊信號處理函數后,當收到 SIGSEGV 信號后,先執行相關的邏輯再退出。

另外當進程接收信號之后也可以不定義自己的信號處理函數,而是選擇忽略信號,如下:

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

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

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

printf("正常結束");
}

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

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

為什么線程崩潰不會導致 JVM 進程崩潰

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

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

現代操作系統為了保護進程之間不受影響,所以使用了虛擬地址空間來隔離進程,進程的尋址都是針對虛擬地址,每個進程的虛擬空間都是一樣的,而線程會共用進程的地址空間,以 32 位虛擬空間,進程的虛擬空間分布如下:

圖片

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

圖片

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

openJDK 源碼解析

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

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

圖片

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

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
// 這段代碼里會調用 siglongjmp,主要做線程恢復之用
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 的內部處理
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 進程不會退出
return true;
}

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

ShouldNotReachHere();
return true; // Mute compiler

}

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

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

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

總結

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

責任編輯:武曉燕 來源: 碼海
相關推薦

2011-11-30 13:59:17

JavaJVM

2011-05-04 09:05:52

網絡崩潰錯誤連接DHCP

2024-10-31 08:50:14

2020-12-21 06:18:15

Android線程主線程

2022-10-19 10:29:08

云原生DevOpsCloudOps

2011-05-27 09:19:32

Windows 7崩潰

2024-04-24 09:02:58

線程池面試鎖升級

2009-07-08 13:22:48

JVM termina

2024-04-01 00:00:00

Redis緩存服務消息隊列

2025-03-25 12:00:00

@Value?Spring開發

2010-04-30 00:28:07

Unix系統

2021-05-08 15:22:56

崩潰Windows 10AMD

2010-09-17 13:45:40

JVM termina

2009-07-10 14:32:06

JVM崩潰

2009-04-30 09:02:36

微軟操作系統Windows 7

2024-04-22 00:00:00

CASCPU硬件

2021-08-30 16:48:24

在線文檔

2023-07-13 09:16:47

循環隊列指針front?

2015-08-26 13:26:55

Windows 10桌面

2009-05-06 09:03:50

微軟Windows 7操作系統
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 操皮视频 | 中文字幕亚洲欧美日韩在线不卡 | 久久精品视频播放 | 久久久精品天堂 | 中文字幕电影在线观看 | 欧美aⅴ在线观看 | 国产一区| 在线免费国产视频 | 999国产精品视频免费 | 亚洲二区在线观看 | 国产精品一码二码三码在线 | 久久精品欧美一区二区三区不卡 | 91亚洲国产成人久久精品网站 | 91视频在线观看 | 亚洲欧洲综合av | 国产精品永久久久久 | 国产精品高潮呻吟久久aⅴ码 | 一本大道久久a久久精二百 国产成人免费在线 | hitomi一区二区三区精品 | 在线观看日韩精品视频 | 久久av在线播放 | 91精品一区二区 | 午夜精品一区 | 久久久精| 国产精品亚洲一区 | 国产亚洲一区二区精品 | 国产精品一区二区久久精品爱微奶 | 日韩电影一区二区三区 | 免费看一区二区三区 | 断背山在线观看 | 日本小电影网站 | 国产精品成人国产乱 | 国产精品永久久久久久久www | 国产精品久久久久久久毛片 | 999精品在线观看 | 免费黄色a视频 | 999久久久| 天堂中文在线播放 | 久久91精品国产一区二区三区 | 国产精品99久久久久久久vr | 欧美一级片黄色 |