jvm 发送`kill -11`到java进程会引发NullPointerException吗?

j9per5c4  于 6个月前  发布在  Java
关注(0)|答案(1)|浏览(74)

bounty还有6天到期。回答这个问题有资格获得+100声望奖励。choxsword希望引起更多关注这个问题:任何JVMMaven都可以帮助我解决这个问题。

例如,HotSpot JVM通过捕获SIGSEGV信号来实现NullPointer检测。因此,如果我们从外部手动生成SIGSEGV,在某些情况下是否也会被识别为NullPointerException**?

wqsoz72f

wqsoz72f1#

kill -11发送到java进程会引发NullPointerException吗?
NullPointerException是一个特定的异常,当应用程序试图使用具有null值的对象引用时会发生。
然而,从JavaSE 17 /故障排除指南/处理信号和故障
Java HotSpot VM安装信号处理程序以实现各种功能并处理致命错误条件。
例如,在java.lang.NullPointerException很少被抛出的情况下,在避免显式空检查的优化中,SIGSEGV信号被捕获和处理,而NullPointerException被抛出。
一般来说,有两类信号/陷阱发生:

  • 当信号被预期和处理时,如隐式空处理。另一个例子是安全点轮询机制,它在需要安全点时保护内存中的页面。任何访问该页面的线程都会导致SIGSEGV,这导致执行一个存根,将线程带到安全点。
  • 意外信号。在VM代码、Java本机接口(JNI)代码或本机代码中执行时,包括SIGSEGV。在这些情况下,信号是意外的,因此调用致命错误处理来创建错误日志并终止进程。

这种方法允许JVM通过减少代码中显式空值检查的开销来优化性能,而是依赖于操作系统的内存保护机制来检测对空值引用的访问。当这种访问发生时,操作系统生成一个SIGSEGV信号,JVM然后将其解释为试图解引用空值指针,导致抛出NullPointerException
但是,需要注意的是,这是JVM的一种内部机制,与外部生成的SIGSEGV信号不同,例如使用kill命令发送的信号。外部SIGSEGV信号通常用于指示严重错误,包括无效的内存访问,并且更有可能导致JVM崩溃或核心转储,而不是NullPointerException

+---------------------+         +-----------------------------------+
| External Process    |         | Java Process running on HotSpot   |
| sending SIGSEGV     | ------> | JVM                               |
| (kill -11)          |         | Likely JVM Crash or Core Dump     |
+---------------------+         +-----------------------------------+

字符串
JVM是否总是能够检测外部SIGSEGV是否是外部SIGSEGV,或者当外部SIGSEGV在特定时间发生时(即当预期潜在的空访问时),是否可能将其混淆为空访问?
同样,它不应该这样做,但这是JVM行为的一个特定于实现的方面。
这意味着在实践中发生这种混淆的可能性可能会因JVM版本、正在执行的特定代码以及信号发出时JVM的状态而异。
例如,参见“How does the JVM know when to throw a NullPointerException
JVM可以使用虚拟内存硬件来实现空值检查,JVM将其虚拟地址空间中的页面零Map到不可读+不可写的页面。
由于null被表示为零,当Java代码试图解引用null时,这将试图访问一个不可寻址的页面,并将导致OS向JVM发送“segfault”信号。
JVM的segfault信号处理程序可以捕获它,找出代码在哪里执行,并在适当线程的堆栈上创建和抛出NPE。
在这种情况下,应该很容易区分来自代码执行中的捕获信号和来自OS的接收信号。
别名:Can a SIGSEGV in Java not crash the JVM?
在某些情况下,JVM的SIGSEGV信号处理程序可能会将SIGSEGV事件转换为Java异常。
只有在JVM硬崩溃无法发生的情况下,才会发生这种情况;例如,当事件发生时,触发SIGSEGV的线程正在本机库中执行代码。
例如:
HotSpot JVM故意在启动时生成SIGSEGV来检查某些CPU功能。没有开关可以关闭它。我建议完全跳过gdb中的SIGSEGV,因为JVM在许多情况下会将其用于自己的目的。
如果SIGSEGV在外部触发时堆栈恰好位于访问地址处,该怎么办?
hotspot在JDK-8255711中的信号处理方面进行了重大重构,导致了commit dd8e4ff
当前代码为os_linux_x86.cpp#PosixSignals::pd_hotspot_signal_handler

// decide if this trap can be handled by a stub
  address stub = nullptr;

  address pc          = nullptr;

  //%note os_trap_1
  if (info != nullptr && uc != nullptr && thread != nullptr) {
    pc = (address) os::Posix::ucontext_get_pc(uc);

    if (sig == SIGSEGV && info->si_addr == 0 && info->si_code == SI_KERNEL) {
      // An irrecoverable SI_KERNEL SIGSEGV has occurred.
      // It's likely caused by dereferencing an address larger than TASK_SIZE.
      return false;
    }

    // Handle ALL stack overflow variations here
    if (sig == SIGSEGV) {
      address addr = (address) info->si_addr;

      // check if fault address is within thread stack
      if (thread->is_in_full_stack(addr)) {
        // stack overflow
        if (os::Posix::handle_stack_overflow(thread, addr, pc, uc, &stub)) {
          return true; // continue
        }
      }
    }

    if ((sig == SIGSEGV) && VM_Version::is_cpuinfo_segv_addr(pc)) {
      // Verify that OS save/restore AVX registers.
      stub = VM_Version::cpuinfo_cont_addr();
    }

    if (thread->thread_state() == _thread_in_Java) {
      // Java thread running in Java code => find exception handler if any
      // a fault inside compiled code, the interpreter, or a stub

      if (sig == SIGSEGV && SafepointMechanism::is_poll_address((address)info->si_addr)) {
        stub = SharedRuntime::get_poll_stub(pc);
      } else if (sig == SIGBUS /* && info->si_code == BUS_OBJERR */) {
        // BugId 4454115: A read from a MappedByteBuffer can fault
        // here if the underlying file has been truncated.
        // Do not crash the VM in such a case.
        CodeBlob* cb = CodeCache::find_blob(pc);
        CompiledMethod* nm = (cb != nullptr) ? cb->as_compiled_method_or_null() : nullptr;
        bool is_unsafe_arraycopy = thread->doing_unsafe_access() && UnsafeCopyMemory::contains_pc(pc);
        if ((nm != nullptr && nm->has_unsafe_access()) || is_unsafe_arraycopy) {
          address next_pc = Assembler::locate_next_instruction(pc);
          if (is_unsafe_arraycopy) {
            next_pc = UnsafeCopyMemory::page_error_continue_pc(pc);
          }
          stub = SharedRuntime::handle_unsafe_access(thread, next_pc);
        }
      }
      else

#ifdef AMD64
      if (sig == SIGFPE  &&
          (info->si_code == FPE_INTDIV || info->si_code == FPE_FLTDIV)) {
        stub =
          SharedRuntime::
          continuation_for_implicit_exception(thread,
                                              pc,
                                              SharedRuntime::
                                              IMPLICIT_DIVIDE_BY_ZERO);
#else
      if (sig == SIGFPE /* && info->si_code == FPE_INTDIV */) {
        // HACK: si_code does not work on linux 2.2.12-20!!!
        int op = pc[0];
        if (op == 0xDB) {
          // FIST
          // TODO: The encoding of D2I in x86_32.ad can cause an exception
          // prior to the fist instruction if there was an invalid operation
          // pending. We want to dismiss that exception. From the win_32
          // side it also seems that if it really was the fist causing
          // the exception that we do the d2i by hand with different
          // rounding. Seems kind of weird.
          // NOTE: that we take the exception at the NEXT floating point instruction.
          assert(pc[0] == 0xDB, "not a FIST opcode");
          assert(pc[1] == 0x14, "not a FIST opcode");
          assert(pc[2] == 0x24, "not a FIST opcode");
          return true;
        } else if (op == 0xF7) {
          // IDIV
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_DIVIDE_BY_ZERO);
        } else {
          // TODO: handle more cases if we are using other x86 instructions
          //   that can generate SIGFPE signal on linux.
          tty->print_cr("unknown opcode 0x%X with SIGFPE.", op);
          fatal("please update this code.");
        }
#endif // AMD64
      } else if (sig == SIGSEGV &&
                 MacroAssembler::uses_implicit_null_check(info->si_addr)) {
          // Determination of interpreter/vtable stub/compiled code null exception
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
      }
    } else if ((thread->thread_state() == _thread_in_vm ||
                thread->thread_state() == _thread_in_native) &&
               (sig == SIGBUS && /* info->si_code == BUS_OBJERR && */
               thread->doing_unsafe_access())) {
        address next_pc = Assembler::locate_next_instruction(pc);
        if (UnsafeCopyMemory::contains_pc(pc)) {
          next_pc = UnsafeCopyMemory::page_error_continue_pc(pc);
        }
        stub = SharedRuntime::handle_unsafe_access(thread, next_pc);
    }

    // jni_fast_Get<Primitive>Field can trap at certain pc's if a GC kicks in
    // and the heap gets shrunk before the field access.
    if ((sig == SIGSEGV) || (sig == SIGBUS)) {
      address addr = JNI_FastGetField::find_slowcase_pc(pc);
      if (addr != (address)-1) {
        stub = addr;
      }
    }
  }


JVM使用各种检查来确定SIGSEGV信号的上下文。然而,我没有看到一种直接的机制来区分外部发送的SIGSEGV和内部由于空引用访问而生成的SIGSEGV
信号处理程序检查执行上下文,包括程序计数器和堆栈,以推断SIGSEGV的原因。在空引用的情况下,它会查找暗示空指针异常的特定模式。但是如果外部SIGSEGV恰好与JVM的执行状态类似于空指针访问的情况相一致,区分两者可能是一个挑战。
然而,由于时间上所需的精确度,这种情况相对不太可能发生。

相关问题