Java虚拟机内存模型学习笔记

运行时数据区(Runtime Data Areas)

Java虚拟机内存模型学习笔记

一、线程私有区域

1、程序计数器(Program Counter Register):

线程私有,生命周期与线程同生灭,当前线程执行的字节码行号指示器。字节码解释器工作时改变该内存值以选择下一条需要执行的字节码指令。若线程执行的是Java方法,则程序计数器记录的是正在执行的虚拟机字节码指令地址;若线程执行Native方法,则计数器值为空(undefined)。该内存区域通常不抛出OOM异常。

Native关键字作用:JNI(Java Native Interface),调用本机方法即非Java程序本身定义的方法,通常由C/C++编写,至当前方法在程序外部定义。Java程序本身无需此关键字。

public static native void sayHello(); //sayHello()为程序外部方法,可为C/C++编译后的文件
2、本地方法栈(Native Method Stack):

服务虚拟机使用到的Native方法,抛出StackOverflowError和OutOfMemoryError异常。

3、虚拟机栈(VM Stack):

线程私有,生命周期与线程同生灭,可抛出StackOverflowError和OOM,是Java方法执行的内存模型。每个Java方法执行过程中均会创建栈帧(Stack Frame)用于存储局部变量区、操作数栈、动态链接、方法返回地址(returnArdess)等。方法调用至结束对应一个栈帧从入栈到出栈过程(遵循后进先出机制:后入栈的栈帧会优先出栈)。

通常说“堆”或“栈”内存模型并不准确,其内存结构远比字面意义复杂。这里的“栈”及虚拟机栈,亦或更多指的是VM Stack中的局部变量区。

Java虚拟机内存模型学习笔记
局部变量表:编译期间(类加载和准备阶段)完成内存分配,存放8种基本数据类型(int、byte、chart、short、boolean、float、long、double)、对象引用(reference)和方法返回地址(returnAdress)类型,入栈时内存大小为固定且已知,方法运行时不会改变大小。

64位长度的long和double类型占用2个局部变量空间,其余数据类型占用1个。

若线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;若虚拟机动态扩展时无法申请到足够的内存,则抛出OOM异常。

/**
 * VM StackOverflowError
 * @author markyellow
 */
public class StackOverflowErrorTest {
    private int stackLength = 0;
    public void stackLeak() throws Exception {
        stackLength++;
        stackLeak(); // 增加栈深度
    }
    public static void main(String[] args) {
        // 设置VM参数测试虚拟机栈StackOverflowError,VM options: -Xss160k(最小值)
        StackOverflowErrorTest sofe = new StackOverflowErrorTest();
        try{
            sofe.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length: " + sofe.stackLength);
            e.printStackTrace();
        }
    }
}

执行结果:

Stack Length: 771
java.lang.StackOverflowError
	at launch.StackOverflowErrorTest.stackLeak(StackOverflowErrorTest.java:13)
    ...

操作数栈:操作数栈也称操作栈,与局部变量区相同,其大小在编译期确定。方法执行过程中,操作数栈栈帧用于存储计算参数和计算结果;方法调用时,操作数栈用于准备调用方法的参数以及接收返回值。

方法刚开始执行时,操作数栈内容为空,在执行过程中,各种不同的字节码指令向操作数栈中写入和读取数据。

二、线程共享区域

1、Java堆(Heap):

线程共享的内存区域,在虚拟机启动时创建,所有的对象实例以及数组都要在堆上分配,是垃圾回收的主要区域,也称为GC堆(Garbage Collected Heap),大小可通过设置虚拟机变量-Xmx和-Xms控制(不设置则表示堆大小可动态扩展)。根据分代收集算法,Java堆可分为新生代(Young)老年代(Old)持久代(Permanent)
Java虚拟机内存模型学习笔记
新生代(Young Generation):分为Eden区和Survivor区,其中Survivor区又可细分为From区和To区,内存大小比例划分默认为Eden:From:To=8:1:1(可通过-XX:SurvivorRatio参数设定)。所有新创建的对象和数组均存放于新生代中,若内存区充满则会触发Minor GC。

由于绝大多数新创建Object存储于Eden Generation中,当该区域内存充满并GC结束后,剩余未被GC的对象将被移动到From区和To区。
Young Gen中的Object会被GC设定一个轮询阈值(可通过-XX:MaxTenuringThreshold配置),当GC次数超出阈值时该对象将被推送到Old Gen中存储。

老年代(Old Generation):存放经过多次Minor GC后仍未被GC的对象,不同于新生代的是,当老年代内存区域充满后,将触发Major GC,耗用时间较长。

老年代中的对象比较稳定,因此Major GC不会太频繁,当Old Gen中的内存也被充满后,程序将抛出OutOfMemoryError异常。

持久代(Permanet Generation):存储类(Class)的元数据,用于描述类及其方法的原始信息,可触发Full GC。类在加载完成后即被存入Perm Gen中,主程序在运行期间不会对该内存区域触发GC,当持久带内存被加载的类元数据充满时,程序将抛出OOM异常。

Java8中,Perm Gen已经被一个称为“元数据区”(元空间)的区域取代。元空间相较持久代,区别在于元空间并不在虚拟机中,而是使用本地内存,从而使加载类的元数据数量不再由-XX:MaxPermSize参数限制,而是由系统实际可用空间控制。

Java虚拟机内存模型学习笔记
Java堆OOM测试代码:

/**
 * Java堆 OutOfMemoryError测试
 * @author markyellow
 */
public class OutOfMemoryErrorTest {
    static class OOMObject {
    }
    public static void main(String[] args) {
        /*
         * 设置VM参数测试OOM
         * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError(VM出现OOM时Dump出当前内存堆转储快照)->dump(垃圾堆)
         * 堆内存溢出,dump提示位置为:Java heap space
         */
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

运行结果为:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid32434.hprof ...
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
    ...
Heap dump file created [27906673 bytes in 0.119 secs]
2、方法区(Method Area):

线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。

HotSpot VM工程人员乐于将其称为Non-Heap(非堆),也叫永久代(Permanent Generation,本质上与堆中的持久带不同)。

Java虚拟机内存模型学习笔记
运行时常量池(RunTime Constant Pool):方法区的一部分,类在编译期产生的各种字面量和符号引用等数据将在类加载后进入运行时常量池。该内存区域具有动态性,除了编译器产生的类数据外,Java程序运行期间亦可能将新的常量放入池中(譬如String.intern()方法)。

/**
 * 运行时常量池测试
 * @author markyellow
 */
public class MethodAreaTest {
    public static void main(String[] args) {
        /*
         * "aaa"存放于字符串运行时常量池中
         * str4为对象引用,存放于堆中
         * str4.intern()将其存放在运行时常量池中
         */
        String str1 = "aaa";
        String str2 = "bbb";
        String str3 = "aaabbb";
        String str4 = str1 + str2;
        String str5 = "aaa" + "bbb";
        String str6 = "aaa" + str2;
        System.out.println(str3 == str4);
        System.out.println(str3 == str4.intern());
        System.out.println(str3 == str5);
        System.out.println(str3 == str6);
    }
}

运行结果:

false
false
true
true
false

附表:常用虚拟机参数变量配置表(参考)
Java虚拟机内存模型学习笔记

参考文献:《深入理解Java虚拟机第二版》——周志明
引用文章:
https://zhuanlan.zhihu.com/p/44694290
https://www.jianshu.com/p/50be08b54bee
https://lixh1986.iteye.com/blog/2351465
https://www.cnblogs.com/ITPower/p/7929010.html