Class文件内容及常量池

2019-03-10  本文已影响0人  LineCutFeng

当JVM运行Java程序的时候,它会加载对应的class文件,并提取class文件中的信息存放在JVM开辟出来的方法区 内存中。那么这个class文件里面到底有些什么内容呢?

一、class文件内容概述

class文件是由8bits的字节流组成,全部字节构成了15个有意义的项目。这些项目之间没有任何无意义的字节,因此class文件非常紧凑。占据多字节空间的项目按照高位在前的顺序存放。下面我们详细讨论这些项目:

access_flags(2byte 访问修饰符)  
name_index(2byte 存储字段名的常量表在常量池中的索引)
description_index(2byte 存储字段的所属类型的常量表在常量池中的索引)
attribute_count(2byte 属性表的数量)
attribute (属性)

其中attribute是由多个attribute_info组成。而JVM规范定义了字段的三种属性:ConstanceValue、Deprecated和Synthetic。

access_flags(2byte 访问修饰符)  
name_index(2byte 存储方法名的常量表在常量池中的索引)
description_index(2byte 存储方法的返回类型和参数类型的常量表在常量池中的索引)
attribute_count(2byte 属性表的数量)
attribute (属性)

其中方法的属性JVM规定了四种:Code,Deprecated,Exceptions,Synthetic。

二、常量池的具体结构

在Java程序中,有很多的东西是永恒的,不会在运行过程中变化。比如一个类的名字,一个类字段的名字/所属类型,一个类方法的名字/返回类型/参数名与所属类型,一个常量,还有在程序中出现的大量的字面值。比如下面小段源码红色显示的东西。

public class ClassTest {
        private String itemS ="我们 ";
        private final int itemI =100 ;
        public void setItemS (String para ){...}
}

而这些在JVM解释执行程序的时候是非常重要的。那么编译器将源程序编译成class文件后,会用一部分字节分类存储这些永恒不变的红色东西。而这些字节我们就成为常量池。事实上,只有JVM加载class后,在方法区中为它们开辟了空间才更像一个“池”。
正如上面所示,一个程序中有很多永恒的红色东西。每一个都是常量池中的一个常量表(常量项)。而这些常量表之间又有不同,class文件共有11种常量表,如下所示:

常量表类型 标志值(占1 byte) 描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型的字面值
CONSTANT_Float 4 float类型的字面值
CONSTANT_Long 5 long类型的字面值
CONSTANT_Double 6 double类型的字面值
CONSTANT_Class 7 对一个类或接口的符号引用
CONSTANT_String 8 String类型字面值的引用
CONSTANT_Fieldref 9 对一个字段的符号引用
CONSTANT_Methodref 10 对一个类中方法的符号引用
CONSTANT_InterfaceMethodref 11 对一个接口中方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用

(1) CONSTANT_Utf8 用UTF-8编码方式来表示程序中所有的重要常量字符串。这些字符串包括: ①类或接口的全限定名, ②超类的全限定名,③父接口的全限定名, ④类字段名和所属类型名,⑤类方法名和返回类型名、以及参数名和所属类型名。⑥字符串字面值

表格式:   
tag(标志1:占1byte)       
length(字符串所占字节的长度,占2byte)      
bytes(字符串字节序列)

(2) CONSTANT_Integer、 CONSTANT_Float、 CONSTANT_Long、 CONSTANT_Double 所有基本数据类型的字面值。比如在程序中出现的1用CONSTANT_Integer表示。3.1415926F用 CONSTANT_Float表示。

表格式:   
tag
bytes(基本数据类型所需使用的字节序列)

(3) CONSTANT_Class 使用符号引用来表示类或接口。我们知道所有类名都以 CONSTANT_Utf8表的形式存储。但是我们并不知道 CONSTANT_Utf8表中哪些字符串是类名,那些是方法名。因此我们必须用一个指向类名字符串的符号引用常量来表明。

表格式:   
tag    
name_index(给出表示类或接口名的CONSTANT_Utf8表的索引)

(4) CONSTANT_String 同 CONSTANT_Class,指向包含字符串字面值的 CONSTANT_Utf8表。

表格式:   
tag    
string_index(给出表示字符串字面值的CONSTANT_Utf8表的索引)

(5) CONSTANT_Fieldref 、 CONSTANT_Methodref、 CONSTANT_InterfaceMethodref 指向包含该字段或方法所属类名的 CONSTANT_Utf8表,以及指向包含该字段或方法的名字和描述符的 CONSTANT_NameAndType 表

表格式:   
tag   
class _index(给出包含所属类名的CONSTANT_Utf8表的索引)  
name_and_type_index(包含字段名或方法名以及描述符的 CONSTANT_NameAndType表 的索引)

(6) CONSTANT_NameAndType 指向包含字段名或方法名以及描述符的 CONSTANT_Utf8表。

表格式:   
tag    
name_index(给出表示字段名或方法名的CONSTANT_Utf8表的索引)  
type_index(给出表示描述符的CONSTANT_Utf8表的索引)

下面是我将一个源程序编译成class文件后,对文件中的每一个字节的分析,可以更好的理解class文件的内容以及常量池的组成。

三、TestClass.class 文件实例分析
//源代码  
package hr.test;  
//ClassTest类  
public class ClassTest {  
    private int itemI=0;  //itemI类字段  
    private static String itemS="我们"; //itemS类字段  
    private final float PI=3.1415926F;  //PI类字段  
    //构造器方法  
    public ClassTest(){  
    }  
    //getItemI方法  
    public int getItemI(){  
        return this.itemI;  
    }  
    //getItemS方法  
    public static String getItemS(){  
        return itemS;  
    }  
    //main主方法  
    public static void main(String[] args) {  
        ClassTest ct=new ClassTest();  
    }  
}  

TestClass.class 字节码分析(字节顺序从上到下,从左到右。每个字节用一个0-255的十进制整数表示)

202 254 186 190   -- 魔数                                                                                                  
0 0     -- 次版本号                                                                                                                                    
0 50   -- 主版本号         
0 43   -- 常量池中常量表的数量有42个,下面红色括号中的数据表明该常量表所在常量池中的索引,从索引1开始

(1) 7 0 2  -- 对类ClassTest的符号引用(7为标志  02指向了常量池的索引2的位置)

(2) 1 0 17 104 114 47 116 101 115 116 47 67 108 97 115 115 84 101 115 116  -- 类全限定名hr\test\ClassTest 
(3) 7 0 4    -- 对类Object的符号引用

(4) 1 0 16 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116   -- 超类全限定名 java/lang/Object    
(5) 1 0 5 105 116 101 109 73    --  第1个类字段名 itemI   
(6) 1 0 1 73    --  I  第1个类字段类型为整型 
(7) 1 0 5 105 116 101 109 83    --  第2个类字段名 itemS 
(8) 1 0 18 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59    --  第2个类字段类型的全限定名 Ljava/lang/String 
(9) 1 0 2 80 73 -- 第3个类字段名PI 
(10) 1 0 1 70  -- 第3个类字段类型为float 
(11) 1 0 13 67 111 110 115 116 97 110 116 86 97 108 117 101    ---  第3个类字段为常量ConstantValue

(12) 4 64 73 15 218   -- 第3个类字段float字面值,占4bytes(3.1415926) 
(13) 1 0 8 60 99 108 105 110 105 116 62    -- <clinit>  初始化方法名 
(14) 1 0 3 40 41 86     -- ()V 方法的返回类型为void

(15) 1 0 4 67 111 100 101     -- Code 
(16) 8 0 17 -- String字符串字面值(0 17表示索引1 7) 
(17) 1 0 6 230 136 145 228 187 172    -- "我们" 
(18) 9 0 1 0 19  -- 指向 第2个 字段的引用(0 1指向索引1,0 19指向索引19) 
(19) 12 0 7 0 8   --指向 第2个 字段的名字和描述符的索引, 
(20) 1 0 15 76 105 110 101 78 117 109 98 101 114 84 97 98 108 101    -- LineNumberTable 
(21) 0 18 76 111 99 97 108 86 97 114 105 97 98 108 101 84 97 98 108 101    -- LocalVariableTable 
(22) 1 0 6 60 105 110 105 116 62   -- <init>   表示初始化方法名 
(23) 10 0 3 0 24 --  指向父类Object的构造器方法,0 3表示父类名常量表的索引,0 24表示存放该方法名称和描述符的引用的常量表的索引 
(24) 12 0 22 0 14  --  指向方法名和描述符的常量表的索引。0 22是方法名的常量表索引,0 14是描述符的常量表索引 
(25) 9 0 1 0 26    -- 指向第1个字段的引用, 0 1表示字段所属类型的索引,0 26表示字段名和描述符的索引 
(26) 12 0 5 0 6    -- 指向第1个字段的名字和描述符的索引 
(27) 9 0 1 0 28    -- 指向第3个字段的引用, 0 1表示字段所属类型的索引,0 28表示字段名和描述符的索引 
(28) 12 0 9 0 10  -- 指向第3个字段的名字和描述符的索引 
(29) 1 0 4 116 104 105 115    --  隐含参数符号this 
(30) 1 0 11 76 67 108 97 115 115 84 101 115 116 59    --  LClassTest; 
(31) 1 0 8 103 101 116 73 116 101 109 73    - - 方法名 getItemI 
(32) 1 0 3 40 41 73    -- ()I  方法描述符:返回类型int 
(33) 1 0 8 103 101 116 73 116 101 109 83   --  方法名 getItemS 
(34) 1 0 20 40 41 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59     --- 方法描述符()Ljava/lang/String; 
(35) 1 0 4 109 97 105 110     --  主方法名main 
(36) 1 0 22 40 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 41 86    ---  ()Ljava/lang/String;)V  主方法中的参数的字符串数组类型名 
(37) 10 0 1 0 24    指向当前 ClassTest 类的构造器方法,0 1表示存放当前类名的常量表的索引。0 24是存放方法名和描述符的符号引用的常量表索引。   
(38) 1 0 4 97 114 103 115  -- 参数args 
(39) 1 0 19 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59   -- 字符串数组 [Ljava/lang/String; 
(40) 1 0 2 99 116    ---  对象符号ct 
(41) 1 0 10 83 111 117 114 99 101 70 105 108 101    -- SourceFile 
(42) 1 0 14 67 108 97 115 115 84 101 115 116 46 106 97 118 97    -- ClassTest.java    

0 33 ---- access_flag 访问标志  public
0 1   ---- this_class  指向当前类的符号引用在常量池中的索引
0 3  ---- super_class 
0 0 ---- inteface_count接口的数量 
0 3   ---  field_count字段的数量
// 字段 itemI 
0 2  ---- private 修饰符
0 5  ---- 字段名在常量池中的索引,字段itemI
0 6   ---- 字段的描述符(所属类型)在常量池中的索引
0 0   ---  字段的属性信息表(attribute_info)的数量
// 字段 itemS 
0 10   ----  private static 修饰符
0 7  ---字段名在常量池中的索引,字段itemS
0 8  ---字段的描述符(所属类型)在常量池中的索引
0 0  ---  字段的属性信息表(attribute_info)的数量
// 字段 PI   
0 18   -- private final 修饰符
0 9 ---字段名在常量池中的索引,//字段PI
0 10 ---字段的描述符(所属类型)在常量池中的索引
0 1  --- 字段的属性信息表(attribute_info)的数量
0 11   --- 属性名在常量池中的索引。即ConstantValue
0 0 0 2 --- 属性所占的字节长度
0 12   --- 属性值在常量池中的索引。即常量字面值 
0 5  -- Method_count方法的数量 
//类的静态数据初始化方法<clinit> 
0 8 ---- static 修饰符(所有的初始化方法都是static的) 
0 13 --- 在常量池中的索引。初始化方法名<clinit>,该方法直接由JVM在特定的时候调用,并非由字节码生成。 
0 14 --- 在常量池中的索引。返回类型为void。
0 1 --- 属性数量 
0 15 -- 属性名 在常量池中的索引。即code 
0 0 0 42 ---  属性所占的字节长度2 
0 1 0 0 0 0 0 6 18 16 179 0 18 177 0 0 0 2 0 20 0 0 0 10 0 2 0 0 0 5 0 5 0 2 0 21 0 0 0 2 0 0 ---该方法的字节码指令序列和其他信息 
//类的普通实例数据的初始化方法,针对类构造器生成的<init>方法。 
0 1 --- public 修饰符 
0 22 --- 构初始化方法名<init> 
0 14 --- 构造器的返回类型为void 
0 1  --- 属性数量 
0 15 ---  属性名在常量池中的索引。即Code 
0 0 0 70 -- 属性所占的字节长度70 
0 2 0 1 0 0 0 16 42 183 0 23 42 3 181 0 25 42 18 12 181 0 27 177 0 0 0 2 0 200 0 0 18 0 4 0 0 0 8 0 4 0 4 0 9 0 6 0 15 0 9 0 21 0 0 0 12 0 10 0 0 16 0 29 0 30 0 0 ---该方法的字节码指令序列和其他信息 
//getItemI方法 
0 1 --- public 修饰符 
0 31 ---  在常量池中的索引。方法名getItemI 
0 32 ---  在常量池中的索引。方法返回类型为int 
0 1 -- 属性数量 
0 15  --- 属性名在常量池中的索引。即Code 
0 0 0 47 ---  属性所占的字节长度70 
0 1 0 1 0 0 0 5 42 180 0 25 172 0 0 0 2 0 20 0 0 0 6 0 1 0 0 0 12 0 21 0 0 0 12 0 1 0 0 0 5 0 29 0 30 0 0  --- 该方法的字节码指令序列和其他信息 
//getItemS方法 
0 9 --- public static 修饰符 
0 33 ---  在常量池中的索引。方法名getItemS 
0 34 - -- 在常量池中的索引。方法返回类型为String 
0 1 --- 属性数量 
0 15 -- 属性名在常量池中的索引。即Code 
0 0 0 36 ---  属性所占的字节长度36 
0 1 0 0 0 0 0 4 178 0 18 176 0 0 0 2 0 20 0 0 0 6 0 1 0 0 0 16 0 21 0 0 0 2 0 0 --该方法的字节码指令序列和其他信息 
//main方法 
0 9 --- public static 修饰符 
0 35 ---  在常量池中的索引。主方法名main 
0 36 -- 在常量池中的索引。方法返回类型为String[] 
0 1 ---  属性数量 
0 15  ---  属性名在常量池中的索引。即Code 
0 0 0  65 ---  属性所占的字节长度36 
0 2 0 2 0 0 0 9 187 0 1 89 183 0 37 76 177 0 0 0 2 0 20 0 0 0 10 0 2 0 0 0 20 0 8 0 21 0 21 0 0 0 22 0 2 0 0 0 9 0 38 0 39 0 0 0 8 0 1 0 40 0 30 0 1 0 1 0 41 0 0 0 2 0 42       

我们分析上面的字节码例子,不难看出:
常量池字节码区域:

(1) 所有的字面值都是存放在常量池中的。 特别注意的是“我们”这个字符串常量也是在常量池中的。如果一个程序出现多个“我们”,那么常量池中也只会有一个。另外,也正是因为“我们”存放在常量池中,使得一些字符串的==比较变的需要琢磨了。

(2)ClassTest并没有任何显示的父类。但在常量池中,我们发现有Object的符号常量存在。 这也证实了在Java中,任何类都直接或间接继承了Object的,而Object并不需要在代码中显示继承,JVM会帮我们做到这一点。

(3)常量池中有一个隐含参数this的符号常量。即使程序中不存在this,JVM也会悄悄的设置一个这样的对象。

类字段字节码区域:

(1)字段PI是浮点型常量,在编译期的字节码中就已经指定好了PI的字面值存储在常量池中的某个索引内 。这一点也证实了Java中的常量在编译期就已经得到了值,在运行过程中是无法改变的。

类方法字节码区域:

(1)主方法main是作为ClassTest的类方法存在的,在字节码中main和其他的类方法并没有什么区别。 实际上,我们也确实可以通过ClassTest.main(..)来调用ClassTest中的main方法。

(2)在class文件常量池字节码中有两个比较特别的方法名符号:<clinit>和<init>。其中<clinit>方法是编译器自己生成的,编译器会把类静态变量的直接初始化语句和静态初始化语句块的代码都放到了class文件的<clinit>方法中。而对所有非静态非常量数据域的初始化工作要靠<init>方法来完成。针对每一个类的构造方法,编译器都会产生一个<init>方法。即使是缺省构造器也不例外。

摘自:
Class文件内容及常量池

上一篇 下一篇

猜你喜欢

热点阅读