Class文件结构(1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dingpf1209/article/details/86644590

1 语言无关性的基础

   实现语言无关性的基础是虚拟机和字节码存储结构,Java虚拟机不与任何编程语言绑定,他只与class文件这种特定的二进制文件格式所关联,class文件中包含了Java虚拟机指令集和符号表以及包干其他辅助信息。jvm规范中有一些强制性的语法和约束,但是任何一个编程语言都可以表示成可以被Java虚拟机接受的有效的Class文件,虚拟机并不关心Class的来源是何种语言

2 Class类文件的结构

   任何一个Class文件都对应着唯一一个类或接口的定义信息,但是 类或者接口不一定定义在文件中,比如可以通过类加载器直接生成,也就是说class文件不一定以磁盘文件的形式存在。
   Class文件是一组8位字节为基础单位的二进制流,各个数据项严格按照顺序紧凑的排列在Class文件中,没有任何分隔符。当遇到8个字节以上的数据项,则会按照高位在前的方式分割成8位字节进行存储。Class文件格式采用一种类似C语言结构体的伪结构来存储数据,Class文件中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1 u2 u4 u8 分别代表1个字节、2个字节、4个字节、8个字节的无符号数,祖父好书可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
在这里插入图片描述
   当需要描述同一类型但是数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列的某一类型的数据位某一类型的集合。

2.1 魔数与Class文件的版本

   每个Class文件的头4个字节称为魔数,使用魔数进行身份验证。使用魔数而不是后缀名区别主要是基于安全方面的考虑,因为文件扩展名可以随意的更改。紧接着魔数的4个字节存储的是Class文件的版本号:第五和第六字节是次版本号,第七和第八个字节是主版本号。高版本的JDK可以向下兼容以前版本的Class文件,但是不能运行以后版本的Class文件。

2.2 常量池

   紧接着主次版本号之后的事常量池入口,常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时他还是Class文件中第一个出现的表类型数据项目。在常量池的入口需放置一个u2类型的数据,代表常量池计数值,这个数值是从1开始不是0开始的。例如,如果这个数值是22 这就代表常量池中有21项,索引值范围是1~21.class文件规定,将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目的含义,这种情况下可以把索引值设置为0来表示。class文件结构中只有常量池的容量计数是从1开始的。
   常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念,如文本字符串、申明为final的常量值等。而符号引用则属于编译原理方面的概念,包括 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
   在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段 方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时需要从常量池中获得相应的内存入口地址,再在类创建时或运行时解析、翻译到具体的内存地址中。
   常量池中每一项常量都是一个表,在jdk1.7之前共有11个结构不同的表结构数据,在1.7中味了更好的支持动态语言调用,有额外增加了3种。这14种表都有一个共同特点,就是表开始的第一位是一个u1类型的标识为(tag),代表当前这个常量属于哪种类型的常量。这14种常量类型所代表的具体含义如下图:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

   Class文件中方法 字段等都需要引用CONSTANT_UTF8_info 型常量来描述名称,所以CONSTANT_UTF8_info型常量的最大长度也就是Java中方法 字段名的最大长度,也就是65536.所以,Java程序中定义了超过64KB英文字符的变量或方法名,将无法编译。

package com.ding;

public class AppTest {
    private int m;
    public int inc(){
        return m+1;
    }
}
cafe babe 0000 0033 
0016 
0a00 0400 1209
0003 0013 0700 1407 0015 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 124c 636f 6d2f 6469
6e67 2f41 7070 5465 7374 3b01 0003 696e
6301 0003 2829 4901 000a 536f 7572 6365
4669 6c65 0100 0c41 7070 5465 7374 2e6a
6176 610c 0007 0008 0c00 0500 0601 0010
636f 6d2f 6469 6e67 2f41 7070 5465 7374
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 74
00 21
00 0300 0400 00 
0001 0002 0005 0006
00 0000 0200 0100 0700 0800 0100
0900 0000 2f00 0100 0100 0000 052a b700
01b1 0000 0002 000a 0000 0006 0001 0000
0003 000b 0000 000c 0001 0000 0005 000c
000d 0000 0001 000e 000f 0001 0009 0000
0031 0002 0001 0000 0007 2ab4 0002 0460
ac00 0000 0200 0a00 0000 0600 0100 0000
0600 0b00 0000 0c00 0100 0000 0700 0c00
0d00 0000 0100 1000 0000 0200 11
魔数
cafe babe
主次版本号
0000 0033
常量池计数
0016
常量池
 1 0a 0004 0012 //  java/lang/Object."<init>":()V
 2 09 0003 0013 //  com/ding/AppTest.m:I
3 07 0014 com/ding/AppTest
4 07 0015 java/lang/Object
5 01 0001 6d m
6 01 0001 49 I
7 01 0006 3c 696e 6974 3e <init>
8 01 0003 2829 56 ()V
9 01 0004 436f 6465  Code
10 01 000f 4c 696e 654e 756d 6265 7254 6162 6c65  LineNumberTable
11 01 0012 4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65 LocalVariableTable
12 01 0004 7468 6973 this
13 01 0012 4c 636f 6d2f 6469 6e67 2f41 7070 5465 7374 3b  Lcom/ding/AppTest;
14 01 0003 696e 63 inc
15 01 0003 2829 49 ()I
16 01 000a 536f 7572 6365 4669 6c65  SourceFile
17 01 000c 41 7070 5465 7374 2e6a 6176 61  AppTest.java
18 0c 0007 0008  "<init>":()V
19 0c00 0500 06 m:I
20 01 0010 636f 6d2f 6469 6e67 2f41 7070 5465 7374  com/ding/AppTest
21  0100 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 java/lang/Object
访问标志
00 21
类的继承关系
00 0300 0400 00

   在JDK的bin目录中,Oracle公司已经为我们提供了专门用于分析Class字节码的工具:javap -verbose 参数输出Class文件字节码内容。

Compiled from "AppTest.java"
public class com.ding.AppTest
  SourceFile: "AppTest.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         //  com/ding/AppTest.m:I
   #3 = Class              #17            //  com/ding/AppTest
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               AppTest.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = NameAndType        #5:#6          //  m:I
  #17 = Utf8               com/ding/AppTest
  #18 = Utf8               java/lang/Object
{
  public com.ding.AppTest();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 3: 0

  public int inc();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0       
         1: getfield      #2                  // Field m:I
         4: iconst_1      
         5: iadd          
         6: ireturn       
      LineNumberTable:
        line 6: 0
}

2.3 访问标志

   在常量池结束之后,紧接着两个字节代表访问标(access_flags),用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否是public类型,是否abstract;类,接口,枚举,final等 如图:
在这里插入图片描述

2.4 类索引、父类索引与接口索引

   类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定类的全限定名,父类索引用于确定父类的全限定名,由于Java不可以多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类的父类索引都不为0.接口索引集合描述了这个类实现了哪些接口,这些被实现的接口按implements(extends)后的顺序从左到右排列在接口索引集合中。
   类索引、父类索引和接口索引集合按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量。对于接口索引集合,入口的第一项-u2类型的数据为接口计数器表示 索引表的容量。如果没有实现任何接口,则计数器值为0,后面的索引表不占用字节;否则,每个接口索引也占用两个字节。

2.5 字段表集合

   字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段表结构如下:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

   字段修饰符放在access_flags项目中,他与类中的access_flags非常类似,都是一个u2的数据类型,其中可以设置的标识位与含义如下:

标志类型 标志值 含义
ACC_PUBLIC 0x0001
ACC_PRIVATE 0x0002
ACC_PROTECTED 0x0004
ACC_STATIC 0x0008
ACC_FINAL 0x0010
ACC_VOLATILE 0x0040
ACC_TRANSIENT 0x0080
ACC_SYNTHETIC 0x1000 是否编译器自动生成
ACC_ENUM 0x4000

   跟随access_flags标志的是两项索引值:name_index 和 descriptor_index 。他们都是常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。全限定名仅仅是把类全名中的“.“替换成了“/”而已,为了使连续的多个全限定名不产生混淆,在使用时最后一班会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称。相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte char double float int long short boolean)以及代表无返回值的void类型都用一个大写字母表述,而对象类型则用自负L加对象的全限定名来表示:

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

   对于数组类型,每一维度将使用一个前置的“”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String;”;
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内

   例如:0001 0002 0005 0006 是描述Class二进制中字段表的一段二进制 0x0001 说明这个类中只有一个字段;0x0002是access_flag标志,代表private修饰符为真其他为假;0x0005代表name_index 指向常量表的m;0x0006代表descriptor_index指向常量池的字符串 I,根据以上可知道字段:“private int m;”。如果将m的声明修改为“final static int m=123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向123。

   字段表不回列出从超类或者副接口中继承来的字段,但有可能列出原本Java代码中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同都必须使用不一样的名称,但是对字节码而言,如果两个字段的描述符不一致,那么字段重名就是合法的。

2.6 方法表集合

   Class文件中方法表集合和字段表集合采用了完全一致的方式,就在访问标志和属性表集合的可选项中有所区别。

   方法的定义可以通过访问标志,name_index,描述符索引表达清楚,但方法里面的代码去哪儿了呢?方法里的Java代码,经过编译器编译成了字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。
与字段表对应,父类方法如果在子类中没有被重写,方法表集合中不会有来自父类的方法信息,但是可能出现编译器自动添加的方法如类构造器方法。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/dingpf1209/article/details/86644590