嚼一嚼 class 文件结构

0、准备

如果需要运行本文中相关的程序或命令,您需要注意以下几点,不然可能得到不同的结果:

  • win10操作系统
  • 安装 jdk1.8.0_161 并配置好环境变量
  • 安装 Notepad++ 和里面的 HEX-Editor 插件

1、生成一个 class 文件

我们平时写的java代码存储在 .java 文件中,称之为 java 源文件。下面是 Test.java 文件的内容:

public class Test{private int i;public int add(){return i + 1;}
}

我们在 Test.java 文件的同目录下,按住 Shift+鼠标右键,选择”在此处打开**窗口“,然后输入javac Test.java。这个时候,会发现当前目录中生成了一个 Test.class 文件。

这个 class 文件就是我们今天的主角,它是由 java 编译器(javac)翻译 java 源文件(Test.java)得到的字节码文件(Test.class),这种字节码就是 JVM 的“机器语言”。

2、查看 class 文件内容

使用 Notepad++ 打开 Test.class 文件,然后依次选择 插件–>HEX-Editor–>View in HEX。

所以此时看到的内容,是class 文件的十六进制形式。十六进制数字两两相连,这是为什么呢?我们都知道,1byte = 8bit,而 4bit 刚好可以用一个十六进制数字表示,如 1111 = 0xf。所以一个字节刚好可以用两个十六进制数字表示。

下面贴上 class 文件内容:

接下来,我们简单翻译一下这个 class 文件,借助翻译的过程,逐渐了解 class 文件结构。

3、文件魔数[ca,fe,ba,be]

一个视频、一张图片,就算将其扩展名改成 txt,依旧可以用相关的软件打开。这是因为软件一般是通过文件的头几个字节来识别其类型,而不是通过扩展名识别,因为扩展名是容易被修改的。而文件的头几个字节,我们将其称之为文件的魔数,也可称作魔法值(Magic Number)。

我们可以看到,Test.class 文件头四个字节是[ca,fe,ba,be],这就是文件魔数。所有的 class 文件,头四个字节均与这相同。

4、class 文件版本[00,00,00,34]

魔数后面的四个字节,存储的是 class 文件的版本号,5、6个字节是次版本号(Minor Version),7、8字节是主版本号(Major Version)。

Test.class 文件的主版本号为 0x34,转成十进制为 52。

这里贴一下 JDK 版本与 class 文件版本号的对应关系:

JDK版本 class文件主版本(十六进制)
jdk1.1 45(0x2d)
jdk1.2 46(0x2e)
jdk1.3 47(0x2f)
jdk1.4 48(0x30)
jdk1.5 49(0x31)
jdk1.6 50(0x32)
jdk1.7 51(0x33)
jdk1.8 52(0x34)

5、常量池

5.1、常量池大小[00,13]

版本过后就是常量池的入口。

9、10字节存储的是常量池的大小,0x13 = 19,表示常量池的大小为19,但是实际上只有 18 个常量,因为计算机中索引是从 0 开始,这里常量池的索引本来是 0~18,即 19 个。但是在制定 class 文件格式规范的时候,设计者将第 0 项常量空了出来,为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值 0 来表示。所以常量池的索引为 1~18,总数为 18。

需要注意的是,class 文件中只有常量池的容量计数是从 1 开始的,其他的集合类型(注意,这可不是指 java 集合)都是从 0 开始。

5.2、常量格式

在具体翻译一个个常量之前,我们现在了解一下翻译的规则。

class 文件中只有两种数据类型,无符号数和表。

无符号数:基本的数据类型,可选值有 u1、u2、u4、u8,分别代表 1 个字节、2 个字节、4 个字节和 8 个字节。

表:多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info"结尾。

我们再回过头来看,可以发现前面介绍的魔数、版本以及常量池大小,分别可以用 u4(魔数)、u2(次版本号)、u2(主版本号)、u2(常量池大小)来表示。

表的类型有很多,这里贴一张结构表,后面翻译常量的时候会频繁用到:

表的类型有很多,在这里以 java 类形式表示一下第一个表类型:

CONSTANT_Utf8_info{u1 tag;// tag 为 1 即表示类型为 Utf8u2 length;// 使用两个字节来存储 Utf8 串的长度u1[length] content;// 根据 length 的长度确定读取后面的多少个字节
}

现在,已经基本了解了常量池中常量项的结构,接下来我们结合上面的表格来翻译常量。

5.3、常量集合

常量池入口指定了常量池的大小为 18,接下来我们一个个读取这些常量。

  • [0a,00,04,00,0f]:0x0a = 10 表示类型是 Methodref,接下来 0x0004 = 4、0x000f=15,表示指向的是第 4 个和第 15 个常量。我猜这是为了缩减 class 文件的大小,故将相同的常量以地址指向的形式来引用。
  • [09,00,03,00,10]:0x09 = 0 表示类型是 Fieldref,后面 0x0003 和 0x0010 表示分别指向第 3 个和第 16 个常量。
  • []07,00,11]:0x07 = 7 表示类型是 Class,引用指向第 17 个常量。
  • [07,00,12]:与上面相同,指向第 18 个常量。
  • [01,00,01,69]:0x01 = 1 表示类型是 Utf8,字符串的长度为 0x0001 = 1,故只需要读取后面一个字节 69。十六进制数字 0x69 对应的字符为小写字母 i,这就是我们前面定义的变量名。

翻译就到这里了,跟着前面的结构对照表,相信你已经可以轻松翻译剩下的常量。

5.4、验证翻译结果

只写代码不做测试是耍流氓,同样的,如果我只告诉各位怎么翻译而不告诉你们怎么验证,那也是在耍流氓。现在我们就通过 JDK 提供的工具,来验证一下,常量到底是不是我前面所说的那样。

在 Test.class 文件的目录中打开命令行,运行 javap -v Test 命令,就可以看到常量池的内容。这里贴一下运行结果:

这里我用红框框起来的,就是常量池。可以看到,大小 18,并且与我前面翻译过的 5 个常量内容是一致的。

6、访问标志[00,21]

常量池结束之后,紧接着的两个字节代表访问标志(access_flags)。用于识别当前 Class 是类还是接口、是否是 public 类型、是否是 abstract 类型、是类的话是否是 final 类等。

2byte = 16bit,故这里有十六个标志位,但并不是全部有用到,下面依次介绍用到的标志位:

0x21 的二进制为 100001 ,表示 ACC_SUPER 和 ACC_PUBLIC 为 1。这与代码情况也是一致的,我们是使用 JDK1.8 编译的,并且 Test 类确为 public 类型。

7、类索引、父类索引和接口索引集合

7.1、类索引[00,03]

指向第 3 个常量,而第 3 个常量引用第 17 个,值为 Test。表示当前类为 Test。

7.2、父类索引[00,04]

与上面类似,值为 java/lang/Object,表示 Test 父类为 Object。

7.3、接口索引集合

类索引和父类索引之后,紧接着的两个字节存储的是一个 u2 类型的数据,表示接口计数器。这里是[00,00],长度为 0 表示没有实现接口。

8、字段表集合

8.1、字段数量[00,01]

接口索引集合之后,紧接着就是字段表集合,头两个字节存储的便是这个集合的大小,这里可以看到集合大小为 1,表示只有一个字段。

相信你也发现了一些规律,比如集合类型的表,前面就会用一个 u2 类型的数据存储大小。

8.2、字段表[00,02,00,05,00,06]

字段表结构可以分为三部分,分别是 access_flags(u2,修饰符)、name_index(u2,名称)、descriptor_index(u2,类型)。

access_flags 各个标志位含义如下:

这里存储的数据是 0x0002,换算成二进制为 10,表示这个字段由 private 修饰。再根据 name_index[00,05] 和 descriptor_index[00,06],从第 5 个常量处取得字段名称 i,从第 6 个常量处取得字段类型 I。

字段类型描述符与其对应类型对照表如下:

描述符 类型
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型,如Ljava/lang/Object

所以这里准确的还原了 private int i; 这一行代码。

8.3、属性表集合

每个字段最后面,即 descriptor_index 之后,都会跟一个属性表集合。这里存储的值为 [00,00],表示没有其他的属性。如果定义的字段为 private int i = 1314;,那么这里就会存在一个属性,指向常量 1314。

9、方法表集合

9.1、方法数量[00,02]

表示这里有两个方法。

不用奇怪,编译器编译 java 源文件的时候,会将默认的无参构造函数加进去,故而会比源文件中看到的方法多一个。

9.2、方法表

结构与字段表几乎一致,这里贴一下方法的标志位含义:

方法1:access_flags(u2,访问标志)存储值为 [00,01],name_index(u2,名称)存储值为 [00,07],descriptor_index(u2,类型)存储值为 [00,08],最终得到的结果是 public <init>()V

表示这是一个构造方法,()V 表示返回值类型是 void。这与我们平常写 java 代码时的语法有点区别,java 代码中构造方法是没有返回值的,想来是因为构造方法都没有返回值,所以让编译器在编译时处理进而方便开发人员,也略微减小了 java 源文件的大小。

9.3、属性表集合

属性表计数器 [00,01] 表示这里有 1 个属性,属性名称 [00,09] 指向第 9 个常量 Code,说明此属性是方法的字节码描述。

对于每个属性,它的名称需要从常量池中引用一个 Utf8 类型,如这里的 [00,09] 引用的 Code。而属性值的结构是完全自定义的,只需要通过一个 u4 的长度属性来说明属性值所占用的位数即可。

这里 [00,00,00,1d] 表示,后面 29 个字节都是属性值。

9.4、另一个方法

前面 9.2 和 9.3 加在一起,翻译完了默认的构造方法。下面快速翻译一下另一个方法。

方法2:access_flags[00,01] --> public,name_index[00,0b] --> 常量11 --> add,descriptor[00,0c] --> 常量12 --> ()I。最终得到的结果是public add()I

属性表计数器 [00,01],属性名称 [00,09] --> Code,属性值长度 [00,00,00,1f] --> 31。

10、附加属性集合

方发表结合之后,就剩最后一部分附加属性集合了。

附加属性计数器 [00,01] 表示这里只有一个附加属性。接下来是一个 u2 类型的数据 [00,0d] --> 常量13 --> SourceFile,SourceFile 属性的结构为 u2(attribute_name_index) + u4(attribute_length) + u2(sourcefile_index),第一个 u2 已经翻译了是 SourceFile,第二个 u4[00,00,00,02] 表示长度是 2,第三个 u2[00,0e] --> 常量14 --> Test.java。

11、总结

最后贴一张图,总结以上的翻译过程:

嚼一嚼 class 文件结构相关推荐

  1. android 定义集合长度,Android Dex文件结构解析

    Java源文件通过Java编译器生成CLASS文件,再通过dx工具转换为classes.dex文件. DEX文件从整体上来看是一个索引的结构,类名.方法名.字段名等信息都存储在常量池中,这样能够充分减 ...

  2. 1 字节的 utf-8 序列的字节 1 无效_字节码文件结构详解

    点击上方" Java资料站 ",选择"标星公众号" 优质文章,第一时间送达 陈建源  |  作者 urlify.cn/INFrUr  |  来源 "一 ...

  3. Nginx入门笔记之————配置文件结构

    在nginx.conf的注释符号位# nginx文件的结构,这个对刚入门的同学,可以多看两眼. 默认的config: #user nobody; worker_processes 1;#error_l ...

  4. Class类文件结构、类加载机制以及字节码执行

    一.Class类文件结构 Class类文件严格按照顺序紧凑的排列,由无符号数和表构成,表是由多个无符号数或其他数据项构成的符合数据结构. Class类文件格式按如下顺序排列: 类型 名称 数量 u4 ...

  5. 深入理解java虚拟机之类文件结构以及加载

    我们都知道,java是一种平台无关的语言.java代码通过java编译器(如javac等),将.java文件编译成字节码,也就是.class文件.字节码是运行在jvm虚拟机之上的.而不同的平台则 有不 ...

  6. 修改class文件_VM实战(六) - 通过案例深入学习class文件结构原理

    0 更多干货关注 JavaEdge 公众号 1 什么是JVM的"无关性"? Java具有平台无关性,也就是任何操作系统都能运行Java代码.之所以能实现这一点,是因为Java运行在 ...

  7. nginx模型概念和配置文件结构

    一. nginx模型概念: Nginx会按需同时运行多个进程: 一个主进程(master)和几个工作进程(worker),配置了缓存时还会有缓存加载器进程(cache loader)和缓存管理器进程( ...

  8. Xamarin XAML语言教程XAML文件结构与解析XAML

    Xamarin XAML语言教程XAML文件结构与解析XAML XAML文件结构 在上文中,我们创建XAML文件后,会看到类似图1.16所示的结构 图1.16  结构 其中,.xaml文件和.xaml ...

  9. C++/C的文件结构

    C++/C的文件结构 对于C++/C的程序开发员来说,文件结构这一内容是很重要的一块,我们知道,每个C++/C程序通常分为两个文件.一个为头文件,用于保存程序的声明(declaration).另一个为 ...

  10. Python 基础语法_Python脚本文件结构

    目录 目录 前言 软件环境 Python Script文件结构 导入模块的流程 Python的包package 最后 前言 Python基础语法这一章,主要记录了Python的文件结构.逻辑运算符.算 ...

最新文章

  1. python学习_19
  2. Kafka Eagle 源码解读
  3. TC的handle绿了……菜鸟表示泪流满面
  4. 考研【研究所和高校的区别、全国375所独立于高校系统之外的 “研究生所”】
  5. jQuery 插件 Validation表单验证 使用步骤(详细的)
  6. envi中的sg滤波_ENVI滤波
  7. jQuery.sap.require
  8. c++学习书籍推荐《Advanced C++》下载
  9. “约见”面试官系列之常见面试题第八篇说说原型与原型链(建议收藏)
  10. 如何实现动态水球图 --》 echars结合echarts-liquidfill实现
  11. 【编译】StreamInsight应用调优入门(一)——概述
  12. 微信小程序项目实例——投骰子
  13. php如何配置gii,yii2 框架使用gii工具创建模块
  14. 融入动画技术的交互应用-雪花
  15. Vue 开始时间与结束时间比较验证
  16. 股市币市:本周交易数据分析与最新公告
  17. 3 Linux 10个主流发行版本
  18. winhex入门基础知识
  19. 里程碑图、横道图、项目进度网络图比较
  20. DIAGNOSTICS

热门文章

  1. 水溶性CdSe/ZnS量子点(520nm)
  2. 空间|时间|对象 圈人 + 目标人群透视 - 暨PostgreSQL 10与Greenplum的对比和选择
  3. Linux centOS 7.2 命令行下 静默安装部署oracle11g数据库
  4. CSS ::backdrop
  5. vue实现图片预加载的几种方式
  6. oracle 9i 启动监听报错误 TNS-12555: TNS:permission denied 解决
  7. MySQL NDB和InnoDB对比
  8. Vin码识别功能实现
  9. 他一定幸福地生活在那里
  10. 【常用的办公软件】万彩办公大师教程丨文件批量压缩工具