Class字节码文件


Java文件经过编译后生产Class字节码文件。JVM时通过字节码来执行。对于程序员来说对class的机制熟悉很重要。

1. Class 文件的组成

上图的class文件可以用下图来表达,U4便是4个无符号字节

Class文件结构的解析:

1. 魔术:

所有的由Java编译器编译而成的class文件的前4个字节都是“0xCAFEBABE” (谐音咖啡宝贝)。 它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为 可以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如 果是,则JVM会认为可以将此文件当作class文件来加载并使用。

2. 版本号

版本号分为次版本号和主版本号。主版本号和次版本号在class文件中各占两个字节,副版本号占用第5、6两个字节,而主版本号则占用 第7,8两个字节。

JDK1.0 主版本号为45, 1.1 为46, 依次类推,到JDK8的版本号为52, 16进制为0x33.

 一个 JVM实例只能支持特定范围内的主版本号 (Mi 至Mj) 和 0 至特定范围内 (0 至 m) 的副版 本号。假设一个 Class 文件的格式版本号为 V, 仅当Mi.0 ≤ v ≤ Mj.m成立时,这个 Class 文件 才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件,反之则不成立。 JVM在加载class文件的时候,会读取出主版本号,然后比较这个class文件的主版本号和JVM本身的版 本号,如果JVM本身的版本号 < class文件的版本号,JVM会认为加载不了这个class文件,会抛出我 们经常见到的" java.lang.UnsupportedClassVersionError: Bad version number in .class file " Error 错误;反之,JVM会认为可以加载此class文件,继续加载此class文件。

3. 常量池计数器

常量池是由一组 constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。常量池计数器 constant_pool_count 的值 =constant_pool表中的成员数+ 1。

constant_pool表的索引值只有在 大于 0 且小于constant_pool_count时才会被认为是有效的。

注意事项: 常量池计数器默认从1开始而不是从0开始: 当constant_pool_count = 1时,常量池中的cp_info个数为0;当constant_pool_count为n时,常 量池中的cp_info个数为n-1。

原因: 在指定class文件规范的时候,将索引#0项常量空出来是有特殊考虑的,这样当:某些数据在特定的情 况下想表达“不引用任何一个常量池项”的意思时,就可以将其引用的常量的索引值设置为#0来表示。

4. 常量池数据区

 5. 访问标志

访问标志,access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。

 6. 类索引

类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表 在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接 口。 

7. 父类索引

父类索引,对于类来说,super_class 的值必须为 0 或者是对constant_pool 表中项目的一个有 效索引值。 如果它的值不为 0,那 constant_pool 表在这个索引处的项必须为CONSTANT_Class_info 类型常 量,表示这个 Class 文件所定义的类的直接父类。当前类的直接父类,以及它所有间接父类的 access_flag 中都不能带有ACC_FINAL 标记。对于接口来说,它的Class文件的super_class项的 值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为 代表 java.lang.Object 的 CONSTANT_Class_info 类型常量 。 如果 Class 文件的 super_class的值为 0,那这个Class文件只可能是定义的是 java.lang.Object类,只有它是唯一没有父类的类。

8. 接口计算器

接口计数器,interfaces_count的值表示当前类或接口的【直接父接口数量】。

9. 接口信息数据区

接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值, 它的长度为 interfaces_count。每个成员interfaces[i] 必须为CONSTANT_Class_info类型常量,其中 【0 ≤ i

10. 字段计数器

字段计数器,fields_count的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组 中每一项都是一个field_info结构的数据项,它用于表示该类或接口声明的【类字段】或者【实例字 段】。 

11. 字段信息数据区

字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接 口中某个字段的完整描述。 fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接 口继承的部分

12. 方法计数器

方法计数器, methods_count的值表示当前Class 文件 methods[]数组的成员个数。Methods[] 数组中每一项都是一个 method_info 结构的数据项。

13. 方法信息数据区

方法表,methods[] 数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类 或接口中某个方法的完整描述。 如果某个method_info 结构的access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需 要引用其它类。 method_info结构可以表示类和接口中定义的所有方法,包括【实例方法】、【类方法】、【实例初始 化方法】和【类或接口初始化方法】。 methods[]数组只描述【当前类或接口中声明的方法】,【不包括从父类或父接口继承的方法】。

14. 属性计数器

属性计数器,attributes_count的值表示当前 Class 文件attributes表的成员个数。 attributes表中每一项都是一个attribute_info 结构的数据项。

15. 属性信息数据区

属性表,attributes 表的每个项的值必须是attribute_info结构。 在Java 7 规范里,Class文件结构中的attributes表的项包括下列定义的属性: InnerClasses 、 EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及 BootstrapMethods属性。 对于支持 Class 文件格式版本号为 49.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes表中的Signature、RuntimeVisibleAnnotations和 RuntimeInvisibleAnnotations属性。对于支持Class文件格式版本号为 51.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes表中的BootstrapMethods属性。Java 7 规范 要求 任一 Java 虚拟机实现可以自动忽略 Class 文件的 attributes表中的若干 (甚至全部) 它不可 识别的属性项。任何本规范未定义的属性不能影响Class文件的语义,只能提供附加的描述信息 。

2. Class中的常量池

常量池的组成:

1.  cp_info:常量池项 

2.  constant_pool_count:常量池计算器

常量池的结构图:

cp_info {

 u1 tag;

   u1 info[]; 

 JVM是根据tag的值来确定常量池项cp_ino的类型字面量的

根据tag可以分为如下两种结构体:

 

2. int 和 float在class 文件的存储结构

 int类型和float 类型的数据类型占用 4 个字节的空间。

接下来做个测试,如下类:

public class Constant {
    private final int a = 10;
    private final int b = 10;
    private float c = 11f;
    private float d = 11f;
    private float e = 11f;    
}

用javap -v Constant 或字节码工具查看,如下图。常量池里有只有一个float和int常量

代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#16个常量池项 (cp_info),即值为 10的结构体CONSTANT_Integer_info,而用到float类型的11f时,也会指向常量池的指针值#7来定位到第#7个常量池项(cp_info) 即值为11f的结构体CONSTANT_Float_info。

3.  long和 double数据类型的常量在常量池中是怎样表示和存储 的?

Java语言规范规定了 long 类型和 double类型的数据类型占用8 个字节的空间。那么存在于class 字节码文件中的该类型的常量是如何存储的呢?

 看如下的列子:

public class Constant {
    private long k = -6076574518398440533L;
    private long m = -6076574518398440533L;
    private long n = -6076574518398440533L;
    private double o = 10.1234567890D;
    private double p = 10.1234567890D;
    private double q = 10.1234567890D;
}

用字节码工具查看,发现#08,#9表示一个Long类型的常量, #13,#14表示一个Double类型的常量。

 

 2.3 String类型的字符串常量在常量池中是怎样表示和存储的?

对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中。这么 说可能有点摸不着北,我们先从直观的Java源码中中出现的用双引号"" 括起来的字符串来看,在编译器编译的时候,都会将这些字符串转换成CONSTANT_String_info结构体,然后放置于常量池中。其结构如下所示:

 而字符串的utf-8编码数据就在这个结构体CONSTANT_Utf8_info

 看如下的列子:CONSTANT_String_info 指向了地址#28,而#28是CONSTANT_Utf8_info存储的真正的字符串常量

 

 2.4 类文件中定义的类名和类中使用到的类在常量池中是怎样被组织和存储的?

JVM会将某个Java 类中所有使用到了的类的完全限定名以二进制形式的完全限定名封装成 CONSTANT_Class_info结构体中,然后将其放置到常量池里。CONSTANT_Class_info 的tag值为 7.

Note :类的完全限定名和二进制形式的完全限定名在某个Java源码中,我们会使用很多个类,比如我们定义了一个 TestClass的类,并把它放到 test包下,则 TestClass类的完全限定名为test.ClassTest,将JVM编译 器将类编译成class文件后,此完全限定名在class文件中,是以二进制形式的完全限定名存储的,即它会把完全限定符的"."换成"/" ,即在class文件中存储的 TestClass类的完全限定名称是"test/ClassTest"。因为这种形式的完全限定名是放在了class二进制形式的字节码文件中,所以就称之为二进制形式的完全限定名。

 请看下面的列子:

package Test;

import java.util.Date;
public class TestClass {
    private Date date = new Date();
}

javap -v Test.TestClass, 可以发现常量池里有3个CONSTANT_Class_info结构体,一个是Test/TestClass, 一个是java/lang/Object,还有一个是java/util/Date. 他们分别指向了地址#21,#22和#19的CONSTANT_Utf8_info字符串。

为什么有3个类呢?首先Test/TestClass是当前类,在常量池出现毋庸置疑。JVM规定所有类都是Object的子类,所以JVM在编译后都会把java/lang/Object加上。至于java/util/Date是因为程序里引进了此类,并且使用此类创建了对象,所以会出现在常量池。

注意点: 对于某个类而言,其class文件中至少要有两个CONSTANT_Class_info常量池项,用来表示自己的类 信息和其父类信息。(除了java.lang.Object类除外,其他的任何类都会默认继承自 java.lang.Object)如果类声明实现了某些接口,那么接口的信息也会生成对应的 CONSTANT_Class_info常量池项。除此之外,如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组 成CONSTANT_Class_info常量池项放置到常量池中。

如果把代码修改为

import java.util.Date;
public class TestClass {
    private Date date;
}

javap 后java/util/Date类就不见了。因为Date类只是申明了变量,没有真正实例化和使用,将类信息放置到常量池中的目的,是为了在后续的代码中有可能会反复用到它。很显然,JDK在编译TestClass类的时候,解析到Date类有没有用到,发现该类在代码中就没有用到过,所以就认为没有必要将它的信息放置到常量 池中了。

 

总结:

1. 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成 CONSTANT_Class_info常量池项放置到常量池中;

2. 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有 对应的CONSTANT_Class_info常量池项;

3. 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成 CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器 只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中。

2.5 哪些字面量会进入常量池中?

1. final类型的8种基本类型的值会进入常量池。

2. 非final类型(包括static的)的8种基本类型的值,只有double、float、long的值会进入常量池。

3. 常量池中包含的字符串类型字面量(双引号引起来的字符串值)。

public class TestConstant {
    private int int_num = 110;
    private char char_num = 'a';
    private short short_num = 120;
    private float float_num = 130.0f;
    private double double_num = 140.0;
    private byte byte_num = 111;
    private long long_num = 3333L;
    private long long_delay_num;
    private boolean boolean_flage = true;
    public void init() {
    this.long_delay_num = 5555L;
}

2.6 class文件中的引用和特殊字符串

        符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。 例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。

       在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地 址,因此只能使用符号引用来代替。 比如 org.simple.People类 引用了 org.simple.Language类 ,在编译时People类并不知道Language 类的实际内存地址,因此只能使用符号 org.simple.Language (假设是这个,当然实际中是由类似于 CONSTANT_Class_info的常量来表示的)来表示Language类的地址。 各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字 面量形式明确定义在Java虚拟机规范的Class文件格式中。

1)直接引用

直接引用可以是:

1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指 向方法区的指针)

2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

3. 一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

引用替换的时机

符号引用替换为直接引用的操作发生在类加载过程(加载 -> 连接(验证、准备、解析) -> 初始化)中的解析阶段,会将符号引用转换(替换)为对应的直接引用,放入运行时常量池中。

2)特殊字符串字面量

特殊字符串包括三种: 类的全限定名, 字段和方法的描述符, 特殊方法的方法名。

2.1) 类的全限定名 Object类,在源文件中的全限定名是 java.lang.Object 。 而class文件中的全限定名是将点号替换成“/” 。 也就是 java/lang/Object 。 源文件中一个类的名字, 在class文件中是用全限定名表述的。

2.2) 描述符:对于字段的数据类型,其描述符主要有以下几种

  • 基本数据类型(byte、char、double、float、int、long、short、boolean):除 long 和 boolean,其他基本数据类型的描述符用对应单词的大写首字母表示long 用 J 表示, boolean 用 Z 表示
  • void:描述符是 V。
  • 对象类型:描述符用字符 L 加上对象的全限定名+;表示,如 String 类型的描述符为 Ljava/lang/String; 。
  • 数组类型:每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述 符为 [I ,二维数组 String[][] 的描述符为 [[Ljava/lang/String 。

字段描述符:字段的描述符就是字段的类型所对应的字符或字符串。

int i 中, 字段i的描述符就是 I
Object o中, 字段o的描述符就是 Ljava/lang/Object;
double[][] d中, 字段d的描述符就是 [[D

方法描述符: 方法的描述符比较复杂, 包括所有参数的类型列表和方法返回值。 它的格式是这样的:(参数1类型 参数2类型 参数3类型 ...) 返回值类型

不管是参数的类型还是返回值类型, 都是使用对应字符和对应字符串来表示的, 并且参数列表使用小括号括起来, 并且各个参数类型之间没有空格,参数列表和返回值类型之间也没有空格。

特殊方法的方法名

首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法。 构造方法就不用多说了,至于类型的初始化方法对应到源码中就是静态初始化块。 也就是说, 静态初始化块, 在class文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名,

具体如下:

  • 类的构造方法的方法名使用字符串
  • 表示 静态初始化方法的方法名使用字符串 表示。
  • 除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同

总结

1. 方法和字段的描述符中, 不包括字段名和方法名, 字段描述符中只包括字段类型, 方法描述符中只包括参数列表和返回值类型。

2. 无论method()是静态方法还是实例方法,它的方法描述符都是相同的。尽管实例方法除了传递自身定义的参数,还需要额外传递参数this,但是这一点不是由方法描述符来表达的。参数this 的传递,是由Java虚拟机实现在调用实例方法所使用的指令中实现的隐式传递。

3. javap指令

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区 (汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

javap的用法格式:javap

options如下:

-help --help -? 输出此用法消息
-version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类 和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散
列)
-constants 显示静态最终常量
-classpath  指定查找用户类文件的位置
-bootclasspath  覆盖引导类文件的位置

一般常用的是 -v -l -c三个选项。

javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用 到的常量池等信息。

javap -l 会输出行号和本地变量表信息。

javap -c 会对当前class字节码进行反编译生成汇编代码。

总结

1、通过javap命令可以查看一个java类反汇编、常量池、变量表、指令代码行号表等等信息。

2、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行 的,可以参考官方文档查看每个指令的含义,很简单: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.areturn 3、通过对前面两个例子代码反汇编中各个指令操作的分析,可以发现,一个方法的执行通常会涉及下 面几块内存的操作:

(1)java栈:局部变量表、操作数栈。这些操作基本上都值操作。

(2)java堆:通过对象的地址引用去操作。

(3)常量池。

(4)其他如帧数据区、方法区(jdk1.8之前,常量池也在方法区)等部分,测试中没有显示出来,这 里说明一下。 在做值相关操作时: 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可 能是指,可能是对象的引用)被压入操作数栈。 一个指令,也可以从操作数数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系 统调用等等操作。

相关