美团一面:为什么线程崩溃崩溃不会导致 JVM 崩溃( 二 )

如代码所示:注册信号处理函数后,当收到 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 位虚拟空间,进程的虚拟空间分布如下

美团一面:为什么线程崩溃崩溃不会导致 JVM 崩溃

文章插图
 
那么 stackoverflow 是怎么发生的呢,进程每调用一个函数,都会分配一个栈桢,然后在栈桢里会分配函数里定义的各种局部变量,假设现在调用了一个无限递归的函数,那就会持续分配栈帧,但 stack 的大小是有限的(Linux 中默认为 8 M,可以通过 ulimit -a 查看),如果无限递归很快栈就会分配完了,此时再调用函数试图分配超出栈的大小内存,就会发生段错误,也就是 stackoverflowError
美团一面:为什么线程崩溃崩溃不会导致 JVM 崩溃

文章插图
 
好了,现在我们知道了 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 崩溃

文章插图
 
可以看到,在启动 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 hereif (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 itif (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}


推荐阅读