JVM相關 - StackOverflowError 與 OutOfMemoryError

本文基於 Java 15html

StackOverflowError 與 OutOfMemoryError 是兩個老生常談的 Java 錯誤。Java 中的虛擬機錯誤 VirtualMachineError 包括如下四種:java

image

咱們比較關心的就是 StackOverflowError 與 OutOfMemoryError,剩下的 InternalError 通常是內部使用錯誤,UnknownError 是虛擬機發生未知異常,這兩種咱們這裏不討論。react

虛擬機規範中的 StackOverflowError 與 OutOfMemoryError

參考 Java 虛擬機規範官方文檔:Run-Time Data Areas,能夠知道,在以下狀況下,會拋出這兩種錯誤:linux

  • 當某次線程運行計算時,須要佔用的 Java 虛擬機棧(Java Virtual Machine Stack)大小,也就是 Java 線程棧大小,超過規定大小時,拋出 StackOverflowError
  • 若是 Java 虛擬機棧大小能夠動態擴容,發生擴容時發現內存不足,或者新建Java 虛擬機棧時發現內存不足,拋出 OutOfMemoryError
  • 當所須要的堆(heap)內存大小不足時,拋出 OutOfMemoryError
  • 當方法區(Method Area)大小不夠分配時,拋出 OutOfMemoryError
  • 當建立一個類或者接口時,運行時常量區剩餘大小不夠時,拋出 OutOfMemoryError
  • 本地方法棧(Native Method Stack)大小不足時,拋出 StackOverflowError
  • 本地方法棧(Native Method Stack)擴容時發現內存不足,或者新建本地方法棧發現內存不足,拋出 OutOfMemoryError

Hotspot JVM 的實現

爲了進一步搞清楚 StackOverflowError 與 OutOfMemoryError,咱們來看具體實現。通常的 JVM 採用的都是官網的 HotSpot JVM,咱們這裏就用 Hotspot JVM 的實現來講明。git

JVM 內存包括什麼

咱們通常經過兩個工具 pmap 還有 jcmd 中的 VM.native_memory 命令去查看 Java 進程內存佔用,因爲 pmap 命令有點複雜並且不少內存映射是 anon 的,這裏採用 jcmd 中的 VM.native_memory 命令,去看一下 JVM 內存的每一部分。須要指出的一點是,github

若是想了解詳細的 Native Memory Tracking,請參考個人另外一篇文章JVM相關 - JVM 內存佔用與分析vim

Native Memory Tracking:

Total: reserved=6308603KB, committed=4822083KB
-                 Java Heap (reserved=4194304KB, committed=4194304KB)
                            (mmap: reserved=4194304KB, committed=4194304KB) 
 
-                     Class (reserved=1161041KB, committed=126673KB)
                            (classes #21662)
                            (  instance classes #20542, array classes #1120)
                            (malloc=3921KB #64030) 
                            (mmap: reserved=1157120KB, committed=122752KB) 
                            (  Metadata:   )
                            (    reserved=108544KB, committed=107520KB)
                            (    used=105411KB)
                            (    free=2109KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=15232KB)
                            (    used=13918KB)
                            (    free=1314KB)
                            (    waste=0KB =0.00%)
 
-                    Thread (reserved=355251KB, committed=86023KB)
                            (thread #673)
                            (stack: reserved=353372KB, committed=84144KB)
                            (malloc=1090KB #4039) 
                            (arena=789KB #1344)
 
-                      Code (reserved=252395KB, committed=69471KB)
                            (malloc=4707KB #17917) 
                            (mmap: reserved=247688KB, committed=64764KB) 
 
-                        GC (reserved=199635KB, committed=199635KB)
                            (malloc=11079KB #29639) 
                            (mmap: reserved=188556KB, committed=188556KB) 
 
-                  Compiler (reserved=2605KB, committed=2605KB)
                            (malloc=2474KB #2357) 
                            (arena=131KB #5)
 
-                  Internal (reserved=3643KB, committed=3643KB)
                            (malloc=3611KB #8683) 
                            (mmap: reserved=32KB, committed=32KB) 
 
-                     Other (reserved=67891KB, committed=67891KB)
                            (malloc=67891KB #2859) 
 
-                    Symbol (reserved=26220KB, committed=26220KB)
                            (malloc=22664KB #292684) 
                            (arena=3556KB #1)
 
-    Native Memory Tracking (reserved=7616KB, committed=7616KB)
                            (malloc=585KB #8238) 
                            (tracking overhead=7031KB)
 
-               Arena Chunk (reserved=10911KB, committed=10911KB)
                            (malloc=10911KB) 
 
-                   Tracing (reserved=25937KB, committed=25937KB)
                            (malloc=25937KB #8666) 
 
-                   Logging (reserved=5KB, committed=5KB)
                            (malloc=5KB #196) 
 
-                 Arguments (reserved=18KB, committed=18KB)
                            (malloc=18KB #486) 
 
-                    Module (reserved=532KB, committed=532KB)
                            (malloc=532KB #3579) 
 
-              Synchronizer (reserved=591KB, committed=591KB)
                            (malloc=591KB #4777) 
 
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB)

這裏的 mmapmalloc 是兩種不一樣的內存申請分配方式,例如:windows

Internal (reserved=3643KB, committed=3643KB)
                            (malloc=3611KB #8683) 
                            (mmap: reserved=32KB, committed=32KB)

表明 Internal 一共佔用 3643KB,其中3611KB是經過 malloc 方式,32KB 是經過 mmap 方式。 arena 是經過 malloc 方式分配的內存可是代碼執行完並不釋放,放入 arena chunk 中以後還會繼續使用,參考:MallocInternals數組

能夠看出,Java 進程內存包括:服務器

  • Java Heap: 堆內存,即-Xmx限制的最大堆大小的內存。
  • Class:加載的類與方法信息,其實就是 metaspace,包含兩部分: 一是 metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 class space,被-XX:CompressedClassSpaceSize限制最大大小
  • Thread:線程與線程棧佔用內存,每一個線程棧佔用大小受-Xss限制,可是總大小沒有限制。
  • Code:JIT 即時編譯後(C1 C2 編譯器優化)的代碼佔用內存,受 -XX:ReservedCodeCacheSize限制
  • GC:垃圾回收佔用內存,例如垃圾回收須要的 CardTable,標記數,區域劃分記錄,還有標記 GC Root 等等,都須要內存。這個不受限制,通常不會很大的。
  • Compiler:C1 C2 編譯器自己的代碼和標記佔用的內存,這個不受限制,通常不會很大的
  • Internal:命令行解析,JVMTI 使用的內存,這個不受限制,通常不會很大的
  • Symbol: 常量池佔用的大小,字符串常量池受-XX:StringTableSize 個數限制,總內存大小不受限制
  • Native Memory Tracking:內存採集自己佔用的內存大小,若是沒有打開採集(那就看不到這個了,哈哈),就不會佔用,這個不受限制,通常不會很大的
  • Arena Chunk:全部經過 arena 方式分配的內存,這個不受限制,通常不會很大的
  • Tracing:全部採集佔用的內存,若是開啓了 JFR 則主要是 JFR 佔用的內存。這個不受限制,通常不會很大的
  • Logging,Arguments,Module,Synchronizer,Safepoint,Other,這些通常咱們不會關心。

除了 Native Memory Tracking 記錄的內存使用,還有兩種內存 Native Memory Tracking 沒有記錄,那就是:

各類 StackOverflowError 與 OutOfMemoryError 場景以及定位方式

1. StackOverflowError

調用棧過深,致使線程棧佔用大小超過-Xss(或者是-XX:ThreadStackSize)的限制,若是沒指定-Xss,則根據不一樣系統肯定默認最大大小。

肯定默認大小的代碼請參考:

總結起來就是,32 位的系統通常是 512k,64 位的是 1024k

通常報這個錯都是由於遞歸死循環,或者調用棧真的太深而線程棧大小不足,好比那種回調背壓模型的框架,netty + reactor 這種,通常線程棧須要調大一點。

2. OutOfMemoryError: Java heap space

堆內存不夠用,沒法分配更多內存,就會拋出這個異常。通常這種狀況發生後,須要查看 heap dump,線上應用通常加上-XX: +HeapDumpOnOutOfMemoryErrorOutOfMemoryError發生的時候,進行 heap dump,以後進行分析。

heap dump 查看工具通常經過 Memory Analyzer (MAT)

image

3. OutOfMemoryError: unable to create native thread

這個在建立太多的線程,超過系統配置的極限。如Linux默認容許單個進程能夠建立的線程數是1024個。

通常報這個錯首先考慮不要建立那麼多線程,線程池化並池子儘可能同業務複用。若是實在要建立那麼多線程,則考慮修改服務器配置:

//查看限制個數
ulimit -u

//編輯修改
vim /etc/security/limits.d/90-nproc.conf

4. OutOfMemoryError: GC Overhead limit exceeded

默認狀況下,並非等堆內存耗盡,纔會報 OutOfMemoryError,而是若是 JVM 以爲 GC 效率不高,也會報這個錯誤。

那麼怎麼評價 GC 效率不高呢?來看下源碼: 呢?來看下源碼gcOverheadChecker.cpp

void GCOverheadChecker::check_gc_overhead_limit(GCOverheadTester* time_overhead,
                                                GCOverheadTester* space_overhead,
                                                bool is_full_gc,
                                                GCCause::Cause gc_cause,
                                                SoftRefPolicy* soft_ref_policy) {

  // 忽略顯式gc命令,好比System.gc(),或者經過JVMTI命令的gc,或者經過jcmd命令的gc
  if (GCCause::is_user_requested_gc(gc_cause) ||
      GCCause::is_serviceability_requested_gc(gc_cause)) {
    return;
  }

  bool print_gc_overhead_limit_would_be_exceeded = false;
  if (is_full_gc) {
    //若是gc時間過長,而且gc回收的空間仍是很少
    //gc時間佔用98%以上爲gc時間過長,能夠經過 -XX:GCTimeLimit= 配置,參考gc_globals.hpp: GCTimeLimit
    //回收空間小於2%爲gc回收空間很少,能夠經過  -XX:GCHeapFreeLimit= 配置,參考gc_globals.hpp: GCHeapFreeLimit
    if (time_overhead->is_exceeded() && space_overhead->is_exceeded()) {
      _gc_overhead_limit_count++;
      //若是UseGCOverheadLimit這個狀態位爲開啓
      //默認狀況下,是開啓的,能夠經過啓動參數-XX:-UseGCOverheadLimit關閉,參考:gc_globals.hpp: UseGCOverheadLimit
      if (UseGCOverheadLimit) {
        //若是超過規定次數,這個次數默認不可配置,必須開啓develop編譯jdk才能配置,參考gc_globals.hpp: GCOverheadLimitThreshold
        if (_gc_overhead_limit_count >= GCOverheadLimitThreshold){
          //設置狀態位,準備拋出OOM
          set_gc_overhead_limit_exceeded(true);
          //清空計數
          reset_gc_overhead_limit_count();
        } else {
          //若是還沒到達次數,可是也快到達的時候,清空全部的軟引用
          bool near_limit = gc_overhead_limit_near();
          if (near_limit) {
            soft_ref_policy->set_should_clear_all_soft_refs(true);
            log_trace(gc, ergo)("Nearing GC overhead limit, will be clearing all SoftReference");
          }
        }
      }
      //須要打印日誌,提示GC效率不高
      print_gc_overhead_limit_would_be_exceeded = true;

    } else {
      // Did not exceed overhead limits
      reset_gc_overhead_limit_count();
    }
  }

  if (UseGCOverheadLimit) {
    if (gc_overhead_limit_exceeded()) {
      log_trace(gc, ergo)("GC is exceeding overhead limit of " UINTX_FORMAT "%%", GCTimeLimit);
      reset_gc_overhead_limit_count();
    } else if (print_gc_overhead_limit_would_be_exceeded) {
      assert(_gc_overhead_limit_count > 0, "Should not be printing");
      log_trace(gc, ergo)("GC would exceed overhead limit of " UINTX_FORMAT "%% %d consecutive time(s)",
                          GCTimeLimit, _gc_overhead_limit_count);
    }
  }
}

默認配置:gc_globals.hpp

product(bool, UseGCOverheadLimit, true,                                   \
          "Use policy to limit of proportion of time spent in GC "          \
          "before an OutOfMemory error is thrown")                          \
                                                                            \
product(uintx, GCTimeLimit, 98,                                           \
      "Limit of the proportion of time spent in GC before "             \
      "an OutOfMemoryError is thrown (used with GCHeapFreeLimit)")      \
      range(0, 100)                                                     \
                                                                        \
product(uintx, GCHeapFreeLimit, 2,                                        \
      "Minimum percentage of free space after a full GC before an "     \
      "OutOfMemoryError is thrown (used with GCTimeLimit)")             \
      range(0, 100)                                                     \
                                                                        \
develop(uintx, GCOverheadLimitThreshold, 5,                               \
      "Number of consecutive collections before gc time limit fires")   \
      range(1, max_uintx)

能夠總結出:默認狀況下,啓用了 UseGCOverheadLimit,連續 5 次,碰到 GC 時間佔比超過 98%,GC 回收的內存不足 2% 時,會拋出這個異常。

5. OutOfMemoryError: direct memory

這個是向系統申請直接內存時,若是系統可用內存不足,就會拋出這個異常,對應的源代碼Bits.java

static void reserveMemory(long size, int cap) {
    synchronized (Bits.class) {
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // -XX:MaxDirectMemorySize limits the total capacity rather than the
        // actual memory usage, which will differ when buffers are page
        // aligned.
        if (cap <= maxMemory - totalCapacity) {
            reservedMemory += size;
            totalCapacity += cap;
            count++;
            return;
        }
    }
    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        if (totalCapacity + cap > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
        totalCapacity += cap;
        count++;
    }
}

在 DirectByteBuffer 中,首先向 Bits 類申請額度,Bits 類有一個全局的 totalCapacity 變量,記錄着所有 DirectByteBuffer 的總大小,每次申請,都先看看是否超限,堆外內存的限額默認與堆內內存(由 -Xmx 設定)相仿,可用 -XX:MaxDirectMemorySize 從新設定。

若是不指定,該參數的默認值爲 Xmx 的值減去1個 Survior 區的值。 如設置啓動參數 -Xmx20M -Xmn10M -XX:SurvivorRatio=8,那麼申請 20M-1M=19M 的DirectMemory 若是已經超限,會主動執行 Sytem.gc(),期待能主動回收一點堆外內存。System.gc() 會觸發一個 full gc,固然前提是你沒有顯示的設置 -XX:+DisableExplicitGC 來禁用顯式GC。而且你須要知道,調用 System.gc() 並不可以保證 full gc 立刻就能被執行。而後休眠一百毫秒,看看 totalCapacity 降下來沒有,若是內存仍是不足,就拋出 OOM 異常。若是額度被批准,就調用大名鼎鼎的sun.misc.Unsafe去分配內存,返回內存基地址

在發生這種異常時,通常經過 JMX 的java.nio.BufferPool.direct裏面的屬性去監控直接內存的變化以及使用(其實就是 BufferPoolMXBean ),來定位問題。

image

6. OutOfMemoryError: map failed

這個是 File MMAP(文件映射內存)時,若是系統內存不足,就會拋出這個異常,對應的源代碼是:

以 Linux 爲例:

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                                     jint prot, jlong off, jlong len)
{
    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
        protections = PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    }
    //調用mmap
    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */
    //內存不足時,拋出OutOfMemoryError
    if (mapAddress == MAP_FAILED) {
        if (errno == ENOMEM) {
            JNU_ThrowOutOfMemoryError(env, "Map failed");
            return IOS_THROWN;
        }
        return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress);
}

這種狀況下,考慮:

  1. 增長系統內存
  2. 採用文件分塊,不要一次 mmap 很大的文件,也就是減小每次 mmap 文件的大小

7. OutOfMemoryError: Requested array size exceeds VM limit

當申請的數組大小超過堆內存限制,就會拋出這個異常。

8. OutOfMemoryError: Metaspace

Metadata 佔用空間超限(參考上面簡述 Java 內存構成, class 這一塊 包含兩種,一種是 metadata,一種是 class space),會拋出這個異常,那麼如何查看元空間內存呢?

能夠經過兩個命令,這兩個輸出是同樣的:

  • jmap -clstats <PID>
  • jcmd <PID> GC.class_stats (這個須要啓動參數: -XX:+UnlockDiagnosticVMOptions)
Index Super InstBytes KlassBytes annotations    CpAll MethodCount Bytecodes MethodAll    ROAll     RWAll     Total ClassName
    1    -1 214348176        504           0        0           0         0         0       24       616       640 [C
    2    -1  71683872        504           0        0           0         0         0       24       616       640 [B
    3    -1  53085688        504           0        0           0         0         0       24       616       640 [Ljava.lang.Object;
    4    -1  28135528        504           0        0           0         0         0       32       616       648 [Ljava.util.HashMap$Node;
    5 17478  12582216       1440           0     7008          64      2681     39040    11232     37248     48480 java.util.ArrayList
 .........
 25255    25         0        528           0      592           3        42       568      448      1448      1896 zipkin2.reporter.metrics.micrometer.MicrometerReporterMetrics$Builder
            472572680   16436464      283592 41813040      225990   8361510  75069552 39924272 101013144 140937416 Total
               335.3%      11.7%        0.2%    29.7%           -      5.9%     53.3%    28.3%     71.7%    100.0%
Index Super InstBytes KlassBytes annotations    CpAll MethodCount Bytecodes MethodAll    ROAll     RWAll     Total ClassName

其中,每一個指標的含義以下所示:

  • InstBytes:實例佔用大小
  • KlassBytes:類佔用大小
  • annotations:註解佔用大小
  • CpAll:常量池中佔用大小
  • MethodCount:方法個數
  • Bytecodes:字節碼大小
  • MethodAll:方法佔用大小
  • ROAll:只讀內存中內存佔用
  • RWAll:讀寫內存中內存佔用

9. OutOfMemoryError: Compressed class space

class space 內存溢出致使的,和上一個異常相似,須要查看類信息統計定位問題。

10. OutOfMemoryError: reason stack_trace_with_native_method

這個發生在 JNI 調用中,內存不足

相關文章
相關標籤/搜索