https://zhuanlan.zhihu.com/p/265039643
 
7 人赞同了该文章

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

这里的 mmapmalloc 是两种不同的内存申请分配方式,例如:

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)

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(文件映射内存)时,如果系统内存不足,就会抛出这个异常,对应的源代码是:

以 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 调用中,内存不足

[转帖]JVM相关 - StackOverflowError 与 OutOfMemoryError的更多相关文章

  1. JVM—内存溢出、OutOfMemoryError、StackOverflowError

    学习jvm时看到几篇非常好的系列文章,转载了: <深入理解Java虚拟机>学习小记一之自动内存管理机制(一) http://my.oschina.net/linuxfelix/blog/1 ...

  2. 3.1日 重温JVM相关信息

    1.JDK.JRE.JVM的关系: JDK是java开发的必备工具箱,JDK其中有一部分是JRE,JRE是JAVA运行环境,JVM则是JRE最核心的部分. 2.JVM的组成: JVM由4大部分组成:C ...

  3. java stackoverflowerror与outofmemoryerror区别(转)

    1.stackoverflow: 每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈,java栈以帧为单位保持线程运行状态:当线程调用一个方法是,jvm压入一个新的栈帧到这个线程的栈中 ...

  4. java stackoverflowerror与outofmemoryerror区别

    1.stackoverflow: 每当java程序启动一个新的线程时,java虚拟机会为他分配一个栈,java栈以帧为单位保持线程运行状态:当线程调用一个方法是,jvm压入一个新的栈帧到这个线程的栈中 ...

  5. [转帖]JVM—深入理解内存模型与垃圾收集机制

    JVM—深入理解内存模型与垃圾收集机制 https://juejin.im/post/5d68dc9ee51d4561ad6548f7 前言 Java是一种跨平台的语言,当初其设计初衷也是为了解决各个 ...

  6. Java面试题:写代码使得分别出现StackOverflowError和OutOfMemoryError

    转载自:http://www.cnblogs.com/xudong-bupt/p/3360206.html 今天做了个笔试,这是其中的一道题目:写代码使得分别出现StackOverflowError和 ...

  7. JVM相关知识

    Java虚拟机学习分享最近主要在学习JVM相关知识,-知识主要来源<深入理解JAVA虚拟机>,深有感触,结合自己的理解,整理出一些经验,由于篇幅较长,就把链接帖出来,希望对大家有所帮助: ...

  8. JVM相关参数配置和问题诊断<转>

    原文连接:http://blog.csdn.net/chjttony/article/details/6240457 1.Websphere JVM相关问题诊断: 由JVM引起的Websphere问题 ...

  9. 了解java虚拟机—JVM相关参数设置(2)

    1.   JVM相关参数设置 JVM相关配置 -XX:+PrintGC 两次次YoungGC,两次FullGC. -XX:+PrintGCDetails 打印GC时的内存,并且在程序结束时打印堆内存使 ...

  10. 几个与JVM相关的JDK工具:jps, jstat, jmap

    在项目中遇到OOM(Out of Memory)的问题,为了分析内存和JVM的垃圾回收器GC问题,一并把JVM相关的一些工具也研究了一下: jps:Java进程查看工具,实际上它和Unix/Linux ...

随机推荐

  1. Python——第二章:文件操作

    文件操作 1. 找到这个文件. 双击打开它 open(文件路径, mode="", encoding="") 文件路径: 1. 绝对路径 d:/test/xxx ...

  2. 文心一言 VS 讯飞星火 VS chatgpt (54)-- 算法导论6.2 6题

    文心一言 VS 讯飞星火 VS chatgpt (53)-- 算法导论6.2 5题 六.证明:对一个大小为 n的堆,MAX-HEAPIFY 的最坏情况运行时间为 Ω(Ign).(提示对于n个结点的堆, ...

  3. 技术实践丨Prometheus+Grafana搭建HBase监控仪表盘

    摘要:Grafana+Prometheus是目前非常流行的监控方案,图形化展示非常强大. 简介 ● HBase是一个分布式的.面向列的开源数据库,开源版本默认提供了展示集群信息的UI,可以显示集群的实 ...

  4. 40个问题让你快速掌握Java多线程的精髓

    摘要:多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这些线程可以同时利用CPU的多个核心运行. 多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这 ...

  5. 华为云VSS漏洞扫描服务之开源组件漏洞检测能力

    摘要:华为云VSS漏洞扫描服务提供针对于Web.主机和软件包的漏洞检测能力. 近日Apache Log4j2漏洞持续发酵,已成为中国互联网2021年年底前最大的安全事件.华为云VSS漏洞扫描服务,提供 ...

  6. max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]

    elasticsearch安装后启动时候,遇到此问题 问题翻译过来就是:elasticsearch用户拥有的可创建文件描述的权限太低,至少需要65536: 解决办法: 切换到root用户修改 vim  ...

  7. 在距离distribution 证书过期一个月(或被手动revoke了)的时候会受到apple的邮件

    ​ ​编辑 虽然distribution过期(或者被手动revoke)了,如果你的开发者账号是company(公司)类型或个人类型的,只要你的每年99$的开发者membership没有过期,就不会对已 ...

  8. 火山引擎 DataTester:如何用 A/B 测试做产品增长?

    技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 随着如今越来越高的获客成本,用户拉新变得不再容易:而且由于获客成本的增高,让用户留存也变得更加重要.同时,一个产品的使 ...

  9. SpringBoot 接口并发限制(Semaphore)

    可以使用 JMeter 辅助测试 https://blog.csdn.net/weixin_45014379/article/details/124190381 @RestController @Re ...

  10. websever

    1 在浏览器键入url,按下回车会经历如下行为 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址; 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TC ...