鲁班学院 java架构师成长路线
JVM内存区域
1、程序计数器:
Java 虚拟机可以支持多条线程同时执行,每一条 Java虚拟机线程都有自己的PC寄存器(程序计数器)。在任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法。如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。PC 寄存器的容量至少应当能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。
该区域是JVM运行时数据区中唯一不会发生OOM异常的内存区域。
2、虚拟机栈:
每个线程都有自己的虚拟机栈,栈与线程同时创建,主要用来存储栈帧,一个方法的开始调用到执行结束对应着栈帧的入栈与出栈。虚拟机规范允许虚拟机栈设定为固定大小或是根据计算动态扩展和收缩。如果采用固定大小的 Java 虚拟机栈设计,那每一条线程的 Java 虚拟机栈容量应当在线程创建的时候独立地选定。Java 虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java 虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
2.1、可能会发生的异常:
如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。
如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
2.2、栈帧
一个线程中一个方法的调用与返回对应着栈帧的入栈与出栈,即一个方法对应一个栈帧。而栈帧中主要包括局部变量表、操作数栈、动态连接、完成出口。局部变量表和操作数栈的容量是在编译期确定,并通过方法的 Code 属性(class字节码中指令)保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于 Java 虚拟机的实现和方法调用时可被分配的内存。
2.2.1、局部变量表
一个局部变量可以保存一个类型为 boolean、byte、char、short、float、reference和 returnAddress 的数据,两个局部变量可以保存一个类型为 long 和 double 的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。
Java 虚拟机使用局部变量表来完成方法调用时的参数传递(即参数会放到栈帧的局部变量表中),当一个方法被调用的时候,它的参数将会传递至从 0 开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第 0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即 Java 语言中的“this”关键字)。后续的其他参数将会传递至从 1 开始的连续的局部变量表位置上。
2.2.2、操作数栈
操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java 虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
2.2.3、动态连接
每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接。在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。
由于动态链接的存在,通过晚期绑定使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响。
2.2.4、完成出口
主要是对方法执行结果正常返回或者异常结束的处理。
示例:
package com.example.learn.note.jvm;
public class TestStack {
public static void main(String[] args) {
// 对应main中code为0和1的指令
int a = 1;
// 对应main中code为2和3的指令
int b = 2;
test(a, b);
}
public static int test(int a, int b){
// 对应test中code为0和2的指令
int c = 10;
// 对应test中code为3和4的指令
int d = 2;
int e = (a + b) * c / d;
return e;
}
}
将该类javac编译生成class字节码文件后,对其进行反汇编
D:\project\git-project\learn-note-parent\learn-note-common\target\classes\com\example\learn\note\jvm>javap -c TestStack
警告: 二进制文件TestStack包含com.example.learn.note.jvm.TestStack
Compiled from “TestStack.java”
public class com.example.learn.note.jvm.TestStack {
public com.example.learn.note.jvm.TestStack();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
4: return
public static void main(java.lang.String[]);
Code:
// iconst将一个常量加载到操作数栈
0: iconst_1
// istore将一个数值从操作数栈存储到局部变量表
1: istore_1
2: iconst_2
3: istore_2
// iload将一个局部变量加载到操作栈
4: iload_1
5: iload_2
// invokestatic指令用于调用类方法(static方法)
6: invokestatic #2 // Method test:(II)I
// pop出栈
9: pop
10: return
public static int test(int, int);
Code:
// bipush将一个常量加载到操作数栈
0: bipush 10
2: istore_2
3: iconst_2
4: istore_3
5: iload_0
6: iload_1
// iadd加法指令
7: iadd
8: iload_2
// imul乘法指令
9: imul
10: iload_3
// idiv除法指令
11: idiv
12: istore 4
14: iload 4
16: ireturn
}
3、本地方法栈
和虚拟机栈类似,只是处理的是native修饰的方法
本地方法栈可能发生如下异常情况:
如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java 虚拟机将会抛出一个StackOverflowError 异常。
如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
4、堆
在 Java 虚拟机中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。Java 堆在虚拟机启动的时候就被创建,它存储了被GC所管理的各种对象。
Java 堆可能发生如下异常情况:
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常。
5、方法区
在 Java 虚拟机中,方法区是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区可能发生如下异常情况:
如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常。
6、运行时常量池
运行时常量池是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:
当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。