第27章:OOM常见各种场景及解决方案

1、OOM案例1:堆溢出

1.1、报错信息

java.lang.OutOfMemoryError: Java heap space

1.2、案例模拟

127.0.0.1:8080/add

/**
 * 案例1:模拟线上环境OOM
 */
@RequestMapping("/add")
public void addObject(){
    System.err.println("add"+peopleSevice);
    ArrayList<People> people = new ArrayList<>();
    while (true){
        people.add(new People());
    }
}

1.3、JVM参数配置

-XX:+PrintGCDetails -XX:MetaspaceSize=64m -XX:+PrintGCDateStamps 
-XX:+HeapDumpOnOutMemoryError -XX:HeapDumpPath=/usr/local/heapdump.hprof
-Xmx80M -Xmx80M -Xloggc:log/gc-oomHeap.log

1.4、运行结果

1.5、原因及解决方案

原因

1、代码中可能存在大对象分配

2、可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象

解决方法

1、检查是否存在大对象的分配,最有可能的是大数组分配

2、通过jmap命令,把堆内存dump下来,使用AT等工具分析一下,检查是否存在内存泄漏的问题

3、如果没有找到明显的内存泄漏,使用 -Xmx 加大堆内存

4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable对象,也有可能是框架内部提供的,考虑其存在的必要性

1.6、dump文件分析

dump文件编号 20210801

 

 

 

 

1.7、gc日志分析

2、OOM案例2:元空间溢出

2.1、元空间数据类型

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-heap(非堆),目的应该是与Java堆区分开来。

Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemory Error异常。

2.2、报错信息

java.lang.OutOfMemoryError: Metaspace

2.3、案例模拟

127.0.0.1:8080/metaSpaceOom

/**
     * 案例2:模拟元空间OOM溢出
     */
    @RequestMapping("/metaSpaceOom")
    public void metaSpaceOom(){
        ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(People.class);
            enhancer.setUseCache(false);
//            enhancer.setUseCache(true);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
                System.out.println("我是加强类,输出print之前的加强方法");
                return methodProxy.invokeSuper(o,objects);
            });
            People people = (People)enhancer.create();
            people.print();
            System.out.println(people.getClass());
            System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
            System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
            System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
        }
    }

 

2.4、JVM参数设置

-XX:+PrintGCDetails
-XX:MetaspaceSize=60m
-XX:MaxMetaspaceSize=60m
-Xss512K
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof
-XX:+TraceClassUnloading
-Xmx60M
-Xmx60M
-Xloggc:log/gc-oomMeta.log

2.5、原因及解决方案

JDK8后,元空间替换了永久代,元空间使用的是本地内存

原因:

1.运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载

2.应用长时间运行,没有重启

3.元空间内存设置过小

解决方法:

因为该OOM原因比较简单,解决方法有如下几种:

1.检查是否永久代空间或者元空间设置的过小

2.检查代码中是否存在大量的反射操作

3.dump之后通过mat检查是否存在大量由于反射生成的代理类

2.6、分析及解决

2.6.1、查看监控

2.6.2、查看GC状态

 

2.6.3、查看GC日志

2.6.4、分析dump文件

dump文件编号 20210802

 

jvisualvm分析

 

 

MAT分析

 

2.6.5、解决方案

公用加载类

enhancer.setUseCache(true);

3、OOM案例3:GC overhead limit exceeded

3.1、案例模拟

3.1.1、示例代码1

JVM配置

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/dumpExceeded.hprof
-Xmx10M
-Xmx10M
-Xloggc:log/gc-oomExceeded.log

 

/**
 * 案例3:测试 GC overhead limit exceeded
 * @author shkstart
 * @create 16:57
 */
public class OOMTest {
    public static void main(String[] args) {
        test1();

//        test2();
    }

    public static void test1() {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true) {
                list.add(UUID.randomUUID().toString().intern());
                i++;
            }
        } catch (Throwable e) {
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }



}

3.1.2、示例代码2

JVM配置

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/dumpHeap1.hprof
-Xmx10M
-Xmx10M
-Xloggc:log/gc-oomHeap1.log
  public static void test2() {
        String str = "";
        Integer i = 1;
        try {
            while (true) {
                i++;
                str += UUID.randomUUID();
            }
        } catch (Throwable e) {
            System.out.println("************i: " + i);
            e.printStackTrace();
            throw e;
        }
    }

3.2、代码解析

第一段代码:运行期间将内容放入常量池的典型案例

intern()方法

  • 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;
  • 如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用

第二段代码:不停的追加字符串str

你可能会疑惑,看似demo也没有差太多为什么第二个没有报 GC overhead limit exceeded呢?以上两个demo的区别在于:

  • Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出
  • GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。

报错信息:

2021-11-21T03:09:20.177-0800: 5.710: [Full GC (Ergonomics) [PSYoungGen: 2048K->2048K(2560K)] [ParOldGen: 7065K->7063K(7168K)] 9113K->9111K(9728K), [Metaspace: 3582K->3582K(1056768K)], 0.0176855 secs] [Times: user=0.11 sys=0.00, real=0.02 secs] 
2021-11-21T03:09:20.196-0800: 5.729: [Full GC (Ergonomics) [PSYoungGen: 2048K->2048K(2560K)] [ParOldGen: 7086K->7086K(7168K)] 9134K->9134K(9728K), [Metaspace: 3582K->3582K(1056768K)], 0.0182876 secs] [Times: user=0.12 sys=0.00, real=0.02 secs] 
2021-11-21T03:09:20.257-0800: 5.790: [Full GC (Ergonomics) [PSYoungGen: 2048K->0K(2560K)] [ParOldGen: 7108K->706K(5632K)] 9156K->706K(8192K), [Metaspace: 3582K->3582K(1056768K)], 0.0053749 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

通过查看GC日志可以发现,系统在频繁性的做 FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆dump文件来具体分析。

3.3、分析及解决

dump文件编号 20210803

第1步:定位问题代码块

 

 

第2步:分析dump文件直方图

 

 

 

第3步:代码修改

根据业务来修改是否需要死循环。

原因:

这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出

解决方法:

1.检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。

2.添加参数 -XX:-UseGCoverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现java.lang.OutOfMemoryError: Java heap space。

3.dump内存,检查是否存在内存泄漏,如果没有,加大内存。

4、OOM案例4:线程溢出

4.1、报错信息

Java.lang.OutOfMemeoryError:unable to create new native thread

4.2、问题原因

出现这种情况,基本上都是创建了大量的线程导致的

4.3、案例模拟

/**
 * 测试4:线程溢出
 * @author shkstart
 * @create 17:45
 */
public class TestNativeOutOfMemoryError {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("i = " + i);
            new Thread(new HoldThread()).start();
        }
    }
}

class HoldThread extends Thread {
    CountDownLatch cdl = new CountDownLatch(1);

    @Override
    public void run() {
        try {
            cdl.await();
        } catch (InterruptedException e) {
        }
    }
}

 

4.4、分析及解决

解决方向1:

  • 通过 -Xss 设置每个线程栈大小的容量
  • JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K
  • 正常情况下,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
  • 能创建的线程数的具体计算公式如下:

(MaxprocessMemory- JVMMemory- ReservedOsMemory)/(ThreadStackSize)=Number of threads

MaxprocessMemory 指的是进程可寻址的最大空间

JVMMemory JVM内存

ReservedOsMemory 保留的操作系统内存

ThreadStackSize 线程栈的大小

  • 在Java语言里,当你创建一个线程的时候,虚拟机会在JVM内存创建一个 Thread 对象同时创建一个操作系统线程,而这个系统线程的内存用的不是 JVMMemory,而是系统中剩下的内存
  • 由公式得出结论:你给JVM内存越多,那么你能创建的线程越少,越容易发生 Java.lang.OutOfMemeoryError:unable to create new native thread

问题解决:

1、如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。

2、如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改 MaxprocessMemory, JVMMemory, ThreadStackSize这三个因素,来增加能创建的线程数。

3、 MaxprocessMemory 使用64位操作系统

4、 JVMMemory 减少 JVMMemory的分配

5、 ThreadStackSize减小单个线程的栈小

经实测,在32位 windows系统下较为严格遵守;64位系统下只能保证正/负相关性,甚至说相关性也不能保证。即:

在测试的过程中,64位操作系统下调整Xss的大小并没有对产生线程的总数产生影响,程序执行到极限的时候,操作系统会死机。无法看出效果

在32位win7操作系统下测试,jdk版本1.8(适配32位操作系统)会发现调整Xss的大小会对线程数量有影响,如下表所示:

省略表

由上可见,64位操作系统对于实验的结果是不明显的,但是32位操作系统对于Xss的设置对于实验结果是明显的,为什么会产生这样的结果?我们上面讲到过线程数量的计算公式

(MaxprocessMemory- JVMMemory- ReservedOsMemory)/(ThreadStackSize)=Number of threads

其中 MaxprocessMemory 表示最大寻址空间,在32位系统中,CPU的“寻址范围”就受到32个二进制位的限制,也就是说,假设它要访问内存,它的能力是,只能访问4G内存。

32位二进制数最大值是11111111111111111112的32次方4294967296=4194304k(1k是1024)=4096M(1M是1048576)=4GB。也就是说32位CPU只能访问4GB的内存。再减去显卡上的显存等内存,可用内存要小于4G,所以32位操作系统可用线程数量是有限的。

64位二进制数的最大值是1111111111111111111111111111111111b,2的64次方=1719869184 GB,大家可以看看64位操作的寻址空间大小比32位操作系统多了太多,所以这也是为什么我们总是无法测试出很好效果的原因。

综上,在生产环境下如果需要更多的线程数量,建议使用64位操作系统,如果必须使用32位操作系统,

解决方向2:

线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:

/proc/sys/kernel/pid_max 系统最大pid值,在大型系统里可适当调大

/proc/sys/kernel/threads-max 系统允许的最大线程数

maxuserprocess(ulimit--u) 系统限制某用户下最多可以运行多少进程或线程

/proc/sys/vm/max_map_count

max_map_count文件包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建。调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上线但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在 NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。

おすすめ

転載: blog.csdn.net/dyangel2013/article/details/121533667