大家好,我是鲲哥回答了很多,但是我发现都没有回答到根源,所以决定回答一下。相信大家看完后都会有所收获。本文分为以下几个部分来讨论线程崩溃。进程会崩溃吗?进程如何崩溃-信号机制简介。为什么JVM中的线程崩溃不会导致JVM进程崩溃。openJDK源码分析。如果线程崩溃,进程会崩溃吗?一般来说,如果线程因为非法访问内存而崩溃,那么进程肯定会崩溃。为什么系统会导致进程崩溃?这主要是因为在进程中,各个线程的地址空间是共享的,既然是共享的,一个线程非法访问地址会导致内存不确定,进而可能影响到其他线程。这个操作是危险的,操作系统会认为它很可能会导致一系列严重的后果,所以索性让整个进程崩溃。线程共享代码段、数据段、地址空间、文件非法访问内存有以下几种情况。以C语言为例:向只读存储器写入数据。#include#includeintmain(){char*s="helloworld";//将数据写入只读内存,崩溃s[1]='H';}access进程无权访问的地址空间(如内核空间)。#include#includeintmain(){int*p=(int*)0xC0000fff;//向进程内核空间写入数据,crash*p=10;}在32位虚拟地址空间中,p指向的是内核空间,显然没有写权限,所以上面的赋值操作会导致崩溃。访问不存在的内存,例如:#include#includeintmain(){int*a=NULL;*一个=1;}以上错误都是访问内存时的错误,因此unity会报SegmentFault错误(即段错误),从而导致进程崩溃。进程是怎么崩溃的——信号机制介绍那么线程崩溃后进程是怎么崩溃的呢?这背后的机制是什么?答案是信号。您是否经常使用kill来终止正在运行的进程?-对于像9pid这样的命令,这里的kill其实就是给指定的pid发送一个终止信号,其中9是一个信号。事实上,信号有很多种。在Linux中,可以通过kill-l查看所有可用的信号。当然,发送kill信号必须要有一定的权限,否则任何一个进程都可以通过发送一个信号来终止其他进程,这显然是不合理的。kill实际上是执行一个系统调用,将控制权交给内核(即操作系统),内核向指定的进程发送信号。那么发送信号后进程是怎么崩溃的呢?这背后的原理是什么?其背后的机制如下:CPU执行正常的进程指令。调用kill系统调用向进程发送信号。当进程接收到操作系统的信号时,CPU会挂起当前程序并将控制权交给操作系统。调用kill系统调用向进程发送信号(假设为11,即SIGSEGV,一般是非法访问内存报错)。操作系统根据情况执行相应的信号处理程序(函数),一般在执行完信号处理程序的逻辑后让进程退出。注意上面的第五步,如果进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序(通常进程最后会退出),但是如果注册了,就会执行它的自己的信号处理函数,这样就给了进程一个垂死的机会。收到kill信号后,可以调用exit()退出,但也可以使用sigsetjmp和siglongjmp这两个函数恢复进程的执行。//自定义信号处理函数示例#include#include#include//自定义信号处理函数,处理完自定义逻辑后调用exit退出voidsigHandler(intsig){printf("信号%d被捕获!\n",sig);退出(信号);}intmain(void){信号(SIGSEGV,sigHandler);int*p=(int*)0xC0000fff;*p=10;//向不属于进程的内核空间写入数据,crash}//以上结果输出:Signal11caught!如代码所示:注册信号处理函数后,当接收到SIGSEGV信号时,先执行相关逻辑再退出。另外,进程接收到信号后,可以不定义自己的信号处理函数,而是选择忽略信号,如下:#include#include#includeintmain(void){//忽略信号signal(SIGSEGV,SIG_IGN);//生成一个SIGSEGV信号raise(SIGSEGV);printf("Normalend");}也就是说,虽然给进程发送了kill信号,但是如果进程自己定义了该信号的处理函数或者忽略该信号,都会给你逃跑的机会。当然,kill-9命令是个例外。不管进程是否定义了信号处理函数,都会被立即杀死。说到这里,是不是想起一道经典的面试题:如何让,15是SIGTERM),JVM可以执行信号处理函数在进行一些资源清理后,调用exit退出。显然你不能在这种情况下使用kill-9。否则,如果一下子杀掉进程,再清除资源就来不及了。为什么线程崩溃不会导致JVM进程崩溃?现在让我们看一下开头的问题。Java中非法访问内存导致的常见异常或错误有哪些?常见的是熟悉的StackoverflowError或NPE(NullPointerException)。我们都知道NPE是指访问不存在的内存。但是为什么堆栈溢出(Stackoverflow)也是非法访问内存呢?这就不得不简单说一下进程的虚拟空间,也就是上面说的共享地址空间。为了保护进程不受影响,现代操作系统使用虚拟地址空间来隔离进程。进程的寻址是基于虚拟地址的。每个进程的虚拟空间是相同的,线程共享进程的地址。空间,用32位的虚拟空间,进程的虚拟空间分布如下:那么stackoverflow是怎么发生的呢?进程每次调用一个函数,都会分配一个栈帧,然后函数中定义的各种局部变量都会在栈帧中分配,假设现在调用一个无限递归的函数,它会继续分配栈帧,但是栈的大小是有限制的(linux默认是8M,可以通过ulimit-a查看),如果无限递归,分配结束后栈很快就会耗尽,如果调用函数在这时候再尝试分配超出栈大小的内存,就会出现segmentationfault,即stackoverflowerror。好了,知道了StackoverflowError是怎么产生的,那么问题来了,既然StackoverflowError或者NPE都是非法访问内存,为什么JVM不会崩溃呢?有了上一节的铺垫,相信你不难回答,其实,因为JVM自定义了自己的信号处理函数,拦截了SIGSEGV信号,防止它们崩溃,如何证明这个推测呢?让我们看看JVM的源代码来一探究竟。OpenJDK源码分析HotSpot虚拟机是目前使用最广泛的Java虚拟机。根据R,OracleJDK和OpenJDK中的JVM都是HotSpotVM。从源码层面来看,两者基本是一回事。OpenJDK是开源的,所以我们主要研究Java8的OpenJDK,地址如下:https://github.com/AdoptOpenJDK/openjdk-jdk8u,有兴趣的可以下载。我们只需要研究Linux下的JVM就可以了。为了方便讲解和参考,我整理了信号处理的关键过程(忽略次要代码)。可以看到,在启动JVM的时候,还设置了一个信号处理函数。接收到SIGSEGV、SIGPIPE等信号后,最终会调用自定义信号处理函数JVM_handle_linux_signal。我们来看看这个函数的主要逻辑。JVM_handle_linux_signal(intsig,siginfo_t*info,void*ucVoid,intabort_if_unrecognized){//必须在SignalHandlerMark之前执行此操作,如果安装了崩溃保护,我们将longjmp离开//这一段代码里会调用siglongjmp,主要做线路修复之用os::ThreadCrashProtection::check_crash_protection(sig,t);if(info!=NULL&&uc!=NULL&&thread!=NULL){pc=(地址)os::Linux::ucontext_get_pc(uc);//在此处处理所有堆栈溢出变化if(sig==SIGSEGV){//由于linux-ppc64内核中的错误,Si_addr可能无效(请参阅下面的//注释)。使用get_stack_bang_address而不是si_addr。地址addr=((NativeInstruction*)pc)->get_stack_bang_address(uc);//判断是否栈溢出了in_Java){//堆栈溢出的内部处理存根JVM=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){//保存所有线程上下文以防万一需要恢复它if(thread!=NULL)thread->set_saved_exception_pc(pc);uc->uc_mcontext.gregs[REG_PC]=(greg_t)stub;//returntrue表示JVM进程不会退出returntrue;}VMErrorerr(t,sig,pc,info,ucVoid);//生成hs_err_pid_xxx.log文件并退出err.report_and_die();ShouldNotReachHere();返回真;//Mutecompiler}从上面的代码(注意粗红线字体部分)我们可以知道下面的信息中出现了stackoverflow和空指针错误,确实发送了SIGSEGV,但是虚拟机并没有选择退出,但在内部做额外的处理,其实就是线程的进程恢复了,抛出了StackoverflowError和NPE,这就是为什么JVM不会crash,我们可以catch这两个错误/异常的原因。对于SIGSEGV之类的信号,JVM在上面的函数处理中并没有做额外的处理,最终会走到report_and_die方法,这个方法主要是生成hs_err_pid_xxx.logcrash文件(记录一些堆栈信息或者错误)),然后退出。至此,相信大家就明白为什么JVM在出现非法访问内存StackoverflowError和NPE这两个错误的情况下并没有崩溃。原因是在虚拟机内部定义了信号处理函数,在信号处理函数中对两者做了额外的处理,防止JVM崩溃。另一方面也可以看出,如果JVM不对信号进行额外的处理,最终会自行退出并生成crash文件hs_err_pid_xxx.log(可以通过-XX:ErrorFile=/var指定/日志/hs_err.log)。这个文件记录了虚拟机崩溃的重要原因,所以也可以说虚拟机是否崩溃取决于它是否会生成这个崩溃日志文件。总结一般情况下,操作系统为了保证系统安全,对于非法内存访问都会发送SIGSEGV信号,操作系统一般会调用默认的信号处理函数(一般是让相关进程崩溃),但是如果进程觉得认为“犯罪不致命”,那么它也可以选择自定义一个信号处理函数,这样它就可以做一些自定义的逻辑,比如记录崩溃信息等有意义的事情,回过头来看,虚拟机为什么要做额外的事情处理StackoverflowError和NullPointerException以允许线程恢复?对于stackoverflow来说,其实是用了一种栈回溯的方式来保证线程可以继续执行,而捕获空指针错误的主要原因是这个错误太常见了,对于这个很常见的错误,让JVM崩溃多少次线上的JVM会宕机,所以为了工程的健壮性,还是让线程起死回生,而不是直接让JVM崩溃,结合这两个错误/Exception抛给用户处理。