字节码入门
Hello world
1public class Helloworld { 2 3 public static void main(String[] args) { 4 System.out.println("hello,world"); 5 6 } 7 8}
如果用javap查看此类结构
javap -c Helloworld.class
输出是
1public class com.beetl.myos.ch1.Helloworld { 2 public com.beetl.myos.ch1.Helloworld(); 3 Code: 4 0: aload_0 5 1: invokespecial #8 // Method java/lang/Object."<init>":()V 6 4: return 7 8 public static void main(java.lang.String[]); 9 Code: 10 0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 11 3: ldc #22 // String hello,world 12 5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13 8: return 14}
注意
javap 是java内置的反编译工具,属于jdk 一部分,确保JAVA_HOME 被正确设置,以及JAVA_HOME\bin 被设置成Path。
从javap输出了俩部分,首先是构造函数,用了三个直接码指令。如果熟悉java编程的,就知道,尽管没有为Helloworld提供构造函数,但Java会提供一个默认的构造函数,我们通过反编译类就能看到,有个叫<init>的构造函数,这是程序在编译成class的时候创建的。这三个指令依次是aload_0 invokespecial return
-
aload_0 此指令告诉虚拟机,将局部变量this放入操作栈里。对于每一个方法(构造函数从字节码角度来讲,也是一个方法,并无区别),方法的参数,以及方法中申明的变量都是在编译期间确定好的,按照出现顺序存放在方法栈(method stack)的局部变量表里,位置0 总是默认留给this,其后的位置留给方法的申明参数列表,再之后是留给方法内部用到的局部变量,我们将在下一节会详细介绍指令的数据结构基础。在这,我们只需要清楚 aload_0 是变量表里第一个对象this放到操作栈里
-
invokespecial #8 此指令,告诉虚拟机,调用常亮池里(constant pool)的方法,也即是如javap输出的Method java/lang/Object."<init>":()V. invokespecial 指令要求操作栈(Operand Stack)有一个对象引用,也就是 也就是刚通过aload_0压入的this,invokespecial 其后的参数指向常量池的init方法。正如invokespecial 名字所暗示的那样,此指令只用于一些特殊方法调用,如实例的初始化方法,私有方法,父类方法
操作栈
索引
内容
0
this
- return 不带值的返回,如果需要返回一个对象,则用aretrun,返回一个整形,用ireturn ,这些指令都要求操作栈里都有响应的值。 4.
对于第二部分,javap输出了4个指令
-
getstatic #16 将静态字段压入操作栈,#16指向了常量池里的System.out 对象
-
ldc,因为我们知道,System.out.println 还需要一个参数,因此ldc #22 指令会压入#22所代表的字符串的引用入操作栈。
-
invokevirtual 是调用方法常用的指令,其后 #24 是常量池里java/io/PrintStream.println:(Ljava/lang/String;)V 方法,invokevirtual 指令会调用操作栈第一个对象上的。此时操作栈应该是这个样子
操作栈
索引
内容
0
System.out的引用
1
hello world 字符串的引用
- return 返回
操作栈
如果学习过计算机原理,或者了解寄存器工作方式的,应该能看不出,操作栈其实很像cpu操作,将值放入寄存器后,cpu指令会取出寄存器值进行运算,虚拟机字节码也有类似这个原理,比如,前面我们看到的aload_0, 从变量表里取出第一个值放入到操作栈里,通常这是this(但对于静态方是例外),让我们看一个更典型的的一段java代码
1public class Ch1Simple { 2 3 public int add(int a,int b){ 4 int c = a+b; 5 return c; 6 } 7}
在命令行运行
javap -c Ch1Simple
输出如下,这里为了节省篇幅,省略了构造函数字节码
1 public int add(int, int); 2 Code: 3 0: iload_1 4 1: iload_2 5 2: iadd 6 3: istore_3 7 4: iload_3 8 5: ireturn
- 0:iload_1 指令将变量表第二个元素放入操作栈中,第二个元素实际上就是int a,再次强调,第一个元素是this,第三个元素是int b,第四个元素是int c.这是在编译的时候就确定好的
add方法的方法栈(method stack)的变量表此时应该是这个样子
变量表
索引
内容
压入操作栈
0
this
iload_0
1
a
iload_1
2
b
iload_2
3
c
iload_3
-
iload_2 此指令将变量第三个元素放入到操作栈中
-
iadd 将操作栈俩个变量想加,i表示操作栈俩个变量是int类型
-
istore_3 操作栈结构存回到变量表里,位置索引是3,也就是变量c
-
iload_3 因为方法要求返回值,return指令 仍然需要调用操作栈,所以又将变量3压入操作栈里
-
ireturn 方法执行结束,弹出操作栈里的值。
栈帧(stack frame)
java运行的时候,会为每一个线程分配一个线程栈,线程执行每个方法,会为其创建一个栈帧(stack frame),执行结束后销毁栈帧。栈帧包含了变量表和操作栈,其长度是在编译期间就能确定的,如下方法
1public class Ch1Simple { 2 3 public int add(int a,int b){ 4 int c = a+b; 5 return c; 6 } 7}
因为有4个变量,分别是this,a,b,c 。 this 是对象指针,占用俩个字节, 变量 a,b,c 存放的int类型,因此也各占用俩个字节,所以变量表占用8个字节。
add方法里,指令iadd,操作俩个数,需要4个字节,而ireturn 需要操作栈有2个字节,因此操作栈只需要4个字节就能满足需求了(这个结论还有点唐突,需要细化)
因此,总的来说,add方法的栈帧应该是如下样子
变量表
索引
内容
压入操作栈
0
this
iload_0
1
a
iload_1
2
b
iload_2
3
c
iload_3
栈帧
索引
内容
0
1
除了变量表和操作栈外,栈帧还包括动态链接