JVM 类加载与字节码技术(2)《字节码指令/图解/条件判断指令/例子》

JVM 类加载与字节码技术(1)《类文件结构》

上面研究类的文件结构时,观察到方法体内有一些字节,他们就是对应着将来要由Java虚拟机执行的方法内的代码。在构造方法里(Hello World())里,有五个字节的代码。2a b7 00 01 b1,这些2a之类的,实际上对应着字节码的指令(五个字节中的2a b7 b1),Java虚拟机内部有解释器,这个解释器能够识别这些平台无关的字节码指令,把它们最终解释为机器码,然后执行。2a对应的指令是aload_0 = 42(0x2a),前面的aload_0是助记符,便于我们人类理解的,真正Java虚拟机解释的还是后面的2a字节。b7invokespecial = 183(0xb7),b1return = 177(0xb1)。

aload_0的作用就是把局部变量表中一个变量给他加载到操作数栈上,因为Java虚拟机的解释器执行的时候他大部分都是要到操作数栈上去读取那个数据,aload_0就是把局部变量表中0号槽位的变量加载到这个操作数栈上,这里的0号变量就是this。那么用this准备执行什么操作呢,就是接下来的b7,即invokespecial,就是准备去进行一个方法的调用(就比如是 this.),调用哪个方法呢,00 01引用常量池中#1项,即【Method java/lang/Object."<init>":()V】,即是Object中的构造方法,()V说明无参、返回值类型是void,所以从2a01就是通过this调用了无参的构造方法,当然他调用的不是本类的,如果自己掉自己就是递归了,他调的是父类的,b1表示返回,执行完了方法后return

在main方法里有9个字节代码。b2 00 02 12 03 b6 00 04 b1b2对应的是getstatic,用来加载静态变量,把他加载到操作数栈,哪个静态变量呢,00 02引用常量池中#2项,即【Field java/lang/System.out:Ijava/io/PrintStream;】,即是System类中的out这个静态变量,Ijava/io/PrintStream是out变量的类型,即b2 00 02是把这个静态变量加载到操作数栈,加载进来之后,在方法调用时还需要参数,12是代表lbc,即加载常量池中的参数,哪个参数呢,03 引用常量池中#3项,即【String hello world】,即12 03代表准备好了一个“hello world”这样一个常量,那么他两(System.out变量和hello world)都被放到操作数栈上,接着b6invokevirtual,是代表执行一个普通的成员方法,也就是“.”开始要执行方法,那执行哪个方法呢,00 04引用常量池中#4项,即要执行【Method java/io/PrintStream.println:(Ljava/lang/String;)V】,即是PrintStream这个类中println这个方法,方法的参数是接受一个字符串,没有返回值,b1表示返回。(值得注意的是这里和Java源代码的编写习惯是不一样的,比如Java源代码是先把对象System.out准备好,再去“.”方法println,最后再加参数“hello world”,但是在Java虚拟机的解释器执行时,他的操作数栈先得把对象和参数准备好,然后再去调方法。

javap 工具

自己分析类文件结构太麻烦了,Oracle提供了javap工具来反编译class文件。具体用法很简单:javap -v HelloWorld.class。其中-v参数是指要输出类文件的详细信息。若没有-v参数,他只会显示出最基本的结果,而不会显示出那些常量池之类的信息就不会显示出。执行之后输入如下:

Classfile /root/HelloWorld.class
  Last modified Jul 7, 2022; size 597 bytes【最后修改时间,大小是597字节(文件本身的信息)】
  MD5 checksum 361dca1c3frae348rra9cd5060ac6dbc【MD5的校验签名(文件本身的信息)】
  Compiled from "HelloWorld.java"Java源文件对应的是HelloWorld.java】
public class com.waca.HelloWorld【类的全路径】
  minor version: 0
  major version: 52【JDK8】
  flags: ACC_PUBLIC, ACC_SUPER【类的访问修饰符(一个public的一个类)】
Constant pool:【常量池】
  #1 = Methodref	#6.#21		// java/lang/Object."<init>":()V【方法引用。查询结果直接以注释的方式写在后面,以下同。】
  #2 = Fieldref		#22.#23		// java/lang/System.out:Ljava/io/PrintStream;【表示要引用一个成员变量,以下省略】
  #3 = String		#24		// hello world
  #4 = Methodref	#25.#26		// java/io/PrintStream.println:(Ljava/lang/String;)V
  #5 = Class		#27			// com/woca/HelloWorld
  #6 = Class		#28			// java/lang/Object
  #7 = Utf8		<init>
  #8 = Utf8		()V
  #9 = Utf8		Code
  #10= Utf8		LineNumberTable
  #11= Utf8		LocalVariableTable
  #12= Utf8		this
  #13= Utf8		Lcom/waca/HelloWorld;
  #14= Utf8		main
  #15= Utf8		([Ljava/lang/String;)V
  #16= Utf8		args
  #17= Utf8		[Ljava/lang/String;
  #18= Utf8		MethodParameters
  #19= Utf8		SourceFile
  #20= Utf8		HelloWorld.java
  #21= NameAndType	#7.#8			// "<init>":()V
  #22= Class		#29			// java/lang/System
  #23= NameAndType	#30:#31			// out:Ljava/io/PrintStream;
  #24= Utf8		hello world
  #25= Class		#32			// java/io/PrintStream
  #26= NameAndType	#32:#34			// println:(Ljava/lang/String;)V
  #27= Utf8		com/waca/HelloWorld
  #28= Utf8		java/lang/Object
  #29= Utf8		java/lang/System
  #30= Utf8		out
  #31= Utf8		Ljava/io/PrintStream;
  #32= Utf8		java/io/PrintStream
  #33= Utf8		println
  #34= Utf8		(Ljava/lang/String;)V

【大括号开始是方法信息,有两段。第一段是构造方法,第二段是main方法。】
{
   
  public com.waca.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1【最大操作栈深度,局部变量表长度,参数的长度】
        0: aload_0【这个以及下面两个是上面分析的字节码。aload_0是把局部变量中的第0项加载到操作数栈,然后紧接着调用invokespecial,要调用的方法是常量池#1,是init方法。0 1 4是代表字节码的代码行号。】
        1: invokespecial #1		// Method java/lang/Object."<init>":()V
        4: return
      LineNumberTable:Code属性的子属性】
        line 4: 04代表Java源代码中的行号,0代表字节码中的行号】
      LocalVariableTable:【对应着本地变量表,0是起始范围,5是变量的作用范围,即上面的0 ~ 4行。变量名是this,类型是HelloWorld。】
        Start	Length	Slot	Name	Signature
            0        5     0    this    Lcom/waca/HelloWorld;
  
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic		#2	// Field java/lang/System.out:Ljava/io/PirntStream;
        3: ldc				#3	// String hello world
		5: invokevirtual		#4	// Method java/io/PrintStream.println:(Ljava/lang/String;)V
		8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start	Length	Slot	Name	Signature
            0        9     0    args    [Ljava/lang/String;
    MethodParameters:
      Name		Flags
      args
}

可以看得出,这种信息比起那种阅读二进制字节码肯定要方便很多了。

图解方法内的代码执行流程
// 演示 字节码指令 和 操作数栈、常量池的关系
public class Demo {
   
	public static void main(String[] args) {
   
		int a = 10;
		int b = Short.MAX_VALUE + 1;// 这里故意让他突破一个界限,因为这个分界线,待会就会看到这个数字值到底存储在什么位置
		int c = a + b
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值