JVM(一)Java内存区域与内存溢出

Java 内存区域

Java 虚拟机所管理的内存将包括以下几个运行时数据区域,如下图所示:
image.png

程序计数器

ProgramCounterRegister 是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变程序计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常跳转、线程恢复等基础功能都需要依赖这个计数器来完成。
由于 Java 虚拟机的多线程时通过线程切换并分配处理器执行时间来实现的,对于单核处理器在某一个时间都只会有一个线程在运行,为了线程切换后能恢复到正确的执行位置,每个线程都需要维护一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储。
如果当前线程正在执行一个 Java 方法,程序计数器记录的是正在执行的虚拟机字节码指令的指令。如果当前线程正在执行一个 Native 方法,程序计数器记录值则为空。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈

与程序计数器一样,Java 虚拟机也是线程私有的,它的生命周期与线程相同。每个方法被执行的时候会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
如果线程请求栈深度大于虚拟机所允许的深度,抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存时会抛出 OutOfMemoryError。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,只不过一个是执行 Java 方法(也是字节码)服务,而本地方法栈则是使用到的 Native 方法服务,HotSpot 虚拟机直接将两者合二为一,与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java 堆是垃圾收集器管理的主要区域,很多时候称为 GC 堆。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError

方法区

Method Area 与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError。
Method Area 与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码等数据。但项目中如果存在类的动态编译,就需要观察方法区的大小是否能够满足类存储。
垃圾回收较少发生在该内存区域,它存储的信息相对比较稳定,回收的主要目标是常量池和对类型的卸载。对类型的卸载相当苛刻,要求满足以下三个条件才能算是“无用”的类:
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
加载该类的 ClassLoader 已经被回收
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError。

运行时常量池

Runtime Constant Pool 是方法区的一部分。用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池相对于 Class 文件常量池的一个重要特征在于其具备动态性,Java 语言并不要求常量一定只有编译期才能产生,在运行期间也能产生新的常量放入常量池中,如 String.intern()方法。当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

直接内存

Direct Memory 并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分也是频繁使用。在 Java 的 NIO 中使用到,服务器管理员忽略直接内存后果是,各个内存区域总和大于物理内存限制,从而导致动态扩展时出现 OutOfMemoryError 异常。

实战:OutOfMemoryError 异常

1、Java 堆溢出

Java 堆用于存储对象实例,我们只要不断创建对象,并且保证 GC Roots 到对象之间有可达路径来避免 GC 清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
XX:+HeapDumpOnOutOfMemoryError 这个参数可以让虚拟机在出现内存溢出异常时 Dump 出当前的内存堆转储快照以便事后进行分析。

import java.util.ArrayList;
import java.util.List;
/**
 * VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {
    static class OOMObject{
        private String name;
        public OOMObject(String name) {
            this.name = name;
        }
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        long i = 1;
        while(true) {
            list.add(new OOMObject("IpConfig..." + i++));
        }
    }
}

抛出的异常:

Dumping heap to java_pid27828.hprof ...
Heap dump file created [14123367 bytes in 0.187 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:45)
at java.lang.StringBuilder.<init>(StringBuilder.java:92)
at com.baoxian.HeapOOM.main(HeapOOM.java:22)

注:出现 Java 堆内存溢出时,异常堆栈信息 java.lang.OutOfMemoryError 后面会紧跟着 JavaHeapSpace。
要解决这个异常,一般手段是首先通过内存映像分析工具比如 Eclipse Memory Analyzer 对 dump 出来的堆转储快照进行分析,重点是确认内存中对象是否是必要的,也就是要弄清楚到底是出现了内存泄露 Memory Leak 还是内存溢出 Memory Overflow。
如果是内存泄露,可进一步通过工具查看泄露对象到 GC Roots 的引用链。于是就能找到泄露对象时通过怎样的路径与 GC Roots 相关联并导致垃圾收集器无法自动回收它们。掌握了泄露对象的类型信息,以及 GC Roots 引用链的信息,就可以比较准确的定位出泄露代码的位置了。
如果不存在泄露,那么就该修改-Xms 和-Xms 堆参数看能否加大点。

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

-Xoss 参数设置本地方法栈大小,对于 HotSpot 没用。栈容量只由-Xss 参数设定。

/**
 * VM Args: -Xss128k
 * @author Administrator
 *
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length: " + oom.stackLength);
            throw e;
        }
    }
}

抛出异常:

stack length: 1007
Exception in thread "main" java.lang.StackOverflowError
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at com.baoxian.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)

3、运行时常量池溢出

运行时常量池分配在方法区内,可以通过-XX:PermSize 和-XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量。

import java.util.ArrayList;
import java.util.List;
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行为
        List<String> list = new ArrayList<String>;();
        // 10MB的PermSize在integer范围内足够产生OOM了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

异常:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.baoxian.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

运行时常量池溢出,在 java.lang.OutOfMemoryError 后面紧跟着是 PermGen space

4、方法区溢出

方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述符、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。比如动态代理会生成动态类。
使用 CGLib 技术直接操作字节码运行,生成大量的动态类。当前很多主流框架如 Spring 和 Hibernate 对类进行增强都会使用 CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的 Class 可以加载入内存。
异常:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method)

同样,跟常量池一样,都是 PermGen space 字符串出现
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是非常苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用 GCLib 字节码技术外,常见的还有:大量 JSP 或动态产生的 JSP 文件的应用(JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 应用等。

5、本机直接内存溢出

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

/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while(true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}

在 OutOfMemoryError 后面不会有任何东西了,这就是 DirectMemory 内存溢出了。

https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. Java 内存区域
    1. 程序计数器
    2. Java 虚拟机栈
    3. 本地方法栈
    4. Java 堆
    5. 方法区
    6. 运行时常量池
    7. 直接内存
  2. 实战:OutOfMemoryError 异常
    1. 1、Java 堆溢出
    2. 2、虚拟机栈和本地方法栈溢出
    3. 3、运行时常量池溢出
    4. 4、方法区溢出
    5. 5、本机直接内存溢出