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