【ASM系列2】字节码介绍

时间:2021-09-09 17:07:29

           这一篇首先介绍下面这些类型的字节码指令:

           装载和存储系列:从局部变量装载到操作数栈的xload系列指令、从常量池中装载数据到操作数栈的xconst和push系列、将操作数栈中的数据保存到局部变量的xstore系列指令。

           字段访问系列:getfield、putfield、getstatic、putstatic等指令。

           方法调用系列:invokevirtual、invokeinterface、invokespecial、invokestatic等指令。

           方法返回系列:ireturn、lreturn、freturn、dreturn、areturn等指令。

           从一个简单的例子开始看,写一个类:

           【ASM系列2】字节码介绍

           然后来到.class文件的目录下,执行javap -verbose -c Main,verbose参数的作用是指定javap显示常量池,而-c的作用是指定javap显示方法的字节码。执行结果如下:

                    【ASM系列2】字节码介绍

           其中第一部分是常量池,从来保存例如字符串、类名、方法名等常量项,这些常量项又有着固定的格式。首先,每个常量项都有一个标识,例如字符串是Asciz,类是class,方法是Method。而常量项又可以包含其他常量项,比如Method常量项就包含一个class常量项和方法的NameAndType常量项,而class常量又包含类名的字符串常量项,方法的NameAndType常量项又包含方法名的字符串常量项和方法签名的字符串常量项。

           下面是每个方法的描述,第一个是Main方法的构造函数,这里我们也可以指定,类中没有显示写构造函数的时候,jvm会给我们在编译的时候自动生成一个构造函数。它的内容只要三个操作:aload_0、invokespecial #8和return。

           load系列的的字节码作用是把本地变量加载到操作数栈当中。比如:iload、lload、fload、dload等是针对不同的类型的操作码,i表示int,l表示long,f表示fload,d表示double,a表示引用,在java的字节码当中,大部分的操作数据的指令都是这个规律。

          延伸一下,和load系列指令对应的是xstore系列的指令,它的作用是把操作数栈中的数据存储到本地变量中,对应的也有istore,lstore,fstore,dstore,astore。

          aload_0:aload的意思是从本地变量列表中加载一个引用到操作数栈中,后面的操作数(这里是0)指示了要加载的本地变量的偏移量。在这里指的是加载方法的this本地变量,在实例对象的方法中,第一个本地变量永远是this。此外,在构造函数中,往往第一个字节码都是aload_0,因为紧接着的默认调用是父类的构造器,super方法需要当前实例作为入参。

           invoke系列的字节码包括:invokevirtual、invokeinterface、invokespecial以及invokestatic。invokevirtual是调用对象的成员方法,invokeinterface是调用接口上的方法,invokespecial是指调用类内部的private方法,而invokestatic则是指调用指定类的静态方法。

           在这里,Main类的构造函数中,invokevirtual #8指定了要调用的是常量池第8位表示的方法,即java.lang.Object的初始化方法,并且使用了aload_0这个步骤放到操作数栈顶的this对应引用作为Object构造函数的第一个也是唯一的参数。

          最后看return系列的指令,这系列的指令也是类似得有ireturn、lreturn、freturn、dreturn、areturn,分别和之前讲到load系列的指令里的i、l、f、d、a对应,标识了方法返回不同的类型,而return系列还多了一种类型的指令是return,它表示void类型方法的返回。相比其他有返回值的return,return指令在返回的时候不会去栈中load数据,而其余几个都会在返回之前从操作数栈中获取返回值,其中lreturn和dreturn会稍微特殊,他们需要从操作数栈中弹出两位,其余的几个都只要弹出一位就直接返回了。

           接下来来看main方法,第一个操作,getstatic #16,常量池第16项的常量项是一个静态的Field常量项,标识java.lang.System的out静态字段。getstatic的意义是把静态字段中的值加载到当前帧的操作栈中。紧接着的操作是ldc #22,它的意义是把常量池22位的字符串常量加载到操作数栈顶,这样,栈顶两项就分别是java.io.PrintStream的对象和String常量“hello world”了。紧接着,invokevirtual指令将要来消费栈顶这两项的操作数了,它的操作数是#24,即常量池第24项的println方法的常量项,它指定了要调用的对象方法。这个指令会消费栈底的java.io.PrintStream的对象作为要调用的对象,并消费栈顶的字符串常量作为println方法的入参。这样,就完成了main方法中指定的所有逻辑了。

           getstatic属于访问字段的指令,这一系列指令包括:getfield、putfield、getstatic、putstatic,getfield操作从一个对象中获取指定的字段值,并把得到的值放入操作数栈顶。getfield指令要执行,指令后需要带一个指向常量池中字段常量项的偏移量,并且操作数栈顶已经压入了需要从中取数据的目标对象的引用了。而putfield指令则刚好相反,它根据操作数栈顶的值和操作数栈顶之下一位的目标对象的引用,并在指令之后带一个指向常量池中字段常量项的偏移量,给目标操作的对象的指定字段设置值。而getstatic和putstatic两个指令和前面两个指令作用基本相同,只不过他们操作的是类的静态字段,所以他们在执行这两个指令的时候,操作数栈中并不需要对象的引用,即:对于getstatic,执行指令时不需要从操作数栈中获取数据;对于putstatic,执行指令时,只需要栈顶的一项,即即将要给静态字段赋的值。

           ldc指令的作用是把常量池中对应的内容压入操作数栈中,指令之后紧跟着的操作数是一个指向常量池偏移量的数字。和ldc指令类似,同样能装载数据到操作数栈上的指令有:bipush、sipush、ldc2、aconst_null、iconst_、fconst_、lconst_等。其中bipush是把byte类型的值压入栈顶,sipush是把short类型的值压入栈顶,aconst_null是把null压入栈顶,iconst_1是把int型值1压入栈顶,fconst_0是压入0f,lconst_1是压入long型值1,ldc2 是把常量池中表示double或者long型的数据压入栈,因为它们占了两位的栈空间。