JVM:运行时数据区

Posted by danner on March 6, 2018

本文探讨 JVM 运行时的数据区,希望你有 cpu 执行指令的基础知识

环境:JDK8

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间:有的区域随着虚拟机进程的启动而存在(全局),有的区域则依赖用户线程的启动和结束而建立和销毁(局部)。按虚拟机规范,如图大致可以分为

  • PC register:程序计数器,指向下一条要执行字节码的行数,功能类似 CPU 中的 PC寄存器(线程私有)
  • Java Virtual Machine Stacks: Java 虚拟机栈存储方法体的中变量、操作数栈、方法出口等信息(线程私有)
  • Heap: 虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域
  • MetaSpace: 元数据区,存放类信息、常量、静态变量、即时编译器编译后的代码等数据(线程共享)
  • Native Method Stacks: 本地方法栈与虚拟机栈作用类似,区别在于虚拟机栈执行字节码,而本地方法栈执行 Native 方法服务(线程私有)

PC register

程序计数器( Program Counter Register ) 是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选定下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是 Native 方法,这个计数器值为空(Undefined)

此内存区域是唯一一个在 Java 虚拟机规范中不会产生 OutOfMemeryError 情况的区域

Java Virtual Machine Stacks

虚拟机栈也是线程私有,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的存模型:每个方法在执行的同时都会创建一个栈帧(类似 cpu 中的 BP 和 SP 指针之间的内存地址)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间完全是确定的,在方法运行期间不会改变局部变量表的大小。

异常:

  • StackOverflowError:如果线程请求的的栈深度大于虚拟机所允许的深度(默认 1M)
  • OutOfMemoryError:当然现代的虚拟机都可以设置动态扩展,但动态扩展时无法申请到足够的内存

StackOverflowError 案例

/**
 * VM Args: -Xss128k
 * 设置 虚拟机栈大小 128k
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

    /**
     * 递归没有出口点,必定栈溢出
     */
    public void stackLeak(){
        stackLength ++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable{
        JavaVMStackSOF javaVMStackSOF = new JavaVMStackSOF();
        try {
            javaVMStackSOF.stackLeak();
        }catch (Throwable e){
            System.out.println("stack length: " + javaVMStackSOF.stackLength);
            throw e;
        }
    }
}

运行输出

Exception in thread "main" java.lang.StackOverflowError
stack length: 989
at com.danner.bigdata.jvm.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
......

OutOfMemoryError 案例

/**
 * VM Args: -Xss2M
 * 设置 虚拟机栈大小 2M,并且使用多线程
 * 每个线程创建都会消耗2M 内存,线程越来越多就会 OOM
 * 当线程数无法削减时,减少栈大小也可解决OOM
 */
public class JavaVMStackOOM {

    private void dontStop(){
        while (true){
        }
    }

    public void stackLeakByThread(){
        while (true){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        new JavaVMStackOOM().stackLeakByThread();
    }
}

运行输出:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:714)
at JavaVMStackOOM.stackLeakByThread(JavaVMStackOOM.java:29)
at JavaVMStackOOM.main(JavaVMStackOOM.java:34)

Native Method Stacks

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别在于虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它( Sun HotSpot 虚拟机把两者合二为一)。

异常

  • StackOverflowError
  • OutOfMemoryError

Heap

对大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块,Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。该内存区域可以存放对象实例,几乎所有的对象实例都在这里分配内存(随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,可以将对象实例分配到其他地方)。

运行时常量池在 JDK8 后被放入 Heap ,Method Area 被取消。Class 文件中除了有关类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于 Class 文件常量池的另一个重要特征是具备动态性,Java 语言并非不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入运行时常量池,运行期间也可以将新的常量池放入池中。

Java 堆是垃圾收集器管理的主要区域;从内存回收的角度看,由于现在的收集器基本都采用分代收集算法,所以 Java 堆中还可以细分:新生代和老生代。根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以按固定大小,也可以是扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。

异常

  • OutOfMemoryError:堆中没有内存完成实例分配,并且堆也无法再扩展时

堆案例

/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 设置JVM 的 Java 堆大小 20M 且不可扩展
 */
public class HeapOOM {

    public static class OOMObject{
    }

    public static void main(String[] args) {

        List<OOMObject> list = new ArrayList<>();

        while (true){
            // 一直创建对象导致 OOM
            list.add(new OOMObject());
        }
    }
}

运行出错:

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.danner.bigdata.jvm.memory.HeapOOM.main(HeapOOM.java:19)

运行时常量池案例

/**
 * Java8 VM Args:-Xms10m -Xmx10m -XX:-UseGCOverheadLimit
 * JDK 8 运行时常量池在 heap 中,设置对大小10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        long i = 0L;
        while(true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

运行输出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.lang.Long.toString(Long.java:399)
at java.lang.String.valueOf(String.java:3113)
at com.danner.bigdata.jvm.memory.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:15)

MetaSpace

在 JDK8 中,原先的 Method Area 被删除替换成 MetaSpace,运行时常量池被放入 Heap。MetaSpace 是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、静态变量、即时编译器编译后的代码等数据。Metaspace 是方法区在 HotSpot 中的实现,它与持久代最大的区别在于:Metaspace 并不在虚拟机内存中而是使用本地内存。因此 Metaspace具体大小理论上取决于32 位/64位系统可用内存的大小,可见也不是无限制的,需要配置参数。

Metaspace 参数

参数名 作用
MetaspaceSize 初始化的 Metaspace 大小,控制 Metaspace 发生 GC 的阈值。GC后,动态增加或者降低 MetaspaceSize,默认情况下,这个值大小根据不同的平台在 12M 到 20M 之间浮动
MaxMetaspaceSize 限制 Metaspace 增长上限,防止因为某些情况导致 Metaspace 无限使用本地内存,影响到其他程序,默认为 4096M
MinMetaspaceFreeRatio 当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机增长 Metaspace 的大小,默认为 40,即 40%
MaxMetaspaceFreeRatio 当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放部分 Metaspace 空间,默认为 70,即 70%
MaxMetaspaceExpanison Metaspace 增长时的最大幅度,默认值为 5M
MinMetaspaceExpanison Metaspace 增长时的最小幅度,默认为 330KB

异常

  • OutOfMemoryError

案例

/**
 * JVM 参数: -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=128m -XX:+PrintFlagsInitial
 * JDK 8 中 即时编译代码是在 Metaspace,指定 Metaspace 空间128m
 */
public class MetaspaceOOM {
    public static void main(String[] args) {
        int i = 0;

        try {
            for (;;) {
                i++;

                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        return proxy.invokeSuper(obj, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("第" + i + "次时发生异常");
            e.printStackTrace();
        }
    }

    static class OOMObject {

    }
}

运行输出

第7893次时发生异常
......
Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	... 8 more

堆外内存

堆外内存并不是虚拟机运行时数据区的一部分,它是本地内存( NIO )不受 Java 堆大小的限制。但既然是内存,肯定还是受到本机总内存大小以及处理器寻址空间的限制。

异常

  • OutOfMemoryError

案例

/**
 * VM Args :-Xmx20M -XX:MaxDirectMemorySize=10M
 * 设置 本地内存 10M
 */
public class NativeMemoryOOM {

    private static final int MB = 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(MB);
        }
    }

}

运行输出:

Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at com.danner.bigdata.jvm.memory.NativeMemoryOOM.main(NativeMemoryOOM.java:21)

HotSpot 虚拟机对象

参考资料

Run-Time Data Areas
深入理解Java虚拟机
Java运行时数据区域
Java虚拟机16:Metaspace
Java8的运行时常量池与MetaSpace
JVM体系结构之七:持久代、元空间(Metaspace) 常量池==了解String类的intern()方法、常量池介绍、常量池从Perm–>Heap
JDK8 的FullGC 之 metaspace