运行时数据区
JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,如下图所示
程序计数器
线程私有,记录当前线程所执行字节码指令的地址,控制着程序的运行、跳转、恢复,若执行的是Native方法则为空,此区域不会出现OutOfMemoryError
虚拟机栈
线程私有,栈内每一个元素称为栈帧,JVM以方法作为最基本的执行单元,而一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
栈帧包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息,其所需内存在编译时已计算出来,并且写入到方法表的Code属性
- 当前栈帧:位于栈顶的栈帧,执行引擎所运行的所有字节码指令都只针对当前栈帧进行
- 当前方法:栈顶栈帧所关联的方法
局部变量表
局部变量表(Local Variables Table)用于存放方法参数和内部定义的局部变量,Class文件中Code属性的max_locals项中确定了该方法所需分配的局部变量表的最大容量
局部变量表的容量以32位的Slot为单位
- 占用一个Solt:boolean、byte、char、short、int、float
- 占用两个Solt:long和doule,高位对齐
- reference长度取决于使用的虚拟机位数
通过索引使用局部变量表,取值范围为[0,max_locals-1],先将实参按照顺序依次占用Slot(非static方法this占用第0位),再根据方法体内部定义的变量顺序和作用域分配其余的Slot
Slot可复用,当PC计数器的值已经超出了某个变量的作用域,其占用的Slot就会被其他变量复用,但有可能会影响gc
public class Test {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
}
如上,分配了64MB内存,调用gc,如下,超过变量作用域但并没有回收内存
修改上面程序,调用gc前加入变量a
public class Test {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
}
下图可看到内存被回收,第一份代码虽超出placeholder的作用域,但后面未发生对局部变量表的读写操作,其占用的Slot还没被复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联
故若一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存但实际上已经不会再使用的变量,可手动将其设置为null值进行回收(但不推荐)
操作数栈
操作数栈用于存放指令运行所需的操作数,其最大深度在编译的时候被写入到Code属性的max_stacks数据项
在方法的执行过程中,会有字节码指令往操作数栈中写入和提取内容,如执行iadd整数加法指令时,把栈顶的两个int值出栈并相加,然后将结果重新入栈
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,如iadd执行时,栈顶的两个元素的数据类型必须为int型
概念模型中栈帧是完全独立的,但实际JVM实现时,会让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起节约空间,实现在方法调用时共享数据,避免额外的参数复制传递
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,为了支持方法调用时的动态连接(Dynamic Linking)
字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数
- 若在类加载阶段或者第一次使用时被转化为直接引用,称为静态解析
- 若在运行期间转化为直接引用,称为动态连接
方法返回地址
方法退出有两种方式
- 遇到方法返回字节码指令,可带返回值
- 方法执行过程出现异常且未解决,无返回值
无论何种退出方式,方法退出后都需返回方法被调用的位置继续执行(可能需要在栈帧中存储信息用于恢复其调用方法的执行状态)
- 方法正常退出时,其调用方法的PC计数器的值就可以作为返回地址,存于栈帧
- 方法异常退出时,返回地址由异常处理器表确定,栈帧中不保存
附加信息
与调试、性能收集相关的信息,取决于具体的虚拟机实现
本地方法栈
线程私有,类似于虚拟机栈,区别在于本地方法栈用于native方法,HotSpot虚拟机不区分虚拟机栈和本地方法栈
Java堆
线程共享,存放对象实例,可以划分出多个Thread Local Allocation Buffer(TLAB),堆可固定大小或者扩展(通过-Xmx和-Xms设定最大最小值)
方法区
线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
JDK8之前的永久代不同于方法区,只不过当时把分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样垃圾收集器能够像管理Java堆一样管理这部分内存
运行时常量池是方法区的一部分,Class文件存在常量池表,用于存放编译时产生的各种字面量和符号引用,这部分内容将在类加载后存放在运行时常量池,除此之外,也可以动态添加常量到池中,如String的intern()方法