本来想回顾下类加载机制,但是感觉学习类加载机制之前应该先了解下类文件结构,所以就有了这篇文章。来次够先学习起来祈祷明天获得一个面试机会呀!!!
首先思考一个问题:
是不是只有java编译器才能完成 java到 .class字节码文件的过程?(突然想来个原谅色)
第一眼看到这个问题时候我好像楞了几秒,额,有点不知所措。其实是个很简单的问题,之前学习过的Groovy不就可以编译为class文件么。虽然学习过但是一直没有使用好像已经忘差不多了,只记得不用写分号有点爽。过几天重新学习下,如果还有疑问请看下图:
Scala、jython、JRuby、Groovy都可以编译成class文件,到这里我好像对。class文件有点感觉了
window环境下想要研究字节码文件首先我们需要一个可以查看字节码文件的编译器,这个选择就很多了你可以在idea上安装HexView,也可以使用notePad或者binary Viewer这样的工具。自行安装下载吧.
第一次看我已经懵逼了,讲道理有点晕。但是有什么办法呢,只能硬着头皮一点一点看了。反正是写给自己看的就不画表格了。。。
Class文件: 是一组以8位字节为基础单位的二进制流,当遇到需要8位字节以上空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。
Class文件由无符号数和表构成。
无符号数: 以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值、按照UTF-8编码构成的字符串值。
表: 由多个无符号数或其他表作为数据项构成的复杂数据类型,所有表都习惯性地以“_info”结尾。
我们先看一下class文件的总体结构再一一分析::
1.魔数(U4): 每个class文件的前四个字节,它的作用是确认此文件是否为能被虚拟机接受的class文件。所以这也是为什么魔数固定存在于class文件的前四个字节。同时它的值也是固定的(ca-fe-ba-be ),很好记忆就是咖啡宝贝么哈哈哈。
2.次版本号:
3.主版本号:
4.常量池计数器(U2): 00-5A:10+5*16=90, 表示常量池数为90-1=89。为什么常量池数不是90而是89呢? ,因为常量池的计数器并不是从0开始计数的,而是从1开始。这里好像不太容易理解,本菜鸟暂时理解为像保留的关键字一样这里java在最初设计时也有考虑。在当时指定class文件规范的时候,为了满足某些指向常量池的索引值的数据在特定情况下能够表达。此时就可以不引用任何一个常量池,从而用保留的0来表达学习常量池我们首先需要一张对照表,如下。
为了区分下面换个原谅色:
ca fe ba be 20 20 20 34 20 5a 0a 20 0a 20 4a 09 20 09 20 4b 09 20 09 20 4c 09 20 09 20 4d 09 20 09 20 4e 09 20 09 20 4f 09 20 09 20 50 0a 20 51 20 52 07 20 53 07 20 54 07 20 55 07 20 56 01 20 0d 53 74 75 44 65 74 61 69 6c 56 69 65 77 01 20 0c 49 6e 6e 65 72 43 6c 61 73 73 65 73 07 20 57 01 20 0b 53 74 75 53 69 6d 70 56 69 65 77 01 20 03 73 6e 6f 01 20 10 4c 6a 61 76 61 2f 6c 61 6e 67 2f 4c 6f 6e 67 3b 01 20 19 52 75 6e 74 69 6d 65 56 69 73 69 62 6c 65 41 6e 6e 6f 74 61 74 69
0A = 10: 这个时候我们就需要根据数值对照下面常量池表查找对应常量了,10的值对应为 CONSTANT_Methodref_info。从表格中可以看出它后面对应的两个索引即为000A 与 004A 转换为10进制分别为10和74。
随所以10与74的意义分别为,声明方法的类描述符CONSTANT_Class_info的索引,与名称及类型描述符CONSTANT_NameAndType的索引
以此类推一直到常量池的结尾,上面提到的89个感觉好多。。。。。就不一一推倒了。
到这里可能会好奇了我们费尽九牛二虎之力查表的目的是什么??
哈哈哈当然是为了摆脱菜鸟称号了
接下来我们需要用javap命令反编译拿到我们的字节码文件与我们之前看到的机器码进行相互印证
我上面测试类反编译后字节码如下(贴一小部分上来):
5.常量:
E:\project\learning2\graduation_project\target\classes\com\chihai\graduation_project\entity>javap -v Stu.class
Classfile /E:/project/learning2/graduation_project/target/classes/com/chihai/graduation_project/entity/Stu.class
Last modified 2019-9-1; size 3073 bytes
MD5 checksum f0dcf3623c3133d43a9fdad54d79fa66
Compiled from "Stu.java"
public class com.chihai.graduation_project.entity.Stu implements java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#74 // java/lang/Object."<init>":()V
#2 = Fieldref #9.#75 // com/chihai/graduation_project/entity/Stu.sno:Ljava/lang/Long;
#3 = Fieldref #9.#76 // com/chihai/graduation_project/entity/Stu.sname:Ljava/lang/String;
#4 = Fieldref #9.#77 // com/chihai/graduation_project/entity/Stu.ssex:Ljava/lang/String;
#5 = Fieldref #9.#78 // com/chihai/graduation_project/entity/Stu.sbirthday:Ljava/util/Date;
#6 = Fieldref #9.#79 // com/chihai/graduation_project/entity/Stu.sclass:Ljava/lang/String;
#7 = Fieldref #9.#80 // com/chihai/graduation_project/entity/Stu.address:Lcom/chihai/graduation_project/entity/Address;
#8 = Methodref #81.#82 // java/lang/String.trim:()Ljava/lang/String;
#9 = Class #83 // com/chihai/graduation_project/entity/Stu
#10 = Class #84 // java/lang/Object
#11 = Class #85 // java/io/Serializable
#12 = Class #86 // com/chihai/graduation_project/entity/Stu$StuDetailView
#13 = Utf8 StuDetailView
#14 = Utf8 InnerClasses
#15 = Class #87 // com/chihai/graduation_project/entity/Stu$StuSimpView
#16 = Utf8 StuSimpView
#17 = Utf8 sno
#18 = Utf8 Ljava/lang/Long;
#19 = Utf8 RuntimeVisibleAnnotations
#20 = Utf8 Ljavax/validation/constraints/NotEmpty;
#21 = Utf8 message
#22 = Utf8 学生编号不能为空
#23 = Utf8 Lcom/fasterxml/jackson/annotation/JsonView;
#24 = Utf8 value
#25 = Utf8 Lcom/chihai/graduation_project/entity/Stu$StuSimpView;
#26 = Utf8 RuntimeVisibleTypeAnnotations
#27 = Utf8 sname
#28 = Utf8 Ljava/lang/String;
#29 = Utf8 ssex
#30 = Utf8 Lcom/chihai/graduation_project/entity/Stu$StuDetailView;
#31 = Utf8 sbirthday
#32 = Utf8 Ljava/util/Date;
#33 = Utf8 Lorg/springframework/format/annotation/DateTimeFormat;
#34 = Utf8 pattern
#35 = Utf8 yyyy-mm-dd HH-mm-ss
#36 = Utf8 sclass
#37 = Utf8 address
#38 = Utf8 Lcom/chihai/graduation_project/entity/Address;
#39 = Utf8 <init>
#40 = Utf8 (Ljava/lang/Long
y/Address;)V
#41 = Utf8 Code
#42 = Utf8 LineNumberTable
#43 = Utf8 LocalVariableTable
#44 = Utf8 this
#45 = Utf8 Lcom/chihai/graduation_project/entity/Stu;
#46 = Utf8 ()V
#47 = Utf8 getAddress
#48 = Utf8 ()Lcom/chihai/graduation_project/entity/Address;
#49 = Utf8 setAddress
#50 = Utf8 (Lcom/chihai/graduation_project/entity/Address;)V
#51 = Utf8 getSno
#52 = Utf8 ()Ljava/lang/Long;
#53 = Utf8 setSno
#54 = Utf8 (Ljava/lang/Long;)V
#55 = Utf8 getSname
#56 = Utf8 ()Ljava/lang/String;
#57 = Utf8 setSname
#58 = Utf8 (Ljava/lang/String;)V
#59 = Utf8 StackMapTable
#60 = Class #83 // com/chihai/graduation_project/entity/Stu
#61 = Class #88 // java/lang/String
#62 = Class #88 // java/lang/String
#63 = Utf8 getSsex
#64 = Utf8 setSsex
#65 = Utf8 getSbirthday
#66 = Utf8 ()Ljava/util/Date;
#67 = Utf8 setSbirthday
#68 = Utf8 (Ljava/util/Date;)V
#69 = Utf8 getSclass
#70 = Utf8 setSclass
#71 = Class #88 // java/lang/String
#72 = Utf8 SourceFile
#73 = Utf8 Stu.java
#74 = NameAndType #39:#46 // "<init>":()V
#75 = NameAndType #17:#18 // sno:Ljava/lang/Long;
#76 = NameAndType #27:#28 // sname:Ljava/lang/String;
#77 = NameAndType #29:#28 // ssex:Ljava/lang/String;
#78 = NameAndType #31:#32 // sbirthday:Ljava/util/Date;
#79 = NameAndType #36:#28 // sclass:Ljava/lang/String;
#80 = NameAndType #37:#38 // address:Lcom/chihai/graduation_project/entity/Address;
#81 = Class #88 // java/lang/String
#82 = NameAndType #89:#56 // trim:()Ljava/lang/String;
#83 = Utf8 com/chihai/graduation_project/entity/Stu
#84 = Utf8 java/lang/Object
#85 = Utf8 java/io/Serializable
#86 = Utf8 com/chihai/graduation_project/entity/Stu$StuDetailView
#87 = Utf8 com/chihai/graduation_project/entity/Stu$StuSimpView
#88 = Utf8 java/lang/String
#89 = Utf8 trim
这个时候好像发现了什么,如果联系上文我们所对照得到的数字在字节码中我们都能找到其相对应的代码指令。version: 52有没有很熟悉呢这就是我们机器码所对应的主版本号0034.我相信看到这里大家跟我一样找到了一点感觉。Constant pool:拉到最下面我们可以清楚的看到#89 正好验证了我们上面的解析不多不少89个。忘了的朋友们可以返回去看一下-------飞雷神之术
篇幅原因常量池我们就先到这里。我们直接跳到最后一个常量,继续往下看(这里有个问题密麻麻们的机器码中怎么能直接找到第89个常量在哪呢?仔细观察我们最后结尾#89 = Utf8 只需要找到最右边对应的字符就可以找到常量池结尾所对应的的行数)
53 74 72 69 6e 67 01 20 04 74 72 69 6d 00 21 00 String…trim.!. 返回上面字节码看一下就明白其中原理了
6.访问标志:
这里我们需要思考0021是什么,其实远没有我们想象中复杂。真相就是1+20,对照图表中我们不难发现当们一个类声明为 public class没有其它的时候就是0021(public class Stu implements Serializable )
接下来我们继续往下看为了方便我把一段机器码粘贴过来如下:
53 74 72 69 6e 67 01 00 04 74 72 69 6d 00 21 00
09 00 0a 00 01 00 0b 00 06 00 02 00 11 00 12 00
访问标志,之后的排序为类索引,父类索引,接口索引集合。那我们来验证下到底是不是这样:
类索引(00-09):
我们去字节码文件中找到对应#9常量-------------------》 #9 = Class #83 // com/chihai/graduation_project/entity/Stu 不偏不倚正好对应我们的类索引
类索引(00-0A):
#10 = Class #84 // java/lang/Object 因为我们的demo类没有继承其他类所以父类自动为Object
接口计数器(00-01):
因为我们的类只实现了Serializable接口所以计数器为1,也完全正确
接口1(00-0B):
#11 = Class #85 // java/io/Serializable
6.字段表集合:
字段表计数器(00-06):
刚好对应下面6个字段:
#17 = Utf8 sno
#27 = Utf8 sname
#29 = Utf8 ssex
#31 = Utf8 sbirthday
#36 = Utf8 sclass
#37 = Utf8 address
字段信息结构表():
字段访问标志:
7.字段表结构::
因为字段表与方发表几乎是一样的所以我们就挑一个看就可以了
8.方法表集合:
00 00 0c 00 01 00 00 00 05 00 2c 00 2d 00 00 00
01 00 2f 00 30 00 01 00 29 00 00 00 2f 00 01 00
方法表的查找顺序为:
1.access_flags ---------2.name _index --------3.descriptor_index-------4.attribute_count
5.对照attribute_info(U2,U4,U1*length) --------6.name_index代表着字段的简单名称
7.对应U4 attribute_length
需要说明的是从第一行末尾00开始的是我们方法表集合,所以我们来看0001-002f-0030代表的是什么
0001代表public
002f为47对应字节码常量: #47 = Utf8 getAddress
0030为48对应字节码常量:()Lcom/chihai/graduation_project/entity/Address;
0001:有一个属性表
对照attribute_info(U2,U4,U1length)
0029为41对应字节码常量: #41 = Utf8 Code
0000-002f对应字节码常量47: 47u1 每一行16个U1 后面2个 ,2+2*16+13 = 47,即往下3行,到第3行的倒数第2个U1,这一块区域即为我们方法块中的信息
到这里我们就可以看出其代表的正式我们类中的:public Address getAddress(){}方法
47个U1包含的方法代码块信息对照下表:
直接截图了复制notpad不知为什么过来数字总会变化,从紫色开始。。。这个是紫色么接上面的机器码
max_stack:方法的栈深:0001代表当前方法栈深为1
max_locals:方法的本地变量数量:0001 我们与字节码对照一下
args_size=1 :代表方法参数个数(为什么明明没有参数却为1,因为代表默认this,如果为static则为0,这就使我们为什么可以使用this的原因)
code_length:是U4所以是0000 0005 代表代码行数
剩下的即为方法字节码,此时我们需要对照虚拟机字节码指令表来解释。
我们简单看一下:2a------- 0x2a aload_0 将第一个引用类型本地变量推送至栈顶
b4---------0xb4 getfield 获取指定类的实例域, 并将其压入栈顶
#7 = Fieldref #9.#80 // com/chihai/graduation_project/entity/Stu.address:Lcom/chihai/graduation_project/entity/Address;
因为demo没有抛出异常,所以字节码中没有异常表
存在方法体中:
本方法throw
LineNumberTable:
line 50: 0
50是指向我们java文件中的行数 ,0则是指向字节码code中行数
完全与我们下面的字节码一致
public com.chihai.graduation_project.entity.Address getAddress();
descriptor: ()Lcom/chihai/graduation_project/entity/Address;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #7 // Field address:Lcom/chihai/graduation_project/entity/Address;
4: areturn
LineNumberTable:
line 50: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/chihai/graduation_project/entity/Stu;
6.属性表集合:
LocalVariableTable: 本地变量表
Signature:在我们JVM中是伪泛型,编译时会进行泛型擦除。改用Signature标识
SourceFile: “Stu.java”
ConstantValues:通知虚拟机自动为静态变量赋值
其实并不难只是有点麻烦写个简单的类对照表一步步分析还是可以的,本菜鸟就先到这里了!!!!要爱惜头发后面就不分析了