ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
### **Class文件详解:** * 什么是Class文件? * 如何生成一个Class文件 * Class文件的作用 * Class文件格式详解 #### **什么是Class文件?** * * * * * Class文件:能够被JVM识别,加载并执行的文件格式。 *.java 文件是人编写的,给人看的。 *.class 是通过工具处理*.java 文件后的产物,它是给VM 看的,给VM 操作的 * * * * * 1. 就只是一种文件格式,类似于txt、mp4、mp3、word、pdf等 2. 不是只有Java语言才可以生成class文件。如下图 ![](https://box.kancloud.cn/b9d80f431ab5af3d75906be3e249d847_782x530.jpg) :-: 图1 class文件 3. 并不是只有Java语言才可以运行在JVM上。 ①早在Java发展之初,就有设计者考虑过并实现了让其他语言运行在Java虚拟机之上(JVM),而且发布规范文档时,分开划分为Java语言规范和Java虚拟机规范。在1997年发布的第一版Java虚拟机规范就曾经承诺过:“未来,我们会对Java虚拟机进行适当的扩展,以便更好地支持其他语言运行在JVM之上,”当Java虚拟机发展到JDK1.7-1.8的时候,JVM便通过JSR-292基本兑现了这个承诺。 ②或许大部分程序员都认为Java虚拟机执行Java程序是一件理所当然和天经地义的事,但时至今日,商业机构和开源机构已经在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Clojure、Groovy、JRuby、Jython、Scale等。 ③实现语言无关性的基础仍然是虚拟机和字节码(ByteCode)存储格式。**JVM不和包括Java在内的任何语言绑定,它只和“Class文件”这种特定的二进制文件格式所关联**,**Class文件中包含了JVM指令集和符号表达式以及若干其他辅助信息**。基于安全方面考虑,JVM规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被JVM所接受的有效的Class文件。**作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将JVM作为语言的产品交付媒介**。比如:使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其它语言的编译器一样可以把程序代码编译成Class文件,**Java之所以能够跨平台运行,是因为Java虚拟机可以载入和执行同一种平台无关的字节码**。也就是说,实现语言平台无关性的基础是虚拟机和字节码存储格式,**虚拟机并不关心Class的来源是什么语言,只要它符合Class文件应有的结构就可以在Java虚拟机中运行**。 ![](https://box.kancloud.cn/d168a400d451d5974e4eb3bbc6480b27_666x402.jpg) :-: 图2 JVM提供的语言无关性 4. JAVA语言中的各种变量、关键字和运算符号的语义最终都是由许多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大。所以,有一些java语言本身无法持有的语言特性不代表字节码本身无法有效持有。 #### **如何生成一个Class文件?** 1. 通过IDE自动帮我们build 2. 通过命令行手动通过javac去生成Class文件 3. 通过java命令去执行Class文件 #### **Class文件的作用** 1. 记录一个类文件的所有信息,所有信息!!!正是因为这个原因,可以解释为什么一个类中没有定义this,super这些关键字,但是却可以使用这些关键字调用父类的方法和当前的变量。 2. Class文件所包含的信息远远大于Java源代码中我们所看到的信息。 #### **Class文件结构** 1. 一种8位字节的二进制流。 2. 各个数据按顺序紧密的排列,无间隙。好处:减少Class文件的体积,JVM加载Class文件时更加快速 3. 每个类或者接口都单独占据一个class文件。好处:每个类或者接口都独自关联自己的内容,而无需交叉 一个Class文件中只能包含一个类或者接口。**注意:任何一个Class文件都对应着唯一一个类或者接口的定义信息,但反过来说,类或者接口不一定都得定义在文件里(譬如类或者接口也可以通过类加载器直接生成)**。 关于Class文件结构的详解,可参考[class类文件的结构](https://www.kancloud.cn/alex_wsc/java/466923) Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。 根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,**这种伪结构中只有两种数据类型:无符号数和表。** - **无符号数** 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值,或者按照UTF-8编码构成字符串值。 - **表** 是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。 它由下表所示的数据项构成。 下图是Class文件格式([大图这里](https://box.kancloud.cn/c0e92d9c6d5781819b8cd289d073943e_600x513.jpg)) ![](https://box.kancloud.cn/c0e92d9c6d5781819b8cd289d073943e_600x513.jpg) :-: 图3 Class文件格式 下图是Class文件字节码结构组织示意图([大图这里](https://box.kancloud.cn/04d43f4eb1df1b8a27378eae66b81a6a_849x993.png)) ![](https://box.kancloud.cn/04d43f4eb1df1b8a27378eae66b81a6a_849x993.png) :-: 图4 Class类文件字节码结构组织示意图 * **magic: 魔数** 所有的由Java编译器编译而成的class文件的前4个字节都是“0xCAFEBABE” 。 作用:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如果是,则JVM会认为可以将此文件当作class文件来加载并使用。**唯一作用是确定这个文件是否是一个能被虚拟机接受的Class文件**。很多文件存储标准都是用魔数来进行身份识别,譬如图片格式,如gif、jpeg等,**之所以使用魔数而不是文件扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。** 它其实就是一个加密段,类似于MD5加密一样,给JVM来判断当前的Class文件是否被篡改过? - **minor_version**:Class文件的次版本号或者叫副版本号 紧接着魔数的4个字节为Class文件的版本号。第五,六个字节为次版本号,第七,八个字节为主版本号。java的主版本从45开始。**高版本的JDK可以兼容低版本的Class文件,但不能运行以后版本的class文件**,即使文件格式并未发生变化。 对于Java虚拟机来说,版本号确定了特定的Class文件格式,**通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取Class文件。** Class文件的主、次版本号是由JDK决定的,JDK1.0~JDK1.1使用了45.0~45.3的版本号(45是主版本号,点”.“之后的是次版本号),从JDK1.1开始,每个大版本的JDK主版本号加1。JDK1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK1.2则能支持45.0~46.65535的Class文件。若现在使用的是JDK1.7编译出来的class文件,则相应的主版本号应该是51,对应的7,8个字节的十六进制的值应该是 0x33。 - **major_version** : Class文件的主版本号 同上 - **constant_pool_count**:常量池计数器 常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。 常量池是由一组constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。 常量池计数器constant_pool_count 的值 =constant_pool表中的成员数+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count时才会被认为是有效的。 - **constant_pool** : 常量池数据区 一、**Java 源码中的类名,方法名,变量名,都是以字符串形式存储在常量池中** 。常量池包含了与文件中类和接口相关的常量。常量池中存储了诸如文字字符串、final变量值、类名和方法名的常量。Java虚拟机把常量池组织为入口列表的形式。在实际列表constant_pool之前,是入口在列表中的计数constant_pool_count。 二、constant_pool是一种表结构,它**包含 Class 文件结构及其子结构中引用的所有字符串常量、 类或接口名、字段名和其它常量**。 常量池中的每一项都具备相同的格式特征——第一个字节作为类型标记用于识别该项是哪种类型的常量,称为 “tag byte” 。常量池的索引范围是 1 至constant_pool_count−1。 紧接着主次版本号之后的是常量池入口。常量池可以理解为Class文件之中的资源仓库。它是class文件结构中与其他项目关联最多的数据类型。,也是占用class文件空间最大的数据项目之一。同时它还是在class文件中第一个出现的表类型数据项目。 由于常量池中常量的数据是不固定的,所以在常量池的入口需要放置一荐u2类型的数据,代表常量池容量计算值(constant_pool_count)。与Java语言习惯不一样的是,这个容量计数是从1而不是0开始的。将第0项常量出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。class文件结构中只有常量池的容量计数是从1开始,对于其它集合类型,包括接口索引集合,字段表集合,方法表集合的容量计算都是从0开始的。 三、class常量池用来存储在“编译期间”产生的字面量和符号引用;常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串,被声明为final的常量值等。而符号引用则属性编译原理方面的概念,包含了下面三类常量: a.类和接口的全限定名(Fully Qualified Name) b.字段的名称和描述符(Descriptor) c.方法的名称和描述符 四、常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,JDK1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info),这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属性哪种常量类型,14种常量类型具体含义如下: 五、常量池的项目类型如下表所示 表一 常量池的项目类型 | 入 口 类 型 | 标 志 值 | 描 述 | | --- | --- | --- | | CONSTANT_Utf8_info | 1 | UTF-8编码的Unicode字符串 | | CONSTANT_Integer_info |3 | int类型字面值 | | CONSTANT_Float_info | 4 | float类型字面值 | | CONSTANT_Long_info | 5 | long类型字面值 | | CONSTANT_Double_info | 6 | double类型字面值 | | CONSTANT_Class_info | 7 | 对一个类或接口的符号引用 | | CONSTANT_String_info | 8 | String类型字面值 | | CONSTANT_Fieldref_info | 9 | 对一个字段的符号引用 | |CONSTANT_Methodref_info | 10 | 对一个类中声明的方法的符号引用 | | CONSTANT_InrerfaceMethodref_info | 11 | 对一个接口中声明的方法的符号引用 | | CONSTANT_NameAndType_info | 12 | 对一个字段或方法的部分符号引用 | | CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | | CONSTANT_MethodType_info | 16 | 表示方法类型 | | CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 | - 每个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型。 每一个标志都有一个相对应的表,表名通过在标志名后加上“_info”后缀来产生。 - 之所以说常量池是最繁琐的数据,是因为这14种常量类型各自均有自己的结构 - CONSTANT_Class_info常量的结构 表二 CONSTANT_Class_info常量的结构 | 类型 |名称 | 数量 | | --- | --- | --- | | u1 | tag | 1 | | u2 | name_index | 1 | tag是标志位,用于区分常量类型 name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,该敞亮代表了这个类(或者接口)的全限定名 - CONSTANT_Utf8_info型常量的结构 表三 CONSTANT_Utf8_info型常量的结构 | 类型 |名称 | 数量 | | --- | --- | --- | | u1 | tag | 1 | | u2 | length | 1 | | u1 | bytes | length | length说明这个UTF-8编码的字符串长度是多少字节。后面的bytes表示长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码和UTF-8编码的区别是:从‘\u0001’到 ‘\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从‘\u0080’到 ‘\u07ff’之间的字符的缩略编码用2个字节表示,从‘\u0800’到 ‘\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用3个字节表示。 另外,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_inf型常量来描述名称,所以CONSTANT_Utf8_inf型常量的最大长度也就是java中方法、字段名的最大长度。这里的最大长度就是length的最大值,既U2类型能表达的最大值65535.所以java程序中如果定义了超过64KB英文字符的变量或方法,将会无法编译。 各常量项结构:常量池中前11种据类型的结构总表([大图这里](https://box.kancloud.cn/520e493821970c76b709b58203d827e3_698x524.png)) ![](https://box.kancloud.cn/520e493821970c76b709b58203d827e3_698x524.png) :-: 图 5 常量池中11中数据类型的结构总表.png([大图这里](https://box.kancloud.cn/5f40d13db06e9c0b9239a7b0aa0851c1_714x409.png)) ![](https://box.kancloud.cn/5f40d13db06e9c0b9239a7b0aa0851c1_714x409.png) :-: 图 6 常量池数据类型结构总表2 示例:假如我们得到的Class文件的十六进制数的一段序列为: ![](https://box.kancloud.cn/ad477ad1bc978cee7a083d8539a75c96_521x63.jpg) 第9位的值为16转换为十进制为22,代表常量池中有21个常量。第10位的07代表的是一个常量的tag值,可以从上表中看到,07代表CONSTANT_Class_info类型,从上表中可以看出,CONSTANT_Class_info类型的结构有一个u1类型的tag,有一个u2类型的name_index,数量都是1。那么可以看出接下来的第11位与第12位的值00和02就是name_index的值。即指向的常量池中的第一个常量。第二项常量的标志位为0x01(看第13位),也就是CONSTANT_Utf8_info类型,CONSTANT_Utf8_info类型有u2型的length与u1型的bytes。依此类推。 如上所述,虚拟机加载Class文件的时候,就是这样从常量池中得到相对应的数值。 - **access_flags** : 访问标志 在常量池结束后(0x00000a0行74(t)处结束),紧接着的两个字节(共16位)代表访问标志,这个标志用于标识一些类或者接口层次的访问信息。包括这个Class是类是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否被声明为final等。具体含义见下表: [大图这里](https://box.kancloud.cn/da649f0f1b404c77e361d07d9fce8530_699x629.png) ![](https://box.kancloud.cn/da649f0f1b404c77e361d07d9fce8530_699x629.png) :-: 图7 访问标志及注意事项 [大图这里](https://box.kancloud.cn/973a47b33cc7cadb1d18f4740d398112_869x663.png) ![](https://box.kancloud.cn/973a47b33cc7cadb1d18f4740d398112_869x663.png) :-: 图8 访问标志位示意图 - **this_class** : 本类索引 类索引用来确定这个类的全限定名 类索引的作用,就是为了指出class文件所描述的这个类叫什么名字。 类索引紧接着访问标志的后面,占有两个字节,在这两个字节中存储的值是一个指向常量池的一个索引,该索引指向的是CONSTANT_Class_info常量池项, [大图这里](https://box.kancloud.cn/e344fcaa8c69d120dc8d497ed9263afd_791x411.png) ![](https://box.kancloud.cn/e344fcaa8c69d120dc8d497ed9263afd_791x411.png) :-: 图9 类索引 - **super_class** : 父类索引 父类索引用来确定父类的权限定名。 class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。 [大图这里](https://box.kancloud.cn/4a97993358e3c8575b51bace11d2046a_738x623.png) ![](https://box.kancloud.cn/4a97993358e3c8575b51bace11d2046a_738x623.png) :-: 图10 父类索引 - **interfaces** : 接口索引 接口索引用来确定接口的全限定名。 一个类可以不实现任何接口,也可以实现很多个接口,为了表示当前类实现的接口信息,class文件使用了如下结构体描述某个类的接口实现信息: [大图这里](https://box.kancloud.cn/5037626405e313de209ed31eddce2813_830x279.png) ![](https://box.kancloud.cn/5037626405e313de209ed31eddce2813_830x279.png) :-: 图11 接口索引 - 由于类实现的接口数目不确定,所以接口索引集合的描述的前部分叫做接口计数器(interfaces_count),接口计数器占用两个字节,其中的值表示着这个类实现了多少个接口,紧跟着接口计数器的部分就是接口索引部分了,每一个接口索引占有两个字节,接口计数器的值代表着后面跟着的接口索引的个数。接口索引和类索引和父类索引一样,其内的值存储的是指向了常量池中的常量池项的索引,表示着这个接口的完全限定名。 - 接口计数器(interfaces_count)记录的是直接继承的接口,不包括间接继承的接口(比如父类继承的接口不会记录) 类索引和父类索引都是一个两字节的数据,而接口索引确实一组两字节的数据,因为接口是可以多实现的。Class文件中于这三个数据来确定这个类的继承关系。 **举例**: 定义一个Worker接口,然后类Programmer实现这个Worker接口,然后我们观察Programmer的接口索引集合是怎样表示的。 ~~~ /** * Worker 接口类 * @author luan louis */ public interface Worker{ public void work(); } ~~~ ~~~ package com.louis.jvm; public class Programmer implements Worker { @Override public void work() { System.out.println("I'm Programmer,Just coding...."); } } ~~~ [大图这里](https://box.kancloud.cn/3e93de273128285fd55c81a11949dcef_666x1018.png) ![](https://box.kancloud.cn/3e93de273128285fd55c81a11949dcef_666x1018.png) :-: 图12 索引 - **field_info** : 字段表 field_info字段表用于描述接口或者类中声明的变量,field字段包括了类级变量(静态变量)和实例级变量(成员变量),但不包括方法内部的局部变量。 fields_count字段数目表示Class文件中的类和实例变量总数,字段存放的信息包括:字段访问标志、是否静态、是否final、是否并发可见volatile、是否可序列化transient、数据类型、字段名称等等。这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 注意:字段表中不包含从父类或者接口中继承而来的字段,但是会添加原本代码中不存在的字段,例如this,以及内部类对外部类访问而自动添加的外部类实例字段等。 ![](https://box.kancloud.cn/3882b180a7dade81725e0982ef9f8c0a_519x220.jpg) :-: 图13 字段表结构 字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的,都是一个u2的数据类型,其中可以设置 的标志位和含义如下表所示 ![](https://box.kancloud.cn/edd66eaf606e6d50d9a6ea1e97172bed_414x237.jpg) :-: 图14 字段访问标志 跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。 ![](https://box.kancloud.cn/5f29c611cdc199467c4bc4edcfd0b8fd_520x342.jpg) :-: 图15 描述符表示字符含义 - **method_info** : 方发表 method_info方法表用于描述类或者接口中声明的方法,methods_count用于表示Class文件中方法总数,method方法存储了方法的访问标识、是否静态、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值类型、方法名称、方法参数列表等信息。 方法的代码指令并没有直接存放在方法表中,而是存放着属性表中的方法表Code中。 注意:如果父类的方法在子类没有被重写,方法表中不会出现来自父类的方法信息,但是编译器会自动添加类构造器”`<clinit>`”方法和实例构造器”`<init>`”方法。 Java编译器的方法特征签名只包括:方法名称、参数顺序和参数类型,不包括方法返回值类型,因此java的方法重载不能通过方法的返回值类区别,但是在Class文件中,方法特征签名包括方法的返回值类型,因此Class文件中可以共存两个名称和参数完全相同而返回值类型不同的方法。 其结构与fields一样,不一样的是访问标志。 ![](https://box.kancloud.cn/920394de705d7412fdd1c4fcd8206d45_410x306.jpg) :-: 图16 方法访问标志 - **attribute_info** : 属性表 在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某种场景专有的信息。在属性表的实现中,只要不与已有属性名重复,可以在编译时添加任何自定义的属性信息,jvm会自动忽略掉它不认识的属性。下表为虚拟机规范预定义的属性: ![](http://img.blog.csdn.net/20170823155312082?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVsb25nMDgwOQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) :-: 图17 属性表 **示例**: 代码 ~~~ public class TestMain { public int mX = 0; public static void main(String[] args) { TestMain testMainObject = new TestMain(); testMainObject.test(); System.out.println("Hello World!"); } public TestMain() { } public void test() { return; } } ~~~ Jvm 规范很聪明,它通过一个C 的数据结构表达了class 文件结构。这个数据结构如下图所示: [大图这里](https://box.kancloud.cn/76c6e762694d497fee5ebf6dec5707fc_1030x726.jpg) ![](https://box.kancloud.cn/76c6e762694d497fee5ebf6dec5707fc_1030x726.jpg) :-: 图18 数据结构表示的Class文件内部结构 使用javap -verbose命令解析TestMain.class后的结果如下图所示: **注意:TestMain.class文件在F盘根目录下** ![](https://box.kancloud.cn/ec3ad5701e2700abea02192b9411514a_808x940.jpg) :-: 图19 TestMain.class解析后的结果 **常量池介绍** 注意,count_pool_count 是常量池数组长度+1。比如,假设某个Class文件常量池只有4 个元素,则count_pool_count=5)。 * * * * * javap 解析class 文件的时候,常量池的索引从1 算起,0 默认是给VM 自己用得,一般不显示0 这一项。这也是为什么上图中常量池第一个元素以#1 开头。所以,如果count_pool_count=5 的话,真正有用的元素是从count_pool[1]到count_pool[4]。 * * * * * 常量池数组的元素类型由下面的代码表示 ~~~ cp_info { //特别注意,这是介绍的cp_info 是相关元素类型的通用表达。 u1 tag; //tag 为1 个字节长。不论cp_info 具体是哪种,第一个字节一定代表tag u1 info[]; //其他信息,长度随tag 不同而不同 } //tag 取值,先列几个简单的: tag=7 <==info 代表这个cp_info 是CONSTANT_Class_info 结构体 tag=9 <==info 代表CONSTANT_Fieldref_info 结构体 tag=10 <==info 代表CONSTANT_Methodref_info 结构体 tag=8 <==info 代表CONSTANT_String_info 结构体 tag=1 <==info 代表CONSTANT_Utf8_info 结构体 ~~~ 在JVM 规范中,真正代表字符串的数据结构是CONSTANT_Utf8_info 结构体,它的结构如下代码所示: ~~~ CONSTANT_Utf8_info { u1 tag; u2 length; //下面就是存储UTF8 字符串的地方了 u1 bytes[length]; } 大家看图19中常量池的内容,比如#2=Utf8 TestMain 这行表示: 数组第二个元素的类型是CONSTANT_Utf8_info,字符串为“TestMain” ~~~ **几个常用的常量池元素类型** **(1) CONSTANT_Class_info** 这个类型是用于描述类信息的,此处的类信息很简单,就是类名(也就是代表类名的字符串) ~~~ CONSTANT_Class_info { u1 tag; //tag 取值为7,代表CONSTANT_Class_info u2 name_index; //name_index 表示代表自己类名的字符串信息位于于常量池数组中哪一个,也就是索引 } ~~~ name_index 对应的那个常量池元素必须是CONSTANT_Utf8_info,也就是字符串。解析图19中的例子,咱们再看看: ~~~ #1 = Class #2 // TestMain #2 = Utf8 TestMain 这说明: 1 常量池第一个元素类型为Class_info,它对应的name_index 取值为2,表示使用第2 个元素 2 常量池第二个元素类型为Utf8 内容为“TestMain” 3 #1 最后的//表示注释,它把第二行的字符串内容直接搬过来,方便我们查看 ~~~ **(2) CONSTANT_NameAndType_Info** 它用来描述方法/成员名以及类型信息的。 有点JNI 基础的童鞋相信不难明白,在JNI 中,一个类的成员函数或成员变量都可以由这个类名字符串+函数名字符串+参数类型字符串+返回值类型来确定(如果是成员变量,就是类名字符串+变量名字符串+类型字符串)来表达。既然是字符串,那么NameAndType_Info 也就是**存储了对应字符串在常量池数组中的索引**: ~~~ CONSTANT_NameAndType_info { u1 tag; u2 name_index; //方法名或域名对应的字符串索引 u2 descriptor_index; //方法信息(参数+返回值),或者成员变量的信息(类型)对应的字符串索引 } //还是来看图19中的例子吧 #13 = Utf8 ()V #15 = NameAndType #16:#13 // test:()V //合起来就是test.()V 函数名是test,参数和返回值是()V #16 = Utf8 test ~~~ * * * * * **注意**,对于构造函数和类初始化函数来说,JVM 要求函数名必须是<init>和<cinit>。当然,这两个函数是编译器生成的。 * * * * * **(3) CONSTANT_MethodrefInfo 三兄弟** Methodref_Info 还有两个兄弟,分别是Fieldref_Info,InterfaceMethodref_Info,他们三用于描述方法、成员变量和接口信息。刚才的NameAndType_Info 其实已经描述了方法和成员变量信息的一部分,唯一还缺的就是没有地方描述它们属于哪个类。而咱这三兄弟就补全了这些信息。他们三的数据结构如图20 所示: ![](https://box.kancloud.cn/a808a445fe3e62e354a4526755e4a467_347x308.jpg) :-: 图20 Methodref,Fieldref 和InterfaceMethoderef 结构 **Field 和Method 描述** 这两个Info 无非是描述了函数或成员变量的名字,参数,类型等信息。但是真正的方法、成员变量信息还包括比如访问权限,注解,源代码位置等。对于方法来说,更重要的还包括其函数功能(即这个函数对应的字节码)。 在Java VM 中,方法和成员变量的完整描述由如图21 所示的数据结构来表达的: ![](https://box.kancloud.cn/172d186188d94a962038f793913d8f98_844x468.jpg) :-: 图21 field_info 和method_info * access_flags:描述诸如final,static,public 这样的访问标志 * name_index:方法或成员变量名在常量池中对应的索引,类型是Utf8_Info * attribute_info:是域或方法中很重要的信息。我们单独用一节来介绍它。 **attribute_info 介绍** attribute_info 结构体很简单,如下代码所示: ~~~ attribute_info {//特别注意,这里描述的attribute_info 结构体也是具体属性数据结构的通用表达 u2 attribute_name_index; //attribute_info 的描述,指向常量池的字符串 u4 attribute_length; //具体的内容由info 数组描述 u1 info[attribute_length]; } ~~~ Java VM 规范中,attribute 类型比较多,我们重点介绍几个,先来看代表一个函数实际内容的Code 属性。 **(1) Code 属性** 代表Code 属性的数据结构如下图所示: ![](https://box.kancloud.cn/81a071769977bebf8e8742681c814aa7_581x334.jpg) :-: 图22 Code 属性数据结构 * 前2 个成员变量就不多说了。属于attribute 的头6 个字节,分别指向代表属性名字符串的常量池元素以及后续属性数据的长度。注意,Code 属性的attribute_name_index 所指向的那个Utf8 常量池元素对应的字符串内容就是“Code”,大家可参考图19 的#9。 * max_stack 和max_locals:虚拟机在执行一个函数的时候,会为它建立一个操作数栈。执行过程中的参数啊,一些计算值啊等都会压入栈中。max_stack 就表示该函数执行时,这个栈的最大深度。这是编译时就能确定的。max_locals 用于描述这个方法最大的栈数和最大的本地变量个数。本地变量个数包括传入的参数。 * code_length 和code:这个函数编译成Java 字节码后对应的字节码长度和内容。 * exception_table_length:用来描述该方法对应异常处理的信息。这块我不打算讲了,其实也蛮简单,就是用start_pc 表示异常处理时候从此方法对应字节码(由code[]数组表示)哪个地方开始执行。 * Code 属性本身还能包含一些属性,这是由attributes_count 和attributes 数组决定的。来看个实际例子吧,如图7 所示(接着图19 的例子): ![](https://box.kancloud.cn/8ae5adcff1d016e8d4b364acb32931cd_862x397.jpg) :-: 图23 main 函数code 属性解析 图23中: * stack=2,locals=2,args_size=1。结合代码,main 函数确实有一个参数,而且还有一个本地变量。**注意,main 函数是static 的。如果对于类的非static 函数,那么locals 的第0个元素代表this。** * stack 后面接下来的就是code 数组,也就是这个函数对应的执行代码。0表示code[]的索引位置。**0:new:代表这个操作是new 操作,此操作对应的字节码长度为3**,所以下一个操作对应的字节码从索引3 开始。 * LineNumberTable 也是属性的一种,用于调试,它将源码和字节码匹配了起来。比如**line 6: 0** 这句话代表该函数字节码0 那一个操作对应代码的第6 行。 * LocalVariableTable:它也是属性一种,用于调试,它用于描述函数执行时的变量信息。比如图23 中的**Start = 0:表示从code[]第0 个字节开始,Length = 21 表示到从start=0 到start+21 个字节**(不包含第21个字节,因为code 数组一共就21 个字节)**这段范围内,这个变量都有效(也就是这个变量的作用域),Slot=0 表示这个变量在本地变量表中第一个元素,还记得前面提到的locals 吗?,name 为“args”**,表示这个参数的名字叫args,类型(**由Signature 表示**)就是String 数组了。 * * * * * 另外,Android SDK build Tools 中的dx 工具dump class 文件得到的信息更全,大家可以试试。 使用方法是:dx --dump --debug xxx.class * * * * * 下图TestMain.class是通过010Editor工具检测的结果 [大图这里](https://box.kancloud.cn/f8460cdd18e474aabb4ca3efc707cac1_605x736.jpg) ![](https://box.kancloud.cn/f8460cdd18e474aabb4ca3efc707cac1_605x736.jpg) :-: 图24 010Editor检测TestMain.class结果 [大图这里 ](https://box.kancloud.cn/6492456d655f453af89b81ed7452c381_1052x897.jpg) [大图2这里](https://box.kancloud.cn/e2a3f939eee6ef16bf63ea760dedb426_1052x340.jpg) ![](https://box.kancloud.cn/6492456d655f453af89b81ed7452c381_1052x897.jpg) ![](https://box.kancloud.cn/e2a3f939eee6ef16bf63ea760dedb426_1052x340.jpg) :-: 图25 010Editor检测TestMain.class的TemplateResult结果 #### **Class文件弊端** - 内存占用大,不适合移动端。 Class文件包含的东西太多了,一个APP包含几百个类也不是不可能,且移动端Android手机内存小。 - 堆栈的加栈模式,加载速度慢。 - 文件IO操作多,类查找慢。 ### **参考链接**: [谈谈Java虚拟机——Class文件结构](http://blog.csdn.net/jiangnan_java/article/details/23535247) [Java虚拟机(四):Class文件结构及字节码指令](http://blog.csdn.net/Luckydog1991/article/details/51654964) [Java虚拟机三:Class类文件的结构](http://blog.csdn.net/yulong0809/article/details/77505290) [《Java虚拟机原理图解》 1.1、class文件基本组织结构](http://blog.csdn.net/luanlouis/article/details/39892027) [《Java虚拟机原理图解》1.3、class文件中的访问标志、类索引、父类索引、接口索引集合](http://blog.csdn.net/xuchishao/article/details/41211831) [Java虚拟机工作原理详解](http://blog.csdn.net/bingduanlbd/article/details/8363734)