1. java运行区域
每个jvm只有一个Runtime实例.即为运行时环境,Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
整个类加载及运行时区域划分如下。
具体介绍:
- .java文件被编译后形成.class文件保存在本地硬盘上
- 被类加载器加载到到jvm中被称为DNA元数据模板然后放在方法区
- 再调用这个模板的构造方法创建对象存放在堆中,由这些具体的对象通过getclass可以获得由那个的创建
class文件–>JVM–>元数据模板,此过程由类加载器完成,扮演快递员的操作.
2. 程序计数器
程序计数器( Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器, 各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
这里并非广义上所指的物理寄存器,pc寄存器更加合适,jvm中的pc寄存器是对物理pc寄存器的一种抽象pc寄存器存储的是指向下一条指令的地址,也即是将要执行引擎读取的下一条指令。
实例解释
public class Demo {
public static void main(String[] args) {
int i = 5;
int k = 7;
int l = 8;
System.out.println(l);
System.out.println("hello");
}
}
如下图:
由上图可以看出,pc寄存器的主要作用是保存当前线程的栈中程序运行到的位置。
3. Java虚拟机栈
3.1 定义
Java虚拟机栈( Java Virtual Machine Stacks): 也是线程私有的 ,虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧( Stack Frame[1]) 用于存储局部变量表(八种数据类型或者引用类型类,数组,接口)、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.生命周期和线程一致可以通过虚拟机参数
-Xss
(例如:java -Xss=512M HackTheJava
)来指定一个程序的 Java 虚拟机栈内存大小。当线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError
异常(如:将一个函数反复递归自己,最终会出现这种异常);如果 JVM 栈可以动态扩展(大部分 JVM 是可以的),当扩展时无法申请到足够内存,则会抛出OutOfMemoryError
异常。
以图形方式展示如下
他的访问速度仅次于程序计数器.jvm对java栈的操作只有两个入栈和出栈
不同线程中的栈帧是不能相互引用的,如果当前方法调用了其他方法,方法返回时当前栈帧会返回此方法的执行结果 给前一个栈帧,接着虚拟机弃当前栈帧使前一个栈帧重新成为当前栈,返回包括两种形式一种是异常,一种是return。
每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。图示如下:
每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A reference to the run-time constant pool)、方法返回地址(Return Address)和附加信息。
下面对栈帧中的结构进行介绍。
3.2 局部变量表
局部变量表:定义为一个数字数组主要用于存储方法中定义的局部变量,返回值类型以及方法的参数存放在这张表中,这些数据包括各类的基本数据类型、对象引用,对象引用类型它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
public class Demo {
public Demo() {
}
public static void main(String[] args) {
int i = true;
int k = true;
int l = 8;
System.out.println(l);
System.out.println("hello");
}
}
反编译后的class文件。
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 args [Ljava/lang/String;
2 22 1 i I
5 19 2 k I
8 16 3 l I
- 局部变量表所需要的容量在编译器被确定下来,在方法运行期间不会更改。
- 其中参数的存放总是在局部变量数组中的index0开始到数组长度-1的索引结束。
- 局部变量表的最基本的存储单元是slot(变量槽,32位以内占据一个变量槽,64位类型占据两个变量槽),每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,当一个实例方法被调用时,他的方法参数和方发体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
- 当虚拟机要使用局部变量表里的数据时通过索引来定位,默认从0开始,由于long和double占用两个局部变量所以它的索引较特殊,取决于最小的那个值,比如某个long类型数据在索引n和n+1里存储了,那么它对应的索引值就是n.
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列
注: 局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收。
3.3 操作数栈
在方法执行的过程中,根据字节码指令,往栈中写入数据或者取出数据,即出栈和入栈.
-
主要用于存储计算过程的临时结果,同时作为计算过程中变量临时的存储空间
-
如果被调用的方法具有返回值的话,其返回值将会被压入当前栈帧的操作数栈中。
-
解释引擎是基于栈的执行引擎,其中栈指的就是操作数栈
// java 代码
public void test() {
byte a = 1;
short b = 1;
int c = 1;
long d = 1L;
float e = 1F;
double f = 1D;
char g = 'a';
boolean h = true;
}
// 字节码指令
0: iconst_1 // 把 a 压入操作数栈栈顶
1: istore_1 // 将栈顶的 a 存入局部变量表索引为1的 Slot
2: iconst_1 // 把 b 压入操作数栈栈顶
3: istore_2 // 将栈顶的 b 存入局部变量表索引为2的 Slot
4: iconst_1 // 把 c 压入操作数栈栈顶
5: istore_3 // 将栈顶的 c 存入局部变量表索引为3的 Slot
6: lconst_1 // 把 d 压入操作数栈栈顶
7: lstore 4 // 将栈顶的 d 存入局部变量表索引为4的 Slot,由于 long 是64位,所以占2个 Slot
9: fconst_1 // 把 e 压入操作数栈栈顶
10: fstore 6 // 将栈顶的 e 存入局部变量表索引为6的 Slot
12: dconst_1 // 把 f 压入操作数栈栈顶
13: dstore 7 // 将栈顶的 f 存入局部变量表索引为4的 Slot,由于 double 是64位,所以占2个 Slot
15: bipush 97 // 把 g 压入操作数栈栈顶
17: istore 9 // 将栈顶的 g 存入局部变量表索引为9的 Slot
19: iconst_1 // 把 h 压入操作数栈栈顶
20: istore 10 // 将栈顶的 h 存入局部变量表索引为10的 Slot
从上面字节码指令可以看出来,除了 long、double、float 类型使用的字节码指令不是
iconst
和istore
,其他类型都是使用这两个字节码指令操作,说明 byte、short、char、boolean 进入操作数栈时,都会被转化成 int 型。
3.4 动态链接
栈顶缓存技术
将栈顶的元素全部缓存在物理cup的寄存器中,以此降低对内存的读、写次数,提升执行引擎的执行效率
动态链接
每个栈帧都包含一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所有属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
在 Class 文件格式的常量池(存储字面量和符号引用)**中存有大量的符号引用(1.类的全限定名,2.字段名和属性,3.方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
看看以下代码的 Class 文件格式的常量池:
// java 代码
public Test test() {
return new Test();
}
// 字节码指令
public class jvm.DynaicLink
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #10 // jvm/DynaicLink
super_class: #11 // java/lang/Object
interfaces: 0, fields: 1, methods: 4, attributes: 1
Constant pool:
#1 = Methodref #11.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #10.#27 // jvm/DynaicLink.i:I
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #30 // 1
#5 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #33 // 2
#7 = Methodref #10.#34 // jvm/DynaicLink.test1:()V
#8 = Methodref #10.#35 // jvm/DynaicLink.test2:()V
#9 = String #36 // 3
#10 = Class #37 // jvm/DynaicLink
#11 = Class #38 // java/lang/Object
#12 = Utf8 i
#13 = Utf8 I
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Ljvm/DynaicLink;
#21 = Utf8 test1
#22 = Utf8 test2
#23 = Utf8 test3
#24 = Utf8 SourceFile
#25 = Utf8 DynaicLink.java
#26 = NameAndType #14:#15 // "<init>":()V
#27 = NameAndType #12:#13 // i:I
#28 = Class #39 // java/lang/System
#29 = NameAndType #40:#41 // out:Ljava/io/PrintStream;
#30 = Utf8 1
#31 = Class #42 // java/io/PrintStream
#32 = NameAndType #43:#44 // println:(Ljava/lang/String;)V
#33 = Utf8 2
#34 = NameAndType #21:#15 // test1:()V
#35 = NameAndType #22:#15 // test2:()V
#36 = Utf8 3
#37 = Utf8 jvm/DynaicLink
#38 = Utf8 java/lang/Object
#39 = Utf8 java/lang/System
#40 = Utf8 out
#41 = Utf8 Ljava/io/PrintStream;
#42 = Utf8 java/io/PrintStream
#43 = Utf8 println
#44 = Utf8 (Ljava/lang/String;)V
void test3();
descriptor: ()V
flags: (0x0000)
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: aload_0
11: invokevirtual #7 // Method test1:()V
14: aload_0
15: invokevirtual #8 // Method test2:()V
18: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
21: ldc #9 // String 3
23: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: return
LineNumberTable:
line 17: 0
line 18: 10
line 19: 14
line 20: 18
line 21: 26
LocalVariableTable:
Start Length Slot Name Signature
0 27 0 this Ljvm/DynaicLink;
public class DynaicLink {
int i = 1;
void test1(){
System.out.println("1");
}
void test2(){
System.out.println("2");
}
void test3(){
i++;
test1();
test2();
System.out.println("3");
}
}
方法调用
静态链接(非虚方法)
- 如果方法在编译期间就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法被称为非虚方法。比如:静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
虚拟机中几条方法调用指令
- invokestatic:调用静态方法,解析阶段确定唯一版本
- invokespecial:调用init方法,私有及父类方法,解析阶段确定唯一版本
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
动态调用指令
- invokedynamic:动态解析出需要调用的方法,然后执行
3.5 方法返回地址和附加信息
方法返回地址
本质上,方法的退出就是当前栈帧出栈的过程。此时需要回复上层方法的局部变量表、操作数栈、将返回地址压入调用者栈帧的操作数栈、设置pc寄存器的值,让调用者方法继续执行下去,异常退出的话不会给它的上层调用者产任何信息。
存放调用该方法的pc寄存器,
方法退出后都返回到该方法的被调用的位置,正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令你的吓一跳指令地址。异常退出栈帧中一般不会保存这部分信息。
- :每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。例如只有在程序运行的时候才能确定某变量的类型。
- 当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。
4. 本地方法接口和本地方法栈
4.1 本地方法接口
简单的讲,一个native method就是一个java调用非java代码的接口,native method是一个由非java语言实现的的方法。比如c语言
/* Some private helper methods 这些都是本地方法,非java编写*/
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);
使用native method方法的原因:
- 又是java应用需要与外面的环境交互,这是本地方法存在的主要原因。
- 与操作系统打交道,通过本地方法,我们得以用java实现jre的与底层系统的交互,甚至jvn的一部分都是c写的。
- sun解释器使用c语言实现,这使得他想一些普通的c一样与外部交互。
注:目前该方法越来越少,除非与硬件有关的应用
4.2 本地方法栈
本地方法不是用 Java 实现,对待这些方法需要特别处理。与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。和 JVM 栈一样,这个区域也会抛出
StackOverflowError
和OutOfMemoryError
异常。
java虚拟机栈用来管理java方法的调用,本地方法栈用来管理本地方法的调用,本地方法栈也是线程私有的,当某个线程调用一个本地方法时,他就进入了一个全新的并且不再收虚拟机限制的世界,他和虚拟机拥有同样的权限。
- 本地方法可以通过本地接口来访问虚拟机内部的运行时数据区。
- 他可以直接使用本地处理器的寄存器。
- 直接从本地内存的对中分配任意数量的内存。
下篇将介绍另一个重要的区域堆。