class类文件结构解析

学习过java的人应该知道java文件会被编译成class文件,然后再由JVM加载去运行,JVM在加载class文件时的步骤:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。

大家应该有思考一个问题:class文件到底长什么样?JVM怎么解析class文件然后使用它的呢?如果我们使用txt去打开一个class,那我们只能看到一串“乱码”。

接下来我们一起来分析一个class文件的结构(由于该部分内容太多,这里只做简单的介绍,有兴趣的同学可以去阅读周志明的《深入理解java虚拟机》一书,书中针对类加载过程做了详细的讲解)。

下面这张图很好的反应了class文件从开头到结尾的内容 ↓
class内容

下面我们就结合上面的知识点来分析这段代码对应的class文件

package com.mli.loader.test;

public class ClassViewTest {

    public static void main(String[] args) {
        System.out.println("Hello Java");
    }
}

我们先来看下class文件原貌(我使用的是notepad++ 安装Hex Editor插件,大家可以选择自己喜欢的工具)
class文件

1.魔数
魔数
魔数就是一个文件拥有的特定的开头,对于class文件来说,它的魔数就是一个U4(代表4个字节),也就是从class文件开头数4个字节得到的字符串“cafebabe”

2.版本号
在这里插入图片描述
如果我们拿一个JDK1.8编译的文件到JDK1.5中去运行,就会提示版本过低,这是怎么做到的呢?
其实秘密就在class文件中,生成的class文件记录了编译时的JDK版本号,验证时会比对当前JDK版本号与class文件版本号。
版本号分小版本号和主版本号,就放在魔数字段后面,分别是一个U2,因此小版本号是00 00,转为为十进制就是0,主版本号是00 34,转换为十进制就是52,因此编译java的JDK版本就是52.0,也就是JDK1.8

3.常量池大小
在这里插入图片描述
紧接着版本号后面是一个U2:00 23,表示常量池大小34(注意常量池计数是从1而不是0开始),我们可以使用javap -verbose ClassViewTest查看具体的常量池内容正好是34个。

javap -verbose ClassViewTest显示结果↓
常量池内容
4.常量池内容
在这里插入图片描述
上一步我们获知到有34个常量,我们可以看到这些常量有Methodref,Fieldref,String,Class,Utf8,NameAndType等类型,大家可以参考上面给出的介绍。
常量池大小后面紧跟着就是具体的34个常量内容,但是JVM怎么知道他们具体类型,开始和终止位置呢?原理就在于这些类型的开头都有一个tag来标识具体类型,然后知道了具体类型就知道了具体长度,也就能够推导出一个常量的开始和结束的位置。第一个常量的tag紧跟着00 23后面,也就是0a,转为十进制就是10,也就是说tag为10,参照上面的表格我们知道tag为10就是Methodref类型,这和我们使用javap命令得到的常量池内容吻合。

我们依次推理出所有的34个常量:

  1. 0a 00 06 00 15
  2. 09 00 16 00 17
  3. 08 00 18
  4. 0a 00 19 00 1a
  5. 07 00 lb
  6. 07 00 1c
  7. 01 00 06 3c 69 6e 69 74 3e
  8. 01 00 03 28 29 56
  9. 01 00 04 43 6f 64 65
  10. 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65
  11. 01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65
  12. 01 00 04 74 68 69 73
  13. 01 00 23 4c 63 6f 6d 2f 6d 6c 69 2f 6c 6f 61 64 65 72 2f 74 65 73 74 2f 43 6c 61 73 73 56 69 65 77 54 65 73 74 3b
  14. 01 00 04 6d 61 69 6e
  15. 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56
  16. 01 00 04 61 72 67 73
  17. 01 00 13 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
  18. 01 00 10 4d 65 74 68 6f 64 50 61 72 61 6d 65 74 65 72 73
  19. 01 00 0a 53 6f 75 72 63 65 46 69 6c 65
  20. 01 00 12 43 6c 61 73 73 56 69 65 77 54 65 73 74 2e 6a 61 76 61
  21. 0c 00 07 00 08
  22. 07 00 1d
  23. 0c 00 1e 00 1f
  24. 01 00 0a 48 65 6c 6c 6f 20 4a 61 76 61
  25. 07 00 20
  26. 0c 00 21 00 22
  27. 01 00 21 63 6f 6d 2f 6d 6c 69 2f 6c 6f 61 64 65 72 2f 74 65 73 74 2f 43 6c 61 73 73 56 69 65 77 54 65 73 74
  28. 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
  29. 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
  30. 01 00 03 6f 75 74
  31. 01 00 15 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d
  32. 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
  33. 01 00 07 70 72 69 6e 74 6c 6e
  34. 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56

这里我们看第一个常量0a 00 06 00 15,去掉第一个tag字节还剩00 06 00 15
其中00 06是class_index,指向常量池第6个常量07 00 1c,同样去掉tag还剩00 1c,指向第28个常量,第28个常量为01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 去掉第一个tag,第二、三是长度,还剩6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74,转字符串为java/lang/Object
00 15是name_and_type_index,指向常量池第21个常量0c 00 07 00 08,去掉第一个tag还剩00 07 00 08,其中00 07指向第7个常量01 00 06 3c 69 6e 69 74 3e,转字符串为,00 08指向第8个常量01 00 03 28 29 56,转字符串为()V
因此第一个常量0a 00 06 00 15转为字符串就是java/lang/Object.<init>:()V,这与我们使用javap -verbose ClassViewTest得到的结果吻合,剩下来的常量感兴趣的话大家可以自行解析。

5.访问标识
访问标识
常量池过后紧跟着就是访问标识字段,长度2个字节:00 21
通过代码我们知道我们有public修饰符,没有什么final,使用的JDK1.8,不是借口,不是抽象类,是我们自己编写的代码,不是注解,不是枚举,因此我们得到ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 = 0x0021
访问标识
6.类索引
00 05,指向常量池第5个常量07 00 lb,最终指向常量池第27项:63 6f 6d 2f 6d 6c 69 2f 6c 6f 61 64 65 72 2f 74 65 73 74 2f 43 6c 61 73 73 56 69 65 77 54 65 73 74,转为字符串为com/mli/loader/test/ClassViewTest,即当前类的全限定名

7.父类索引
00 06,指向常量池第6个常量07 00 1c,最终指向常量池第28项:6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74,转为字符串为java/lang/Object,即父类为Object

8.接口个数
00 00,表示该类并没有实现任何接口

9.接口类索引信息
因为没有实现任何接口,所以该项内容为空,不占用字节

10.字段数
00 00,表示该类没有任何字段

11.字段表信息
因为没有任何字段,所以该项内容为空,不占用字节

12.方法数
00 02,表示该类中有两个方法(一个构造函数,一个main方法,下面我们分析第一个方法,剩下的方法留给大家自己分析)

构造方法↓
在这里插入图片描述
13.方法表信息

方法表结构图↓
在这里插入图片描述
00 01 access_flags,对应ACC_PUBLIC
00 07 name_index,指向常量池第7项:
00 08 descriptor_index,指向常量池第8项:()V
00 01 attributes_count



方法属性结构图↓
在这里插入图片描述
00 09 attribute_name_index 指向常量池第9个常量:Code,说明是方法的字节码描述
00 00 00 2f attribute_length
00 01 max_stack
00 01 max_locals
00 00 00 05 code_length
2a b7 00 01 b1 5个字节的字节码指令
00 00 exception_table_length 值为0说明没有异常表
00 02 attributes_count 值为2表示有2个属性



line_number_table结构图↓
在这里插入图片描述
00 0a attribute_name_index 指向常量池第10项,是一个LineNumberTable
00 00 00 06 attribute_value
00 01 line_number_table_length



line_number_info结构图↓
在这里插入图片描述
00 00 start_pc
00 03 line_number



local_variable_table结构图↓
在这里插入图片描述
00 0b attribute_name_index 指向常量池第11项,是一个LocalVariableTable
00 00 00 0c attribute_length
00 01 local_variable_table_legth



local_variable_info结构图↓
在这里插入图片描述
00 00 start_pc
00 05 length
00 0c name_index 指向常量池第12项:this
00 0d descriptor_index 指向常量池第13项:Lcom/mli/loader/test/ClassViewTest;
00 00 index

剩下的一个方法留给大家自行分析
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值