在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

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

jf_ro2CN3Fa ? 來(lái)源:碼海 ? 2023-01-09 10:39 ? 次閱讀

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

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

a54dbe82-7dff-11ed-8abf-dac502259ad0.jpg

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

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

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

#include
#include

intmain(){
char*s="helloworld";
//向只讀內(nèi)存寫入數(shù)據(jù),崩潰
s[1]='H';
}

訪問(wèn)了進(jìn)程沒(méi)有權(quán)限訪問(wèn)的地址空間(比如內(nèi)核空間)

#include
#include

intmain(){
int*p=(int*)0xC0000fff;
//針對(duì)進(jìn)程的內(nèi)核空間寫入數(shù)據(jù),崩潰
*p=10;
}

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

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

#include
#include

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

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

那么線程崩潰后,進(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 中可以通過(guò) kill -l查看所有可用的信號(hào)

a57668a0-7dff-11ed-8abf-dac502259ad0.jpg

當(dāng)然了發(fā) kill 信號(hào)必須具有一定的權(quán)限,否則任意進(jìn)程都可以通過(guò)發(fā)信號(hào)來(lái)終止其他進(jìn)程,那顯然是不合理的,實(shí)際上 kill 執(zhí)行的是系統(tǒng)調(diào)用,將控制權(quán)轉(zhuǎn)移給了內(nèi)核(操作系統(tǒng)),由內(nèi)核來(lá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,一般非法訪問(wèn)內(nèi)存報(bào)的都是這個(gè)錯(cuò)誤)

操作系統(tǒng)根據(jù)情況執(zhí)行相應(yīng)的信號(hào)處理程序(函數(shù)),一般執(zhí)行完信號(hào)處理程序邏輯后會(huì)讓進(jìn)程退出

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

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

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

//以上結(jié)果輸出:Signal11catched!

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

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

#include
#include
#include

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

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

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

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

說(shuō)到這大家是否想起了一道經(jīng)典面試題:如何讓正在運(yùn)行的 Java 工程的優(yōu)雅停機(jī),通過(guò)上面的介紹大家不難發(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 退出。這種場(chǎng)景顯然不能用 kill -9,不然一下把進(jìn)程干掉了資源就來(lái)不及清除了

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

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

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

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

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

a59d99d4-7dff-11ed-8abf-dac502259ad0.jpg

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

a5bb1176-7dff-11ed-8abf-dac502259ad0.jpg

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

openJDK 源碼解析

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

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

a5d9c6fc-7dff-11ed-8abf-dac502259ad0.jpg

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

JVM_handle_linux_signal(intsig,
siginfo_t*info,
void*ucVoid,
intabort_if_unrecognized){

//MustdothisbeforeSignalHandlerMark,ifcrashprotectioninstalledwewilllongjmpaway
//這段代碼里會(huì)調(diào)用siglongjmp,主要做線程恢復(fù)之用
os::check_crash_protection(sig,t);

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

//HandleALLstackoverflowvariationshere
if(sig==SIGSEGV){
//Si_addrmaynotbevalidduetoabuginthelinux-ppc64kernel(see
//commentbelow).Useget_stack_bang_addressinsteadofsi_addr.
addressaddr=((NativeInstruction*)pc)->get_stack_bang_address(uc);

//判斷是否棧溢出了
if(addrstack_base()&&
addr>=thread->stack_base()-thread->stack_size()){
if(thread->thread_state()==_thread_in_Java){//針對(duì)棧溢出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)){
//此處會(huì)做空指針檢查
stub=SharedRuntime::continuation_for_implicit_exception(thread,pc,SharedRuntime::IMPLICIT_NULL);
}


//如果是棧溢出或者空指針最終會(huì)返回true,不會(huì)走最后的report_and_die,所以JVM不會(huì)退出
if(stub!=NULL){
//saveallthreadcontextincaseweneedtorestoreit
if(thread!=NULL)thread->set_saved_exception_pc(pc);

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

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

ShouldNotReachHere();
returntrue;//Mutecompiler

}

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

發(fā)生 stackoverflow 還有空指針錯(cuò)誤,確實(shí)都發(fā)送了 SIGSEGV,只是虛擬機(jī)不選擇退出,而是自己內(nèi)部作了額外的處理,其實(shí)是恢復(fù)了線程的執(zhí)行,并拋出 StackoverflowError 和 NPE,這就是為什么 JVM 不會(huì)崩潰且我們能捕獲這兩個(gè)錯(cuò)誤/異常的原因

如果針對(duì) SIGSEGV 等信號(hào),在以上的函數(shù)中 JVM 沒(méi)有做額外的處理,那么最終會(huì)走到 report_and_die 這個(gè)方法,這個(gè)方法主要做的事情是生成 hs_err_pid_xxx.log crash 文件(記錄了一些堆棧信息或錯(cuò)誤),然后退出

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

總結(jié)

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

主線程異常會(huì)導(dǎo)致 JVM 退出?

a6069862-7dff-11ed-8abf-dac502259ad0.jpg

有讀者讀完前面部分的文章后,問(wèn)出了上面這個(gè)問(wèn)題。

他認(rèn)為如果 JVM 中的主線程異常沒(méi)有被捕獲,JVM 還是會(huì)崩潰,那么這個(gè)說(shuō)法是否正確呢,我們做個(gè)試驗(yàn)看看結(jié)果是否是他說(shuō)的這樣

publicclassTest{
publicstaticvoidmain(String[]args){
TestThreadtestThread=newTestThread();
TestThread.start();
Integerp=null;
//這里會(huì)導(dǎo)致空指針異常
if(p.equals(2)){
System.out.println("hahaha");
}
}
}

classTestThreadextendsThread{
@Override
publicvoidrun(){
while(true){
System.out.println("test");
}
}
}

試驗(yàn)很簡(jiǎn)單,首先啟動(dòng)一個(gè)線程,在這個(gè)線程里搞一個(gè) while true 不斷打印, 然后在主線程中制造一個(gè)空指針異常,不捕獲,然后看是否會(huì)一直打印 test

結(jié)果是會(huì)不斷打印 test,說(shuō)明主線程崩潰,JVM 并沒(méi)有崩潰 ,這是怎么回事, JVM 又會(huì)在什么情況下完全退出呢?

其實(shí)在 Java 中并沒(méi)有所謂主線程的概念,只是我們習(xí)慣把啟動(dòng)的線程作為主線程而已,所有線程其實(shí)都是平等的,不管什么線程崩潰都不會(huì)影響到其它線程的執(zhí)行,注意我們這里說(shuō)的線程崩潰是指由于未 catch 住 JVM 拋出的虛擬機(jī)錯(cuò)誤(VirtualMachineError)而導(dǎo)致的崩潰,虛擬機(jī)錯(cuò)誤包括 InternalError,OutOfMemoryError,StackOverflowError,UnknownError 這四大子類

a6186286-7dff-11ed-8abf-dac502259ad0.jpg

JVM 拋出這些錯(cuò)誤其實(shí)是一種防止整個(gè)進(jìn)程崩潰的自我防護(hù)機(jī)制,這些錯(cuò)誤其實(shí)是 JVM 內(nèi)部定義了信號(hào)處理函數(shù)處理后拋出的,JVM 認(rèn)為這些錯(cuò)誤"罪不致死",所以選擇恢復(fù)線程再給這些線程拋錯(cuò)誤(就算線程不 catch 這些錯(cuò)誤也不會(huì)崩潰)的方式來(lái)避免自身崩潰,但如果線程觸發(fā)了一些其他的非法訪問(wèn)內(nèi)存的錯(cuò)誤,JVM 則會(huì)認(rèn)為這些錯(cuò)誤很嚴(yán)重,從而選擇退出,比如下面這種非法訪問(wèn)內(nèi)存的錯(cuò)誤就會(huì)被認(rèn)為是致命錯(cuò)誤,JVM 就不會(huì)向上層拋錯(cuò)誤,而會(huì)直接選擇退出

Fieldf=Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafeunsafe=(Unsafe)f.get(null);
unsafe.putAddress(0,0);

回過(guò)頭來(lái)看,除了這些致命性錯(cuò)誤導(dǎo)致的 JVM 崩潰,還有哪些情況會(huì)導(dǎo)致 JVM 退出呢,在 javadoc 上說(shuō)得很清楚

a63ac682-7dff-11ed-8abf-dac502259ad0.jpg

The Java Virtual Machine exits when the only threads running are all daemon threads

也就是說(shuō)只有在 JVM 的所有線程都是守護(hù)線程(daemon thread)的時(shí)候才會(huì)完全退出,什么是守護(hù)線程?守護(hù)線程其實(shí)是為其他線程服務(wù)的線程,比如垃圾回收線程就是典型的守護(hù)線程,既然是為其他線程服務(wù)的,那么一旦其他線程都不存在了,守護(hù)線程也沒(méi)有存在的意義了,于是 JVM 也就退出了,守護(hù)線程通常是 JVM 運(yùn)行時(shí)幫我們創(chuàng)建好的,當(dāng)然我們也可以自己設(shè)置,以開(kāi)頭的代碼為例,在創(chuàng)建完 TestThread 后,調(diào)用 testThread.setDaemon(true) 方法即可將線程轉(zhuǎn)為守護(hù)線程,然后再啟動(dòng),這樣在主線程退出后,JVM 就會(huì)退出了,大家可以試試

Java 線程模型簡(jiǎn)介

我們可以看看 Java 的線程模型,這樣大家對(duì) JVM 的線程調(diào)度也會(huì)有一個(gè)更全面的認(rèn)識(shí),我們可以先從源碼角度看看,啟動(dòng)一個(gè) Thread 到底在 JVM 內(nèi)部發(fā)生了什么,啟動(dòng)源碼代碼在 Thread#start 方法中

publicclassThread{

publicsynchronizedvoidstart(){
...
start0();
...
}
privatenativevoidstart0();
}

可以看到最終會(huì)調(diào)用 start0 這個(gè) native 方法,我們?nèi)ハ螺d一下 openJDK(地址:https://github.com/AdoptOpenJDK/openjdk-jdk8u) 來(lái)看看這個(gè)方法對(duì)應(yīng)的邏輯

a65ed004-7dff-11ed-8abf-dac502259ad0.jpg

可以看到 start0 對(duì)應(yīng)的是 JVM_startThread 這個(gè)方法,我們主要觀察在 Linux 下的線程啟動(dòng)情況,一路追蹤下去

//jvm.cpp
JVM_ENTRY(void,JVM_StartThread(JNIEnv*env,jobjectjthread))
native_thread=newJavaThread(&thread_entry,sz);

//thread.cpp
JavaThread::JavaThread(ThreadFunctionentry_point,size_tstack_sz)
{
os::create_thread(this,thr_type,stack_sz);
}

//os_linux.cpp
boolos::create_thread(Thread*thread,ThreadTypethr_type,size_tstack_size){
intret=pthread_create(&tid,&attr,(void*(*)(void*))java_start,thread);
}

可以看到最終是通過(guò)調(diào)用 pthread_create 來(lái)啟動(dòng)線程的,這個(gè)方法是一個(gè) C 函數(shù)庫(kù)實(shí)現(xiàn)的創(chuàng)建 native thread 的接口,是一個(gè)系統(tǒng)調(diào)用,由此可見(jiàn) pthread_create 最終會(huì)創(chuàng)建一個(gè) native thread,這個(gè)線程也叫內(nèi)核線程 ,操作系統(tǒng)只能調(diào)度內(nèi)核線程,于是我們知道了在 Java 中,Java 線程和內(nèi)核線程是一對(duì)一的關(guān)系,Java 線程調(diào)度實(shí)際上是通過(guò)操作系統(tǒng)調(diào)度實(shí)現(xiàn)的,這種一對(duì)一的線程也叫 NPTL(Native POSIX Thread Library) 模型,如下

a68acf9c-7dff-11ed-8abf-dac502259ad0.jpg

NPTL線程模型

那么這個(gè)內(nèi)核線程在內(nèi)核中又是怎么表示的呢, 其實(shí)在 Linux 中不管是進(jìn)程還是線程都是通過(guò)一個(gè) task_struct 的結(jié)構(gòu)體來(lái)表示的, 這個(gè)結(jié)構(gòu)體定義了進(jìn)程需要的虛擬地址,文件描述符,寄存器,信號(hào)等資源

早期沒(méi)有線程的概念,所以每次啟動(dòng)一個(gè)進(jìn)程都需要調(diào)用 fork 創(chuàng)建進(jìn)程,這個(gè) fork 干的事其實(shí)就是 copy 父進(jìn)程對(duì)應(yīng)的 task_struct 的多數(shù)字段(pid 等除外),這在性能上顯然是無(wú)法接受的。于是線程的概念被提出來(lái)了,線程除了有自己的棧和寄存器外,其他像虛擬地址,文件描述符等資源都可以共享

a6c49ca4-7dff-11ed-8abf-dac502259ad0.jpg

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

于是針對(duì)線程,我們就可以指定在創(chuàng)建 task_struct 時(shí),采用共享 而不是復(fù)制字段的方式。其實(shí)不管是創(chuàng)建進(jìn)程(fork)還是創(chuàng)建線程(pthread_create)最終都會(huì)通過(guò)調(diào)用 clone() 的形式來(lái)創(chuàng)建 task_struct,只不過(guò) pthread_create 在調(diào)用 clone 時(shí),指定了如下幾個(gè)共享參數(shù)

clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0);

畫(huà)外音 :CLONE_VM 共享頁(yè)表,CLONE_FS 共享文件系統(tǒng)信息,CLONE_FILES 共享文件句柄,CLONE_SIGHAND 共享信號(hào)

通過(guò)共享而不是復(fù)制資源的形式極大地加快了線程的創(chuàng)建,另外線程的調(diào)度開(kāi)銷也會(huì)更小,比如在(同一進(jìn)程內(nèi))線程間切換的時(shí)候由于共享了虛擬地址空間,TLB 不會(huì)被刷新從而導(dǎo)致內(nèi)存訪問(wèn)低效的問(wèn)題

提到這相信你已經(jīng)明白了教科書(shū)上的一句話:進(jìn)程是資源分配的最小單元,而線程是程序執(zhí)行和調(diào)度的最小單位。在 Linux 中進(jìn)程分配資源后,線程通過(guò)共享資源的方式來(lái)被調(diào)度得以提升線程的執(zhí)行效率

由此可見(jiàn),在 Linux 中所有的進(jìn)程/線程都是用的 task_struct,它們之間其實(shí)是平等的 ,那怎么表示這些線程屬于同一個(gè)進(jìn)程的概念呢,畢竟線程之間也是要通信的,一組線程以及它們所共同引用的一組資源就是一個(gè)進(jìn)程。, 它們還必須被視為一個(gè)整體。

task_struct 中引入了線程組的概念,如果線程都是由同一個(gè)進(jìn)程(即我們說(shuō)的主線程)產(chǎn)生的, 那么它們的 tgid(線程組id) 是一樣的,如果是主線程,則 pid = tgid,如果是主線程創(chuàng)建的線程,則這些線程的 tgid 會(huì)與主線程的 tgid 一致,

那么在 LInux 中進(jìn)程,進(jìn)程內(nèi)的線程之間是如何通信或者管理的呢,其實(shí) NPTL 是一種實(shí)現(xiàn)了 POSIX Thread 的標(biāo)準(zhǔn) ,所以我們只需要看 POSIX Thread 的標(biāo)準(zhǔn)即可,以下列出了 POSIX Thread 的主要標(biāo)準(zhǔn):

查看進(jìn)程列表的時(shí)候, 相關(guān)的一組 task_struct 應(yīng)當(dāng)被展現(xiàn)為列表中的一個(gè)節(jié)點(diǎn)(即進(jìn)程內(nèi)如果有多個(gè)線程,展示進(jìn)程列表 ps -ef 時(shí)只會(huì)展示主線程,如果要查看線程的話可以用 ps -T)

發(fā)送給這個(gè)進(jìn)程的信號(hào)(對(duì)應(yīng) kill 系統(tǒng)調(diào)用), 將被對(duì)應(yīng)的這一組 task_struct 所共享, 并且被其中的任意一個(gè)”線程”處理

發(fā)送給某個(gè)線程的信號(hào)(對(duì)應(yīng) pthread_kill), 將只被對(duì)應(yīng)的一個(gè) task_struct 接收, 并且由它自己來(lái)處理

當(dāng)進(jìn)程被停止或繼續(xù)時(shí)(對(duì)應(yīng) SIGSTOP/SIGCONT 信號(hào)), 對(duì)應(yīng)的這一組 task_struct 狀態(tài)將改變

當(dāng)進(jìn)程收到一個(gè)致命信號(hào)(比如由于段錯(cuò)誤收到 SIGSEGV 信號(hào)), 對(duì)應(yīng)的這一組 task_struct 將全部退出

畫(huà)外音 : POSIX 即可移植操作系統(tǒng)接口(Portable Operating System Interface of UNIX,縮寫為 POSIX ),是一種接口規(guī)范,如果系統(tǒng)都遵循這個(gè)標(biāo)準(zhǔn),可以做到源碼級(jí)的遷移,這就類似 Java 中的針對(duì)接口編程

這樣就能很好地滿足進(jìn)程退出線程也退出,或者線程間通信等要求了

NPTL 模型的缺點(diǎn)

NPTL 是一種非常高效的模型,研究表明 NPTL 能夠成功地在 IA-32 平臺(tái)上在兩秒內(nèi)生成 100,000 個(gè)線程,而 2.6 之前未采用 NPTL 的內(nèi)核則需耗費(fèi) 15 分鐘左右,看起來(lái) NPTL 確實(shí)很好地滿足了我們的需求,但針對(duì)內(nèi)核線程來(lái)調(diào)度其實(shí)還是有以下問(wèn)題

不管是進(jìn)程還是線程,每次阻塞、切換都需要陷入系統(tǒng)調(diào)用(system call),系統(tǒng)調(diào)用開(kāi)銷其實(shí)挺大的,包括上下文切換(寄存器切換),特權(quán)模式切換等,而且還得先讓 CPU 跑操作系統(tǒng)的調(diào)度程序,然后再由調(diào)度程序決定該跑哪一個(gè)進(jìn)程(線程)

不管是進(jìn)程還是線程,都屬于搶占式調(diào)度(高優(yōu)先級(jí)線進(jìn)程優(yōu)先被調(diào)度),由于搶占式調(diào)度執(zhí)行順序無(wú)法確定的特點(diǎn),使用線程時(shí)需要非常小心地處理同步問(wèn)題

線程雖然更輕量級(jí),但這只是相對(duì)于進(jìn)程而言,實(shí)際上使用線程所消耗的資源依然很大,比如在 linux 上,一個(gè)線程默認(rèn)的棧大小是1M,創(chuàng)建幾萬(wàn)個(gè)線程就吃不消了

協(xié)程

NPTL 模型其實(shí)已經(jīng)足夠優(yōu)秀了,上述問(wèn)題本質(zhì)上其實(shí)還是因?yàn)榫€程還是太“重”所致,那能否再在線程上抽出一個(gè)更輕量級(jí)的執(zhí)行單元(可被 CPU 調(diào)度和分派的基本單位)呢,答案是肯定的,在線程之上我們可以再抽象出一個(gè)協(xié)程(coroutine)的概念,就像進(jìn)程是由線程來(lái)調(diào)度的,同樣線程也可以細(xì)化成一個(gè)個(gè)的協(xié)程來(lái)調(diào)度

a6db1ff6-7dff-11ed-8abf-dac502259ad0.jpg

針對(duì)以上問(wèn)題,協(xié)程都做了非常好的處理

協(xié)程的調(diào)度處于用戶態(tài),也就沒(méi)有了系統(tǒng)調(diào)用這些開(kāi)銷

協(xié)程不屬于搶占式調(diào)度,而是協(xié)作式調(diào)度,如何調(diào)度,在什么時(shí)間讓出執(zhí)行權(quán)給其它協(xié)程是由用戶自己決定的,這樣的話同步的問(wèn)題也基本不存在,可以認(rèn)為協(xié)程是無(wú)鎖的,所以性能很高

我們可以認(rèn)為線程的執(zhí)行是由一個(gè)個(gè)協(xié)程組成的,協(xié)程是更輕量的存在,內(nèi)存使用大約只有線程的十分之一甚至是幾十分之一,它是使用棧內(nèi)存按需使用的,所以創(chuàng)建百萬(wàn)級(jí)的協(xié)程是非常輕松的事

協(xié)程是怎么做到上述這些的呢

協(xié)程(coroutine)可以分為兩個(gè)角度來(lái)看,一個(gè)是 routine 即執(zhí)行單元,一個(gè)是 co 即 cooperative 協(xié)作,也就是說(shuō)線程可以依次順序執(zhí)行各個(gè)協(xié)程,但協(xié)程與線程不同之處在于,如果某個(gè)協(xié)程(假設(shè)為 A)內(nèi)碰到了 IO 等阻塞事件,可以主動(dòng)讓出自己的調(diào)度權(quán),即掛起(suspend),轉(zhuǎn)而執(zhí)行其他協(xié)程,等 IO 事件準(zhǔn)備好了,再來(lái)調(diào)度協(xié)程 A

a6f459f8-7dff-11ed-8abf-dac502259ad0.jpg

這就好比我在看電視的時(shí)候碰到廣告,那我可以先去倒杯水,等廣告播完了再回來(lái)繼續(xù)看電視。而如果是函數(shù),那你必須看完廣告再去倒水,顯然協(xié)程的效率更高。那么協(xié)程之間是怎么協(xié)作的呢,我們可以在兩個(gè)協(xié)程之間碰到 IO 等阻塞事件時(shí)隨時(shí)將自己掛起(yield),然后喚醒(resume)對(duì)方以讓對(duì)方執(zhí)行,想象一下如果協(xié)程中有挺多 IO 等阻塞事件時(shí),那這種協(xié)作調(diào)度是非常方便的

a7159da2-7dff-11ed-8abf-dac502259ad0.jpg兩個(gè)協(xié)程之間的“協(xié)作”

不像函數(shù)必須執(zhí)行完才能返回,協(xié)程可以在執(zhí)行流中的任意位置 由用戶決定掛起和喚醒,無(wú)疑協(xié)程是更方便的

a72c21c6-7dff-11ed-8abf-dac502259ad0.jpg

函數(shù)與協(xié)程的區(qū)別

更重要的一點(diǎn)是不像線程的掛起和喚醒等調(diào)度必須通過(guò)系統(tǒng)調(diào)用來(lái)讓內(nèi)核調(diào)度器來(lái)調(diào)度,協(xié)程的掛起和喚醒完全是由用戶決定的 ,而且這個(gè)調(diào)度是在用戶態(tài),幾乎沒(méi)有開(kāi)銷!

前面我們一直提到一般我們?cè)趨f(xié)程中碰到 IO 等阻塞事件時(shí)才會(huì)掛起并喚醒其他協(xié)程,所以可知協(xié)程非常適合 IO 密集型的應(yīng)用 ,如果是計(jì)算密集型其實(shí)用線程反而更加合適

為什么 Go 語(yǔ)言這么最近這么火,一個(gè)很重要的原因就是因?yàn)橐驗(yàn)樗焐С謪f(xié)程,可以輕而易舉地創(chuàng)建成千上萬(wàn)個(gè)協(xié)程,而如果是創(chuàng)建線程的話,創(chuàng)建幾百個(gè)估計(jì)就夠嗆了,不過(guò)比較遺憾的是 Java 原生并不支持協(xié)程,只能通過(guò)一些第三方庫(kù)如 Quasar 來(lái)實(shí)現(xiàn),2018 年 OpenJDK 官方創(chuàng)建了一個(gè) loom 項(xiàng)目來(lái)推進(jìn)協(xié)程的官方支持工作

總結(jié)

從進(jìn)程,到線程再到協(xié)程,可知我們一直在想辦法讓執(zhí)行單元變得更輕量級(jí),一開(kāi)始只有進(jìn)程的概念,但是進(jìn)程的創(chuàng)建在 Linux 下需要調(diào)用 fork 全部復(fù)制一遍資源,雖然后來(lái)引入了寫時(shí)復(fù)制的概念,但進(jìn)程的創(chuàng)建開(kāi)銷依然很大,于是提出了更輕量級(jí)的線程,在 Linux 中線程與進(jìn)程其實(shí)都是用 task_struct 表示的,只是線程采用了共享資源的方式來(lái)創(chuàng)建,極大了提升了 task_struct 的創(chuàng)建與調(diào)度效率,但人們發(fā)現(xiàn),線程的阻塞,喚醒都要通過(guò)系統(tǒng)調(diào)用陷入內(nèi)核態(tài)才能被調(diào)度程度調(diào)度,如果線程頻繁切換,開(kāi)銷無(wú)疑是很大的,于是人們提出了協(xié)程的概念,協(xié)程是根據(jù)棧內(nèi)存按需求分配的,所需開(kāi)銷是線程的幾十分之一,非常的輕量,而且調(diào)度是在用戶態(tài),并且它是協(xié)作式調(diào)度,可以很方便的掛起恢復(fù)其他協(xié)程的執(zhí)行,在此期間,線程是不會(huì)被掛起的,所以無(wú)論是創(chuàng)建還是調(diào)度開(kāi)銷都很小,目前 Java 官方還不支持,不過(guò)支持協(xié)程應(yīng)該是大勢(shì)所趨,未來(lái)我們可以期待一下。






審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10984

    瀏覽量

    214790
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    159

    瀏覽量

    12439
  • openjdk
    +關(guān)注

    關(guān)注

    0

    文章

    8

    瀏覽量

    2382

原文標(biāo)題:美團(tuán)一面:為什么線程崩潰崩潰不會(huì)導(dǎo)致 JVM 崩潰

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Jvm的整體結(jié)構(gòu)和特點(diǎn)

    的代碼等數(shù)據(jù)?! 《褏^(qū)  所有線程共享的一塊內(nèi)存區(qū)域,虛擬機(jī)啟動(dòng)時(shí)被創(chuàng)建用來(lái)存放對(duì)象實(shí)例。  JVM?! 】梢詤⒖剂私鈼5臄?shù)據(jù)結(jié)構(gòu),存放Java方法執(zhí)行的內(nèi)存模型,Java開(kāi)發(fā)中,一個(gè)功能實(shí)現(xiàn)需要
    發(fā)表于 01-05 17:23

    看看基于JDK中自帶JVM工具的用法

    用的手段;輕松解決開(kāi)發(fā):由于經(jīng)驗(yàn)不足,程序出現(xiàn)重大BUG導(dǎo)致JVM異常,進(jìn)而引起系列的連鎖反應(yīng),這種不會(huì)絕地反彈,只有一地雞毛;解決常規(guī)的JVM
    發(fā)表于 11-16 15:30

    Jvm工作原理學(xué)習(xí)筆記

    JVM實(shí)例對(duì)應(yīng)了一個(gè)獨(dú)立運(yùn)行的java程序它是進(jìn)程級(jí)別 a) 啟動(dòng)。啟動(dòng)一個(gè)Java程序時(shí),一個(gè)JVM實(shí)例就產(chǎn)生了,任何一個(gè)擁有public static void main(String
    發(fā)表于 04-03 11:03 ?5次下載

    如何解決JVM中一個(gè)極小概率發(fā)生的bug

    到 CMS 代碼存在 bug,導(dǎo)致 JVM 弱內(nèi)存模型的平臺(tái)上 Crash。分析過(guò)程中,涉及到 CMS 垃圾回收原理、內(nèi)存屏障、對(duì)象頭、以及 ParNew 并行回收算法中多個(gè)
    的頭像 發(fā)表于 08-23 17:35 ?3555次閱讀

    如何解決JVM解釋器導(dǎo)致應(yīng)用崩潰的bug

    編者按:筆者遇到一個(gè)非常典型的問(wèn)題,應(yīng)用在 X86 正常運(yùn)行, AArch64 上 JVM 就會(huì)崩潰。這個(gè)典型的 JVM 內(nèi)部問(wèn)題。筆者通過(guò)分析最終定位到是由于
    的頭像 發(fā)表于 08-27 09:58 ?2586次閱讀
    如何解決<b class='flag-5'>JVM</b>解釋器<b class='flag-5'>導(dǎo)致</b>應(yīng)用<b class='flag-5'>崩潰</b>的bug

    OOM會(huì)導(dǎo)致JVM虛擬機(jī)退出嗎

    OutOfMemoryError (OOM)。 這種錯(cuò)誤是 Error 的一個(gè)子類,通常表示某種無(wú)法恢復(fù)的問(wèn)題。 回到主題,先說(shuō)下結(jié)論: OutOfMemoryError 本身不會(huì)直接導(dǎo)致JVM退出,但由于其
    的頭像 發(fā)表于 09-30 10:14 ?912次閱讀

    jvm的dump太大了怎么分析

    分析大型JVM dump文件可能會(huì)遇到的一些挑戰(zhàn)。首先,JVM dump文件通常非常大,可能幾百M(fèi)B或幾個(gè)GB。這是因?yàn)樗鼈儼?b class='flag-5'>JVM的完整內(nèi)存快照,包括堆和棧的所有對(duì)象和線程信息。
    的頭像 發(fā)表于 12-05 11:01 ?3085次閱讀

    jvm內(nèi)存模型和內(nèi)存結(jié)構(gòu)

    JVM(Java虛擬機(jī))是Java程序的運(yùn)行平臺(tái),它負(fù)責(zé)將Java程序轉(zhuǎn)換成機(jī)器碼并在計(jì)算機(jī)上執(zhí)行。JVM中,內(nèi)存模型和內(nèi)存結(jié)構(gòu)是兩個(gè)重要的概念,本文將詳細(xì)介紹它們。 一、JVM內(nèi)存
    的頭像 發(fā)表于 12-05 11:08 ?1091次閱讀

    什么場(chǎng)景需要jvm調(diào)優(yōu)

    ,如果JVM的性能不夠優(yōu)越,可能會(huì)導(dǎo)致應(yīng)用程序的性能下降甚至崩潰。此時(shí)需要對(duì)JVM進(jìn)行調(diào)優(yōu),以提高應(yīng)用程序的并發(fā)處理能力。例如,調(diào)整線程池的
    的頭像 發(fā)表于 12-05 11:14 ?1617次閱讀

    jvm調(diào)優(yōu)參數(shù)

    JVM(Java虛擬機(jī))是Java程序的運(yùn)行環(huán)境,它負(fù)責(zé)解釋Java字節(jié)碼并執(zhí)行相應(yīng)的指令。為了提高應(yīng)用程序的性能和穩(wěn)定性,我們可以調(diào)優(yōu)JVM的參數(shù)。 JVM調(diào)優(yōu)主要涉及到堆內(nèi)存、垃圾收集器、
    的頭像 發(fā)表于 12-05 11:29 ?793次閱讀

    jvm參數(shù)的設(shè)置和jvm調(diào)優(yōu)

    JVM(Java虛擬機(jī))參數(shù)的設(shè)置和調(diào)優(yōu)對(duì)于提高Java應(yīng)用程序的性能和穩(wěn)定性非常重要。本文中,我們將詳細(xì)介紹JVM參數(shù)的設(shè)置和調(diào)優(yōu)方法。 一、JVM參數(shù)的設(shè)置 內(nèi)存參數(shù): -Xms
    的頭像 發(fā)表于 12-05 11:36 ?1917次閱讀

    jvm調(diào)優(yōu)工具有哪些

    、基于GUI的監(jiān)控和故障排查工具,提供了對(duì)JVM各種資源的可視化監(jiān)控和分析,例如CPU使用率、內(nèi)存使用情況、線程狀態(tài)等??梢酝ㄟ^(guò)JMX(Java Management Extensions)來(lái)連接和監(jiān)控
    的頭像 發(fā)表于 12-05 11:44 ?1262次閱讀

    jvm哪些區(qū)域會(huì)發(fā)生oom

    of Memory,OOM),本文將詳細(xì)介紹 JVM 內(nèi)容可能發(fā)生 OOM 的區(qū)域。OOM 是指應(yīng)用程序申請(qǐng)分配內(nèi)存時(shí),沒(méi)有足夠的內(nèi)存供其使用,導(dǎo)致程序無(wú)法正常執(zhí)行。 堆(Heap)區(qū)域: 堆是
    的頭像 發(fā)表于 12-05 11:51 ?1616次閱讀

    weblogic jvm參數(shù)配置

    WebLogic中,JVM參數(shù)配置是非常重要的,它可以對(duì)應(yīng)用程序的性能和穩(wěn)定性產(chǎn)生直接影響。JVM參數(shù)通過(guò)調(diào)整Java虛擬機(jī)的運(yùn)行時(shí)行為,可以優(yōu)化內(nèi)存管理、垃圾回收以及線程管理等方面
    的頭像 發(fā)表于 12-05 14:31 ?1698次閱讀

    eclipse設(shè)置jvm內(nèi)存大小

    內(nèi)存大小,并對(duì)其背后的原理進(jìn)行解釋。 JVM(Java虛擬機(jī))是Java程序的運(yùn)行環(huán)境,它負(fù)責(zé)將Java字節(jié)碼翻譯成機(jī)器碼,以便在不同的平臺(tái)上執(zhí)行。JVM使用內(nèi)存來(lái)存儲(chǔ)運(yùn)行時(shí)對(duì)象和執(zhí)行過(guò)程中的臨時(shí)數(shù)據(jù)。如果JVM的內(nèi)存不足,就會(huì)
    的頭像 發(fā)表于 12-06 11:43 ?2133次閱讀
    主站蜘蛛池模板: 欧美性色欧美a在线播放 | 日本一区二区免费在线观看 | 夜间视频在线观看 | 午夜免费毛片 | 天天操天天舔天天干 | 夜夜操天天 | 五月婷婷综合网 | 日本黄色片在线观看 | 免费亚洲视频在线观看 | 免费无毒片在线观看 | 天天上天天操 | 天堂网在线.www天堂在线资源 | 夜夜操操| 亚洲一区二区三区四区五区六区 | 视频在线观看免费网址 | 九色视频播放 | 色多多视频在线播放 | 亚洲综合在线最大成人 | 午夜视频欧美 | 好看的一级毛片 | 成人啪啪免费视频 | 69精品久久久久 | 男人的天堂免费网站 | 亚洲图片 欧美色图 | 国产福利在线免费 | 亚洲男人的天堂在线播放 | 色偷偷亚洲综合网亚洲 | 91夜夜人人揉人人捏人人添 | 精品视频一区二区三区四区五区 | 97人人视频 | 高颜值露脸极品在线播放 | 亚洲成人在线播放 | 国产午夜视频在永久在线观看 | 福利观看| 亚洲男同tv | 国产98在线传媒在线视频 | 免费视频一区 | 在线免费午夜视频 | 在线观看黄色一级片 | 天天视频国产免费入口 | 伊人久久香 |