本文基于 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)