本篇只做一些基本的命令行介绍,准备入坑class文件的小白可以看看。因字节码命令太多,本篇只介绍用到的字节码命令。
原文地址:https://www.jianshu.com/p/47c48fdbef43。
写一java文件,内容如下:
public class A{
public int i=1;
public int add(int x){
return i+x;
}
}
编译为class文件,然后执行javap -verbose A.class
,出现如下界面:
文件解析
线程内部分布
To understand the details of the bytecode, we need to discuss how a Java Virtual Machine (JVM) works regarding the execution of the bytecode. A JVM is a stack-based machine. Each thread has a JVM stack which stores frames. A frame is created each time a method is invoked, and consists of an operand stack, an array of local variables, and a reference to the runtime constant pool of the class of the current method.
为了更好理解字节码文件,我们需要理解JVM中字节码的执行。JVM是基于栈的,每一个线程有一个JVM栈存储的架构,其架构包括一个`操作数栈`、一个`本地变量数组`、一个`该类中当前方法到运行时常量池的引用`。
类文件版本号
说明这个文件可以被执行的JDK版本。如52对应的JDK8,标识可以被JDK8及其之前的JDK所执行。(minor version~major version之间)(可以查询Class文件版本号的资料)
类访问标志
说明这个类的修饰符、访问符等,这个类的成分(有可能是接口、枚举等)。
常见的访问标志:
标志名称 | 含义 |
---|---|
ACC_PUBLIC | 是否是public类型 |
ACC_SUPER | 是否使用invokespecial字节码指令的新语意(JDK1.0.2之后都为true) |
ACC_ENUM | 标识这是一个枚举 |
ACC_FINAL | 是否是final修饰(只有类可设置) |
ACC_INTERFACE | 标识这是一个接口 |
ACC_ABSTRACT | 是否为abstract类型(接口和抽象类为true,其他为false) |
tips:ACC_SUPER的拓展:
在早期的JVM(JDK1.0.2之前)编译器中,子继承父并重载其方法,JVM将忽略ACC_SUPER,只执行父方法而不执行子方法。在JDK1.0.2之后,先查找ACC_SUPER设置,然后查找本体,结果是既执行父方法又执行子方法。而这也是我们所期望的。
常量池部分
常量池中主要存放字面量
和符号引用
。字面量比如文本字符串、final常量值等。符号引用指类和接口的全限定名
、字段的名称和描述符
、方法的名称和描述符
。
常量池计数从1开始,0是特指某些情况下表达“不引用任何一个常量池项目”的含义。
常见的常量池项目类型:
类型 | 描述 |
---|---|
CONSTANT_Utf8_info | UTF-8编码的字符串 |
CONSTANT_Methodref_info | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 |
CONSTANT_Fieldref_info | 字段的符号引用 |
CONSTANT_Class_info | 类或接口的符号引用 |
CONSTANT_NameAndType_info | 字段或方法的部分符号引用 |
CONSTANT_String_info | 字符串类型字面量 |
CONSTANT_Integer_info | 整形字面量 |
常量池逐行解析:
#1是方法引用,该方法class A的构造方法。
#2是字段引用,该字段即class A中的i,类型为I(即Integer)。
#3是类的符号引用,该类为A。#4同理,是java.lang.Object。
#11是add方法的简写,#12是输入参数为int,输出参数为int。
#9是JVM预定义属性,指java代码编译成的字节码命令。可以看到下面的“编译后的Bytecode字节码”中有一行是Code。其下面就是code的描述。
#10是JVM预定义属性,java源码行号与字节码指令的对应关系。
#13是JVM预定义属性,记录源文件名称。
Tips:
- 所有的方法都是在第一行记录方法的简写,第二行记录方法的使用格式:按照先描述入参,再描述返回值的形式。
- 在字节码文件中构造方法会被视为无返回值,或者说返回值是void,对应的简写为V。
- 在字节码文件中,基本类型的简写一般对应其首字母的大写,如int对应I,byte对应B。为防止冲突,有一些特殊的,如long对应的J,boolean对应的是Z,void对应V。其他都是无冲突的。
而L对应对象类型,如Integer对应Ljava/lang/Integer,即L+全路径名。
数组对应[,几维数组就有几个[,如String[][]对应[[Ljava/lang/String。
字节码部分解析
首先解释了字段,说明其描述符,修饰符。
然后解释了方法——构造方法和普通方法。在javac中,构造方法视为返回为void的普通方法。
stack=2, locals=2, args_size=2
表示该本地操作栈的slot(槽数)为2,第1个是this。操作数栈在每一次运行完栈顶的几个元素后(一般取决于该操作符关联几个操作数),会弹出并将新值(前面操作符的运行结果)放入栈顶。在javac中,java中的this关键字是通过传参的形式来访问的,所以该构造方法中locals和args为2,其中args是传入参数。(如果是static方法,this会在本地变量会自动添加this,不会在本地操作栈中传入this)
所有的操作符、操作数都是放在byte array中。左边的0,1,4等都是数组下标。
aload_0
是将这个对象的引用,即this关键字,加载到栈顶。
invokespecial
是调用父类的构造方法,在调用期间,this,被弹出操作栈。
putfield
将第5行声明的常量1存入到#2的i中。
Tips:
在bytecode字节码中,有很多iload、aload、iadd之类的操作码。其中,前缀a
说明这个命令操作一个对象引用。i
对应int,b
对应byte,c
对应char,d
对应double,等等。这个前缀可以让你明确要操作数据类型。
aload_0
表示将本地变量表的下表为0的元素推到操作栈。本地变量表就是形参入口,默认第一个是this。
getfield
,首先将this从栈顶弹出。#2是从运行时常量池中的#2位置获取字段,然后放入操作栈。
putfield
,通过this引用将值存入到变量中。
iconst_1
,表示整形常量,且该常量的值为1.
iadd
整形相加操作。
ireturn
表示整形返回。return
有中断,返回void的意思。
bipush 33
表示将byte型的33压入堆栈。
结合上面的操作符的解释,应该已经懂了会经历哪些操作了吧。但是有人会发现0,1,4,5...既然是数组下标,为什么不是0,1,2,3,4,5。因为在下标1的位置,跳到了常量#1的位置,而#1需要两个参数,所以此处会跳过2,3。而实际上2,3位置存放的是需要进行运算或者操作的参数。
LineNumberTable
中,前者是字节码行号,后者是java源码行号。输出是为了便于出现异常时可以快速定位到出错的源码行号。
参考:https://www.cnblogs.com/frank-pei/p/5432949.html。