Java字节码详解(二)字节码的运行过程
2018年10月23日 17:31:04 talex 阅读数 677
<article class="baidu_pl" style="box-sizing: inherit; outline: 0px; margin: 0px; padding: 16px 0px 0px; display: block; position: relative; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, "SF UI Text", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: common-ligatures; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">
文章目录
前一章讲述了java字节码文件的生成以及字节码文件中各个字段代表的含义,在本章节将讲述字节码是什么运行的
JVM的一些基础概念
要理解java字节码的运行情况,首先要了解有关JVM的一些知识,这些是java字节码运行的先决条件。
JVM数据类型
Java是静态类型的,它会影响字节码指令的设计,这样指令就会期望自己对特定类型的值进行操作。例如,就会有好几个add指令用于两个数字相加:iadd、ladd、fadd、dadd。他们期望类型的操作数分别是int、long、float和double。大多数字节码都有这样的特性,它具有不同形式的相同功能,这取决于操作数类型。
JVM定义的数据类型包括:
- 基本类型:
- 数值类型: byte (8位), short (16位), int (32位), long (64-bit位), char (16位无符号Unicode), float(32-bit IEEE 754 单精度浮点型), double (64-bit IEEE 754 双精度浮点型)
- 布尔类型
- 指针类型: 指令指针。
- 引用类型:
- 类
- 数组
- 接口
在字节码中布尔类型的支持是受限的。举例来说,没有结构能直接操作布尔值。布尔值被替换转换成 int 是通过编译器来进行的,并且最终还是被转换成 int 结构。Java 开发者应该熟悉所有上面的类型,除了 returnAddress,它没有等价的编程语言类型。类数组接口在字节码中布尔类型的支持是受限的。举例来说,没有结构能直接操作布尔值。布尔值被替换转换成 int 是通过编译器来进行的,并且最终还是被转换成 int 结构。
Java 开发者应该熟悉所有上面的类型,除了 returnAddress,它没有等价的编程语言类型。
JVM的内存结构
Java字节码详解(二)字节码的运行过程JVM的内存分布如上图所示。方法区和堆是线程共享的,而寄存器、java方法栈、本地方法栈是各个线程私有的。
1.方法区
方法区是用来存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
这个区域很少进行垃圾回收,回收目标主要是针对常量池的回收和对类型的卸载。
2.堆
此区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存.
3.PC寄存器
程序计数器是一块较小的内存空间,线程私有。它可以看作是当前线程所执行的字节码的行号指示器
4. Java方法栈和本地方法栈
JVM栈描述的是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息.
Java字节码的运行就是在JVM方法栈中进行的
Java字节码运行过程
简单的示例
1.示例源码
先来看我们的例子代码,源码如下:
public class Test{
public static void main(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = a + b;
}
}
2.main函数的字节码展示
使用javac Test.java
进行编译,然后使用javap -v Test.class
查看该java文件的字节码,为了排除干扰,去除了很多不必要的字节码
*** 省略部分字节码
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #15.#17 // java/lang/Integer.intValue:()I
*** 省略部分字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: aload_1
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: aload_2
15: invokevirtual #3 // Method java/lang/Integer.intValue:()I
18: iadd
19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: astore_3
23: return
3.字节码指令运行过程
接下来分析Code
中字节码运行的过程。这里说一下,每个指令前的数字为指令在寄存器中的偏移量。
0: iconst_1
将int常量1进行放入操作数栈。这里稍微做个拓展,如果将float常量2进行入栈操作,name该指令是fconst_2
,详细的指令种类及意义请查看下一章 Java字节码指令详解。
1: invokestatic #2
调用常量池中序号为#2
的静态方法,这里调用的是 Integer.valueOf()方法,表示将该int类型进行装箱操作,变为Integer类型
4: astore_1
在索引为1的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量a。
5: iconst_2
将int常量2进行放入操作数栈
6: invokestatic #2
调用常量池中序号为#2
的静态方法,这里调用的是 Integer.valueOf()方法,表示将该int类型进行装箱操作,变为Integer类型
9: astore_2
在索引为2的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量b。
10: aload_1
从索引1的本地变量中加载一个int值,放入操作数栈
11: invokevirtual #3
调用常量池中序号为#3
的实例方法,这里调用的是 Integer.intValue()方法
14: aload_2
从索引1的本地变量中加载一个int值,放入操作数栈
15: invokevirtual #3
调用常量池中序号为#3
的实例方法,这里调用的是 Integer.intValue()方法
18: iadd
把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。
19: invokestatic #2
调用常量池中序号为#2
的静态方法,这里调用的是 Integer.valueOf()方法
22: astore_3
在索引为3的位置将第一个操作数出栈(一个Integer值)并且将其存进本地变量,相当于变量c。
23: return
方法结束
方法调用
上面的示例是比较简单的,而且只有一个main函数,接下来将展示在多个函数时候字节码的形式以及运行的具体过程。这里就直接拿参考文章的示例,原文写得真的很好,有条件可以去看英文原文。 字节码的介绍
1.示例源码
public class Test{
public static void main(String[] args){
int a = 1;
int b = 2;
int c = calc(1,2);
}
static int calc(int a,int b){
return (int) Math.sqrt(Math.pow(a,2)+Math.pow(b,2));
}
}
2.字节码展示
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_1
5: iconst_2
6: invokestatic #2 // Method calc:(II)I
9: istore_3
10: return
static int calc(int, int);
descriptor: (II)I
flags: ACC_STATIC
Code:
stack=6, locals=2, args_size=2
0: iload_0
1: i2d
2: ldc2_w #3 // double 2.0d
5: invokestatic #5 // Method java/lang/Math.pow:(DD)D
8: iload_1
9: i2d
10: ldc2_w #3 // double 2.0d
13: invokestatic #5 // Method java/lang/Math.pow:(DD)D
16: dadd
17: invokestatic #6 // Method java/lang/Math.sqrt:(D)D
20: d2i
21: ireturn
3. 指令执行过程详解
上面就是main方法和calc方法的字节码,由于main方法的指令跟上个例子很相似,唯一不同的是 c=a+b
变为由calc方法去执行并且返回。这里就不再赘述main方法,接下来主要讲解calc方法的执行过程。
0: iload_0
将方法中第一个参数入栈
1: i2d
将int类型转为double类型
2: ldc2_w #3
将常量池序号为#3
的long型常量从常量池推送至栈顶(宽索引)
5: invokestatic #5
调用静态方法:Math.pow:() ,并且将结果放入栈顶
8: iload_1
9: i2d
10: ldc2_w #3
13: invokestatic #5
以上的指令跟上一个一样,进行平方运算
16: dadd
将result和result2相加,并推入栈顶
17: invokestatic #6
调用Math.sqrt()方法
20: d2i
将double类型转为int类型
21: ireturn
返回int类型的数值
实例调用
修改上面的代码,加入对象,并调用对象的方法。
public class Test {
public static void main(String[] args){
Point a =new Point (1,2);
Point b = new Point (3,4);
int c = a.area(b);
}
static class Point{
private int x;
private int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
public int area(Point p){
int length = Math.abs(p.y-this.y);
int width = Math.abs(p.x-this.x);
return length*width;
}
}
}
使用javap -v Test
查看编译后的字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: new #2 // class Test3$Point
3: dup
4: iconst_1
5: iconst_2
6: invokespecial #3 // Method Test3$Point."<init>":(II)V
9: astore_1
10: new #2 // class Test3$Point
13: dup
14: iconst_3
15: iconst_4
16: invokespecial #3 // Method Test3$Point."<init>":(II)V
19: astore_2
20: aload_1
21: aload_2
22: invokevirtual #4 // Method Test3$Point.area:(LTest3$Point;)I
25: istore_3
26: return
这个main方法比上一个例子多了几个新的指令:new
,dup
,invokespecial
-
new
new 指令与编程语言中的 new 运算符类似,它根据传入的操作数所指定类型来创建对象(这是对 Point 类的符号引用)。 -
在这里插入图片描述dup
dup指令会复制顶部操作数的栈值,这意味着现在我们在栈顶部有两个指向Point对象的引用。
-
在这里插入图片描述iconst_1
,iconst_2
,invokespecial
,将x,y的值(1,2)压入栈顶,接下来进行Point初始化工作,将x,y的值进行赋值。初始化完成后会将栈顶的三个操作引用销毁,只留下最初的Point的对象引用。
-
在这里插入图片描述astore_1
将该Point引用出栈,并将其赋值到索引1所保存的本地变量(astore_1中的a表明这是一个引用值)
接下来进行第二个Point实例的初始化和赋值操作
在这里插入图片描述
-
20: aload_1
,21: aload_2
将a,b的Point实例的引用入栈 -
22: invokevirtual #4
调用area
方法, -
25: istore_3
将返回值放入索引3中(即赋值给c) -
return
方法结束
总结
本章节写了字节码运行的详细过程,详细的指令介绍在下一章,有兴趣可以看看。
</article>
网友评论