//环境的搭建:
1.创建一个类并编译它
2.使用java命令:javap -c -verbose build/classes/java/main/com/yang/jvm/Test.class 将16进制的字节码文件转成可视化的文件内容如下:
具体内容如下:
Classfile /F:/jvmdemo/build/classes/java/main/com/yang/jvm/Test.class Last modified 2019-10-17; size 573 bytes MD5 checksum f85b42e59f656b2cae810d27e91ccd7d Compiled from "Test.java" public class com.yang.jvm.Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#25 // java/lang/Object."":()V #2 = Fieldref #5.#26 // com/yang/jvm/Test.i:I #3 = String #27 // hello #4 = Fieldref #5.#28 // com/yang/jvm/Test.str:Ljava/lang/String; #5 = Class #29 // com/yang/jvm/Test #6 = Class #30 // java/lang/Object #7 = Utf8 i #8 = Utf8 I #9 = Utf8 str #10 = Utf8 Ljava/lang/String; #11 = Utf8 #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 Lcom/yang/jvm/Test; #18 = Utf8 getI #19 = Utf8 ()I #20 = Utf8 setI #21 = Utf8 (I)V #22 = Utf8 #23 = Utf8 SourceFile #24 = Utf8 Test.java #25 = NameAndType #11:#12 // " ":()V #26 = NameAndType #7:#8 // i:I #27 = Utf8 hello #28 = NameAndType #9:#10 // str:Ljava/lang/String; #29 = Utf8 com/yang/jvm/Test #30 = Utf8 java/lang/Object { public com.yang.jvm.Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: aload_0 5: iconst_5 6: putfield #2 // Field i:I 9: return LineNumberTable: line 3: 0 line 4: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/yang/jvm/Test; public int getI(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field i:I 4: ireturn LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/yang/jvm/Test; public void setI(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field i:I 5: return LineNumberTable: line 13: 0 line 14: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/yang/jvm/Test; 0 6 1 i I static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #3 // String hello 2: putstatic #4 // Field str:Ljava/lang/String; 5: return LineNumberTable: line 6: 0 } SourceFile: "Test.java"
此外:idea也有插件可以得到上面的字节码内容,安装插件jclasslib:使用该插件的好处是,对应的指令如aload_0可以点击,链接到oracle官网进行解释
安装重启后,鼠标点击在对应的java文件上,不是class文件,如Test.java文件,然后点击view菜单如图:
在右边可以看到:
3.使用16进制查看器查看字节码文件,此处是Test.class文件:
我用EditPlus工具,直接打开,会提示如下:
得到如下内容:
字节码文件的结构:
或者:
字节码文件的分析:主要是分析16进制文件与javap -c 命令生成的可阅读的ASCII编码的文件的对应关系:
1. 魔数:字节码开头的 4个字节,用于校验字节码是否合法,所有的字节码的魔数都是相同的,如:
2. 版本号version,4个字节:
上图版本号:minor_version 对应的16进制为00 00 ,转成10进制就是0,major_version版本号对应的16进制是00 34 转成10进制就是52,对应的jdk就是1.8,因此版本号是1.8.0,经常看到jdk版本号格式: 1.8.0_45后面的_n代表小更新版本,在自己码文件没有体现
版本号在ASCII码文件下如图:
常量池常量的数量: 16进制为00 1F 在10进制中表示31,所以常量池常量的数量为31,由于索引是从0开始的,但所有的字节码文件0位索引代表null,不显示出来,因此能看到的常量池数是31-1=30个:
在ASCII码文件下如图:
至此,我们已经知道常量池长度是30个,那在16进制文件中,到哪里截止呢,此时需要用到常量池表:
下图来自博客:https://blog.csdn.net/m0_37701628/article/details/86684589
根据上图,继续分析16进制文件,每个常量第一项都是tag的标志位,通过该值可以在上表找到对应的信息:
在常量池表中找到10对应是CONSTANT_Methodref_info,它后面2字节代表该方法属于的类的常量的索引,接着2个字节代表方法名称和类型的描述符索引项:
所以00 06 和 00 19 在10进制中表示: 6和25
对照着ASCII码文件可以看到:
因此,可以分析出来,第一个常量代表的是java.lang.Object对象的无参构造方法
第二个常量,标志位,09,查常量池表,该标志位代表的是字段,之后4个字节(00 05 00 1A )分别代表字段所属的类的常量池索引和字段描述符的索引:
00 05 00 1A分别对应10进制的5和26,所以在ASCII码文件中如图:
由此可见,第二个常量表示的是com/yang/jvm/Test类的字段i;其他常量按照上面的方法去分析,不再一一叙述了:下图是常量池内容的16进制:
至此,常量池分析完毕
常量池后2个字节表示的是访问标志符(Access Flags),根据上图知道为0021
根据上表知道0021是public 和super,在ASCII码文件可以看到:
访问标志符后的2个字节代表0005代表类名在常量池的索引,之后的2个字节0006代表父类的类名在常量池的索引,索引对应:
父类之后2个字节代表接口的数量:这里是00 00所以接口数量是0
紧接着2个字节代表field的个数:这里是0002所以字段为2
字段表的结构:
根据字段表结构分析:字段个数后2个字节是第一个字段的访问标志符0002,查表知道是private,之后的4个字节,前2个是字段名索引,后2个是字段描述符索引,这里是0007和0008,在ASCII码文件中如图:
接着2个字节是字段的attribute数量,这里是0000,所以是0,
由此可知,其为private i=5这个字段,
第一个字段之后2个字节是字段的访问修饰符,这里是000A 根据字段访问修饰符表知道,这个是private(0x0002) 和static(0x0008)的并集,因此第二个字段是private static修饰,
之后4个字节分别代表字段名和字段描述符在常量池的索引,0009和000A对应十进制的9和10,根据下图可知
该字段就是str;之后2个字节就是字段attribute数量,这里是0000,所以是0,
至此字段分析完毕;
方法的分析:字段之后仅跟着是方法,方法的结构,
前2个字节代表方法的个数,这里是0004,所以有4个方法:
方法的结构如下:
读取方法数量后2个字节,0001,对照访问修饰符表知道代表的是public,接着4个字节代表名字在常量池的索引和描述符在常量池的索引000B 和000C在
10进制中代表11和12,查下图知道:
由此可知是一个无参数的构造方法,之后2个字节是方法的attribute的数量,这里是0001,所以这里的属性有一个;
属性的结构如下图:
之后2个字节000D(10进制代表14)代表属性名字在常量池的索引
之后4个字节代表attribute_info的长度,这里是00 00 00 38,转成10进制就是56个字节:
code属性有特殊的结构:
在生成的ASCII码文件也可以看到:
attribute_info(00 00 00 38)长度后的2个字节代(00 02)表方法栈stack的深度最深多少,这里是2,
stack后2个字节代表方法局部变量的个数(包括方法参数),这里是00 01 代表1个局部变量,有人会问,这个方法是无参构造器,为何会有一个变量呢,这个变量又是什么呢,
其实这个变量就是this,JVM在编译时就已经将当前对象的引用作为一个方法的第一个参数传进去了,因此所有的非静态方法都会有一个this参数,接着个字节00 00 00 0A(十进制代表10)为code_length长度;
后面10个字节,每个字节代表一个指令;
第一个字节2A代表的指令为aload_0,在oracle官网(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aload_n)可以看到:
B7 代表:
上图可知,该指令有2个参数indexbyte1 和indexbyte2 分别为00和01,其中01在常量池表示如下:
因此表示的是执行Object的无参构造方法;
之后的2A又表示aload_0,紧跟着08在指令中表示如下:
将常量5入栈;
接着的B5代表的指令:
读取2个参数 00 和 02,其中02在常量池代表
由此可见是给字段i进行赋值,进一步可以知道,非静态字段的赋值操作是在构造器中进行的;
仅接着是B1代表的指令:
,因此构造方法其实也是有个默认的return返回值的;
接着2个字段代表异常表的长度00 00,所以没有异常,接着是2个字节的attribute_count 00 02,因此有2个Attribute;
根据attribute的结构:
知道紧跟着的2个字节是属性在常量池的索引:000E在十进制代表14:
这个是行号表的意思,记录方法执行代码在源码中的行号,这样就方便调试和异常抛出时,知道在哪一行出错;
之后4个字节代表该属性的长度: 00 00 00 0A 在十进制中表示10,因此行号表内容长度为10, 接着2个字节代表LineNumberTable数组长度00 02 代表有2行,
每一行的结构都有start PC 跟Line Number,前面的Nr,代表的是数组索引,从0开始,start PC 代表的是指令的行号索引,Line Number代表的是源码索引;如图:
startPC 2个字节表示 Line Number 也是2个字节,因此 00 00 和 00 03对上了第一行,00 04 和 00 04 对上了第一行,至此,行号表分析完成,
之后2个字段代表方法attribute的第二个属性名的在常量池的索引: 00 0F 在十进制中表示为:15,常量池表中知道:
这个是本地变量表,之后4个字节代表整个本地变量表字节长度00 00 00 0C (十进制中表示12个字节),本地变量表长度:
之后2个字节表示本地变量表索引长度 00 01,表示只有一个本地变量,接着2个字段表示start_pc 00,之后2好字段表示length 00 0A(十进制表示10),接着
2个字段是变量在常量池的索引,00 10 表示的是16,查下图得知:
代表的是this变量,之后2个字段代表描述符索引 00 11 表示的是17,从上图可知是Lcom/yang/jvm/Test;所以this代表的是当前类;
最后2个字节是this在变量表中的索引,00 00表示是第一个
至此,构造方法分析完毕,其他方法以此类推