自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

解密JVM崩潰(Crash):如何通過(guò)日志分析揭開(kāi)神秘面紗

開(kāi)發(fā)
當(dāng)使用Java來(lái)構(gòu)建一個(gè)復(fù)雜的軟件系統(tǒng)時(shí),系統(tǒng)偶發(fā)性崩潰(也會(huì)被稱為Crash)是一個(gè)不小的挑戰(zhàn)。這種情況不出現(xiàn)則已,一出現(xiàn)可能會(huì)對(duì)系統(tǒng)的穩(wěn)定性和可靠性產(chǎn)生相當(dāng)大的負(fù)面影響,同時(shí)也會(huì)給終端用戶帶來(lái)不良體驗(yàn)。在本文中,我們將基于崩潰的現(xiàn)場(chǎng)進(jìn)行深入探討以及如何通過(guò)技術(shù)手段來(lái)識(shí)別、調(diào)試和解決這些問(wèn)題。

一、前言

當(dāng)使用Java來(lái)構(gòu)建一個(gè)復(fù)雜的軟件系統(tǒng)時(shí),系統(tǒng)偶發(fā)性崩潰(也會(huì)被稱為Crash)是一個(gè)不小的挑戰(zhàn)。這種情況不出現(xiàn)則已,一出現(xiàn)可能會(huì)對(duì)系統(tǒng)的穩(wěn)定性和可靠性產(chǎn)生相當(dāng)大的負(fù)面影響,同時(shí)也會(huì)給終端用戶帶來(lái)不良體驗(yàn)。在本文中,我們將基于崩潰的現(xiàn)場(chǎng)進(jìn)行深入探討以及如何通過(guò)技術(shù)手段來(lái)識(shí)別、調(diào)試和解決這些問(wèn)題。同時(shí)我們將深入研究如何利用現(xiàn)代開(kāi)發(fā)工具和最佳實(shí)踐來(lái)減少系統(tǒng)崩潰的可能性,進(jìn)而提高系統(tǒng)的穩(wěn)定性和可靠性。 

二、什么是崩潰?

簡(jiǎn)而言之,就是我們常說(shuō)的 - 系統(tǒng)掛了,進(jìn)程消失了。

當(dāng)發(fā)生一般的問(wèn)題情況時(shí),研發(fā)人員就像是消防員一樣,需要火速趕到現(xiàn)場(chǎng),分析日志,檢查堆棧,然后試圖重現(xiàn)并解決問(wèn)題。這個(gè)過(guò)程就像一場(chǎng)緊急救援任務(wù),需要迅速、果斷的行動(dòng),同時(shí)也需要謹(jǐn)慎對(duì)待,以避免引入更多問(wèn)題。

但偶發(fā)性崩潰會(huì)更難處理。因?yàn)橄到y(tǒng)留給我們可用于排查的信息并不夠多,甚至于重啟后這樣的崩潰再也不會(huì)發(fā)生。這個(gè)解決問(wèn)題的過(guò)程就像是一場(chǎng)復(fù)雜的推理游戲,需要耐心和技巧。幸運(yùn)的是,虛擬機(jī)還是給我們留下了一點(diǎn)蛛絲馬跡供我們來(lái)進(jìn)行深入的定位和跟蹤。 

三、一個(gè)例子

為了更進(jìn)一步深入的探尋虛擬機(jī)崩潰背后的細(xì)節(jié),我嘗試給出一段異常代碼:

import sun.misc.Unsafe;


import java.lang.reflect.Field;


public class Crash {


    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        unsafe.getInt(-1);
    }
}

Unsafe類(lèi)是一種非常強(qiáng)大且危險(xiǎn)的工具,它提供了直接操作內(nèi)存和執(zhí)行低級(jí)別代碼的能力。這個(gè)類(lèi)通常被用于開(kāi)發(fā)高性能的并發(fā)框架和底層操作系統(tǒng)相關(guān)的功能。由于使用Unsafe類(lèi)可以繞過(guò)Java語(yǔ)言的一些安全檢查,因此需要非常小心地使用,否則可能會(huì)導(dǎo)致嚴(yán)重的內(nèi)存錯(cuò)誤和安全漏洞。如上代碼所示,我們嘗試在一個(gè)非法的內(nèi)存區(qū)域讀取數(shù)據(jù)。當(dāng)執(zhí)行完成后我們立刻會(huì)在控制臺(tái)看到如下輸出:

圖片

好了,我們的程序此刻已經(jīng)崩潰了。虛擬機(jī)在也在崩潰的控制臺(tái)日志清楚的提示我們“A fatal error has been detected by the Java Runtime Environment.”

四、崩潰日志詳解

上述控制臺(tái)日志只是對(duì)于當(dāng)前虛擬機(jī)崩潰的一個(gè)概要說(shuō)明。在崩潰時(shí)虛擬機(jī)會(huì)幫我們生成兩份日志文件。一份是進(jìn)程的coredump文件,由操作系統(tǒng)負(fù)責(zé)生成;另一份是Java虛擬機(jī)自己在崩潰時(shí)生成。這兩部分日志對(duì)我們問(wèn)題的排查都有極大的作用。這里我們著重介紹Java虛擬機(jī)生成的這部分日志的細(xì)節(jié)。

文件路徑

當(dāng)Java程序崩潰時(shí),默認(rèn)情況下會(huì)在當(dāng)前進(jìn)程運(yùn)行目錄來(lái)生成形如hs_err_pid%p.log 的文件。這里的%p為通配符,系統(tǒng)會(huì)自動(dòng)轉(zhuǎn)化為當(dāng)前的進(jìn)程PID。例如Java進(jìn)程編號(hào)為1941生成的文件名即為hs_err_pid1941.log。同時(shí),虛擬機(jī)還提供了-XX:ErrorFile選項(xiàng)來(lái)幫助我們自定義文件的生成位置。比如我們想要把文件存儲(chǔ)到系統(tǒng)任意指定目錄下,可以指定啟動(dòng)參數(shù)形如:-XX:ErrorFile=/home/admin/hs_err_pid%p.log。需要注意的是,如果由于一些異常原因在工作目錄無(wú)法存儲(chǔ)(比如寫(xiě)入權(quán)限不足),則該文件會(huì)被存儲(chǔ)在操作系統(tǒng)的臨時(shí)目錄下。在Linux操作系統(tǒng)環(huán)境下,這個(gè)地址是/tmp。 

信息概要

崩潰日志包含在系統(tǒng)崩潰發(fā)生時(shí)獲取到的相關(guān)信息,具體包括如下(包括但不限于):

  • 導(dǎo)致進(jìn)程崩潰的操作異?;蛐盘?hào)
  • 版本和配置信息
  • 導(dǎo)致進(jìn)程崩潰的線程的詳細(xì)信息和線程的堆棧跟蹤
  • 正在運(yùn)行的線程列表及其狀態(tài)
  • 關(guān)于堆的摘要信息
  • 加載的本地庫(kù)列表
  • 命令行參數(shù)
  • 環(huán)境變量
  • 關(guān)于操作系統(tǒng)和CPU的詳細(xì)信息

而在Java虛擬機(jī)崩潰時(shí),生產(chǎn)的崩潰日志文件默認(rèn)會(huì)分為4大部分內(nèi)容:

  • 提供崩潰簡(jiǎn)要描述的頭部信息
  • 線程信息
  • 進(jìn)程信息
  • 系統(tǒng)信息

為了方便查閱,以下的章節(jié)中,我在認(rèn)為相對(duì)關(guān)鍵的地方都打上了星號(hào)(※),用來(lái)表示該小節(jié)內(nèi)容在問(wèn)題排查時(shí)要多留意,是日志能給出線索的相對(duì)高發(fā)地點(diǎn)。 

頭部信息

圖片

一個(gè)示例:虛擬機(jī)收到意外信號(hào)發(fā)生崩潰

如上所示,崩潰日志的開(kāi)始部分其實(shí)就是我們?cè)诳刂婆_(tái)剛才看到的那部分內(nèi)容,崩潰日志文件的頭部用簡(jiǎn)明扼要的方式描述了Java進(jìn)程崩潰的根本原因。在這個(gè)區(qū)域我們暫時(shí)只需要關(guān)心3個(gè)信息。

崩潰原因

崩潰的具體原因可見(jiàn)圖中第二行,這里一般會(huì)給出系統(tǒng)崩潰的原因。我們可以簡(jiǎn)單的分析為下圖:

SIGSEGV (0xb) at pc=0x417789d7, pid=21139, tid=1024
      |      |           |             |         +--- 線程ID
      |      |           |             +------------- 進(jìn)程ID
      |      |           +--------------------------- 程序計(jì)數(shù)器
      |      +--------------------------------------- 信號(hào)編號(hào)
      +---------------------------------------------- 信號(hào)名稱

這里有三個(gè)信息極為重要:信號(hào)名稱、程序計(jì)數(shù)器、線程ID。它會(huì)是我們后續(xù)排查問(wèn)題用到的前置信息,在此先按下不表,下文中會(huì)陸續(xù)說(shuō)明。

問(wèn)題幀棧

第二個(gè)關(guān)鍵的點(diǎn)是出問(wèn)題的幀棧,這等于是告訴了我們代碼是運(yùn)行在什么地方觸發(fā)了上面的崩潰原因。比如下方的示例,我們可以看到是執(zhí)行到了虛擬機(jī)內(nèi)部代碼(用大寫(xiě)字母V來(lái)表示)的函數(shù)Unsafe_GetNativeInt進(jìn)而觸發(fā)了系統(tǒng)崩潰。

# Problematic frame:
# V  [libjvm.so+0xa6d702]  Unsafe_GetNativeInt+0x52

棧幀(frame)表示程序運(yùn)行時(shí)函數(shù)調(diào)用棧中的某一幀。簡(jiǎn)單來(lái)說(shuō)可以理解為調(diào)用某個(gè)方法。

這里幀棧前面的大寫(xiě)字母也有特定含義。

圖片


核心轉(zhuǎn)儲(chǔ)文件

頭部信息里有一句話也很重要。

圖片

這意味著進(jìn)程在崩潰的時(shí)候順便幫我們生成了核心轉(zhuǎn)儲(chǔ)文件(coredump)。關(guān)于核心轉(zhuǎn)儲(chǔ)文件,它是操作系統(tǒng)在進(jìn)程收到某些信號(hào)而終止運(yùn)行時(shí),將此時(shí)進(jìn)程地址空間的內(nèi)容以及有關(guān)進(jìn)程狀態(tài)的其他信息寫(xiě)入一個(gè)磁盤(pán)文件。相比Java本身的崩潰文件,他后續(xù)還可以使用諸如gdb的工具進(jìn)行調(diào)試。核心轉(zhuǎn)儲(chǔ)文件對(duì)于問(wèn)題調(diào)試還是非常有用的:

  • 完整性和全局視角:操作系統(tǒng)的coredump包含了整個(gè)進(jìn)程的內(nèi)存轉(zhuǎn)儲(chǔ),包括了JVM 本身的內(nèi)存以及 JVM 進(jìn)程所依賴的操作系統(tǒng)資源的狀態(tài)。這提供了更全面的視角,有助于診斷問(wèn)題的根源。
  • 內(nèi)存信息:操作系統(tǒng)的coredump包含了進(jìn)程的完整內(nèi)存鏡像,包括了堆和棧的信息,這對(duì)于分析內(nèi)存狀態(tài)以及發(fā)現(xiàn)內(nèi)存相關(guān)的問(wèn)題非常重要。
  • 操作系統(tǒng)資源狀態(tài):coredump還可以提供操作系統(tǒng)級(jí)別的資源狀態(tài)信息,例如打開(kāi)的文件、網(wǎng)絡(luò)連接等。這些信息可以幫助診斷和解決一些與操作系統(tǒng)資源相關(guān)的問(wèn)題。
  • 與調(diào)試器的兼容性:一些調(diào)試器可能更喜歡使用操作系統(tǒng)的coredump進(jìn)行分析和調(diào)試。在某些情況下,使用coredump可能更容易進(jìn)行深入的調(diào)試和分析。

線程信息

這部分包含了剛剛崩潰的線程的信息,大部分情況下,這里是我們需要核心關(guān)注的點(diǎn)。虛擬機(jī)從線程視角清晰的描繪了當(dāng)系統(tǒng)崩潰的那一時(shí)刻的數(shù)據(jù)情況。

※ 線程概要 

圖片

虛擬機(jī)開(kāi)門(mén)見(jiàn)山的給出了線程的基本信息。我們可以用下方的圖示來(lái)簡(jiǎn)要說(shuō)明:

7feed0009800 JavaThread "main" [_thread_in_vm, id=37696, stack]
      |         |        |             |           |       +--------- 線程棧的范圍
      |         |        |             |           +----------------- 線程ID
      |         |        |             +----------------------------- 線程狀態(tài)(僅限Java線程)
      |         |        +------------------------------------------- name(線程名稱)
      |         +---------------------------------------------------- type(線程類(lèi)型)
      +-------------------------------------------------------------- pointer(線程內(nèi)存指針)

上述線程信息我們可以主動(dòng)先嘗試忽略部分內(nèi)容(比如線程內(nèi)存指針是指向虛擬機(jī)內(nèi)部線程結(jié)構(gòu)的指針,大部分情況下我們排查問(wèn)題都用不到),我們這里可以優(yōu)先關(guān)注2個(gè)內(nèi)容,以上圖為例:

線程類(lèi)型

如上圖所示,我們的線程類(lèi)型是JavaThread。在Hotspot虛擬機(jī)中,線程的類(lèi)型有很多種:

  • JavaThread
  • VMThread
  • CompilerThread
  • GCTaskThread
  • WatcherThread
  • ConcurrentMarkSweepThread
  • JvmtiAgentThread
  • ServiceThread
  • CodeCacheSweeperThread
  • ...

它們有的負(fù)責(zé)Java代碼的編譯、有的負(fù)責(zé)GC的執(zhí)行、有的負(fù)責(zé)緩存的清理,最終各司其職完成了虛擬機(jī)內(nèi)部的各種操作。上述例子中的JavaThread我們暫時(shí)可以認(rèn)為他是一個(gè)標(biāo)準(zhǔn)的Java代碼啟動(dòng)的線程。

※ 線程狀態(tài)

上述信息里還有一個(gè)關(guān)鍵信息是_thread_in_vm,它表示了線程當(dāng)前所處的狀態(tài)。

圖片

這些狀態(tài)基本上和我們熟知的線程狀態(tài)所對(duì)應(yīng)。在一些比較極限的場(chǎng)景下,還會(huì)有一些瞬時(shí)的“中轉(zhuǎn)”狀態(tài)。

圖片

Hotspot虛擬機(jī)在這里將線程的狀態(tài)劃分的更為清楚,便于技術(shù)人員第一時(shí)間知道線程到底處于一種什么樣的狀態(tài)下,進(jìn)而可以更快速的定位到具體的問(wèn)題。

※ 信號(hào)量概要

接下來(lái),虛擬機(jī)在這里描述了導(dǎo)致進(jìn)程終止的意外信號(hào)。

圖片

在本文的示例中,異常碼是11,它對(duì)應(yīng)的信號(hào)量是SIGSEGV,這是崩潰時(shí)最為常見(jiàn)的一種信號(hào)量,意味著當(dāng)前進(jìn)程正在從非法內(nèi)存值讀取內(nèi)容(si_addr)。簡(jiǎn)單來(lái)說(shuō),每個(gè)進(jìn)程所能讀寫(xiě)的內(nèi)存是有一定范圍的,如果超出這個(gè)范圍進(jìn)行內(nèi)存讀寫(xiě)是會(huì)被操作系統(tǒng)認(rèn)為是非法的操作。SIGSEGV同時(shí)下面有2種不同的si_code,分別是SEGV_MAPERR(未映射的地址)、SEGV_ACCERR(無(wú)效的訪問(wèn)許可)。

我們知道了信號(hào)量的概念后,再聊點(diǎn)題外話,SIGSEGV的本質(zhì)是內(nèi)存的非法訪問(wèn),這其實(shí)是非常嚴(yán)重的錯(cuò)誤。所以很多語(yǔ)言碰到類(lèi)似問(wèn)題會(huì)默認(rèn)讓系統(tǒng)崩潰(因?yàn)榉欠ㄔL問(wèn)的后果誰(shuí)也不知道,所以干脆崩了算了避免更意外的情況),Java虛擬機(jī)當(dāng)然也遵從了這個(gè)約定。但是在Java虛擬機(jī)內(nèi)部,有兩種特殊的場(chǎng)景本質(zhì)也是內(nèi)存非法訪問(wèn)且接收到了SIGSEGV,但系統(tǒng)并沒(méi)有崩潰且還能繼續(xù)運(yùn)行:

  • NullPointerException
  • StackoverflowError

這種例外情況我們需要特殊說(shuō)明。我們先來(lái)看看一個(gè)標(biāo)準(zhǔn)的SIGSEGV信號(hào)到來(lái)時(shí)進(jìn)程的時(shí)序:

  • 進(jìn)程正常執(zhí)行指令
  • 內(nèi)存非法訪問(wèn)
  • 進(jìn)程收到操作系統(tǒng)發(fā)來(lái)的SIGSEGV信號(hào)量
  • 若進(jìn)程注冊(cè)了信號(hào)回調(diào)函數(shù),則執(zhí)行,否則默認(rèn)結(jié)束進(jìn)程

上面的關(guān)鍵就在最后的回調(diào)函數(shù)。如果進(jìn)程主動(dòng)注冊(cè)了信號(hào)回調(diào)函數(shù),則可以在注冊(cè)的函數(shù)內(nèi)部選擇性的忽略指定信號(hào)(這樣系統(tǒng)就不會(huì)掛了),來(lái)提升整體的代碼穩(wěn)定性和健壯性。Java虛擬機(jī)內(nèi)部的處理源碼感興趣的可以見(jiàn)下方:

==================== os_linux_x86.cpp ==================== 


extern "C" JNIEXPORT int
JVM_handle_linux_signal(int sig,
                        siginfo_t* info,
                        void* ucVoid,
                        int abort_if_unrecognized) {
     


    // 這里處理StackoverflowError的情況
    // 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->on_local_stack(addr)) {
        // stack overflow
        if (thread->in_stack_yellow_reserved_zone(addr)) {
          if (thread->thread_state() == _thread_in_Java) {
            if (thread->in_stack_reserved_zone(addr)) {
              frame fr;
              if (os::Linux::get_frame_at_stack_banging_point(thread, uc, &fr)) {
                assert(fr.is_java_frame(), "Must be a Java frame");
                frame activation =
                  SharedRuntime::look_for_reserved_stack_annotated_method(thread, fr);
                if (activation.sp() != NULL) {
                  thread->disable_stack_reserved_zone();
                  if (activation.is_interpreted_frame()) {
                    thread->set_reserved_stack_activation((address)(
                      activation.fp() + frame::interpreter_frame_initial_sp_offset));
                  } else {
                    thread->set_reserved_stack_activation((address)activation.unextended_sp());
                  }
                  return 1;
                }
              }
            }
            // Throw a stack overflow exception.  Guard pages will be reenabled
            // while unwinding the stack.
            thread->disable_stack_yellow_reserved_zone();
            stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::STACK_OVERFLOW);
          } else {
            // Thread was in the vm or native code.  Return and try to finish.
            thread->disable_stack_yellow_reserved_zone();
            return 1;
          }
        } else if (thread->in_stack_red_zone(addr)) {
          // Fatal red zone violation.  Disable the guard pages and fall through
          // to handle_unexpected_exception way down below.
          thread->disable_stack_red_zone();
          tty->print_raw_cr("An irrecoverable stack overflow has occurred.");


          // This is a likely cause, but hard to verify. Let's just print
          // it as a hint.
          tty->print_raw_cr("Please check if any of your loaded .so files has "
                            "enabled executable stack (see man page execstack(8))");
        } else {
          // Accessing stack address below sp may cause SEGV if current
          // thread has MAP_GROWSDOWN stack. This should only happen when
          // current thread was created by user code with MAP_GROWSDOWN flag
          // and then attached to VM. See notes in os_linux.cpp.
          if (thread->osthread()->expanding_stack() == 0) {
             thread->osthread()->set_expanding_stack();
             if (os::Linux::manually_expand_stack(thread, addr)) {
               thread->osthread()->clear_expanding_stack();
               return 1;
             }
             thread->osthread()->clear_expanding_stack();
          } else {
             fatal("recursive segv. expanding stack.");
          }
        }
      }
    }
    
    ......
   
    // 這里處理NullPointerException的情況
    if (sig == SIGSEGV &&
               !MacroAssembler::needs_explicit_null_check((intptr_t)info->si_addr)) {
          // Determination of interpreter/vtable stub/compiled code null exception
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
      }
    }   
  }
 
  ...


  // StackoverflowError和NullPointerException會(huì)主動(dòng)返回并被記錄, 系統(tǒng)不掛
  if (stub != NULL) {
    // save all thread context in case we need to restore it
    if (thread != NULL) thread->set_saved_exception_pc(pc);


    os::Linux::ucontext_set_pc(uc, stub);
    return true;
  }
 
  ...
  
  // 虛擬機(jī)不主動(dòng)處理的信號(hào)到達(dá)這里會(huì)觸發(fā)系統(tǒng)掛掉 
  VMError::report_and_die(t, sig, pc, info, ucVoid);


  ShouldNotReachHere();
  return true; // Mute compiler
}

※ 寄存器

圖片

錯(cuò)誤日志中的下一個(gè)信息顯示了致命錯(cuò)誤發(fā)生時(shí)的寄存器上下文。這個(gè)輸出的確切格式取決于處理器。當(dāng)與[指令上下文]這一節(jié)結(jié)合來(lái)看時(shí),寄存器的數(shù)值可能會(huì)很有用。在當(dāng)前基本都是x86-64的架構(gòu)下,寄存器的簡(jiǎn)單描述如下:

圖片

但我們這里如果單純記每個(gè)寄存器的作用的話未免過(guò)于枯燥和乏味,我們需要的是找到系統(tǒng)崩潰的原因。除了和指令結(jié)合使用,這里也可以優(yōu)先關(guān)注下通用寄存器中的內(nèi)存值,很多時(shí)候是個(gè)初步的突破口。

圖片

以上圖為例,64位操作系統(tǒng)下用戶空間地址范圍為[0-0x00007FFFFFFFF000],我們對(duì)照上述寄存器內(nèi)的數(shù)據(jù)來(lái)看很快便發(fā)現(xiàn)R12寄存器的地址是0xffffffffffffffff。這顯然不是一個(gè)合法的地址,從而也進(jìn)一步的印證了日志開(kāi)頭給出的SIGSEGV錯(cuò)誤。 

棧地址

圖片

進(jìn)一步的,虛擬機(jī)給出了出問(wèn)題的線程的幀棧地址數(shù)據(jù),如果不考慮后面的core文件分析的情況下,大多數(shù)場(chǎng)景下這些密密麻麻的字符對(duì)我們來(lái)說(shuō)可參考的意義并不大,可以簡(jiǎn)短了解即可。

  • 虛擬機(jī)給出了sp指針指向的地址,這會(huì)和上面的rsp寄存器(棧頂)位置對(duì)應(yīng)。
  • X86-64的機(jī)器上,每一行對(duì)應(yīng)了16個(gè)字節(jié),后續(xù)兩列每一列的內(nèi)容16進(jìn)制輸出。

※ 指令上下文

圖片

指令上下文輸出了出問(wèn)題前后的64字節(jié)的操作碼。這在一些比較難以排查的問(wèn)題場(chǎng)景下還是很有極大作用的。簡(jiǎn)單來(lái)說(shuō),雖然我們處在Java的語(yǔ)言環(huán)境里,但和操作系統(tǒng)的背后交互其實(shí)還是一條條毫無(wú)感情的機(jī)器碼。而上述地址對(duì)應(yīng)的指令數(shù)據(jù)就是背后要執(zhí)行的機(jī)器碼。但因?yàn)闄C(jī)器碼過(guò)于晦澀,我們的前人才搞出了匯編語(yǔ)言這么個(gè)東西讓開(kāi)發(fā)的過(guò)程變得不那么繁瑣。而這些操作碼可以通過(guò)反匯編器進(jìn)行解碼,以生成崩潰位置附近的指令。

上述pc地址對(duì)應(yīng)的匯編是(網(wǎng)上有很多在線反匯編解碼工具):

0x00007faaf1397702:  45 8B 24 24             mov r12d, dword ptr [r12]
0x00007faaf1397706:  C6 80 3C 03 00 00 00    mov byte ptr [rax + 0x33c], 0
0x00007faaf139770d:  48 8B 7B 50             mov rdi, qword ptr [rbx + 0x50]

由于我們已經(jīng)已知R12的地址有問(wèn)題,無(wú)法讀取自己管控內(nèi)存地址之外的數(shù)據(jù)。所以我們很快可以初步判定是這一行匯編的問(wèn)題:mov r12d, dword ptr [r12]。它的含義是從R12寄存器取雙字讀取32位的值,然后保存在R12寄存器的低位中。在結(jié)合上下文函數(shù)幀知道出錯(cuò)的地方是unsafe的getInt方法,而getInt方法的入?yún)⒌暮x本身就需要傳入準(zhǔn)確的地址信息,那么這個(gè)問(wèn)題的答案就基本已經(jīng)付出水面了。

※ 寄存器內(nèi)容

圖片

這部分可以看做是對(duì)于之前的寄存器那一節(jié)的具體說(shuō)明。之前相對(duì)晦澀的地址,在這里虛擬機(jī)給出了當(dāng)前地址代表的具體內(nèi)容,以截圖為例:

  • RAX、RBX、R15是線程
  • RCX、R11指向動(dòng)態(tài)鏈接庫(kù)中
  • RDX、RSP、RBP、R14均指向線程中
  • R8、R13指向某個(gè)具體方法
  • R10指向解釋模式中的某個(gè)代碼片段
  • RSI、RDI、R9、R12均指向一個(gè)未知的值(an unknown value)

這里還是要說(shuō)明,未知的值并不一定是問(wèn)題,但是如果地址本身就是一個(gè)非法地址則是需要重點(diǎn)關(guān)注的事情。

※ 幀棧明細(xì) 

圖片

按照順序,接下來(lái)的輸出是線程幀棧的細(xì)節(jié),如上所示。

首先給出的是問(wèn)題棧的區(qū)間和棧頂?shù)刂芬约笆S嗟目臻g,接著,如果可能的話會(huì)打印堆棧幀且最多打印 100 個(gè)幀。對(duì)于 C/C++ 幀,可能還會(huì)打印庫(kù)名稱。

然后為了更清楚的看到調(diào)用幀的明細(xì),虛擬機(jī)把這里的內(nèi)容分成了兩部分:

  • Native frames 
  • Java frames  

簡(jiǎn)單來(lái)說(shuō)他們的區(qū)別是Native幀沒(méi)有考慮到由運(yùn)行時(shí)編譯器內(nèi)聯(lián)的Java方法,而Java幀會(huì)考慮內(nèi)聯(lián)。同時(shí)Native幀會(huì)打印線程的所有函數(shù)調(diào)用,相對(duì)Java幀會(huì)更詳細(xì)。 

進(jìn)程信息

※ 進(jìn)程里的線程 

首先映入眼簾的是進(jìn)程內(nèi)的線程信息,這里展示了虛擬機(jī)內(nèi)部的Java線程以及其它線程信息。特別需要注意的是“=>”這個(gè)符號(hào),它標(biāo)識(shí)了哪個(gè)線程是當(dāng)前的線程。

圖片

具體線程的描述過(guò)程在線程信息這一節(jié)已經(jīng)說(shuō)過(guò),在此不做贅述。

※ 虛擬機(jī)安全狀態(tài)

接下來(lái)的內(nèi)容是虛擬機(jī)的安全點(diǎn)狀態(tài)信息。注意這里的狀態(tài)描述 not at safepoint

圖片

這里的虛擬機(jī)狀態(tài)可分為3種情況:

  • not at safepoint (正常情況)
  • at safepoint (所有線程處于安全點(diǎn)等待VM進(jìn)行一些重要的操作。如GC、Debug等)
  • synchronizing (這是個(gè)一個(gè)瞬時(shí)的狀態(tài)。VM等待所有線程到達(dá)safepoint,已經(jīng)達(dá)到safepoint的線程會(huì)掛起)

很自然的,如果你看到這里的VM state顯示的如果是at safepoint,那就要稍微留意一下和GC相關(guān)的細(xì)節(jié)(雖然safepoint并不代表一定有GC)。關(guān)于safepoint更多的細(xì)節(jié),可見(jiàn)之前我寫(xiě)的一篇文章Java程序陷入時(shí)間裂縫:探索代碼深處的神秘停頓|得物技術(shù)

 鎖&監(jiān)視器

圖片

接下來(lái),虛擬機(jī)給出了當(dāng)前線程擁有的互斥鎖Mutex和監(jiān)視器Monitor列表。特別需要注意的是:這些互斥鎖和監(jiān)視器是虛擬機(jī)內(nèi)部的鎖和監(jiān)視器,而不是與Java對(duì)象相關(guān)的鎖和監(jiān)視器。所以絕大多數(shù)的崩潰文件下,這里的輸出都是None。

※ 內(nèi)存使用摘要 

圖片

這里是虛擬機(jī)堆區(qū)的內(nèi)容,對(duì)于Java程序員來(lái)說(shuō),這塊內(nèi)容看著就熟悉多了。我們逐一來(lái)分析看看。

內(nèi)存基礎(chǔ)模型

  • heap address: 0x00000006c0000000
  • size: 4096 MB

這里交代了內(nèi)存的基本信息,heap address 說(shuō)明了當(dāng)前我們進(jìn)程的虛擬內(nèi)存地址的起始地址。而size則表示當(dāng)前進(jìn)程預(yù)留(申請(qǐng))了多大的內(nèi)存,如圖所示為4096MB,即4G。這些信息得記下來(lái),如果遇到諸如物理內(nèi)存不足的問(wèn)題,這些都是關(guān)鍵的上下文信息。

  • Compressed Oops mode: Zero based
  • Oop shift amount: 3

這里的 Compressed Oops mode 稍微有點(diǎn)難懂,它表示了虛擬機(jī)在內(nèi)部對(duì)于對(duì)象的壓縮模式(也稱之為壓縮指針技術(shù))。在32位系統(tǒng)上,對(duì)象指針通常占用4個(gè)字節(jié)(32位),而在64位系統(tǒng)上,對(duì)象指針通常需要8個(gè)字節(jié)(64位)。這意味著在64位系統(tǒng)上,對(duì)象指針的大小會(huì)比在32位系統(tǒng)上大。但因?yàn)槲覀儸F(xiàn)在基本上都在使用64位的系統(tǒng),為了節(jié)省內(nèi)存空間,JVM引入了壓縮指針技術(shù)。這種技術(shù)通過(guò)在64位系統(tǒng)上使用更小的指針來(lái)表示對(duì)象的引用,從而減小了對(duì)象指針的大小。在壓縮指針模式下,JVM會(huì)將對(duì)象指針壓縮為32位,然后通過(guò)一些額外的計(jì)算來(lái)還原成對(duì)應(yīng)的64位地址。在大內(nèi)存服務(wù)場(chǎng)景下,帶來(lái)的收益會(huì)相當(dāng)可觀。然后在Hotspot內(nèi)部定義了四種模式分為四種 UnscaledNarrowOop(32位)、ZeroBasedNarrowOop(無(wú)基址的32位壓縮)、DisjointBaseNarrowOop(分離基址壓縮指針)、HeapBasedNarrowOop(指定基地址的32位壓縮,可以用-XX:HeapBaseMinAddress來(lái)指定)??梢钥吹轿覀兿到y(tǒng)默認(rèn)時(shí)會(huì)使用壓縮指針來(lái)完成空間的節(jié)省。

再來(lái)看看Oop shift amount,它實(shí)際上代表了內(nèi)存對(duì)象壓縮的偏移算法。在講這個(gè)之前要提一下當(dāng)前Hotspot虛擬機(jī)的一個(gè)重要基礎(chǔ):HotSpot虛擬機(jī)的內(nèi)存模型要求對(duì)象起始地址和對(duì)象大小必須是8字節(jié)的整數(shù)倍,換句話說(shuō)就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍,如果大小不足會(huì)強(qiáng)制填充(向上對(duì)齊)到8字節(jié)的整數(shù)倍。比如一個(gè)對(duì)象實(shí)際大小為12字節(jié),那么實(shí)際在Hotspot內(nèi)部存儲(chǔ)會(huì)被對(duì)齊為16字節(jié)。這個(gè)基礎(chǔ)的前提極為重要,虛擬機(jī)內(nèi)部的多處優(yōu)化都是因?yàn)橛羞@個(gè)內(nèi)存限制的前提才得以進(jìn)行。

我們繼續(xù)說(shuō)回Oop shift amount。它的值是3,虛擬機(jī)采用的對(duì)象壓縮方式是地址右移,所以它的根本意思就是內(nèi)存需要右移3位來(lái)存儲(chǔ),內(nèi)存真正使用時(shí)按照左移3位來(lái)使用。由于對(duì)象和內(nèi)存起始地址都是按照8字節(jié)對(duì)齊,所以內(nèi)存地址的后3位必然是0,該過(guò)程不會(huì)丟失任何信息。所以一個(gè)完整的內(nèi)存換算公式如下所示:

<narrow-oop-base> + (<narrow-oop> << 3) + <field-offset>

以上述截圖中的地址0x00000006c02bb8e8來(lái)舉例,它實(shí)際經(jīng)過(guò)壓縮后在虛擬機(jī)內(nèi)部存儲(chǔ)的地址是

0 + (0x00000006c02bb8e8<<<3) = 0xd805771d。

  • Narrow klass base: 0x0000000000000000
  • Narrow klass shift: 3

和之前類(lèi)似,只不過(guò)前面說(shuō)的是對(duì)象,這里說(shuō)的是klass(類(lèi)的元數(shù)據(jù))。和對(duì)象相比,元數(shù)據(jù)的基地址默認(rèn)從0開(kāi)始,存儲(chǔ)位置位于我們熟知的Metaspace空間內(nèi)。其他語(yǔ)義類(lèi)似,在此不做贅述。

  • Compressed class space size: 1073741824
  • Address: 0x00000007c0000000

繼續(xù)看細(xì)節(jié)。這里的 Compressed class space size 便是我們熟悉的Metaspace區(qū)域大大小。這里的1073741824換算過(guò)來(lái)就是1GB。這也是未配置該區(qū)域大小時(shí)Hotspot給出的默認(rèn)值。Address代表了Metaspace的大小是從內(nèi)存地址0x00000007c0000000開(kāi)始。

內(nèi)存使用明細(xì)

這里虛擬機(jī)描述的很清楚了,大多數(shù)信息不用解釋大家也看的明白。這里唯一需要提一下的是這個(gè)內(nèi)存邊界表達(dá),比如from區(qū)域的內(nèi)存被描述為[0x00000006c4450000, 0x00000006c4450000, 0x00000006c4cd0000)。它的含義是內(nèi)存區(qū)域從0x00000006c4450000開(kāi)始,到0x00000006c4cd0000結(jié)束,0x00000006c4450000是當(dāng)前已使用的標(biāo)記位置。大多數(shù)情況下,如果下次有內(nèi)存分配將會(huì)從0x00000006c4450000處開(kāi)始繼續(xù)分配。但需要注意的是,我的示例采用了cms垃圾回收,如果采用g1模型,那這塊內(nèi)容的輸出會(huì)稍有不同。

雖然這個(gè)圖描述的已經(jīng)足夠清晰,但是我還是畫(huà)一個(gè)圖來(lái)描述下上述內(nèi)存區(qū)域的具體使用情況,會(huì)更直觀:

圖片

GC輔助模型

圖片

這里的內(nèi)容依然和GC相關(guān),但對(duì)于問(wèn)題的排查相關(guān)性都不大,在這里只簡(jiǎn)單說(shuō)明下卡表(Card table byte_map):

卡表是一個(gè)標(biāo)準(zhǔn)的空間換時(shí)間的解決方案??ū硗ǔT? JVM 中實(shí)現(xiàn)為單字節(jié)數(shù)組。當(dāng)我們把 JVM 劃分成不同區(qū)域的時(shí)候,每一個(gè)區(qū)域(通常是4k字節(jié))就得到了一個(gè)標(biāo)志位。每一位為 1 的時(shí)候,表明這個(gè)區(qū)域?yàn)?dirty,持有新生代對(duì)象,否則這個(gè)區(qū)域不持有新生代對(duì)象。這樣,新生代垃圾收集器在收集live set 的時(shí)候,可以通過(guò)以下兩個(gè)操作:

  • 從棧/方法區(qū)出發(fā),進(jìn)入新生代掃描。
  • 從棧/方法區(qū)出發(fā),進(jìn)入老年代掃描,且只掃描 dirty 的區(qū)域,略過(guò)其他區(qū)域。 

polling page

圖片

這里的Polling page其實(shí)對(duì)我們的問(wèn)題排查并無(wú)多大意義,但確實(shí)是一個(gè)有意思的知識(shí)點(diǎn),它和我們熟知的JIT下的GC有關(guān)系。簡(jiǎn)單來(lái)說(shuō):


  • JIT下虛擬機(jī)在指定代碼位置完成安全點(diǎn)代碼的放置,代碼的內(nèi)容是讀取一小塊內(nèi)存(就是這里的polling page)
  • GC時(shí)設(shè)置這塊內(nèi)存不可讀
  • 其他線程代碼運(yùn)行到安全點(diǎn)發(fā)現(xiàn)代碼不可讀,操作系統(tǒng)層面拋出SIGSEGV信號(hào)
  • 虛擬機(jī)在啟動(dòng)是就完成SIGSEGV信號(hào)注冊(cè),來(lái)監(jiān)聽(tīng)本次SIGSEGV的內(nèi)存代碼是否是polling page位置
  • 如果是,進(jìn)程不終止,當(dāng)前線程進(jìn)入安全點(diǎn),線程掛起為后續(xù)GC做準(zhǔn)備

可以看到,一個(gè)很細(xì)小的功能,虛擬機(jī)也是花費(fèi)了很大精力,這里回頭可以總結(jié)一篇文章來(lái)深入聊聊。

CodeCache

圖片

關(guān)于CodeCache是什么這里就不多做贅述。這里稍微解釋下日志里出現(xiàn)的部分內(nèi)容。

首先日志給出了整個(gè)CodeCahe的使用情況,第一行給出了總大小(245760kb),已使用(1080kb),空閑(244679kb)。第二行用更直觀的方式給出了內(nèi)存上下界(bounds)。以上圖為例,這里的0x00007faadcbc0000代表了CodeCache內(nèi)存區(qū)域的下界,對(duì)應(yīng)的0x00007faaebbc0000則是內(nèi)存上界,0x00007faadce30000代表的是已使用內(nèi)存的邊界(可以理解這個(gè)邊界就是已使用的指針)。

CodeCache內(nèi)部本質(zhì)上是各種大小不一的內(nèi)存塊。有的是方法經(jīng)由JIT編譯后的代碼,有的是動(dòng)態(tài)生成的元數(shù)據(jù)等等。當(dāng)前系統(tǒng)一共有253個(gè)內(nèi)存塊,其中10個(gè)JIT編譯過(guò)的方法,以及160個(gè)adapters。這些adapters其實(shí)是虛擬機(jī)內(nèi)部在解釋模式和編譯模式下的一種適配,所有方法的默認(rèn)入口都是解釋模式,虛擬機(jī)內(nèi)部會(huì)判斷當(dāng)前方法是否已經(jīng)編譯過(guò)了,如果已編譯則基于adapter路由到已編譯的方法,否則繼續(xù)解釋執(zhí)行。整體如下圖所示。

圖片

事件分析

※ 編譯事件(Compilation events)

圖片

Compilation events 顯示了最近10次從Java字節(jié)碼編譯為機(jī)器碼的方法。這里有一個(gè)需要關(guān)注的點(diǎn):如果你的機(jī)器多次崩潰看到的編譯事件都相同,可能意味著JVM正在錯(cuò)誤的編譯當(dāng)前方法。如果不明白為何虛擬機(jī)會(huì)出現(xiàn)這種錯(cuò)誤,你可以嘗試重寫(xiě)方法將其簡(jiǎn)化、拆分等,這將有助于你避免相關(guān)的崩潰問(wèn)題?;蛘咭部梢岳锰摂M機(jī)提供的命令來(lái)禁止指定方法的編譯,比如:

-XX:CompileCommand="exclude java.util.ArrayList::add"

GC歷史事件(GC Heap History)

圖片

這里列舉出了系統(tǒng)在崩潰時(shí)出現(xiàn)的近10次GC。這里的堆的輸出模型和之前章節(jié)講述的模型類(lèi)似,在此就不再做過(guò)多說(shuō)明。

※ 逆優(yōu)化事件(Deoptimization events)

圖片

關(guān)于逆優(yōu)化簡(jiǎn)而言之,就是JIT基于特定條件和假設(shè)完成代碼編譯后,在一些場(chǎng)景下,虛擬機(jī)突然發(fā)現(xiàn)當(dāng)前的特定條件和假設(shè)不滿足,當(dāng)前編譯好的方法如果繼續(xù)執(zhí)行會(huì)發(fā)生一些意料之外的情況,此時(shí)虛擬機(jī)就會(huì)觸發(fā)自動(dòng)逆優(yōu)化,針對(duì)已經(jīng)編譯好的方法做特定處理,確保整體運(yùn)行準(zhǔn)確。

我們來(lái)簡(jiǎn)單基于日志分析下當(dāng)前的事件內(nèi)容:

首先能看到的是觸發(fā)本次逆優(yōu)化的原因(reason),在Hotspot虛擬機(jī)內(nèi)部可能的共有32種。限于篇幅我們不可能全部拿出來(lái)在這里進(jìn)行說(shuō)明,我們可以選一些有代表性的:

  • Reason_null_check(虛擬機(jī)看到了預(yù)期之外的null值)
  • Reason_range_check(虛擬機(jī)看到了預(yù)期之外的數(shù)組下標(biāo))
  • Reason_class_check(虛擬機(jī)看到了預(yù)期之外的對(duì)象類(lèi)型)
  • Reason_unstable_if(分支預(yù)測(cè)失效,比如一個(gè)if始終返回false,突然一次返回了true)

其次我們看到的是Hotspot對(duì)于這些觸發(fā)的場(chǎng)景的針對(duì)動(dòng)作(action),一共5種:

  • Action_none(直接切換到之前的解釋模式執(zhí)行方法,保留之前JIT編譯好的方法)
  • Action_maybe_recompile(JIT重新編譯當(dāng)前方法)
  • Action_reinterpret(臨時(shí)切回解釋模式執(zhí)行方法,廢棄之前JIT編譯的方法然后內(nèi)部再?zèng)Q策是否需要重新編譯)
  • Action_make_not_entrant(JIT立即重新編譯且廢棄之前JIT編譯好的方法,下次調(diào)用就會(huì)使用新的編譯結(jié)果)
  • Action_make_not_compilable(直接切換到之前的解釋模式執(zhí)行方法,廢棄之前JIT編譯好的方法且不再編譯)

可以看到虛擬機(jī)基于不同的逆優(yōu)化給出了各種解決方案,確保程序的正常運(yùn)行。

最后,method給出了具體涉及本次逆優(yōu)化的方法,如上圖所示的"java.util.Matcher.match()" 。

和前文所說(shuō)的編譯事件(Compilation events)類(lèi)似,如果你的機(jī)器多次崩潰看到的逆優(yōu)化事件都相同,可能意味著JVM正在錯(cuò)誤的逆優(yōu)化當(dāng)前方法。對(duì)應(yīng)的解決方案也是相同的:如果不明白為何虛擬機(jī)會(huì)出現(xiàn)這種錯(cuò)誤,你可以嘗試重寫(xiě)方法將其簡(jiǎn)化、拆分或者使用指令-XX:CompileCommand。 

※ 類(lèi)重定義事件(Classes redefined)

圖片

這里列舉出了當(dāng)前Java進(jìn)程中類(lèi)被重定義(redefined)的相關(guān)信息。上述時(shí)間可以簡(jiǎn)單的理解為:Crash這個(gè)類(lèi)在應(yīng)用啟動(dòng)的第18.707秒被重新定義,當(dāng)前類(lèi)總計(jì)被重定義1次。這里有個(gè)小細(xì)節(jié),java.lang.Class中會(huì)保存一個(gè)類(lèi)被重定義的次數(shù)到它的實(shí)例元數(shù)據(jù)中,可見(jiàn)java.lang.Class#classRedefinedCount

上面說(shuō)的重定義是JDK5中引入的Instrument接口提供的一種在運(yùn)行時(shí)修改Java類(lèi)文件的機(jī)制,以實(shí)現(xiàn)類(lèi)的重新定義。我們熟知的Arhtas、Byteman、Greys、Skywalking、Pinpoint等都是基于Instrument機(jī)制來(lái)完成了各種強(qiáng)大的監(jiān)控功能。但盡管Instrument接口當(dāng)前已經(jīng)被如此的廣泛運(yùn)用,但它仍然存在一些潛在的問(wèn)題和壞處:

  • 難以調(diào)試:由于動(dòng)態(tài)修改類(lèi)文件會(huì)改變程序的行為,可能會(huì)導(dǎo)致調(diào)試?yán)щy,不利于排查問(wèn)題。
  • 兼容性問(wèn)題:重新定義類(lèi)可能會(huì)影響現(xiàn)有代碼的兼容性,導(dǎo)致原有功能無(wú)法正常運(yùn)行。
  • 不穩(wěn)定性:對(duì)類(lèi)進(jìn)行重新定義可能會(huì)導(dǎo)致系統(tǒng)不穩(wěn)定,產(chǎn)生意外的行為(如系統(tǒng)崩潰)。

因此,盡管Instrument接口提供了強(qiáng)大的功能,但大多時(shí)候我們建議在生產(chǎn)環(huán)境使用時(shí)還需要慎重考慮,確保當(dāng)前使用的組件足夠成熟,否則帶來(lái)的后果會(huì)非常嚴(yán)重。以我們常見(jiàn)的Arthas為例,導(dǎo)致線上應(yīng)用崩潰的問(wèn)題很常見(jiàn):


內(nèi)部異常事件(Internal exceptions)

圖片

這里透出了虛擬機(jī)內(nèi)部的異常事件,這些事件包括了虛擬機(jī)內(nèi)部的各種異常諸如生成OopMap的異常、jni調(diào)用相關(guān)的異常、虛擬機(jī)內(nèi)部執(zhí)行的逆優(yōu)化問(wèn)題、JIT相關(guān)的NPE等等等等。需要特別注意“內(nèi)部”這兩個(gè)字。這意味著我們業(yè)務(wù)代碼層面的異常拋出是不會(huì)在這里出現(xiàn)的,所以這里我們可暫時(shí)無(wú)需過(guò)多關(guān)注。

通用事件(Events)

圖片

這里的日志輸出稍微有點(diǎn)迷惑,初看會(huì)給人一種所有事件匯總在一起的感覺(jué)。但這個(gè)區(qū)域?qū)嶋H的含義是:A log for generic messages that aren't well categorized。即通用未分類(lèi)的事件內(nèi)容都在這里,是個(gè)事件的大雜燴。包括但不限于如下場(chǎng)景:

  • 類(lèi)加載(如上圖所示)
  • 新增線程
  • GC后的內(nèi)存整理
  • 虛擬機(jī)線程執(zhí)行重要的操作(VMOperation)
  • JIT方法廢棄的內(nèi)存清理
  • ...

總之,這里依然是一個(gè)可以用來(lái)了解虛擬機(jī)在做什么事情,但是大多數(shù)情況對(duì)于我們問(wèn)題的排查起不到太大作用的信息區(qū)域。

內(nèi)存映射明細(xì)

圖片

這里其實(shí)就是當(dāng)前進(jìn)程在臨"死"前,pmap -d pid命令輸出的結(jié)果。我們關(guān)注一些重要的列:

  • [第1列]顯示內(nèi)存區(qū)域的起始地址。每個(gè)區(qū)域代表進(jìn)程的不同內(nèi)存映射。
  • [第2列]內(nèi)存映射區(qū)域的訪問(wèn)權(quán)限。這里的p代表的是(private)
  • [第5列]指實(shí)際在內(nèi)存中占用的字節(jié)數(shù)。
  • [第6列]顯示內(nèi)存區(qū)域的映射信息,通常包括文件名或其他標(biāo)識(shí)符。如果是匿名內(nèi)存(未映射到文件),則會(huì)顯示類(lèi)似 [heap]、[stack]、[anon] 這樣的標(biāo)識(shí)。

但在系統(tǒng)崩潰的情況下,當(dāng)前內(nèi)存映射明細(xì)大多時(shí)候都幫不上忙。但在排查內(nèi)存泄漏問(wèn)題時(shí),內(nèi)存映射的輸出還是很有用(如上所示利用pmap命令可以實(shí)時(shí)輸出),可以通過(guò)輸出看到是否有大段的連續(xù)內(nèi)存占用、過(guò)多的匿名內(nèi)存塊、異常的文件映射等等。

啟動(dòng)參數(shù)&環(huán)境變量

圖片

Java進(jìn)程的啟動(dòng)參數(shù)以及系統(tǒng)環(huán)境變量,這里不過(guò)多展開(kāi)。

信號(hào)處理

圖片

在信號(hào)量概要一節(jié)里有簡(jiǎn)單聊過(guò),虛擬機(jī)對(duì)于SIGSEGV信號(hào)注冊(cè)了信號(hào)回調(diào)函數(shù),這意味著當(dāng)發(fā)生SIGSEGV內(nèi)存異常方位,Java進(jìn)程未必會(huì)崩潰結(jié)束。但實(shí)際上,虛擬機(jī)對(duì)于很多信號(hào)也注冊(cè)了回調(diào)函數(shù),他們同樣的會(huì)在程序崩潰前再檢查一把是否需要?dú)⒌暨M(jìn)程,從而確保系統(tǒng)的整體穩(wěn)定性。

系統(tǒng)信息

系統(tǒng)信息概要

圖片

這塊內(nèi)容主要描述了操作系統(tǒng)的基本信息,包括系統(tǒng)相關(guān)信息,比如主機(jī)名、內(nèi)核版本號(hào)、硬件架構(gòu)等等。 輸出的內(nèi)容基本可以和下列命令的含義相同。


  • cat /etc/os-release
  • uname -a
  • ldd --version
  • ulimit -a
  • w

如果是公司的機(jī)器的話,這塊不必過(guò)多關(guān)心。大多數(shù)情況出現(xiàn)問(wèn)題我們要關(guān)注的是差異性問(wèn)題,而公司一般會(huì)配有對(duì)應(yīng)的SRE來(lái)管理應(yīng)用配置,相關(guān)的配置也會(huì)有基線,相同應(yīng)用的機(jī)器配置均會(huì)相同。 

系統(tǒng)內(nèi)存概括


圖片

這些內(nèi)容其實(shí)就是系統(tǒng)崩潰瞬時(shí)cat /proc/meminfo的輸出明細(xì)。/proc/meminfo是了解Linux系統(tǒng)內(nèi)存使用狀況的主要接口(需要注意的是在當(dāng)前的MacOS開(kāi)發(fā)環(huán)境并不適用),我們最常用的free、vmstat等命令就是通過(guò)它獲取數(shù)據(jù)的 ,但一般來(lái)說(shuō)在排查一些內(nèi)存泄漏問(wèn)題,利用meminfo的輸出還是非常有幫助,但是遇到進(jìn)程崩潰這種情況其實(shí)有幫助的信息不多,這里不做過(guò)多介紹。 

CPU信息概況

圖片

這里其實(shí)就是命令cat /proc/cpuinfo的輸出明細(xì),給出了當(dāng)前機(jī)器的CPU型號(hào)、CPU制造商、產(chǎn)品代號(hào)、主頻、二級(jí)緩存、浮點(diǎn)運(yùn)算單元等信息,這些對(duì)于系統(tǒng)崩潰問(wèn)題排查一般來(lái)說(shuō)幫助不大,可以暫不關(guān)注。 

其他信息

圖片

日志的結(jié)尾給出了內(nèi)存,虛擬機(jī)版本等內(nèi)容,但大多和之前重合,這里有一個(gè)有用的信息是elapsed time,它反映了從應(yīng)用啟動(dòng)到崩潰一共耗時(shí)多久(我的例子里直接啟動(dòng)就主動(dòng)讓虛擬機(jī)立刻崩潰了所以這里是0)。

五、core文件

好了,到這里我們介紹完了Java虛擬機(jī)提供的錯(cuò)誤文件。但當(dāng)Java虛擬機(jī)如上描述崩潰時(shí),會(huì)同時(shí)生成在當(dāng)前目錄生成一個(gè)叫core的文件。假設(shè)我的進(jìn)程崩潰之前進(jìn)程id為29296,那么core文件的名稱默認(rèn)會(huì)是core.29296(這里會(huì)根據(jù)用戶的個(gè)人配置不同文件名稱和路徑可能稍有差異)。這個(gè)core文件里記錄了程序在崩潰時(shí)的內(nèi)存狀態(tài),在有些時(shí)候崩潰文件不能給我們進(jìn)一步信息的情況下,可以進(jìn)一步的打開(kāi)我們的思路,發(fā)現(xiàn)一些意想不到的結(jié)果。

分析core文件最常見(jiàn)的工具便是gdb。使用gdb打開(kāi)core文件后常用的命令可以簡(jiǎn)要介紹如下:

問(wèn)題調(diào)用棧

圖片

bt(backtrace)命令是 GDB 中用于查看當(dāng)前調(diào)用堆棧的重要工具,可以說(shuō)是問(wèn)題排查的首要命令。通過(guò)當(dāng)前命令,開(kāi)發(fā)者可以顯示程序在崩潰或暫停時(shí)的函數(shù)調(diào)用歷史,了解執(zhí)行狀態(tài)和調(diào)用關(guān)系。輸出包含每個(gè)函數(shù)的名稱、源文件及行號(hào),以及參數(shù)信息,且從最近的調(diào)用開(kāi)始逐步追溯。該命令還支持限制輸出的幀數(shù)(例如 bt 5

幀快照

圖片

當(dāng)鎖定到第7個(gè)函數(shù)幀時(shí),我們可以利用命令frame 7來(lái)定位到這里。這表示我們已經(jīng)切換到編號(hào)為7的調(diào)用幀。調(diào)用幀代表函數(shù)調(diào)用的上下文,我們后續(xù)可以進(jìn)一步看到函數(shù)的參數(shù)、局部變量和調(diào)用位置等信息。進(jìn)一步的我們輸入i r(info registers的縮寫(xiě)),代表我們需要查看此刻寄存器中的內(nèi)容。自然的,我們會(huì)發(fā)現(xiàn)這里和之前Java虛擬機(jī)給出的錯(cuò)誤日志的寄存器的每個(gè)值都相同。

為了更清楚的還原出問(wèn)題的上下文,我們可以進(jìn)一步探究:

圖片

  • 第一行查看當(dāng)前指令正在做什么操作,發(fā)現(xiàn)這和我們前文【指令上下文】中推出的結(jié)果是相同的!(這里需要注意匯編的語(yǔ)法有2種-INTEL和AT&T。gdb當(dāng)前這里指AT&T語(yǔ)法,和上文中的格式稍有不同。)
  • 第二行看下當(dāng)前r12寄存器的地址(當(dāng)然最開(kāi)始的截圖已經(jīng)有了)。
  • 第三行嘗試范圍r12寄存器的地址對(duì)應(yīng)的值,被提示內(nèi)存無(wú)法訪問(wèn)(和最開(kāi)始的SIGSEGV對(duì)應(yīng))。

匯編代碼還原

圖片

對(duì)于更為復(fù)雜的問(wèn)題,我們可能要深入到對(duì)應(yīng)的C++層面的方法去深入探究(比如JIT編譯出了bug,編譯出了一個(gè)有問(wèn)題的方法導(dǎo)致程序崩潰之類(lèi))。此時(shí)我們需要深入到指定方法的機(jī)器碼層面,此時(shí)基于gdb的調(diào)試可以利用disas命令完整還原出當(dāng)前的匯編執(zhí)行過(guò)程(如果我們當(dāng)前的幀左邊有一個(gè)箭頭“=>”來(lái)表示)。

內(nèi)存映射

圖片

最后介紹的指令是i proc m(是info proc mappings的縮寫(xiě))。這里輸出含義跟上面提到的【內(nèi)存映射明細(xì)】 類(lèi)似。就不過(guò)多贅述了,總之還是告訴了開(kāi)發(fā)者內(nèi)存的上下范圍界及其對(duì)應(yīng)映射的文件在哪里。

六、一些經(jīng)驗(yàn)

虛擬機(jī)崩潰的原因分類(lèi) 

雖然系統(tǒng)崩潰的原因千奇百怪,但是大多數(shù)情況下,我們都可以將錯(cuò)誤原因都可分為2種情況:


  • 內(nèi)存非法訪問(wèn)
  • 物理內(nèi)存不足


內(nèi)存非法訪問(wèn)最為常見(jiàn),如本文中介紹的例子就是這樣一種嚴(yán)重的錯(cuò)誤情況。其次還有一種場(chǎng)景比較重要,那就是物理內(nèi)存不足。當(dāng)系統(tǒng)物理內(nèi)存耗盡時(shí)它的錯(cuò)誤原因大概長(zhǎng)相如下圖所示:

圖片

物理內(nèi)存不足系統(tǒng)崩潰虛擬機(jī)給出的原因及其解決方案

物理內(nèi)存的崩潰類(lèi)型相比內(nèi)存非法訪問(wèn)會(huì)更友好,它直接給出了研發(fā)可能的解決方案。

留意JNI

JNI(Java Native Interface)是Java與C語(yǔ)言之間進(jìn)行交互的重要機(jī)制,但進(jìn)行有效的JNI編程并不簡(jiǎn)單。開(kāi)發(fā)者必須深入理解Java虛擬機(jī)的內(nèi)部工作原理,以避免潛在的錯(cuò)誤和問(wèn)題。尤其是對(duì)于那些主要從事C語(yǔ)言開(kāi)發(fā)的人員來(lái)說(shuō),缺乏對(duì)JVM原理的了解可能會(huì)導(dǎo)致程序在運(yùn)行時(shí)出現(xiàn)意外的崩潰。

在之前的章節(jié)中,我們提到了NullPointerException的機(jī)制。其本質(zhì)是虛擬機(jī)攔截SIGSEGV信號(hào)并拋出異常而不終止進(jìn)程。然而,如果第三方C語(yǔ)言組件在沒(méi)有充分理解Java虛擬機(jī)的情況下,錯(cuò)誤地注冊(cè)了SIGSEGV信號(hào)處理回調(diào)函數(shù),那么在Java進(jìn)程出現(xiàn)NullPointerException時(shí),系統(tǒng)會(huì)因?yàn)镾IGSEGV的回調(diào)被覆蓋導(dǎo)致進(jìn)程崩潰。因此,在使用JNI時(shí)(包括應(yīng)用依賴的第三方JNI組件),開(kāi)發(fā)者必須特別注意,確保對(duì)JVM的工作原理有充分的認(rèn)識(shí),以維護(hù)系統(tǒng)的穩(wěn)定性和可靠性。

敢于懷疑

Java虛擬機(jī)自身bug雖然相對(duì)概率小,但是在出現(xiàn)極端問(wèn)題時(shí)也要適當(dāng)考慮,畢竟大家都是人,寫(xiě)個(gè)bug么在所難免。避免持續(xù)性的陷入bug自證的死結(jié)中??梢詠?lái)https://bugs.openjdk.org/projects/JDK/issues/ 利用你崩潰時(shí)的關(guān)鍵字嘗試找找是否已經(jīng)有人提出過(guò)類(lèi)似的系統(tǒng)崩潰問(wèn)題,有時(shí)候可能會(huì)事半功倍。 

七、寫(xiě)在最后

在深入探討JVM崩潰的各種細(xì)節(jié)及其解決方案后,我們可以看到,理解和掌握J(rèn)VM的內(nèi)在機(jī)制不僅對(duì)開(kāi)發(fā)者至關(guān)重要,也對(duì)整個(gè)應(yīng)用的穩(wěn)定性和性能有著深遠(yuǎn)的影響。通過(guò)合理的配置、監(jiān)控工具的使用和適當(dāng)?shù)恼{(diào)優(yōu)策略,我們可以有效地降低JVM崩潰的風(fēng)險(xiǎn)。

然而,面對(duì)復(fù)雜的系統(tǒng),完全避免所有崩潰是困難的。關(guān)鍵在于具備快速診斷和恢復(fù)的能力,這要求我們?cè)谌粘i_(kāi)發(fā)和運(yùn)維中,持續(xù)關(guān)注系統(tǒng)的健康狀態(tài),并及時(shí)進(jìn)行故障排查。希望本文能夠?yàn)槟阍贘VM的使用和維護(hù)上提供一些有價(jià)值的見(jiàn)解與幫助。 

責(zé)任編輯:龐桂玉 來(lái)源: 得物技術(shù)
相關(guān)推薦

2021-07-28 21:49:01

JVM對(duì)象內(nèi)存

2015-08-20 13:43:17

NFV網(wǎng)絡(luò)功能虛擬化

2010-05-26 19:12:41

SVN沖突

2010-05-17 09:13:35

2014-03-12 11:11:39

Storage vMo虛擬機(jī)

2021-06-07 08:18:12

云計(jì)算云端阿里云

2009-09-15 15:34:33

Google Fast

2023-11-02 09:55:40

2016-04-06 09:27:10

runtime解密學(xué)習(xí)

2010-05-11 10:19:17

VMforceJava云計(jì)算

2009-06-01 09:04:44

Google WaveWeb

2018-03-01 09:33:05

軟件定義存儲(chǔ)

2015-09-06 10:54:29

HTTP網(wǎng)絡(luò)協(xié)議

2015-09-08 10:06:15

2010-09-17 14:57:34

JAVA數(shù)據(jù)類(lèi)型

2010-06-17 10:53:25

桌面虛擬化

2011-08-02 08:59:53

2017-10-16 05:56:00

2021-08-11 09:01:48

智能指針Box

2021-05-25 09:01:21

Linux命令Bash histor
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)