class文件和字节码解析
讲解了Java类和对象在内存中的表示机制,Java对象是根据Java类创建的,表示一个Java类实例;Java类是根据Class文件创建的,Class文件被类加载器加载、链接、初始化后就变成Hotspot中的Klass。注意这里的Class文件是指符合Class文件格式规范的字节流,并不特指磁盘文件形式的以.class结尾的class文件,如类加载器也可读取网络字节流形式的Class文件。Class文件格式是JVM自己定义的用于表示Java类的二进制字节流规范,与操作系统本身无关,该文件格式正是Java代码一次编译,跨平台运行的关键,其他的与底层操作系统强耦合的编译执行的语言如C/C++需要在每个操作系统上都编译一遍才能正常执行,因为不同操作系统基本都有特定的汇编器和链接器,支持的二进制文件格式也可能不同。那么类的字段,方法,继承的父类,实现的接口等这些信息在Class文件中是如何组织和表示的了?方法对应的字节码是是如何运行的?
class文件
1、整体结构
每个class文件都有一个ClassFile结构,称为Class文件格式,如下图:
image.png
其中u<n> 表示n个无符号字节,如u4 magic 表示magic的取值用4个无符号字节表示;cp_info描述常量池的结构,field_info描述字段的数据结构,method_info描述方法的数据结构,attribute_info描述属性的数据结构。ClassFile结构各项的含义如下:
magic: 魔数,用于标识当前Class文件的文件格式,JVM可据此判断该文件是否可以被解析,目前固定为0xCAFEBABE
minor_version, major_version:minor_version是副版本号,major_version是主版本号,这两个版本是生成Class文件时根据编译的JDK版本来确定的,用标识编译时的JDK版本,常见的一个异常Unsupported major.minor version 52.0就是因为运行时的JDK版本低于编译时的JDK版本,52是Java8的主版本号。
constant_pool_count:常量池计数器,等于常量池中的成员数加1。
constant_pool:常量池,是一种表结构,包含class文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量,其有效索引范围是1- (constant_pool_count-1)。其中类和接口名采用全限定形式,即在整个JVM中的绝对名称,如java.lang.Object,方法名,字段名、局部变量名和形参名都采用非限定名,即在源代码文件中使用相对名称,如属性名name。
access_flags:用于表示某个类或者接口的访问权限和属性
this_class:类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个CONSTANT_Class_info类型的结构体,表示这个class文件所定义的类和接口
super_class:父类索引,同this_class,该值必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,如果该值为0,则只能表示java.lang.Object类,因为该类是唯一一个没有父类的类。
interfaces_count:接口计数器,表示当前类或者接口的直接超接口的数量
interfaces:接口表,是一个表结构,每个成员同this_class,必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,其有效索引范围为0~interfaces_count,接口表中成员的顺序与源代码中给定的接口顺序是一致的,interfaces[0]表示源代码中最左边的接口。
fields_count:字段计数器,当前class文件所有字段的数量
fields:字段表,是一个表结构,表中每个成员必须是filed_info数据结构,用于表示当前类或者接口的某个字段的完整描述,不包含从父类或者父接口继承的字段
methods_count:方法计数器,表示当前类方法表的成员个数
methods:方法表,是一个表结构,表中每个成员必须是method_info数据结构,用于表示当前类或者接口的某个方法的完整描述,包含当前类或者接口定义的所有方法,如实例方法、类方法、实例初始化方法等,不包含从父类或者父接口继承的方法
attributes_count:属性计数器,表示当前class文件attributes属性表的成员个数
attributes:属性表,是一个表结构,表中每个成员必须是attribute_info数据结构,这里的属性是对class文件本身,方法或者字段的补充描述,如SourceFile属性用于表示class文件的源代码文件名。
2、描述符
描述符有两种,字段描述符和方法描述符,本质就是一个基于特定规则的字符串,其中字段描述符用来表示类,实例和局部变量的类型,具体如下:
image.png
如I表示一个int变量,Ljava.lang.Object;表示一个Object实例,[[I表示一个二维int数组实例。方法描述符包含一个或者多个参数描述符合一个返回值描述符,参数描述符和返回值描述符都是上面的字段描述符,再加一个特殊的V,表示该方法不返回任何值。如方法Object m(int i, double d, Thread t) {...}对应的方法描述符就是(IDLjava/lang/Thread;)Ljava/lang/Object;
3、cp_info
Java虚拟机指令不依赖类,接口,类实例或数组的运行时内存布局,而是依赖依赖常量池表中的符号信息,常量池表中所有项都有如下通用格式:
image.png
其中tag作为类型标记,用于确定后面的info的格式,tag是一个字节,info是两个或者多个字节,取决于tag的值,如下图:
image.png以CONSTANT_Utf8_info,CONSTANT_Class_info 和CONSTANT_Fieldref_info 的结构为例说明,如下图:
image.png
CONSTANT_Utf8_info用于表示一个Utf8编码的字符串,tag取值1,length是后面的byte数组的长度,byte数组就是字符串对应的byte数组数据了;
CONSTANT_Class_info用于表示一个Java类或者接口名,的tag取值就是上表中的7,name_index表示对常量池的有效索引,该索引处的成员必须是一个CONSTANT_Utf8_info结构;
CONSTANT_Fieldref_info用于描述一个字段,其中class_index是常量池中的有效索引,该索引处的成员必须是一个CONSTANT_Class_info结构,表示该字段所属的类,name_and_type_index是常量池中的有效索引,该索引处的成员必须是一个CONSTANT_NameAndType_info结构,该结构用于表示一个字段或者方法描述符。
比较特殊的是最后三个,CONSTANT_MethodHandle_info用于表示方法句柄,CONSTANT_MethodType_info用于记录方法的类型信息,即方法描述符,CONSTANT_InvokeDynamic_info用于表示invokedynamic指令使用的动态调用名,参数和返回值等系列静态参数的常量。
4、filed_info
字段表的成员用field_info结构表示,该结构如下图:
其中access_flags表示字段的访问权限和属性,是个由标识构成的掩码,所谓标识就是某个特定的二进制位,取值为1表示开启,0表示关闭,各标识开启后的含义如下:
image.png
以public static final为例说明,public对应的标识位0000 0000 0000 0001,static对应的标识位是0000 0000 0000 0100,final对应的标识位是0000 0000 0001 0000,合起来就是0000 0000 0001 0101。
name_index表示常量池中一个类型为CONSTANT_Utf8_info的有效索引,表示该字段的字段名
descriptor_index 表示常量池中一个类型为CONSTANT_Utf8_info的有效索引,表示该字段的字段描述符
attributes_count 表示当前字段的附加属性表的属性数量
attributes 属性表,表示该字段的所有附加属性,每个成员都是attribute_info结构
5、method_info
所有的方法都用method_info结构表示,如下图:
image.png
access_flags表示方法的访问权限和基本属性,同filed_info,也是一个由标识构成的掩码,各标识开启的含义如下:
image.png
其他各字段同filed_info,name_index表示方法名,descriptor_index表示方法描述符,attributes为该方法的属性表。
6、attribute_info
ClassFile、filed_info、method_info结构和Code属性都有属性表,所有的属性都通过attribute_info结构表示,其通用格式如下:
其中attribute_name_index是常量池中一个类型为CONSTANT_Utf8_info的有效索引,表示该属性的属性名,attribute_length表示后面的info信息的字节长度,这个长度不包括attribute_name_index和attribute_length的6字节。Java8预定义了23种属性,用户在编译源代码文件时可以添加新的属性,只要JVM实现能够正确识别该属性即可,注意用户自定义的属性不能使用这些预定义属性的属性名,各属性的作用如下:
ConstantValue:位于filed_info的属性表中,表示static字段的初始值,非对象类型的常量值。
Code:位于method_info的属性表中,表示该方法的虚拟机指令及辅助信息,method_info中有且仅有一个Code属性,其结构如下:
image.png其中max_stack表示当前方法操作数栈的最大深度;max_locals表示此方法引用局部变量表中的局部变量的个数,包含传递方法入参的局部变量;code_length表示后面的code数组的字节长度;code数组表示当前方法的虚拟机指令的数据;exception_table_length表示后面的exception_table数组的长度;exception_table中表示此方法的捕获的各异常的异常处理逻辑,每个成员对应一个异常类型,每个成员包含4个属性,start_pc, end_pc表示try/catch的代码范围,具体来说是起止代码对应的虚拟机指令在code数组中的索引,handler_pc是异常处理逻辑的代码的虚拟机指令在code数组中的索引,catch_type是常量池中一个类型为CONSTANT_Class_info的有效索引,表示捕获的异常类型。
StackMapTable:位于Code属性的属性表中,最多只能包含一个,用于虚拟机的类型检查验证阶段,验证某个局部变量的类型与操作数栈顶所需的核查类型是否一致。
Exceptions:位于method_info的属性表,表示该方法可能抛出的受检异常的异常类型。
InnerClasses: 位于ClassFile的属性表中,表示该类定义的内部类信息,如果有内部类,则有且仅有一个InnerClasses属性
EnclosingMethod :位于ClassFile的属性表中,如果当前类是局部类或者匿名类时才有EnclosingMethod属性,表示该类的闭包方法。
Synthetic:位于ClassFile,method_info或者filed_info结构的属性表中,表示该成员没有在源文件中出现,如编译器自动添加的默认构造方法。
Signature:位于ClassFile,method_info或者filed_info结构的属性表中,表示该成员使用的参数化类型的签名信息
SourceFile: 位于ClassFile的属性表中,表示该class文件对应的源代码文件的文件名
SourceDebugExtension:位于ClassFile的属性表中,表示该类的扩展调试信息
LineNumberTable:位于Code的属性表中,表示虚拟机指令同源文件代码行的对应关系,注意LineNumberTable与源文件的代码行没有一一对应关系,可能多个LineNumberTable属性对应同一个代码行,且LineNumberTable的属性顺序是任意的。
LocalVariableTable:位于Code的属性表中,表示方法的局部变量,每个局部变量最多对应一个LocalVariableTable属性,Code中的多个LocalVariableTable属性的顺序是任意的。每个局部变量通过5个属性表示,start_pc和length表示该局部变量的作用域范围,start_pc是Code数组的索引,name_index属性表示局部变量的变量名,descriptor_index表示该变量的字段描述符,index表示该变量在局部变量表中的索引
LocalVariableTypeTable:位于Code的属性表中,只针对参数化类型的变量,用于提供该变量参数化类型的签名信息,这类变量会同时出现在LocalVariableTable和LocalVariableTypeTable中,其他的变量只在LocalVariableTable中出现。
Deprecated:位于ClassFile,method_info或者filed_info结构的属性表中,表示此成员在未来版本中被取代
RuntimeVisibleAnnotations:位于ClassFile,method_info或者filed_info结构的属性表中,最多只能含有一个,表示加在此成员声明上面的运行时可见的注解,注解用annotation结构表示,保存了注解的多个键值对属性。
RuntimeInvisibleAnnotations:与RuntimeVisibleAnnotations相对,表示加在此成员声明上面的运行时不可见的注解
RuntimeVisibleParameterAnnotations:位于method_info结构的属性表中,最多只能含有一个,表示方法入参的运行时可见的注解
RuntimeInvisibleParameterAnnotations:与RuntimeVisibleParameterAnnotations相对,表示方法入参的运行时不可见的注解
RuntimeVisibleTypeAnnotations:位于ClassFile,method_info、filed_info或者Code结构的属性表中,记录了标注在对应类声明,字段声明或者方法声明所使用的类型上面的运行时可见注解,如某个类implements的各个接口的所有注解都会记录在该类ClassFile结构中的RuntimeVisibleTypeAnnotations属性中,某个字段的字段类型的所有注解都会记录在该字段对应的filed_info结构体的RuntimeVisibleTypeAnnotations属性中。
RuntimeInvisibleTypeAnnotations:与RuntimeVisibleTypeAnnotations相对,表示运行时不可见注解
AnnotationDefault:位于method_info结构的属性表中,用来记录注解类型的元素的默认值
BootstrapMethods:位于ClassFile结构的属性表中,用于保存invokedynamic指令引用的引导方法限定符,如果常量池中包含CONSTANT_InvokeDynamic_info成员,则ClassFile的属性表中必须包含且只能包含一个BootstrapMethods属性。
MethodParameters:位于method_info结构的属性表中,用来记录方法的形参的个数,形参名,形参是否final等。
LineNumberTable 属性
LineNumberTable 属性 用于 Java 的调试,可指明某条指令对应于源码哪一行。
LineNumberTable 属性用来描述 Java 源码行号与字节码行号之间的对应关系。它默认会生成到 Class 文件中,可以在 javac 中分别使用 -g:none 或者 -g:lines 来取消或者要求生成
LineNumberTable 属性的结构如下所示:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
其中最重要的是 line_number_table 数组,该数组元素包含如下 两个成员变量:
- start_pc:为 code[] 数组元素的索引,用于指向 Code_attribute 中 code 数组某处指令。
- line_number:为 start_pc 对应源文件代码的行号。需要注意的是,多个 line_number_table 元素可以指向同一行代码,因为一行 Java 代码很可能被编译成多条指令。
LocalVariableTable 属性
LocalVariableTable 属性用于 描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中, 可以在 javac 中分别使用 -g:none 或者 -g:vars 选项生成该信息。
LocalVariableTable 的数据结构如下所示:
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
其中最重要的元素是 local_variable_table 数组,其中的 start_pc 与 length 这两个参数 决定了一个局部变量在 code 数组中的有效范围。
需要注意的是,每个非 static 函数都会自动创建一个叫做 this 的本地变量,代表当前是在哪个对象上调用此函数。
并且,this 对象是位于局部变量数组第1个位置(即 Slot = 0),它的作用范围是贯穿整个函数的。
此外,在 JDK 1.5 引入泛型之后,LocalVariableTable 属性增加了一个 “姐妹属性”: LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录 的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对于非泛型类型来 说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。
Slot 是什么?
JVM 在调用一个函数的时候,会创建一个局部变量数组(即 LocalVariableTable),而 Slot 则表示当前变量在数组中的位置。
字节码
C/C++的方法会被编译成特定于CPU架构的汇编指令,然后交由CPU逐一执行,因为汇编指令与CPU架构是强绑定的,所以C/C++程序在执行前需要在不同CPU架构的机器上编译一遍。Java为了实现一处编译,跨平台运行的目标,在汇编指令之上引入了一个独立于平台的中间层,虚拟机指令,由Java虚拟机规范提供指令标准定义,由Java虚拟机厂商提供指令实现,不同平台的Java虚拟机都遵循相同的指令集规范,从而实现跨平台运行目标。一个方法对应的一组虚拟机指令称为这个方法的字节码(byte codes)。
虚拟机指令定义
一条虚拟机指令由一个指定要完成的操作的操作码和表示待操作值的的零或者多个操作数构成,所谓操作数就是该指令的入参。操作数可能在编译器产生并嵌入到字节码指令中,即在内存中操作数紧挨着字节码指令,也可能在运行期通过计算得出保存在操作数栈中,这种情形下虚拟机指令的操作数的个数为0,直接从操作数栈中读取操作数,通常是栈顶的N个元素。所谓操作数栈就是保存虚拟机指令执行结果的一个后进先出的栈,由Java虚拟机维护,其功能类似于汇编指令执行过程中使用的进程栈或者内核栈。
Java虚拟机规范定义指令时使用了一张指令格式表,包含助记符,操作码,指令格式,操作数栈,功能描述,链接时异常,运行时异常,注意共8项,class文件中的虚拟机指令就是数字形式的操作码,助记符是对该数字的含义的简单描述,指令格式描述该指令后面的操作数,操作数栈描述该指令执行时操作数栈的状态和执行完成操作数栈的状态,功能描述项会详细描述该指令在各种场景下的处理逻辑,链接时异常项描述在链接环节执行该指令校验时应该抛出的异常类型,运行时异常项描述了运行时该指令执行过程中应该抛出的异常类型。
以无操作数的aload指令和有一个操作数的iload指令为例说明:
image.png
图中的50是该指令的十进制的操作码,0x32是十六进制的;图中的箭头表示栈顶,aload指令执行时操作数栈的栈顶的两个元素必须是arrayref和index,其中arrayref表示一个组件类型为reference的数组,即对象数组,是待读取的目标数组,index表示待读取数组元素的索引,arrayref和index的类型校验在链接环节的类型检查时完成。
image.pngiload的操作数index表示当前栈帧中的局部变量的索引,iload指令将该索引处的int值压入到操作数栈栈顶,index的值在编译期确定并随iload指令一起写入到class文件中。
内存表示
在class文件中,某个Java方法的虚拟机指令保存在表示该方法的method_info结构的Code属性的code项中,code项是一个byte数组,code_length项描述了该数组的长度。Java8定义的虚拟机指令总共有204个,其中3个是Java虚拟机内部使用的保留指令,因此指令操作码用一个字节即可表示,每个指令后面的字节按照规范表示该指令的操作数,操作数的类型和字节数在规范中都有定义(指令格式表中格式项每一行都表示一个字节),Java方法对应的一组指令按照指令执行的先后顺序依次保存,code[0]就是第一条执行的指令。解析code数组包含的虚拟机指令时,先从code[0]读取第一条指令,判断其是否有操作数及其占用的字节数,如果有则读取指定字节数的操作数,如果没有或者操作数读取完毕则下一个字节是下一条虚拟机指令,依次往下循环即可,在class文件校验环节会检查code数组是否合法完整。
3、运行逻辑
在《Hotspot Klass模型——Java类内存表示机制》中讲到方法的字节码实际保存在ConstMethod对象的后面,可以通过HSDB具体的字节码及其内存数据,测试代码:
package jvmTest;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public class MainTest4 {
public static void main(String[] args) {
test();
while (true) {
try {
System.out.println(getProcessID());
Thread.sleep(600 * 1000);
} catch (Exception e) {
}
}
}
private static void test(){
int a=1;
int a2=2;
int a3=3;
int[] b={a,a2,a3};
b[2]=4;
int c=b[2];
test2(c);
}
private static void test2(int c){
System.out.println(c);
}
public static final int getProcessID() {
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
System.out.println(runtimeMXBean.getName());
return Integer.valueOf(runtimeMXBean.getName().split("@")[0])
.intValue();
}
}
image.png](https://img.haomeiwen.com/i26273155/32a2f5dcb81badf4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
其中line表示行号,bci表示该字节码在code数组中的索引。再用Inspect,根据方法的地址找到该方法的ConstMethod对象的地址,如下图:
image.png
接着用CLHSDB的mem命令查看ConstMethod对象的内存,不过先得执行printas命令查看该对象的内存大小,从而确定该方法字节码在内存中的起始位置,如下图:
image.png该对象的大小是48字节,即6个字段,从第7个字宽开始即是字节码的内存起始位置。字节码的内存大小是37字节,即5个字宽,用mem命令查看这5个字宽的内存数据,如下图:
image.pngIntel按照小端的方式存储数据,因为mem命令打印内存数据时做了一道转换,所以实际的内存数据与上图打印的结果是相反的,以第一行0xbc 06 3d 06 3c 05 3b 04为例,实际存储和读取的顺序是从04开始的,从右往左,一直到bc,然后从下一行的末尾的0a开始。将内存数据与上面的虚拟机指令的操作码逐一比对,结果如下:
iconst_1 04 将int变量1压入栈顶
istore_0 3b 将栈顶的int变量放入局部变量表索引为0的位置,即变量a的初始化
iconst_2 05 将int变量2压入栈顶
istore_1 3c 将栈顶的int变量放入局部变量表索引为1的位置,即变量a2的初始化
iconst_3 06
istore_2 3d 变量a3的初始化
iconst_3 06 将int变量3压入栈顶,3是数组的初始化大小
newarray int bc 0a 初始化数组,0a表示数组的元素类型
dup 59 复制栈顶的操作数并压入栈顶,此时栈顶的操作数是newarray创建的int数组的引用,dup执行完成栈顶头2个元素 就是该引用
iconst_0 03 将int变量0压入栈顶
iload_0 1a 将局部变量表中索引为0的变量压入栈顶,即变量a,执行完的状态如下图
iastore 4f 读取栈顶的三个操作数,将指定value插入到指定数组的指定索引处,然后三个操作数弹出,即变量a插入int数组b 索引0处
dup 59 此时栈顶还是int数组b的引用,执行完后栈顶头两个元素都是该引用
iconst_1 04
iload_1 1b
iastore 4f 同上,将变量a2插入int数组b索引1处
dup 59
iconst_2 05
iload_2 1c
iastore 4f 同上,将变量a3插入int数组b索引1处,此时栈顶的数据是int数组b的引用
astore_3 4e 将栈顶的引用数据保存到局部变量表索引为3的地方,前面0,1,2分别是变量a,a2,a3
aload_3 2d 从局部变量表加载索引为3的引用变量到栈顶,即int数组b的引用重新放入栈顶
iconst_2 05 将int变量2压入栈顶,即待读取的数组索引2
iconst_4 07 将int变量4压入栈顶,此时操作数栈的状态如下图。
iastore 4f 读取栈顶的三个元素,将指定数组的指定索引处的值修改为目标值,即int数组b索引为2的元素修改为4
aload_3 2d int数组b的引用重新放入栈顶
iconst_2 05 将int变量2压入栈顶,即待读取的数组索引
iaload 2e 读取栈顶的头两个元素,读取指定数组的指定索引的元素并加载到栈顶,此时操作数栈只有一个该元素
istore #4 36 04 将栈顶int元素放入局部变量表索引为4的地方
iload #4 e0 04 读取局部变量表索引为4的元素到栈顶
invokestatic #10 b8 06 00 调用static方法,test2,后面的06和00用于计算目标方法的在常量池的方法描述符的索引
return b1 返回到调用该方法的指令处
javap命令
上节中我们使用HSDB查看方法字节码,除此之外,Java还提供了解析class文件的javap命令,javap会将其中的二进制数据转换成方便阅读的文本数据,可以借此具体了解Class文件格式各部分的构成。不过javap对class文件有要求,如果需要输出完整的信息,在javac编译源代码文件时需要加上-g选项,类似于gcc的-g选项,此选项会加上额外的源码的相关信息。
1、命令选项
如下图,注意-c并不是反汇编,只是查看class文件中的方法字节码,-v是输出最完整全面的信息
2、命令输出说明
测试代码如下:
package jvmTest;
import org.junit.runner.RunWith;
import org.junit.runners.BlockJUnit4ClassRunner;
import javax.xml.bind.annotation.XmlElement;
class MainBase{
public void show(){
System.out.println("MainBase");
}
}
@RunWith(BlockJUnit4ClassRunner.class)
public class MainTest4 extends MainBase implements Runnable {
@XmlElement(name = "ax")
private int a=12;
private long l=13;
private static final String s="test";
@Override
public void run() {
show();
}
public void test(int a) throws NullPointerException {
try {
int a2=2;
int a3=3;
int[] b={a,a2,a3};
b[2]=4;
int c=b[2];
test2(new myInner(c));
} catch (NullPointerException e) {
throw e;
}
}
public static void test2(myInner inner){
System.out.println(inner.getA());
}
@Override
public void show(){
System.out.println("MainTest4");
}
class myInner{
private int a;
public myInner(int a) {
this.a = a;
}
public int getA(){
return a;
}
}
}
代码中使用了Junit包,为了解决使用javac编译找不到Junit相关类的问题,可利用ideal完成该类的编译,在Java Compile里面加上-g选项即可,如下图:
image.png
编译完成后,进入target/test-class目录下,执行javap -v MainTest4.class > javap.txt即可,结果如下:
Classfile /D:/git/MyJavaTest/target/test-classes/jvmTest/MainTest4.class
Last modified 2019-8-18; size 1616 bytes
MD5 checksum 7b6af831678f64966f42e7b998f3a374
Compiled from "MainTest4.java"
public class jvmTest.MainTest4 extends jvmTest.MainBase implements java.lang.Runnable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池表
Constant pool:
//#1表示常量池的索引,Methodref表示该项的类型,即CONSTANT_Fieldref_info,#17.#63实际是该类型的两个属性,class_index和name_and_type_index
//最后的//的内容是对该属性的翻译
#1 = Methodref #17.#63 // jvmTest/MainBase."<init>":()V
#2 = Fieldref #16.#64 // jvmTest/MainTest4.a:I
#3 = Long 13l
#5 = Fieldref #16.#65 // jvmTest/MainTest4.l:J
#6 = Methodref #16.#66 // jvmTest/MainTest4.show:()V
#7 = Class #67 // jvmTest/MainTest4$myInner
#8 = Methodref #7.#68 // jvmTest/MainTest4$myInner."<init>":(LjvmTest/MainTest4;I)V
#9 = Methodref #16.#69 // jvmTest/MainTest4.test2:(LjvmTest/MainTest4$myInner;)V
#10 = Class #70 // java/lang/NullPointerException
#11 = Fieldref #71.#72 // java/lang/System.out:Ljava/io/PrintStream;
#12 = Methodref #7.#73 // jvmTest/MainTest4$myInner.getA:()I
#13 = Methodref #74.#75 // java/io/PrintStream.println:(I)V
#14 = String #76 // MainTest4
#15 = Methodref #74.#77 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #78 // jvmTest/MainTest4
#17 = Class #79 // jvmTest/MainBase
#18 = Class #80 // java/lang/Runnable
#19 = Utf8 myInner
#20 = Utf8 InnerClasses
#21 = Utf8 a
#22 = Utf8 I
#23 = Utf8 RuntimeVisibleAnnotations
#24 = Utf8 Ljavax/xml/bind/annotation/XmlElement;
#25 = Utf8 name
#26 = Utf8 ax
#27 = Utf8 l
#28 = Utf8 J
#29 = Utf8 s
#30 = Utf8 Ljava/lang/String;
#31 = Utf8 ConstantValue
#32 = String #41 // test
#33 = Utf8 <init>
#34 = Utf8 ()V
#35 = Utf8 Code
#36 = Utf8 LineNumberTable
#37 = Utf8 LocalVariableTable
#38 = Utf8 this
#39 = Utf8 LjvmTest/MainTest4;
#40 = Utf8 run
#41 = Utf8 test
#42 = Utf8 (I)V
#43 = Utf8 a2
#44 = Utf8 a3
#45 = Utf8 b
#46 = Utf8 [I
#47 = Utf8 c
#48 = Utf8 e
#49 = Utf8 Ljava/lang/NullPointerException;
#50 = Utf8 StackMapTable
#51 = Class #70 // java/lang/NullPointerException
#52 = Utf8 Exceptions
#53 = Utf8 test2
#54 = Utf8 (LjvmTest/MainTest4$myInner;)V
#55 = Utf8 inner
#56 = Utf8 LjvmTest/MainTest4$myInner;
#57 = Utf8 show
#58 = Utf8 SourceFile
#59 = Utf8 MainTest4.java
#60 = Utf8 Lorg/junit/runner/RunWith;
#61 = Utf8 value
#62 = Utf8 Lorg/junit/runners/BlockJUnit4ClassRunner;
#63 = NameAndType #33:#34 // "<init>":()V
#64 = NameAndType #21:#22 // a:I
#65 = NameAndType #27:#28 // l:J
#66 = NameAndType #57:#34 // show:()V
#67 = Utf8 jvmTest/MainTest4$myInner
#68 = NameAndType #33:#81 // "<init>":(LjvmTest/MainTest4;I)V
#69 = NameAndType #53:#54 // test2:(LjvmTest/MainTest4$myInner;)V
#70 = Utf8 java/lang/NullPointerException
#71 = Class #82 // java/lang/System
#72 = NameAndType #83:#84 // out:Ljava/io/PrintStream;
#73 = NameAndType #85:#86 // getA:()I
#74 = Class #87 // java/io/PrintStream
#75 = NameAndType #88:#42 // println:(I)V
#76 = Utf8 MainTest4
#77 = NameAndType #88:#89 // println:(Ljava/lang/String;)V
#78 = Utf8 jvmTest/MainTest4
#79 = Utf8 jvmTest/MainBase
#80 = Utf8 java/lang/Runnable
#81 = Utf8 (LjvmTest/MainTest4;I)V
#82 = Utf8 java/lang/System
#83 = Utf8 out
#84 = Utf8 Ljava/io/PrintStream;
#85 = Utf8 getA
#86 = Utf8 ()I
#87 = Utf8 java/io/PrintStream
#88 = Utf8 println
#89 = Utf8 (Ljava/lang/String;)V
//方法表
{
public jvmTest.MainTest4();
//方法描述符
descriptor: ()V
//访问标识
flags: ACC_PUBLIC
//method_info结构的Code属性
Code:
//stack表示操作数栈的最大深度,locals表示本地变量的个数,args_size表示参数个数
stack=3, locals=1, args_size=1
//方法字节码
0: aload_0
1: invokespecial #1 // Method jvmTest/MainBase."<init>":()V
4: aload_0
5: bipush 12
7: putfield #2 // Field a:I
10: aload_0
11: ldc2_w #3 // long 13l
14: putfield #5 // Field l:J
17: return
//虚拟机指令对应的代码行号
LineNumberTable:
line 15: 0
line 17: 4
line 19: 10
//本地变量表,this是默认传入方法的参数
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this LjvmTest/MainTest4;
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #6 // Method show:()V
4: return
LineNumberTable:
line 24: 0
line 25: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LjvmTest/MainTest4;
public void test(int) throws java.lang.NullPointerException;
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=4, locals=6, args_size=2
0: iconst_2
1: istore_2
2: iconst_3
3: istore_3
4: iconst_3
5: newarray int
7: dup
8: iconst_0
9: iload_1
10: iastore
11: dup
12: iconst_1
13: iload_2
14: iastore
15: dup
16: iconst_2
17: iload_3
18: iastore
19: astore 4
21: aload 4
23: iconst_2
24: iconst_4
25: iastore
26: aload 4
28: iconst_2
29: iaload
30: istore 5
32: new #7 // class jvmTest/MainTest4$myInner
35: dup
36: aload_0
37: iload 5
39: invokespecial #8 // Method jvmTest/MainTest4$myInner."<init>":(LjvmTest/MainTest4;I)V
42: invokestatic #9 // Method test2:(LjvmTest/MainTest4$myInner;)V
45: goto 51
48: astore_2
49: aload_2
50: athrow
51: return
//方法中捕获的异常,try/catch的范围0-45,异常类型NullPointerException
Exception table:
from to target type
0 45 48 Class java/lang/NullPointerException
LineNumberTable:
line 29: 0
line 30: 2
line 31: 4
line 32: 21
line 33: 26
line 34: 32
line 37: 45
line 35: 48
line 36: 49
line 38: 51
LocalVariableTable:
Start Length Slot Name Signature
2 43 2 a2 I
4 41 3 a3 I
21 24 4 b [I
32 13 5 c I
49 2 2 e Ljava/lang/NullPointerException;
0 52 0 this LjvmTest/MainTest4;
0 52 1 a I
//将异常重新抛出使用
StackMapTable: number_of_entries = 2
frame_type = 112 /* same_locals_1_stack_item */
stack = [ class java/lang/NullPointerException ]
frame_type = 2 /* same */
//此方法可能抛出的异常类型
Exceptions:
throws java.lang.NullPointerException
public static void test2(jvmTest.MainTest4$myInner);
descriptor: (LjvmTest/MainTest4$myInner;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #12 // Method jvmTest/MainTest4$myInner.getA:()I
7: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
10: return
LineNumberTable:
line 41: 0
line 42: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 inner LjvmTest/MainTest4$myInner;
public void show();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #14 // String MainTest4
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 46: 0
line 47: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LjvmTest/MainTest4;
}
//ClassFile结构的属性表
//源代码文件名
SourceFile: "MainTest4.java"
//运行时可见的注解,#60是注解的类名,#61是属性名,c#62是属性值
RuntimeVisibleAnnotations:
0: #60(#61=c#62)
//该类的内部类
InnerClasses:
#19= #7 of #16; //myInner=class jvmTest/MainTest4$myInner of class jvmTest/MainTest4
javap命令不支持fileds字段表和interfaces接口表的打印,可参考OpenJDK langtools包下javap命令的实现,如下图:
image.png