JVM | Java程序如何执行
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM | Java程序如何执行相关的知识,希望对你有一定的参考价值。
-
类文件结构基础
Class文件是一组以8位字节为基础的单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件之中,中间没有任何分隔符。
Class文件存储结构中只有两种数据类型:无符号数和表(表又是由多个无符号数或者其他表构成)。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数是Class类文件的基石。
-
字节码指令基础
-
回顾JVM运行时数据区域
- 方法区:
线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码。
- 堆:
线程共享,存储对象实例。
- 程序计数器:
线程私有,存储当前线程正在执行的虚拟机字节码指令的偏移地址(指令行号)。
- 虚拟机栈:
线程私有,描述Java方法执行的内存模型。
-
方法执行的内存模型
- 局部变量表:
变量存储空间,存放方法参数、方法内定义的局部变量。
- 操作数栈:
执行操作的空间,执行过程中,会有各种字节码指令网操作数栈写入和提取内容,入栈、出栈操作。
- 动态连接:
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。
在每一次运行期间转化为直接引用的符号引用。
相对的在类加载阶段或者第一次使用的时候就转化为直接引用的符号引用,转化过程称为静态解析。
- 返回地址:
方法被调用的位置。
-
局部变量表slot槽复用问题:
public class SlotReuse {
public static void methodA(){
byte[] placeholder = new byte[64 1024 1024];
System.gc();
}
public static void methodB(){
{
byte[] placeholder = new byte[64 1024 1024];
}
System.gc();
}
public static void methodC(){
{
byte[] placeholder = new byte[64 1024 1024];
}
int a = 0;
System.gc();
}
public static void main(String[] args) {
// methodA();
// methodB();
// methodC();
}
}
虚拟机运行参数加上“-verbose:gc”,查看垃圾收集过程。
- 执行methodA();
[GC 69499K->66048K(251392K), 0.0010970 secs]
[Full GC 66048K->65866K(251392K), 0.0098170 secs]
处于变量placeholder的作用域,不敢回收placeholder的内存。
- 执行methodB();
[GC 69499K->66048K(251392K), 0.0011820 secs]
[Full GC 66048K->65866K(251392K), 0.0112650 secs]
逻辑上出了placeholder的作用域,placeholder的内存仍然没有被回收,原因就是离开了placeholder的作用域后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot槽还没有被其他变量复用,作为GC Roots一部分的局部变量表仍然保持着对它的关联。
- 执行methodC();
[GC 69499K->66000K(251392K), 0.0012780 secs]
[Full GC 66000K->330K(251392K), 0.0099390 secs]
placeholder原本所占用的Slot槽被其他变量复用,切断了GC Roots关联,正常垃圾回收。
-
如何找到正确的方法
确定被调用方法的版本(即调用哪一个方法)是方法调用阶段的唯一任务。
- 5种方法调用字节码指令:
1)invokestatic:调用静态方法。
2)invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
3)invokevirtual:调用所有的虚方法。非虚方法以外的都是虚方法,非虚方法包括使用invokestatic、invokespecial调用的方法和被final修饰的方法。
4)invokeinterface:调用接口方法,运行时再确定一个实现此接口的对象。
5)invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
ireturn(返回值是boolean、byte、char、short、int)、lreturn、freturn、dreturn、areturn:方法返回指令。
- 静态解析
方法在程序真正运行之前就有一个可确定的调用版本,并且这个调用版本在运行期不可变。
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类。
public class StaticResolution {
public static void sayHello(){
System.out.println("hello world");
}
public static void main(String[] args) {
sayHello();
}
}
字节码:
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method sayHello:()V
3: return
LineNumberTable:
line 14: 0
line 15: 3
- 静态分派
所有依赖静态类型来定位方法执行版本的分派动作。
分派时机:编译阶段
典型应用:方法重载
e.g.
public class StaticDispatch {
public static void sayHello(short arg){
System.out.println("hello short");
}
public static void sayHello(byte arg){
System.out.println("hello byte");
}
public static void sayHello(Object arg){
System.out.println("hello Object");
}
public static void sayHello(int arg){
System.out.println("hello int");
}
public static void sayHello(long arg){
System.out.println("hello long");
}
public static void sayHello(char arg){
System.out.println("hello char");
}
public static void sayHello(char... args){
System.out.println("hello char...");
}
public static void sayHello(Serializable arg){
System.out.println("hello Serializable");
}
public static void main(String[] args) {
sayHello(‘a‘);
}
}
当前main方法编译字节码:
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 97
2: invokestatic #12 // Method sayHello:(C)V
5: return
LineNumberTable:
line 46: 0
line 47: 5
注释public static void sayHello(char arg)构造方法后编译字节码:
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 97
2: invokestatic #11 // Method sayHello:(I)V
5: return
LineNumberTable:
line 46: 0
line 47: 5
自动转型顺序:char --> int --> long --> float --> double --> Serializable --> Object --> char...(可变长字符)
可以宽化,不可以窄化。
- 动态分派
运行期根据实际类型确定方法执行版本的分派动作。
分派时机:运行阶段
典型应用:方法重写
e.g.
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
字节码:
public static void main(java.lang.String[]);
Signature: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class edu/atlas/demo/java/jvm/DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Man."<init>":()V
7: astore_1
8: new #4 // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
24: new #4 // class edu/atlas/demo/java/jvm/DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method edu/atlas/demo/java/jvm/DynamicDispatch$Human.sayHello:()V
36: return
LineNumberTable:
line 32: 0
line 33: 8
line 34: 16
line 35: 20
line 36: 24
line 37: 32
line 38: 36
- invokevirtual指令的运行时解析过程:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相等的方法,则进行访问权限校验,
如果通过则返回这个方法的直接引用,查找过程结束;
如果不通过,则返回java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。由于invokevirtual指令的第一步就是在运行期间确定接收者的世界类型,所以两次调用的invokevirtual指令把常量池中类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。
- 虚拟机动态分派的实现
上面介绍了动态分派的过程,然而由于动态分派是非常频繁的动作,因此虚拟机实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。常用的稳定优化手段就是为类在方法区建立一个虚方法表(vtable),接口方法建立接口方法表(itable)。
虚方法表存放着各个方法的实际入口地址。
如果某个方法在子类中没有被重写,那么子类的虚方法里面的地址和父类相同方法的地址入口是一致的,都指向父类方的实现入口。
如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
-
如何执行方法内的字节码
- 基于栈的解释器执行过程
e.g.
public static int methodA(int a, int b){
return a + b;
}
public static int methodB(int a, int b){
return a - b;
}
public static int methodC(int a, int b){
return a * b;
}
public static int heavyMethod(){
int a = 200;
int b = 100;
int c = methodC(methodA(a, b), methodB(a, b));
return c;
}
字节码:
public static int methodA(int, int);
Signature: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
LineNumberTable:
line 18: 0
public static int methodB(int, int);
Signature: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: isub
3: ireturn
LineNumberTable:
line 22: 0
public static int methodC(int, int);
Signature: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: imul
3: ireturn
LineNumberTable:
line 26: 0
public static int heavyMethod();
Signature: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=0
0: sipush 200
3: istore_0
4: bipush 100
6: istore_1
7: iload_0
8: iload_1
9: invokestatic #17 // Method methodA:(II)I
12: iload_0
13: iload_1
14: invokestatic #18 // Method methodB:(II)I
17: invokestatic #19 // Method methodC:(II)I
20: istore_2
21: iload_2
22: ireturn
LineNumberTable:
line 128: 0
line 129: 4
line 130: 7
line 140: 21
图例分析:
以上是关于JVM | Java程序如何执行的主要内容,如果未能解决你的问题,请参考以下文章