java字节码入门(上)

 

 

字节码

 

 

Hello world

public class Helloworld {

	public static void main(String[] args) {
		System.out.println("hello,world");

	}

}

如果用javap查看此类结构

javap -c Helloworld.class

输出是

public class com.beetl.myos.ch1.Helloworld {
  public com.beetl.myos.ch1.Helloworld();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #22                 // String hello,world
       5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

注意

javap 是java内置的反编译工具,属于jdk 一部分,确保JAVA_HOME 被正确设置,以及JAVA_HOME\bin 被设置成Path。

从javap输出了俩部分,首先是构造函数,用了三个直接码指令。如果熟悉java编程的,就知道,尽管没有为Helloworld提供构造函数,但Java会提供一个默认的构造函数,我们通过反编译类就能看到,有个叫<init>的构造函数,这是程序在编译成class的时候创建的。这三个指令依次是aload_0 invokespecial return

  1. aload_0 此指令告诉虚拟机,将局部变量this放入操作栈里。对于每一个方法(构造函数从字节码角度来讲,也是一个方法,并无区别),方法的参数,以及方法中申明的变量都是在编译期间确定好的,按照出现顺序存放在栈帧里(stack frame)的局部变量表里,位置0 总是默认留给this,其后的位置留给方法的申明参数列表,再之后是留给方法内部用到的局部变量,我们将在下一节会详细介绍指令的数据结构基础。在这,我们只需要清楚 aload_0 是变量表里第一个对象this放到操作栈里

  2. invokespecial #8 此指令,告诉虚拟机,调用常亮池里(constant pool)的方法,也即是如javap输出的Method java/lang/Object."<init>":()V. invokespecial 指令要求操作栈(Operand Stack)有一个对象引用,也就是 也就是刚通过aload_0压入的this,invokespecial 其后的参数指向常量池的init方法。正如invokespecial 名字所暗示的那样,此指令只用于一些特殊方法调用,如实例的初始化方法,私有方法,父类方法

操作栈

索引

内容

0

this

  1. return 不带值的返回,如果需要返回一个对象,则用aretrun,返回一个整形,用ireturn ,这些指令都要求操作栈里都有响应的值。 4.

对于第二部分,javap输出了4个指令

  1. getstatic #16 将静态字段压入操作栈,#16指向了常量池里的System.out 对象

  2. ldc,因为我们知道,System.out.println 还需要一个参数,因此ldc #22 指令会压入#22所代表的字符串的引用入操作栈。

  3. invokevirtual 是调用方法常用的指令,其后 #24 是常量池里java/io/PrintStream.println:(Ljava/lang/String;)V 方法,invokevirtual 指令会调用操作栈第一个对象上的。此时操作栈应该是这个样子

操作栈

索引

内容

0

System.out的引用

1

hello world 字符串的引用

  1. return 返回

操作栈

如果学习过计算机原理,或者了解寄存器工作方式的,应该能看不出,操作栈其实很像cpu操作,将值放入寄存器后,cpu指令会取出寄存器值进行运算,虚拟机字节码也有类似这个原理,比如,前面我们看到的aload_0, 从变量表里取出第一个值放入到操作栈里,通常这是this(但对于静态方是例外),让我们看一个更典型的的一段java代码

public class Ch1Simple {

	public int add(int a,int b){
		int c = a+b;
		return c;
	}
}

在命令行运行

javap -c Ch1Simple

输出如下,这里为了节省篇幅,省略了构造函数字节码

  public int add(int, int);
   Code:
      0: iload_1
      1: iload_2
      2: iadd
      3: istore_3
      4: iload_3
      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),执行结束后销毁栈帧。栈帧包含了变量表和操作栈,其长度是在编译期间就能确定的,如下方法

public class Ch1Simple {

	public int add(int a,int b){
		int c = a+b;
		return c;
	}
}

因为有4个变量,分别是this,a,b,c 。 this 是对象指针,占用俩个字节, 变量 a,b,c 存放的int类型,因此也各占用俩个字节,所以变量表占用8个字节。

add方法里,指令iadd,操作俩个数,需要4个字节,而ireturn 需要操作栈有2个字节,因此操作栈只需要4个字节就能满足需求了(这个结论还有点唐突,需要细化)

因此,总的来说,add方法的栈帧应该是如下样子

变量表

索引

内容

压入操作栈

0

this

aload_0

1

a

iload_1

2

b

iload_2

3

c

iload_3

操作栈

索引

内容

0

 

1

 

 链 接

数据类型

字节码指令对操作数据类型是有要求的,比如iadd, 就要求操作栈里有俩个int类型数据。ireturn 指令要求操作栈顶部有一个int类型数据,而areturn 指令要求操作栈顶部有一个referncetype类型数据,即对象的引用。 同样,对变量表的操作指令,istore_x, 将操作栈顶部的int类型存放到变量表x位置处。从虚拟机的角度来,虚拟机有如下数据类型

  • 原始类型

    • 数字类型

      • Integer ,所有的int,byte,short ,char, 在虚拟机中都认为是Int类型,占用俩个字节,操作int的指令往往以i开头,如iadd,iload_n,istore_n

      • Long 对应java的long,占用了4个字节。操作long的指令以l开头,如ladd,此时要求操作栈里存放的有俩个long类型数据

      • Float 对应float类型,占用俩个字节,操作float的指令以f开头,如fadd,fload_n,fsotre_n.

      • double,对应double类型,占用4个字节,操作double的指令以d开头,如dadd dload_n, 如下俩个浮点类型字符相加

public double add(double a,double b){
	double c = a+b;
	return c;
}
   public double add(double, double);
   Code:
      0: dload_1
      1: dload_3
      2: dadd
      3: dstore        5
      5: dload         5
      7: dreturn

因为是浮点操作,所以浮点数a,浮点数b,各占俩个字(word),分配到变量表索引位置是1,和 3,变量表如下分配

索引

内容

压入操作栈

0

this

aload_0

1

a(高位)

dload_1

2

a(低位)

 

3

b(高位)

dload_3

4

b(低位)

 

5

c(高位)

dload 5

6

c(低位)

 

由于dload_n 只支持0..3, 所以用了dload index 指令。虚拟机子所以提供dload_n,而不是只用dload index, 是因为前者只占用一个字节,dload_n, 在字节码中,分别是0x38,0x39,0x40,0x41.(将在下一节简要介绍字节码指令集),dload指令是0x18,因此指令0x39 与0x18 0x01 都是从变量表索引为1的地方取出一个浮点数放入操作栈里

  • Boolean: 虚拟就中也使用int类型来表示,1表示为true,0表示false。

  • returnAddress,用于try final,代表了执行某个字节码的指针(需要细化)

Refernce Type,java虚拟机有3中引用类型,分别是类类型(class Type),接口类型(interface Type),数组类型(arrayType),分别指向了运行时动态创建的类实例,接口实例,以及数组。如指令checkcast,就要求操作栈里存放的是Refernce Type

public List cast(Object a){
		List list = (List)a;
		return list;
}

对应的字节码是

public java.util.List cast(java.lang.Object);
  Code:
     0: aload_1
     1: checkcast     #16                 // class java/util/List
     4: astore_2
     5: aload_2
     6: areturn

第一个指令是aload_1,aload指令操作的及时一个reference,将变量表索引是1的压入操作栈,即变量a 第二个指令checkcast #16, 从操作栈弹出变量a,并检测是否是java/util/List类型。指令执行到这里的时候,变量表和操作栈应该是这样的

变量表

索引

内容

压入操作栈

0

this

aload_0

1

a

aload_1

操作栈

索引

内容

0

a

 

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值