JVM相關 - StackOverflowError 與 OutOfMemoryError
本文基於 Java 15
StackOverflowError 與 OutOfMemoryError 是兩個老生常談的 Java 錯誤。Java 中的虛擬機器錯誤 VirtualMachineError 包括以下四種:
我們比較關心的就是 StackOverflowError 與 OutOfMemoryError,剩下的 InternalError 一般是內部使用錯誤,UnknownError 是虛擬機器發生未知異常,這兩種我們這裡不討論。
虛擬機器規範中的 StackOverflowError 與 OutOfMemoryError
參考 Java 虛擬機器規範官方文件:Run-Time Data Areas,可以知道,在如下情況下,會丟擲這兩種錯誤:
- 當某次執行緒執行計算時,需要佔用的 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 的實現來說明。
JVM 記憶體包括什麼
我們一般通過兩個工具 pmap 還有 jcmd 中的 VM.native_memory
命令去檢視 Java 程式記憶體佔用,由於 pmap 命令有點複雜而且很多記憶體對映是 anon 的,這裡採用 jcmd 中的 VM.native_memory
命令,去看一下 JVM 記憶體的每一部分。需要指出的一點是,
如果想了解詳細的 Native Memory Tracking,請參考我的另一篇文章JVM相關 - JVM 記憶體佔用與分析
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 是兩種不同的記憶體申請分配方式,例如:
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
- jcmd 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 呼叫中,記憶體不足
相關文章
- 面試準備——JVM相關面試JVM
- JVM相關知識點總結JVM
- JVM相關知識整理和學習JVM
- Java中JVM相關面試題-整理JavaJVM面試題
- JVM相關 - 深入理解 System.gc()JVMGC
- 如何解決JVM OutOfMemoryError記憶體洩漏問題?JVMError記憶體
- 一次StackOverflowError排查,原因竟然和Dubbo有關!Error
- 關於JVM的組成與classloaderJVM
- StackOverFlowError(棧溢位)Error
- 與數學相關的類
- CAP 與 Raft 相關知識Raft
- OutOfMemoryError異常Error
- 一個與CONNECT BY相關的BUG
- linux系統相關概念與配置Linux
- 好程式設計師Java學習路線分享JVM相關概念程式設計師JavaJVM
- Java開發程式設計師:JVM相關的知識講解Java程式設計師JVM
- Java中的OutOfMemoryErrorJavaError
- 什麼是SQL 語句中相關子查詢與非相關子查詢SQL
- HTTP與快取相關的頭部HTTP快取
- ZBlog首頁與列表頁相關模板
- webpack基礎配置與css相關loaderWebCSS
- 5G與WiFi6相愛相殺的關係WiFi
- Java記憶體溢位OutOfMemoryError的產生與排查Java記憶體溢位Error
- StackOverflowError堆疊溢位錯誤Error
- StackOverflowError是無法捕獲的Error
- 無限遞迴導致StackOverflowError遞迴Error
- CSS與中文相關的一些特性CSS
- 除法與GCD演算法的相關分析GC演算法
- 通俗解釋協方差與相關係數
- html 列印相關操作與實現詳解HTML
- java與作業系統相關的操作Java作業系統
- BMJ:研究發現環境與肥胖相關
- Java與Mysql鎖相關知識總結JavaMySql
- 與堆和堆排序相關的問題排序
- css檔案與資源路徑相關CSS
- 運維相關環境與工具掃盲運維
- SNAT、DNAT策略相關與防火牆配置解析防火牆
- 分享一個新出爐的JVM裡不痛不癢的BUG(Attach機制相關)JVM