第六章 类文件结构

本篇只做一些基本的命令行介绍,准备入坑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,出现如下界面:

第六章 类文件结构_第1张图片
类文件结构.png

文件解析

线程内部分布

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栈存储的架构,其架构包括一个`操作数栈`、一个`本地变量数组`、一个`该类中当前方法到运行时常量池的引用`。
第六章 类文件结构_第2张图片
线程内部分布.png

类文件版本号
说明这个文件可以被执行的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:

  1. 所有的方法都是在第一行记录方法的简写,第二行记录方法的使用格式:按照先描述入参,再描述返回值的形式。
  2. 在字节码文件中构造方法会被视为无返回值,或者说返回值是void,对应的简写为V。
  3. 在字节码文件中,基本类型的简写一般对应其首字母的大写,如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。

你可能感兴趣的:(第六章 类文件结构)