JVM
Java运行时数据区
运行时数据区主要包括 JVM stacks 、PC寄存器(Program Counter Register)、方法区(Method Area)、本地方法栈(Native Method Stacks)、堆(Heap)。 Java 虚拟机定义了在程序执行期间使用的不同的运行时数据区。有些数据区是线程共享,有的则被线程独享。线程独享的数据区跟随线程的创建退出而创建销毁。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,也可能会导致OOM(OutOfMemoryError)
线程共享的有堆和方法区,每个线程独享pc,虚拟机栈、本地方法栈
PC寄存器
PC:线程私有
先看一下官方文档当中的描述:
PC寄存器用来存储指向下一条指令的地址,也就是将要执行那一条指令。由执行引擎读取系一条指令。
1.是一块很小的内存空间,也是运行最快的存储区域
2.线程私有
3.只会记录当前线程正在执行Java代码的JVM指令地址。如果是native方法,则为空
4.Java虚拟机规范中,这是唯一一块没有OOM的区域。
运行类似于
while(线程运行){
取Pc当中的位置,找到对应的指令;
执行该指令;
pc++;
}
举个例子:现在有线程A,B;线程A运行到一半的时候被B抢占,这时候,pc寄存器就可以记录下A运行到哪,然后在A再次运行的时候就可以从中断的地方开始运行。
JVM Stacks:
JVM Stacks :线程私有
先看一下官方文档当中的描述:
jvm 栈是描述java方法执行的内存模型,它的生命周期和线程相同,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个方法对应一个栈帧。具体类型结构如图:
局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表以变量槽(Variable Slot)为最小单位。特点有以下几个:
1.虚拟机没有明确指定slot应该占的大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。一般是32位。
2.它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化,在64位操作系统中采用填充对齐的方法使其一致
3.long、double这种64位的数据会被分配两个连续Slot空间
4.连续空间可以通过较小的索引来访问数据。
我们知道在java中有两种方法,一种是类方法,一种是实例方法。
在类方法调用中,所有参数都从局部变量0开始在连续的局部变量中传递。
在实例方法调用中,局部变量0始终指向的是该实例对象,也就是this。也就是说真实的参数是从局部变量1开始存储的。
操作数栈
操作数栈(Operand Stack)也常称为操作栈,是一个后入先出的栈(LIFO LAST IN FIRST OUT).操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。
让我们通过两段段代码来理解一下:
1.事先我们得准备一个plugin:jclasslib Bytecode Viewer。然后通过show bytecode with jclasslib 打开文件。
代码 A
public static void main(String[] args) {
int i = 8;
i = i++;
System.out.println(i);
}
//结果为8
代码 B
public static void main(String[] args) {
int i = 8;
i = ++i;
System.out.println(i);
}
//结果为9
A 本地方法表
代码A 点开 方法-code 字节码文件:
可以直接点击指令进入文档对应位置进行阅读。这边讲解几条
bipush
(压栈)
istore_< n>
栈顶元素出栈,放入局部变量表号位
iload_< n>
将局部变量表 n 号位的值 推入操作数栈
iinc
根据索引将局部变量表索引位的数增加常量
我们来根据解释一下 代码A:
1.bipush 8 将8压入操作数栈 , **此时栈内值为8**
2.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
3.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 8 ,操作数栈8**
4.iinc 1 by 1 将局部变量表1号位 +1 **此时局部变量表1号位 9 ,操作数栈8**
5.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
7.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 8 ,操作数栈8**
代码B:
1.bipush 8 将8压入操作数栈 , **此时栈内值为8**
2.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
3.iinc 1 by 1 将局部变量表1号位 +1 **此时局部变量表1号位 9 ,操作数栈空**
4.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 9 ,操作数栈9**
5.istore_1 9 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位9**
……
动态连接
从网上偷张图(https://www.cnblogs.com/caca/p/jvm_stack_frame.html)。
简而言之,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。字节码中的方法调用会用常量池中指向方法的符号引用作为参数,在类加载阶段就转变成直接引用的就是静态解析,在运行期间转换的为动态连接。
JVM提供了五种invoke指令:
invokestatic:调用静态方法。
invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
invokevirtual:(自带多态)调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic:(lambda表达式函数式调用的时候多用到这条) 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
程序运行正常的返回处理
简而言之,将其他栈帧的返回值推入调用方操作数栈,适当修改pc使得跳过调用的指令。
附加信息
Native Method Stacks:本地方法栈:线程私有
本地方法栈和JVM栈大体相同,区别是JVM Stacks是为java方法服务,Native Method Stacks是为本地方法服务。
Heap:堆:线程共享
堆当中保存着所有的对象的实例,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。关于垃圾回收器和垃圾回收算法,下节再讲。
可以通过这几个指令设置堆大小
Method Area:方法区:线程共享
方法区即我们常说的永久代(Perm Space (1.8之前,之后是Meta Space 元空间,注意的是,Perm Space 和Meta Space是方法区不同版本的具体实现,方法区是个抽象的概念)), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。还有运行时常量池(Runtime Constant Pool)
Runtime Constant Pool:运行时常量池
用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
Directory Memory 直接内存
直接内存 ,OS管理,Nio访问的就是直接内存(零拷贝)
参考文档
1.官方文档:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5
2.《深入理解Java虚拟机》