本文基於 Java 15html
StackOverflowError 與 OutOfMemoryError 是兩個老生常談的 Java 錯誤。Java 中的虛擬機錯誤 VirtualMachineError 包括如下四種:java
咱們比較關心的就是 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)
這裏的 mmap,malloc 是兩種不一樣的內存申請分配方式,例如: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 沒有記錄,那就是:
- Direct Buffer:直接內存,請參考:JDK核心JAVA源碼解析(4) - Java 堆外內存、零拷貝、直接內存以及針對於NIO中的FileChannel的思考
- MMap Buffer:文件映射內存,請參考:JDK核心JAVA源碼解析(5) - JAVA File MMAP原理解析
各類 StackOverflowError 與 OutOfMemoryError 場景以及定位方式
1. StackOverflowError
調用棧過深,致使線程棧佔用大小超過-Xss
(或者是-XX:ThreadStackSize
)的限制,若是沒指定-Xss
,則根據不一樣系統肯定默認最大大小。
肯定默認大小的代碼請參考:
- windows:os_windows.cpp
- linux:os_linux.cpp
總結起來就是,32 位的系統通常是 512k,64 位的是 1024k
通常報這個錯都是由於遞歸死循環,或者調用棧真的太深而線程棧大小不足,好比那種回調背壓模型的框架,netty + reactor 這種,通常線程棧須要調大一點。
2. OutOfMemoryError: Java heap space
堆內存不夠用,沒法分配更多內存,就會拋出這個異常。通常這種狀況發生後,須要查看 heap dump,線上應用通常加上-XX: +HeapDumpOnOutOfMemoryError
在OutOfMemoryError
發生的時候,進行 heap dump,以後進行分析。
heap dump 查看工具通常經過 Memory Analyzer (MAT)
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 ),來定位問題。
6. OutOfMemoryError: map failed
這個是 File MMAP(文件映射內存)時,若是系統內存不足,就會拋出這個異常,對應的源代碼是:
- Windows:FileDispatcherImpl.c
- Linux:FileDispatcherImpl.c
以 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); }
這種狀況下,考慮:
- 增長系統內存
- 採用文件分塊,不要一次 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 調用中,內存不足