使用 java 的人很多,使用 kotlin 的也不少,不过,对于它们编译后的文件,也就是 class 文件,却很多人望而却步,一是它晦涩难懂,二是它对于实际上的业务开发的帮助没那么常用。但是,它并不是没有用处的,只是你不懂而已。
开始正文。
查看 class 文件
首先,我们得先写个 Java 文件:
public class Test {
public void sayHello(){
System.out.println("Hello");
}
}
然后使用 javac 进行编译。
不怕看不懂,连流程图我都给你们写好:
使用 vim 打开:
vim -b Test.class
怎么回事,怎么变成这样,怎么跟之前看过的 class 文件长得不一样?
没事,我们换成 16 进制看看:
:%!xxd
这样舒服多了。好了,我们开始进行说明。
class 文件结构
首先,我们得先想下,由 java 代码编译成字节码,说明字节码是包含了 java 代码里面的全部内容,既然如此,那么字节码使用什么方式进行存储数据会比较高效?是使用换行符进行区分数据吗?还是使用特殊符号?似乎都不太理想。
而字节码实际上是使用流式进行存储,例如竹子,一节节的,每一节存储不同的数据,每一节的长度也不一定相同。大概是通过这种方式进行存储:
有可能有同学会有疑问,既然是像竹子一样,一节节的,那么怎么确定这一节的长度为多少?
一般会有两种解决方式:
- 固定的字节数
- 在这一节的开头使用固定的字节数来表示该节的长度
假如是以上面这种方式进行划分,那么,还可以这样进行说明:
魔数
固定 4 个字节,并且固定为 cafebabe,可能有人会好奇,为什么是 cafebabe?
java 图标和 Google 翻译应该能解决我们这个问题:
版本号
固定 4 个字节,前两个字节为次要版本,后两个字节为主要版本:
次要版本:0000
主要版本:0034
,转为十进制为:52
我们来对一下表:
所以,我当前的版本为:Java SE 8.0
常量池
可变长度,由前两个字节决定后续的表的数量。
常量池其实并不是我们 java 代码里面使用 static 修饰的成员变量,而是指这些表:
其中,我抽取 CONSTANT_Utf8_info 表格看看:
- tag:一个字节,标识为哪个表,基本每个表都有这个标识
- length:两个字节,表明下面的 bytes 长度
- bytes[length]:length 个字节,存储真正的数据
讲到到这里,可能有些人会懵。
嗯?我不知道在讲字节码吗?字节码不是以流的方式吗?怎么跟表有联系了,表在数据流上是怎么表示的?
我用一个图简单明了:
访问标志
占两个字符,用来标记当前 class 文件的访问信息,例如:该 class 为接口?类?枚举?有没有使用 public 修饰,有下面这些值:
类/父类/接口:
- 类:占用两个字节,标识类索引。其实就是标识为常量池的哪个表,如 0x0007,指的是常量池中第七个表
- 父类:占用两个字节,标识父类索引。作用同上。
- 接口:可变长度,前面两个字节标识接口索引的个数,剩下的为接口索引。
字段描述集合
可变长度,前面两个字节标识字段表的格式,后续为字段表:
- access_flags:固定两个字节,字段访问的唯一标识,有以下几种:
- name_index:固定两个字节,字段名称索引,标识常量池中的表。
- descriptor_index:固定两个字节,字段描述索引,标识常量池中的表。
- attributes_count:固定两个字节,标识后续的属性表个数。
- attributes[attributes_count]:属性表:
这个就不往下说了,都是同个套路,有兴趣可以去查询。
方法描述集合
- methods_count:固定两个字节,标识后续的方法表个数。
- methods[methods_count]:方法表:
属性描述集合
- attributes_count:固定两个字节,标识后续的属性表个数。
- attributes[attributes_count]:属性表:
只要之前我所讲解的东西大家都懂了的话,后续那些自己依葫芦画瓢去看下就行,也没必要去背,知道整个逻辑结构即可,若有兴趣的话,可以通过该链接仔细查看:文档
是不是觉得,其实字节码也没那么难,只要遵循规律即可。
不过的话,这个 16 进制的字节码主要并不是给开发者看的,我们可以通过 javap 进行反编译,这样查看更清楚。
class 反编译
javap -v Test.class
看,是不是跟我们上面讲的结构很像。由于部分数据没有,就省略了些,如字段描述集合就省略了。
这是我的公众号,关注获取第一信息!!欢迎关注支持下,谢谢!