「轻盈」之旅:OOM故障重现与解决

前期准备

1. 本项目均采用 VisualVM 2.1.10 进行dump文件的分析。JDK1.8及之前所在目录的bin目录下有自带的VisualVM,JDK1.8以后需要自行手动安装下载。

下载地址:https://visualvm.github.io/download.html
IDEA插件配置:在Plugins里搜索visualVM Launcher即可。(也可以不用配置,直接下载客户端软件)后续只要在配置下载安装好的VisualVM程序地址即可,这样就能直接在IDEA中根据指定的类启动VisualVM了,不需要在独立的VisualVM里找指定路径装配。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 项目采用JDK1.8对OOM溢出进行分析。在进行模拟时,需要配置一些参数,例如:

注意:这里可以通过jmap指定打印他的内存快照dump文件,不过有的情况打印不了,我们会设置vm参数让程序自动生成dump文件。

-XX:+PrintGCDetails (让 JVM 在执行垃圾收集时输出详细的日志信息)
-XX:MetaspaceSize=64m 
-XX:+HeapDumpOnOutOfMemoryError (打印出现OOM的异常信息dump文件)
-XX:HeapDumpPath=heap/heapdump3.hprof (打印的文件的具体位置)
-XX:SurvivorRatio=8 (设置年轻代中 Survivor 空间的比例)
-XX:+PrintGCDateStamps (使得 GC 日志中的每一条记录都会带上时间戳)
-Xms50M -Xmx50M 
-Xloggc:log/gc-oomHeap.log  (打印的GC日志文件的路径)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 分析GC和dump文件

一般我们在命令中会设置JVM参数,执行启动类的时候会自动生成gc日志和dump文件。gc日志我们可以在GC Easyhttps://gceasy.io/gc-dashboard.jsp)中分析即可
在这里插入图片描述

接下来我们就要在VisualVM中装入我们要分析的dump文件即可。

在这里插入图片描述
在这里插入图片描述

案例一二三代码链接

https://pan.baidu.com/s/1C8IMG4ZXrqjQdYb4B-Z5gg 提取码: syhh


OOM案例一:堆溢出(Java heap space)

案例模拟

这是一个简单的SpringBoot项目,DemoApplication类是整个当前模块项目的启动类,端口号为8848,如果端口号被占用,可以杀掉当前占用的进程(kill -9 PID),或者在application-dev.yml中修改端口号:

在这里插入图片描述

我们模拟发送请求:http://localhost:8848/add

在这里插入图片描述

JVM参数配置

-XX:+PrintGCDetails 
-XX:MetaspaceSize=64m 
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=heap/heapdump3.hprof 
-XX:SurvivorRatio=8 
-XX:+PrintGCDateStamps 
-Xms50M -Xmx50M 
-Xloggc:log/gc-oomHeap.log

运行结果

在这里插入图片描述

gc文件分析

GC Easyhttps://gceasy.io/gc-dashboard.jsp)中分析即可:
通过下图可以看出,我们的程序进行了大量的Full GC操作,导致内存溢出了。

在这里插入图片描述

dump文件分析

首先我们在Summary中找到出现异常的线程,红色标记:
在这里插入图片描述

这时我们可以点击查看view all

在这里插入图片描述
找到出现异常线程的线程报告后,再找到我们代码中出现错误的行号(这点很重要)。另外,还可以通过对象分配来查看,是否存在大对象:

在这里插入图片描述
图片中标记为红色的区域显示了每个类的对象数量。例如,“com.atguigu.demo.bean.People”类有1,215,488个实例,这是所有对象中最多的。另一个红色的区域显示了每个类占用的内存大小。例如,“com.atguigu.demo.bean.People”类占用了大约39MB的内存空间。这表明该类可能存在内存泄漏或者过度分配的情况。

回到代码,果然:

在这里插入图片描述

原因及解决方法

原因
1、代码中可能存在大对象分配。
2、可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法
1、检查是否存在大对象的分配,最有可能的是大数组分配。
2、如果没有找到明显的内存泄漏,使用 -Xmx 加大堆内存。
3、还有一点容易被忽略,检査是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性。

OOM案例二:元空间溢出(Metaspace)

案例模拟

这段代码使用了 CGLIB(Code Generation Library)框架中的 Enhancer 类来动态创建 People 类的子类实例。

在这里插入图片描述

JVM参数配置

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

运行结果

当我们正常启动程序时,使用VisualVM查看对应的类的元空间(Metaspace)。此时一切正常,最大元空间我们设置为60M,此时只使用了30M左右。

在这里插入图片描述

模拟一下,访问http://localhost:8848/metaSpaceOom,访问前记得先clear下控制台输出。

当我们访问http://localhost:8848/metaSpaceOom这个页面时,发现出现了OOM异常了。

在这里插入图片描述

gc文件分析

在这里插入图片描述

除了可以使用GC Easy这个工具外呢,我们还可以使用控制台的jps命令,列出正在运行的 JVM 进程的状态信息,包括进程 ID 和主类名称。然后使用jstat命令统计对应进程ID的垃圾收集状况,每1秒钟执行一次。

在这里插入图片描述
可以看到元空间已使用的空间(MU)非常接近于上限了(MC),而且出现了大量的Full GC(829次)。
在这里插入图片描述

如果直接观察生成的gc日志,我们也不难看出:后期出现了大篇幅的Full GC。

在这里插入图片描述

dump文件分析

回到VisualVM,我们明显可以看到元空间占用和CPU明显飙升了,出现了满负载的情况,元空间也达到了我们所预设的最大上限。

在这里插入图片描述

老样子,在Summary中找出现OOM的线程,定位到具体的代码行号。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

原因及解决方法

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

原因:
1.运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
2.应用长时间运行,没有重启
3.元空间内存设置过小

解决方法:
因为该 OOM 原因比较简单,解决方法有如下几种:
1.检查是否永久代空间或者元空间设置的过小
2.检查代码中是否存在大量的反射操作
3.dump之后通过VisualVM检査是否存在大量由于反射生成的代理类

setUseCache(true) 时,CGLIB 会尝试重用已存在的类定义,从而减少了类定义的数量,避免了由于频繁创建类而导致的内存问题。因此,设置为 true 可以有效防止因类定义过多而导致的内存溢出。

在这里插入图片描述

OOM案例三:GC overhead limit exceeded

案例模拟

注意模拟的时候,一定要分开执行,每次调整JVM的日志和dump输出的文件名,避免冲突!!!

Java源代码:

package com.atguigu.oom;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * 案例3:测试 GC overhead limit exceeded
 * @author shkstart
 * @create 16:57
 */
public class OOMTest {
    public static void main(String[] args) {
    	// 出现GC overhead limit exceeded异常
        test1();
        
		// 出现Java heap space异常
        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;
        }
    }

    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;
        }
    }

}

JVM参数配置

执行test1():

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

执行test2():

-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/dumpExceeded1.hprof
-XX:+PrintGCDateStamps
-Xms5M
-Xmx5M
-Xloggc:log/gc-oomExceeded1.log

运行结果

执行test1():
在这里插入图片描述

执行test2():

在这里插入图片描述

gc文件分析

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

dump文件分析

test1():
在这里插入图片描述

在这里插入图片描述

test2():

在这里插入图片描述

在这里插入图片描述

原因及解决方法

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

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

第一段代码: 运行期间将内容放入常量池的典型案例intern()方法。

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

第二段代码: 不停的追加字符串str
你可能会疑惑,看似demo也没有差太多,为什么第二个没有报GCoverhead limit exceeded呢?以上两个demo的区别在于:

  • Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出。
    在这里插入图片描述

  • GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。
    在这里插入图片描述

解决方法:
1.检查项目中是否有大量的死循环或有使用大内存的代码,优化代码
2.添加参数-XX:-UseGcoverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space.
3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。

OOM案例四:线程溢出(unable to create new native thread)

(一定不要在本地执行,当系统资源耗尽的时候,电脑直接挂机重启了)

案例模拟

在下面这个例子中,我们创建了一个无限循环,不断地创建并启动新线程:

package com.atguigu.oom;

import java.util.concurrent.CountDownLatch;

/**
 * 案例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) {
        }
    }
}

注意:在真实环境中,你应该谨慎使用这样的代码,因为它可能会耗尽系统资源。在生产环境中,你需要检查线程的生命周期管理,确保及时释放不再使用的线程资源。同时,你也应该关注系统的线程限制和线程栈大小设置,以确保它们满足应用程序的需求。

运行结果

如果你的机器上的可用线程数量达到系统的限制,或者剩余的线程栈空间不足以创建新的线程,就会抛出 java.lang.OutOfMemoryError: unable to create new native thread 异常。

原因及解决方法

出现这种异常大多都是创建了大量的线程导致的。

解决方向1:

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

MaxProcessMemory 指的是进程可寻址的最大空间
JVMMemory:JVM内存
ReservedOsMemory:保留的操作系统内存
ThreadStackSize:线程栈的大小

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

问题解决:

  1. 如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。
  2. 如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory,JVMMemory,ThreadstackSize这三个因素,来增加能创建的线程数。
  3. MaxProcessMemory 使用64位操作系统
  4. JVMMemory 减少JVMMemory的分配
  5. ThreadStackSize 减小单个线程的栈大小

解决方向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区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值