性能文章>JVM相关 - StackOverflowError 与 OutOfMemoryError>

JVM相关 - StackOverflowError 与 OutOfMemoryError原创

2年前
6495147

本文基于 Java 15

StackOverflowError 与 OutOfMemoryError 是两个老生常谈的 Java 错误。Java 中的虚拟机错误 VirtualMachineError 包括以下四种:

image.png

我们比较关心的就是 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:

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 没有记录,那就是:

  • Direct Buffer:直接内存
  • MMap Buffer:文件映射内存

各种 StackOverflowError 与 OutOfMemoryError 场景以及定位方式

1. StackOverflowError

调用栈过深,导致线程栈占用大小超过-Xss(或者是-XX:ThreadStackSize)的限制,如果没指定-Xss,则根据不同系统确定默认最大大小。

确定默认大小的代码请参考:

总结起来就是,32 位的系统一般是 512k,64 位的是 1024k

一般报这个错都是因为递归死循环,或者调用栈真的太深而线程栈大小不足,比如那种回调背压模型的框架,netty + reactor 这种,一般线程栈需要调大一点。

2. OutOfMemoryError: Java heap space

堆内存不够用,无法分配更多内存,就会抛出这个异常。一般这种情况发生后,需要查看 heap dump,线上应用一般加上-XX: +HeapDumpOnOutOfMemoryError在OutOfMemoryError发生的时候,进行 heap dump,之后进行分析。

heap dump 查看工具一般通过 Memory Analyzer (MAT)

image.png

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.png

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

点赞收藏
张哈希
请先登录,查看14条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

万字完整深入解析JVM面试必备,原来这就是和年薪百万的差距

万字完整深入解析JVM面试必备,原来这就是和年薪百万的差距

搭稳Netty开发的地基,用漫画帮你分清同步异步阻塞非阻塞

搭稳Netty开发的地基,用漫画帮你分清同步异步阻塞非阻塞

为什么 JVM 叫做基于栈的 RISC 虚拟机

为什么 JVM 叫做基于栈的 RISC 虚拟机

如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

如何随心所欲调试HotSpot VM源代码?(改造为CMakeLists项目)

朋友们,就在今天,JDK 21,它终于带着重磅新特性正式发布了!!!

朋友们,就在今天,JDK 21,它终于带着重磅新特性正式发布了!!!

13 轻量级锁的重入 以及 线程1获取轻量级锁并释放线程2获取锁 的调试

13 轻量级锁的重入 以及 线程1获取轻量级锁并释放线程2获取锁 的调试

7
14