Java 内存溢出(一)原因、复现、排查

  • Post author:
  • Post category:java



内存溢出:

是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。这篇文章整理自《深入理解 java 虚拟机》。



一、内存溢出原因

内存溢出就是内存不够,引起内存溢出的原因有很多种,常见的有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的 BUG;
  5. 启动参数内存值设定的过小。

当然实际情况中内存溢出的原因就太多了。

请添加图片描述

以上的图是基于 java7 来描述的,从上面这张图我们能够得到如下信息:java 虚拟机把内存分为 5 个模块。

(1)程序计数器:程序计数器是线程私有的,主要作用是通过改变这个计数器的值来选取下一条需要执行的字节码指令。既然每个线程都有一个,那么这些线程的计数器是互不影响的,也不会抛出任何异常。

(2)虚拟机栈和本地方法栈:虚拟机栈描述的是 java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈与虚拟机栈的区别是,虚拟机栈为虚拟机执行 java 方法服务,而本地方法栈则为虚拟机提供 native 方法服务。

在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是 StackOverflowError 异常,而不会得到 OutOfMemoryError 异常。而在多线程环境下,则会抛出 OutOfMemoryError 异常。

(3)java 堆和方法区:java 堆区主要存放对象实例和数组等,方法区保存类信息、常量、静态变量等等。运行时常量池也是方法区的一部分。这两块区域是线程共享的区域,只会抛出 OutOfMemoryError。



二、内存溢出实例



1、堆溢出

既然堆是存放实例对象的,那我们就无限创建实例对象。这样堆区迟早会满。


执行时需要设置VM参数:-Xmx10M,限制最大堆内存为10M

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

public class HeapOOM {
    static class User {}
    public static void main(String[] args) {
        List<User> list = new ArrayList<>();
        while (true) {
            list.add(new User());
        }
    }
} 


异常内容:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.demo.HeapOOM.main(HeapOOM.java:6)

因为我提前设置了堆区内存限制,所以无限创建就会抛出异常。



2.虚拟机栈和本地方法栈溢出

Java 虚拟机规范中描述了两种异常:

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。


第一种我们只需要使用方法递归调用即可模拟:

public class StackOutOfMemoryError {
    public static void main(String[] args) {
        test();
    }
    private static void test() {
        System.out.println("StackOverflowError 异常测试");
        test();
    }
}


异常内容:

Exception in thread "main" java.lang.StackOverflowError
	at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
	at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
	at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
	at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
	at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
	at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
	at java.io.PrintStream.write(PrintStream.java:526)
	at java.io.PrintStream.print(PrintStream.java:669)
	at java.io.PrintStream.println(PrintStream.java:806)
	at com.demo.StackOutOfMemoryError.test(StackOutOfMemoryError.java:6)


第二种也可以递归调用模拟,但是使用的是类直接调用:

public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        oom.stackLeak();
    }
}


异常内容:

Exception in thread "main" java.lang.StackOverflowError
	at com.demo.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:5)



3.方法区和运行时常量池溢出


执行时需要设置VM参数:-Xmx10M,限制最大堆内存为10M

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class JavaMethodAreaOOM {
    private static class User {}
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(User.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object obj, Method method,
                                        Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}


异常内容:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)
	at java.lang.StringBuffer.<init>(StringBuffer.java:116)
	at org.objectweb.asm.Type.getDescriptor(Unknown Source)
	at net.sf.cglib.core.ClassEmitter.declare_field(ClassEmitter.java:193)
	at net.sf.cglib.proxy.MethodInterceptorGenerator.generate(MethodInterceptorGenerator.java:94)
	at net.sf.cglib.proxy.Enhancer.emitMethods(Enhancer.java:994)
	at net.sf.cglib.proxy.Enhancer.generateClass(Enhancer.java:498)
	at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
	at com.demo.JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:22)



4.本机直接内存溢出


执行时需要设置VM参数:-Xmx10M,限制最大堆内存为10M

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}


异常内容:

Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at com.demo.DirectMemoryOOM.main(DirectMemoryOOM.java:12)

DirectMemory 容量可通过 -XX:MaxDirectMemorySize指定,如果不指定,则默认与 Java 堆最大值(-Xmx 指定)一样。



三、内存溢出排查

排查最主要的就是检查代码,而且内存溢出往往都是代码的问题。当然以下几点都是需要注意的:

(1)内存中加载的数据量过于庞大,如一次性从数据库取出过多数据;

(2)集合类中有对对象的引用,使用完后未清空,是的 JVM 不能回收;

(3)代码中存在死循环或循环产生过多重复的对象实体;

(4)使用的第三方软件中的 BUG;

(5)启动参数内存值设定的过小。


内存溢出解决方法:

方法一:修改 JVM 启动参数,直接增加内存,暂时解决问题;

方法二:检查错误日志,定位可能发生内存溢出的位置,优化代码;

方法三:如果设置了内存参数

-XX:+HeapDumpOnOutOfMemoryError

,当内存溢出时会产生 .hprof 文件(记录了 Java 进程在某个时间内的快照),可以通过 MAT 工具进行分析,优化代码。

整理完毕,完结撒花~

参考地址:

1.java内存溢出,https://blog.csdn.net/u014401141/article/details/122825443



版权声明:本文为qq_33204709原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。