JVM与DVM(一)程序运行时内存如何分配

Java 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分为不同的数据区域。


  • HelloWorld.java 文件首先需要经过编译器编译,生成 HelloWorld.class 字节码文件。
  • Java 程序中访问HelloWorld这个类时,需要通过 ClassLoader(类加载器)将HelloWorld.class 加载到 JVM 的内存中。
  • JVM 中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。

1.1 程序计数器(Program Counter Register)

Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。“程序计数器”是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。

关于程序计数器还有几点需要格外注意:

  • 在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。
  • 线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
  • 当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。

1.2 虚拟机栈

虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:

StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。

OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。

JVM 是基于栈的解释器执行的,DVM 是基于寄存器解释器执行的。

上面这句话里的“基于栈”指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧,接下来看下这个栈帧是什么。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。

我们可以这样理解:一个线程包含一个虚拟机栈,一个虚拟机栈包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等。如下图所示:

局部变量表

局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 locals 数据项中,确定该方法需要分配的最大局部变量表的容量。

1
2
3
4
5
public static int add(int k) {
int i = 1;
int j = 2;
return i + j + k;
}

Terminal : javac test.java / javap -v test.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int add(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: iload_0
8: iadd
9: ireturn

上面的 locals=3 就是代表局部变量表长度是 3,也就是说经过编译之后,局部变量表的长度已经确定为3,分别保存:参数 k 和局部变量 i、j。

注意:系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值),也就是说不存在类变量那样的准备阶段。

操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。
同局部变量表一样,操作数栈的最大深度也在编译的时候写入方法的Code属性表中的stacks数据项中。
栈中的元素可以是任意Java数据类型,包括long和double。(比如:iadd指令就是将操作数栈中栈顶的两个元素弹出,执行加法运算,并将结果重新压回到操作数栈中)。

动态链接

动态链接的主要目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
在一个 class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态连接(Dynamic Linking)。

返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法:正常退出 / 异常退出
无论当前方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。


实例讲解

1
2
3
4
5
6
public int add() {
  int i = 1;
  int j = 2;
  int result = i + j;
  return result + 10;
}

Terminal : javac test.java / javap -v test.class

1
2
3
4
5
6
7
8
9
10
11
12
0: iconst_1    (把常量 1 压入操作数栈栈顶)
1: istore_1    (把操作数栈栈顶的出栈放入局部变量表索引为 1 的位置)
2: iconst_2    (把常量 2 压入操作数栈栈顶)
3: istore_2    (把操作数栈栈顶的出栈放入局部变量表索引为 2 的位置)
4: iload_1     (把局部变量表索引为 1 的值放入操作数栈栈顶)
5: iload_2     (把局部变量表索引为 2 的值放入操作数栈栈顶)
6: iadd        (将操作数栈栈顶的和栈顶下面的一个进行加法运算后放入栈顶)
7: istore_3    (把操作数栈栈顶的出栈放入局部变量表索引为 3 的位置)
8: iload_3     (把局部变量表索引为 3 的值放入操作数栈栈顶)
9: bipush 10   (把常量 10 压入操作数栈栈顶)
11: iadd       (将操作数栈栈顶的和栈顶下面的一个进行加法运算后放入栈顶)
12: ireturn    (结束)
  • iconst 和 bipush,这两个指令都是将常量压入操作数栈顶,区别就是:当 int 取值 -1-5 采用 iconst 指令,取值 -128-127 采用 bipush 指令。
  • istore 将操作数栈顶的元素放入局部变量表的某索引位置,比如 istore_5 代表将操作数栈顶元素放入局部变量表下标为 5 的位置。
  • iload 将局部变量表中某下标上的值加载到操作数栈顶中,比如 iload_2 代表将局部变量表索引为 2 上的值压入操作数栈顶。
  • iadd 代表加法运算,具体是将操作数栈最上方的两个元素进行相加操作,然后将结果重新压入栈顶。

Add.java 被编译成 Add.class 的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的 Code 属性中。

1.3 本地方法栈

本地方法栈和上面介绍的虚拟栈基本相同,只不过是针对本地(native)方法。在开发中如果涉及 JNI 可能接触本地方法栈多一些,在有些虚拟机的实现中已经将两个合二为一了(比如HotSpot)。

1.4 堆

Java 堆(Heap)是 JVM 所管理的内存中最大的一块,该区域唯一目的就是存放对象实例,几乎所有对象的实例都在堆里面分配,因此它也是 Java 垃圾收集器(GC)管理的主要区域,有时候也叫作“GC 堆”,同时它也是所有线程共享的内存区域,因此被分配在此区域的对象如果被多个线程访问的话,需要考虑线程安全问题。

按照对象存储时间的不同,堆中的内存可以划分为新生代(Young)和老年代(Old),其中新生代又被划分为 Eden 和 Survivor 区。具体如下图所示:

1.5 方法区

方法区(Method Area)也是 JVM 规范里规定的一块运行时数据区。方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域同堆一样,也是被各个线程共享的内存区域。

1.6 异常再现

StackOverflowError 栈溢出异常

递归调用是造成StackOverflowError的一个常见场景,比如以下代码:

1
2
3
4
public void method( ){
i++;
method( );
}
OutOfMemoryError 内存溢出异常

理论上,虚拟机栈、堆、方法区都有发生OutOfMemoryError的可能。但是实际项目中,大多发生于堆当中。比如以下代码:

1
2
3
4
Arraylist list = new Arraylist();
while(true){
list.add(xxx)
}

参考文章:
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1855