网站首页 > java教程 正文
Java虚拟机是整个Java平台的基石,是Java技术用以实现硬件无关和操作系统无关的关键部分,是Java语言生成极小体积的编译代码的运行平台,是保障用于机器免于恶意代码损害的屏障。
01 运行时数据区
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。而另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
PC寄存器
PC寄存器(program counter)也称为程序计数器。Java虚拟机可以支持多条线程同时执行,为了保证线程切换后能恢复到正确的执行位置,每一条Java虚拟机线程都有自己的pc寄存器,各个线程之间互相不影响,独立存储。我们称这类内存区域为“线程私有”内存。
在任意时刻,一条Java虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法我们把它称为当前方法。如果这个方法不是native的,那么pc寄存器保存的就是Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那么pc寄存器的值是undefined。
此内存区域是唯一一个Java虚拟机中没有规定任何OutOfMemoryError(之后全部简称为OOM)情况的区域。
Java虚拟机栈
每一条Java虚拟机线程都有自己私有的Java虚拟机栈(Java Virtual Machine Stack),这个栈与线程同时创建同时销毁,因此此内存区域与pc寄存器一样也是属于“线程私有”内存。
Java虚拟机栈用来存储栈帧。Java虚拟机规范既允许Java虚拟机栈被实现成固定大小,也允许根据计算动态扩展和收缩。如果采用固定大小的Java虚拟机栈,那么每一个线程的Java虚拟机容量可以在线程创建的时候独立选定。
Java虚拟机栈可能发生的异常情况:
- 如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出OOM异常。
本地方法栈
Java虚拟机实现可能会使用到传统的栈来支持native方法(使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈。这个栈一般会在线程创建的时候按照线程分配。
Java虚拟机规范允许本地方法栈实现固定大小或者根据计算来动态扩展和收缩。如果采用固定大小的本地方法栈,那么每一个线程的本地方法容量可以在创建栈的时候独立选定。
本地方法栈可能发生的异常情况:
- 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。
- 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将抛出OOM异常。
Java堆
在Java虚拟机中,堆是可供各个线程共享的运行时内存区域,也是提供所有类实例和数组对象内存分配的区域。
Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统也就是垃圾收集器所管理的各种对象,这些受管理的对象无需也无法显式地销毁。
Java堆的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩(提供最大和最小容量设置),Java堆所使用的内存不需要保证是连续的。
Java堆可能发生的异常情况:
- 如果实际所需的堆超过了自动内存管理系统所提供的最大容量,那么Java虚拟机将会抛出OOM异常。
方法区
在Java虚拟机中,方法区是可供各个线程共享的运行时内存区域。它存储了每一个类的结构信息,比如运行时的常量池、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
运行时常量池是class文件中每一个类或者接口的常量池表的运行时表示形式,它包括了若干种不同的常量,从编译器可知的数值字面量到必须在运行期解析后才能获得的方法或者字段引用。
方法区在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集和压缩。
方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩(提供最大和最小容量设置),方法区所使用的内存不需要保证是连续的。
方法区可能发生的异常情况:
- 如果方法区的内存空间不能满足内存分配请求,那么Java虚拟机将会抛出OOM异常。
02 栈帧结构
栈帧(frame)是用来存储数据和部分过程结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁(无论方法是正常结束还是异常完成)。栈帧的存储空间由创建它的线程分配在Java虚拟机中,每个栈帧都有自己的本地变量表、操作数栈和指定当前方法所属的类的运行时常量池。
本地变量表和操作数栈的容量在编译器确定,并通过相关方法的code属性保存及提供给栈帧使用。
在某条线程指定过程中的某个时间点上,只有目前正在执行的那个方法的栈帧是活动的,这个栈帧被称为当前栈帧,这个栈帧对应的方法被称为当前方法,定义这个方法的类称为当前类。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的局部变量表和操作数栈进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那么这个方法的栈帧就不再是当前栈帧了。调用新的方法时,新的栈帧会随之而创建,并且会随着程序控制权移交到新方法而成为新的当前栈帧。方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
局部变量表
每个栈帧内部都包含一组称为局部变量表的列表。栈帧局部变量表的长度由编译器决定,并且存储于类或者接口的二进制表示之中,即通过方法的code属性保存及提供给栈帧使用。
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference或者returnAddress的数据。两个局部变量可以保存一个类型为long或者double的数据。
局部变量使用索引来进行定位访问。首个局部变量的所以值为0。局部变量的索引值是个整数,大于0且小于局部变量表的长度。
Java虚拟机使用局部变量表来完成方法调用时的参数传递。当调用类方法时,它的参数将会依次传递到局部变量表中从0开始的连续位置上。当调用实例方法时,第0个局部变量一定用来存储该实例方法所在的对象的引用(即Java中的this关键字)。后续的其他参数将会传递到局部变量表中从1开始的连续位置上。
我们通过一段代码查看不同的实例方法参数在局部变量表的索引值:
public class Example {
static int addTwoStatic(int i, int j) {
return i+j;
}
public int addTwo(int i, int j) {
return i+j;
}
}
javap -c查看字节码指令:
public class com.study.test.code.girl.base.jvm.Example {
public com.study.test.code.girl.base.jvm.Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
static int addTwoStatic(int, int);
Code:
0: iload_0 //局部变量表索引从0开始
1: iload_1
2: iadd
3: ireturn
public int addTwo(int, int);
Code:
0: iload_1 //局部变量表索引从1开始
1: iload_2
2: iadd
3: ireturn
}
操作数栈
每个栈帧内部包含一个称为操作数栈的后进先出的栈。栈帧中操作数栈的最大深度由编译器决定,并且通过方法的code属性保存及提供给栈帧使用。
栈帧在刚刚创建时,操作数栈是空的。Java虚拟机提供一些字节码指令从局部变量表或者对象的实例字段中复制常量或者变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接收方法的返回结果。
在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位的栈深度。
我们通过代码查看两个数相加时,局部变量表和操作数栈之间的交互:
public class AddTest {
public AddTest(){
super();
}
public int add(){
int a = 1;
int b = 2;
int c = a+b;
return c;
}
public static void main(String[] args) {
AddTest addTest = new AddTest();
System.out.println(addTest.add());
}
}
javap -c 查看字节码指令:
大部分Java虚拟机的指令都是操作从栈中弹出的值,而不是局部变量本身,所以在字节码中我们可以看到大量的局部变量表和操作数栈之间传递值的指令。
public class com.study.test.code.girl.base.jvm.AddTest {
public com.study.test.code.girl.base.jvm.AddTest();
Code://这里是code属性
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add();
Code://这里是add方法的code属性
0: iconst_1
1: istore_1 //从操作数栈弹出一个int类型的值,并保存到局部变量
2: iconst_2
3: istore_2
4: iload_1 //将第一个局部变量表值压入到操作数栈
5: iload_2
6: iadd //将栈顶的两个元素相加
7: istore_3 //将int类型的数据从操作数栈存储到局部变量表
8: iload_3
9: ireturn //方法返回
public static void main(java.lang.String[]);
Code://这里是main方法的code属性
0: new #2 //new关键字创建对象实例
3: dup
4: invokespecial #3 //调用对象的构造函数 // Method "<init>":()V
7: astore_1
8: getstatic #4
11: aload_1 //从局部变量表将reference类型的数据加载到操作数栈
12: invokevirtual #5 //调用对象的实例方法 // Method add:()I
15: invokevirtual #6
18: return
}
动态链接
每个栈帧内部都包含一个指定当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。在class文件里面,一个方法若要调用其他方法,或者访问成员变量,则需要通过符号引用来表示,动态链接的作用就是把这些符号引用所表示的方法转换为实际方法的直接引用。
方法调用正常完成
方法调用正常完成是指在方法的执行过程中,没有抛出任何异常,包括直接从Java虚拟机中抛出的异常以及在执行时通过throw语句显式抛出的异常。
如果当前方法调用正常完成,它可能会返回一个值给调用它的方法。方法的正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令,使用哪种返回指令取决于方法返回值的数据类型。在这种场景下,当前栈帧承担着恢复调用者状态的责任,包括恢复调用者的局部变量表和操作数栈,以及正确递增程序计数器。调用者的代码在被调用方法的返回值压入调用者栈帧的操作数栈后会继续正常执行。
方法调用异常完成
方法调用异常是指在方法的执行过程中,某些指令导致了Java虚拟机异常,并且虚拟机抛出的异常在该方法中没有办法处理或者在执行过程中遇到了athrow字节码指令并显式的抛出异常,同时该方法内部没有捕获异常。如果方法调用异常,那么一定没有返回值给调用者。
03 虚拟机异常
Java虚拟机中的异常使用Throwable或者其子类的实例来表示,抛异常的本质实际上是程序控制权的一种即时的,非局部的转换--从异常抛出的地方转换至处理异常的地方。
绝大多数的异常都是由于当前线程执行的某个操作导致的,这种异常我们称为同步异常。那么在程序执行过程中随时发生的异常我们称为异步异常。
Java虚拟机异常出现的原因:
- athrow字节码指令被执行;
- 虚拟机同步检测到程序发生了非正常的执行情况,这时异常必将紧着着在发生非正常执行情况的字节码指令之后抛出,而不会在程序执行的过程中随机抛出。比如数组下标越界、连接异常、OOM等;
- 调用了thread的stop方法而导致其他线程异常或者Java虚拟机实现内部发生了异常。
我们之前学习synchronized关键字的时候了解到如果同步块发生异常,锁也是会释放。我们通过字节码查看其实现:
public void fun(String name){
synchronized (name){
System.out.println(name);
}
}
正常情况下即程序没有抛出异常,字节码就是3进入代码块,12退出代码块。但是程序异常的时候为了保证锁一定会释放我们看到了18针对异常情况退出,并通过athrow指令抛出异常信息。
我们通过Exception table里面的内容可以看到异常类型是any。
public class com.study.test.code.girl.base.jvm.Example2 {
public com.study.test.code.girl.base.jvm.Example2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void fun(java.lang.String);
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter //进入同步代码块
4: getstatic #2
7: aload_1
8: invokevirtual #3
11: aload_2
12: monitorexit //退出同步代码块
13: goto 21
16: astore_3
17: aload_2
18: monitorexit //这里就是针对异常情况下的退出
19: aload_3
20: athrow //athrow关键字抛出异常
21: return
Exception table:
from to target type
4 13 16 any
16 19 16 any
}
04 总结
Java虚拟机运行时数据区域包括:Java堆、方法区、虚拟机栈、本地方法栈和程序计数器。
Java堆是GC主要管理的区域,因此又称为GC堆。
程序计数器是内存中比较小的一块区域,也是唯一不会发生OOM的区域。
虚拟机栈主要用于存储栈帧,栈帧是随着方法的创建而生成的,包含:局部变量表、操作数栈、动态连接、方法返回地址。
本地方法栈主要用于存储非Java语言编写的代码运行信息。
方法区是类加载时的数据存储区域。
猜你喜欢
- 2024-11-27 大小仅1M的SHP文件读写APP Shapefile over Map
- 2024-11-27 「深入理解Java虚拟机」第二章 Java内存区域与内存溢出异常
- 2024-11-27 vue上传大文件的解决方案
- 2024-11-27 Springboot+VUE+MiniO来优雅实现文件存储
- 2024-11-27 Tomcat项目内存参数调优
- 2024-11-27 Java应届毕业生应该掌握哪些技能
- 2024-11-27 VUE-超大文件上传-如何上传文件-大文件上传
- 2024-11-27 一个文件占多少内存?看字节
- 2024-11-27 Springboot项目修改文件传输(minio)限制大小
- 2024-11-27 「编译引擎」-学习阅读Class文件结构(javap版)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)