JVM(Java虚拟机)是一个抽象的计算模型。就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。


1. 前言

Java内存模型.png

JVM提供的内存管理机制和自动垃圾回收极大的解放了用户对于内存的管理,大部分情况下不会出现内存泄漏和内存溢出问题。但是基本不会出现并不等于不会出现,所以掌握Java内存模型原理和学会分析出现的内存溢出或内存泄漏,对于使用Java的用户来说仍然十分重要。

Java中内存溢出常见于如下的几种情形:

  • 栈内存溢出(StackOverflowError)
  • 堆内存溢出(OutOfMemoryError:java heap space)
  • 永久代溢出(OutOfMemoryError:PermGen sapce)
  • ……

不同的内存溢出错误可能会发生在内存模型的不同区域,因此,我们需要根据出现错误的代码具体分析来找出可能导致错误发生的地方,并想办法进行解决。

2. 栈内存溢出

栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行Java方法(字节码)和本地方法,其余部分原理是类似的(以虚拟机栈为例说明)。Java虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。

具体来说,当线程执行某个方法时,JVM会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到栈中栈帧所使用的内存超出了栈内存的最大容量,此时JVM就会抛出StackOverflowError。

下面我们下一个不断的递归调用自己的方法,然后执行该程序:

  1. public class StackOverflowErrorDemo {
  2. private static int stackLength = 0;
  3. public static void main(String[] args) {
  4. StackOverflowErrorDemo demo = new StackOverflowErrorDemo();
  5. try {
  6. demo.pusStack();
  7. } catch (Throwable e){
  8. System.out.println("stack length is: " + demo.stackLength);
  9. throw e;
  10. }
  11. }
  12. public void pusStack(){
  13. stackLength++;
  14. pusStack();
  15. }
  16. }

运行程序很快就会抛出异常,异常信息如下所示。从输出信息中发现,出现问题的地方就是程序中递归调用方法自身的地方。

  1. stack length is: 20315
  2. Exception in thread "main" java.lang.StackOverflowError
  3. at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
  4. at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
  5. at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
  6. ......

总之,不论是因为栈帧太大还是栈内存太小,当新的栈帧内存无法被分配时,JVM就会抛出StackOverFlowError。通常栈内存可以通过设置-Xss参数来改变大小。

3. 堆内存溢出

堆内存的唯一作用就是存放数组和对象实例,即通过new指令创建的对象,包括数组和引用类型。堆内存溢出又分为两种情况:

  • 堆内存溢出:当堆中对象实例所占的内存空间超出了堆内存的最大容量,JVM就会抛出OutOfMemoryError:java heap space异常
  • 堆内存泄露:当堆中一些对象不再被引用但垃圾回收器无法识别时,这些未使用的对象就会在堆内存空间中无限期存在,不断的堆积就会造成内存泄漏

如果是因为堆内存空间太小,可以通过改变-Xmx来进行调整,或者分析程序中对象的生命周期和存储结构等信息进行调整;如果发生了内存泄漏,则可以先找出导致泄漏发生的对象是如何被GC ROOT引用起来的,然后通过分析引用链找到发生泄漏的地方。

例如,我们通过-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError来设置堆内存大小为20M,并且设定不支持自动扩展,同时使用-XX:+HeapDumpOnOutOfMemoryError实现当异常抛出时Dump出当前的内存堆转储快照进行分析。

  1. import java.util.ArrayList;
  2. public class HeapOOMDemo {
  3. static class OOMObject{}
  4. public static void main(String[] args) {
  5. ArrayList<OOMObject> list = new ArrayList<>();
  6. HeapOOMDemo demo = new HeapOOMDemo();
  7. try {
  8. while (true) {
  9. list.add(new OOMObject());
  10. }
  11. } catch (Throwable e){
  12. System.out.println(list.size());
  13. throw e;
  14. }
  15. }
  16. }

运行程序一段时间后输出如下信息:

  1. 70091070
  2. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  3. at java.base/java.util.Arrays.copyOf(Arrays.java:3721)
  4. at java.base/java.util.Arrays.copyOf(Arrays.java:3690)
  5. at java.base/java.util.ArrayList.grow(ArrayList.java:235)
  6. ......

4. 内接内存溢出

JDK8及之后不再使用方法区,而是变成了使用直接内存的元空间,虽然直接内存依赖于系统内存,但是Java堆和直接内存的总和依然受限于系统内存的最大值,当堆所占空间太大时,直接内存太小时就可能会出现OOM。例如下面的例子中,我们不断在使用NIO的方式获取ByteBuffer

  1. import java.nio.ByteBuffer;
  2. import java.util.ArrayList;
  3. public class OOMTest {
  4. private static final int BUFFER = 1024 * 1024 * 20;//20MB
  5. public static void main(String[] args) {
  6. ArrayList<ByteBuffer> list = new ArrayList<>();
  7. int count = 0;
  8. try {
  9. while(true){
  10. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
  11. list.add(byteBuffer);
  12. count++;
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. } finally {
  20. System.out.println(count);
  21. }
  22. }
  23. }

当直接内存用尽时,程序就会抛出OOM。

  1. 89
  2. Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
  3. at java.nio.Bits.reserveMemory(Bits.java:694)
  4. at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
  5. at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
  6. at DirectBuffer.OOMTest.main(OOMTest.java:14)

5. 方法区溢出

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。例如设置参数为-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m,然后利用反射机制生产大量的动态类,当元空间用尽时,程序就会抛出OOM。

  1. import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
  2. import jdk.internal.org.objectweb.asm.Opcodes;
  3. public class OOMTest extends ClassLoader {
  4. public static void main(String[] args) {
  5. int j = 0;
  6. try {
  7. OOMTest test = new OOMTest();
  8. for (int i = 0; i < 10000; i++) {
  9. //创建ClassWriter对象,用于生成类的二进制字节码
  10. ClassWriter classWriter = new ClassWriter(0);
  11. //指明版本号,修饰符,类名,包名,父类,接口
  12. classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
  13. //返回byte[]
  14. byte[] code = classWriter.toByteArray();
  15. //类的加载
  16. test.defineClass("Class" + i, code, 0, code.length);//Class对象
  17. j++;
  18. }
  19. } finally {
  20. System.out.println(j);
  21. }
  22. }
  23. }
  1. Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space
  2. at java.lang.ClassLoader.defineClass1(Native Method)
  3. at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
  4. at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
  5. at MethodArea.OOMTest.main(OOMTest.java:19)
  6. 3331