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

news2024/11/18 4:36:19

网上看到一个很有意思的美团面试题:为什么线程崩溃崩溃不会导致 JVM 崩溃,这个问题我看了不少回答,但发现都没答到根上,所以决定答一答,相信大家看完肯定会有收获,本文分以下几节来探讨

  1. 线程崩溃,进程一定会崩溃吗

  2. 进程是如何崩溃的-信号机制简介

  3. 为什么在 JVM 中线程崩溃不会导致 JVM 进程崩溃

  4. openJDK 源码解析

线程崩溃,进程一定会崩溃吗

一般来说如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,为什么系统要让进程崩溃呢,这主要是因为在进程中,各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃

线程共享代码段,数据段,地址空间,文件

非法访问内存有以下几种情况,我们以 C 语言举例来看看

  1. 针对只读内存写入数据

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
       char *s = "hello world";
    // 向只读内存写入数据,崩溃
       s[1] = 'H'; 
    }
    
  2. 访问了进程没有权限访问的地址空间(比如内核空间)

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
       int *p = (int *)0xC0000fff;
       // 针对进程的内核空间写入数据,崩溃
       *p = 10; 
    }
    

    在 32 位虚拟地址空间中,p 指向的是内核空间,显然不具有写入权限,所以上述赋值操作会导致崩溃

  3. 访问了不存在的内存,比如

    #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 执行的是系统调用,将控制权转移给了内核(操作系统),由内核来给指定的进程发送信号

那么发个信号进程怎么就崩溃了呢,这背后的原理到底是怎样的?

其背后的机制如下

  1. CPU 执行正常的进程指令

  2. 调用 kill 系统调用向进程发送信号

  3. 进程收到操作系统发的信号,CPU 暂停当前程序运行,并将控制权转交给操作系统

  4. 调用 kill 系统调用向进程发送信号(假设为 11,即 SIGSEGV,一般非法访问内存报的都是这个错误)

  5. 操作系统根据情况执行相应的信号处理程序(函数),一般执行完信号处理程序逻辑后会让进程退出

注意上面的第五步,如果进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序(一般最后会让进程退出),但如果注册了,则会执行自己的信号处理函数,这样的话就给了进程一个垂死挣扎的机会,它收到 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

}

从以上代码(注意看加粗的红线字体部分)我们可以知道以下信息

  1. 发生 stackoverflow 还有空指针错误,确实都发送了 SIGSEGV,只是虚拟机不选择退出,而是自己内部作了额外的处理,其实是恢复了线程的执行,并抛出 StackoverflowError 和 NPE,这就是为什么 JVM 不会崩溃且我们能捕获这两个错误/异常的原因

  2. 如果针对 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 崩溃倒不如让线程起死回生,并且将这两个错误/异常抛给用户来处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/63277.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

公众号美食文案怎么写?怎么写才能吸引人?

美食类公众号的文案还是比较难写的&#xff0c;毕竟文案没有图片那么直观&#xff0c;让用户看着就有食欲。 公众号美食文案怎么写&#xff1f;怎么写才能吸引人&#xff1f;怎么写才能在字里行间透露着美食的诱惑力&#xff1f; 作为一个有着十年丰富经验的文案人&#xff0c…

Java编程最常见的208道面试题,一文解析

相比与这些问题&#xff0c;我的这 208 道面试题具备以下优点&#xff1a; 披沙拣金提炼出每个 Java 模块中最经典的面试题&#xff1b;答案准确&#xff0c;每个题目都是我仔细校对过的&#xff1b;接近最真实的企业面试&#xff0c;题目实用有效果&#xff1b;难懂的题目&am…

SAP IDoc状态70 - This IDoc is saved as the original of an edited document.

SAP IDoc状态70 - This IDoc is saved as the original of an edited document. 根据SAP的标准逻辑&#xff0c;一个IDoc一旦被修改了&#xff0c;SAP系统会自动创建一个新的IDoc(状态70)来存储IDoc修改日志。 比如idoc # IDoc 208828452&#xff0c;由于库存不够&#xff0c;所…

SQL注入【SQLi-LABS Page-1(Basic Challenges Less1-Less22)】

文章目录前言sqlmapless-1&#xff08;基于错误的GET单引号字符型注入&#xff09;less-2&#xff08;基于错误的GET整型注入&#xff09;less-3&#xff08;基于错误的GET单引号变形注入&#xff09;less4&#xff08;基于错误的GET双引号字符型注入&#xff09;less5&#xf…

Docker的资源管理控制(CPU、内存、磁盘IO配额)

目录 一、CPU 资源控制 1、设置CPU使用率上限 &#xff08;1&#xff09;查看CPU使用率 &#xff08;2&#xff09;进行CPU压力测试 &#xff08;3&#xff09;设置CPU使用率 2、设置CPU资源占用比&#xff08;设置多个容器时才有效&#xff09; 3、设置容器绑定指定的CP…

word如何转化成pdf,最新图文教学,学会只需1分钟

​在日常生活中&#xff0c;我们经常会遇到需要将word文档转换成PDF格式的情况。那word如何转化成pdf呢&#xff1f;word文件转换成PDF并不难&#xff0c;只要你操作正确&#xff0c;就可以将其快速地转化成PDF。下面就给大家分享一下操作步骤吧。 一、第一步&#xff0c;打开需…

嵌入式开发--STM32硬件SPI驱动74HC595

本篇是硬件SPI口驱动74HC595。 IO口模拟时序软件驱动74HC595见这一篇文章&#xff1a;嵌入式开发–IO口扩展–74HC595 硬件连接如下&#xff1a; 只用了一个74HC595&#xff0c;如果需要多个74HC595级联&#xff0c;参考前面的链接&#xff0c;里面有电路&#xff0c;不需要额…

KubeEdge云原生边缘计算公开课02——云原生边缘计算千行百业核心应用

KubeEdge云原生边缘计算公开课02——云原生边缘计算千行百业核心应用张琦&#xff1a;云原生边缘计算产业发展现状与趋势展望——KubeEdge云原生边缘计算产业发展概述一、云原生边缘计算的产业场景1.云计算与边缘计算的对比2.传统的把边缘计算逐步向CloudNative技术演进3.传统本…

cookie加密解密和保证数据完整性(不被篡改)

cookie加密解密和保证数据完整性 AES-128-CBC加密算法 AES-128-CBC是一种分组对称加密算法&#xff0c;即用同一组key进行明文和密文的转换&#xff0c;以128bit为一组&#xff0c;128bit16Byte&#xff0c;意思就是明文的16字节为一组对应加密后的16字节的密文。 若最后剩余…

计算机SCI论文的摘要怎么写? - 易智编译EaseEditing

摘要第一部分的 1-3 句话必须让读者知道你为什么进行这项研究。 引导性句子既陈述了主要议题&#xff0c;也说明了问题。这样&#xff0c;读者的注意力被立刻抓住了。 下一个句子可以接着讲述这个领域缺乏什么样的信息&#xff0c;或者以前的研究者曾做了哪些努力来解决这个问…

你居然还去服务器上捞日志,搭个日志收集系统难道不香吗?

摘要 ELK日志收集系统进阶使用&#xff0c;本文主要讲解如何打造一个线上环境真实可用的日志收集系统。有了它&#xff0c;你就可以和去服务器上捞日志说再见了&#xff01; ELK环境安装 ELK是指Elasticsearch、Kibana、Logstash这三种服务搭建的日志收集系统&#xff0c;具体…

序列化工具Protobuf在Idea中的配置和在java中的使用实例

一、什么是 protobuf &#xff1f; 1、protobuf 来源&#xff1f; Protocal Buffers(简称protobuf)是谷歌的一项技术&#xff0c;用于结构化的数据序列化、反序列化。 由于protobuf是跨语言的&#xff0c;所以用不同的语言序列化对象后&#xff0c;生成一段字节码&#xff0c;…

5G无线技术基础自学系列 | 5G上下行解耦技术

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 C-Band TDD系统拥有大带宽&#xff0c;…

Spring Boot 3.0 正式发布

导读Spring Boot 3.0 现已正式发布&#xff0c;此版本包含了 12 个月以来 151 个人的 5700 多次 commit 的工作结晶。这是自 4.5 年前发布 2.0 以来&#xff0c;Spring Boot 的第一次重大修订。它也是第一个支持 Spring Framework 6.0 和 GraalVM 的 Spring Boot GA 版本。 由于…

Windows10安装DM8-达梦数据库

目录1. 下载安装包1.1 点击官网下载DM8安装包1.2 安装包解压2. 安装数据库2.1 打开解压后的文件夹&#xff0c;双击打开该iso文件2.2 点击setup.exe进行安装2.3 选择语言2.4 执行安装向导2.5 数据库初始化3. SQL交互查询工具验证数据库安装4. 参考文档及简单的试炼5.DM管理工具…

System.UnauthorizedAccessException: 对路径“xxx”的访问被拒绝的解决办法

环境&#xff1a;.net、spring.core、C#、MVC、IIS 8、Windows server standard 2016 操作&#xff1a;读取本地文件夹 现象&#xff1a;爆出如标题异常&#xff0c;因为是在后台调用接口处理本地文件(夹)&#xff0c;前端相关接口显示500 Internal Server Error。 简单理解…

将“万词王”离线转化为“文络之心“插件之一:复现 wantwords Python/Pytorch 开源项目

在电影里&#xff0c;无限之书&#xff0c;每一页都是不一样的内容。简单的随机算法其实有点无聊&#xff0c;但借助深度学习技术&#xff0c;化王为心&#xff0c;实现心灵大道的转换&#xff0c;可以让AI生成相互之间有所关联的无限词典之书&#xff01; 访问 wantwords开源…

python @ 装饰器(修饰器,语法糖)使用与不使用对比总结记录

由于看python代码里面函数上的 不爽很久了&#xff0c;为了避免自己又忘记了这里来记录下。 简单总结&#xff1a; 的作用就是在使用 下面的函数&#xff08;如下图的cs2&#xff09;的时候&#xff0c;会在该函数执行前将该函数作为参数扔到后跟着的处理函数先行处理&#x…

再见 Spring Boot 1.X ,Spring Boot 2.X 走向舞台中心

2019年8月6日,Spring 官方在其博客宣布,Spring Boot 1.x 停止维护,Spring Boot 1.x 生命周期正式结束。 其实早在2018年7月30号,Spring 官方就已经在博客进行过预告,Spring Boot 1.X 将维护到2019年8月1日。 1.5.x 将会是 Spring Boot 1.0 系列的最后一个大版本。 如今…

[附源码]计算机毕业设计停车场管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…