Class文件结构(五)

在onenote上复制过来格式全乱了,将就着看吧。。。

1. JVM的平台无关性和语言无关性

Java实现“一次编写,处处运行”。是因为有Java虚拟机夹在操作系统和应用程序之间。

JVM解决了平台无关性和语言无关性问题

平台无关性各操作系统提供的系统调用是不一样的,程序的组织形式也不一样,例如,Windows系统的可执行程序格式是PE(包括exe,dll,vxd等),而Linux系统下可执行文件是ELF格式(包括.o, .so等)还有老的GCC默认生成的.out格式。而编译完成的Java程序只要符合标准的class文件规范,即可在JVM上运行。这都是由于JVM屏蔽了不同操作系统的系统调用和程序格式等细节。所以想要实现平台无关性是需要适配不同的JVM版本的。

语言无关性JVM只认识符合JVM规范(注意与Java语言规范区别)的class文件。至于这个class文件是哪种语言编译出的是不用管的。如下图所示:

 

2.Class文件结构

class文件全是0/1组成。且以8字节为基础单位。这便是其称为“字节码文件”的原因。如果某个数据项有多个字节,则按照大端存储。

文件中数据类型分为 无符号数 和 表:

- 无符号数 

它表示class文件中的值,这些值没有任何类型,但有不同的长度。根据这些值长度的不同分为:u1、u2、u4、u8,分别代表1字节的无符号数、2字节的无符号数、4字节的无符号数、8字节的无符号数。 可以用来描述数字,引用,字符串等

-  

class文件中所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表。即class文件中的数据要么是单个值,要么是二维表。

整个class文件中没有分隔符,没有空格。所以哪个字节代表什么含义,长度多少,先后顺序如何都要严格定义。差错一个位,其他后面全部错乱。

 

2.1 class文件的组织结构

1.魔数

2.版本号

3.常量池

4.访问标志

5.类索引、父类索引、接口索引集合

6.字段表集合

7.方法表集合

  • Class文件的构成1:魔数

位于class文件的头4个字节,表示这个文件是class文件。

魔数的作用就相当于文件扩展名,但后缀名容易被修改,魔数更安全。

class文件的魔数是用16进制表示的“CAFEBABE”。

  • Class文件的构成2:版本信息

紧接着魔数的4个字节是版本号(次版本号(2字节)+主版本号(2字节))。它表示本class中使用的是哪个版本的JDK。

在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM上无法运行高版本的class文件,即使该class文件中没有用到任何高版本JDK的特性也无法运行! 

  • Class文件的构成3:常量池

(1)什么是常量池?

紧接着版本号之后的就是常量池。可以理解为class文件的资源仓库。常量池中存放两种类型的常量:

  • 字面量
    字面量即Java语言层面的常量概念,如字符串、被final修饰的常量值。
  • 符号引用 
    符号引用就是我们定义的各种名字: 
  1. 类和接口的全限定名
  2. 字段的名字 和 描述符
  3. 方法的名字 和 描述符 

描述符:

 public 、static、final、 abstract、synchronized、volatile等都属于描述符

 

(2)常量池的特点

  • 常量池长度不固定 
    常量池的大小是不固定的,因此常量池开头放置一个u2类型的无符号数,用来存储当前常量池的容量。JVM根据这个值就知道常量池的开始和结束。
    注意:这个值是从1开始的,若为5表示池中有4个常量。
  • 常量池中的常量由二维表来表示 
    常量池开头有个常量池容量计数器,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息。
  • 常量池是class文件的资源仓库
  • 常量池是与本class中其它部分关联最多的部分
  • 常量池是class文件中空间占用最大的部分之一 

(3)常量池中常量的类型

        刚才介绍了,常量池主要存放两大类常量:字面值常量 和符号引用。在此基础上,根据常量的数据类型不同,又可以被细分为14种常量类型。这14种常量类型都有各自的二维表示结构。每种常量类型的头1个字节都是tag,用于表示当前常量属于14种类型中的哪一个。

CONSTANT_Class_info常量为例,它的二维表示结构如下: 

CONSTANT_Class_info表:

类型

名称

数量

u1

tag

1

u2

name_index

1

tag表示常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表示一个类或接口的全限定名); 

name_index表示这个类或接口全限定名的位置。它的值表示指向常量池的第几个常量。它会指向一个CONSTANT_Utf8_info类型的常量,它的二维表结构如下: 

CONSTANT_Utf8_info表:

类型

名称

数量

u1

tag

1

u2

length

1

u1

bytes

length

CONSTANT_Utf8_info表示字符串常量; 

tag表示当前常量的类型,这里应该是1; 

length表示这个字符串的长度; 

bytes为这个字符串的内容(采用缩略的UTF8编码)

注:为什么Java中定义的类、变量名字必须小于64K? 

类、接口、变量等名字都属于符号引用,它们都存储在常量池中。而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info类型的常量表示,这种类型的常量使用u2存储字符串的长度。由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K。

注:什么是UTF-8编码?什么是缩略UTF-8编码? 

前者每个字符使用3个字节表示,而后者把1~127ASCII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。 

  • Class文件的构成4:访问标志

      在常量池之后紧接着是2字节的访问标志。用来表示类或结构的访问信息。如这个class文件是类还是接口?是否是annotation?是否被public修饰?是否被abstract修饰?是否被final修饰等。 

由于这些标志都由是/否表示,因此可以用0/1表示。 

访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的写0

  • Class文件的构成5:类索引、父类索引、接口索引集合

      类索引、父类索引、接口索引集合是用来表示当前class文件所表示类的名字、父类名字、接口们的名字。 

它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量又指向CONSTANT_Utf8_info常量,其中的bytes字段记录了本类、父类的全限定名。 

由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后。这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引。 

  • Class文件的构成6:字段表的集合
(字段表集合1)  什么是字段表集合?

    接下来是字段表的集合。字段表集合用于存储本类所涉及到的成员变量(区别于常量池中的常量),包括实例变量和类变量,但不包括方法中的局部变量。 

每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合。

(字段表集合2) 字段表结构的定义

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

  • access_flags 
    字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。

    public volatile static transientenumfinalsynthetic(字段是否由编译器自动生成)。每个标识都有一个标志值如public(0x0001)final(0x0010)。最后进行或运算即可。

  • name_index 
        
    本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。

值为0x0005,指向常量池中一个CONSTANT_Utf8_info类型的常量,值为字符串“j”。

  • descriptor_index 
          
    描述符。用于描述本字段在Java中的数据类型等信息(下面详细介绍)

    如0x0006 表示常量池的字符串“I”。再结合上面的例子,推断出源代码应为private int j;

  • attributes_count 
    属性表集合的长度。
  • attributes 
    属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值为100。(下面会详细介绍)
(字段表集合3) 什么是描述符?

  • 成员变量(包括类成员变量和实例成员变量)和 方法都有各自的描述符。 
  • 对于字段而言,描述符用于描述字段的数据类型; 
  • 对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。

在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 "java.lang.String[][]" 被记录为 "[[Ljava/lang/String" ;“int[]”被记录为“[I”。

描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。如方法 int func(char[]ch1, int a, int b, char[] ch2, int c, int d)的描述符为"([CII[CII)I"; 

(字段表集合4) 字段表集合的注意点
  • 一个class文件的字段表集合中不能出现从父类/接口继承而来字段;
  • 一个class文件的字段表集合中可能会出现程序员没有定义的字段                                                                                      如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。
  • Java中只要两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段。  

  • Class文件的构成7:方法表的集合

在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。 

方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有少数不同。

类型

名称

数量

u2

access_flags

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。

方法表集合的注意点

  1. 如果本class没有重写父类的方法,那么本class文件的方法表集合中是不会出现父类/父接口的方法表;
  2. 本class的方法表集合可能出现程序员没有定义的方法 
    如编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。
  3. 重载一个方法需要有相同的简单名称和不同的特征签名。JVM的特征签名和Java的特征签名有所不同: 
  • Java特征签名:方法参数在常量池中的字段符号引用的集合
  • JVM特征签名:方法参数+返回值

一个经典问题:为什么重写返回值不构成重载?

【答】Java语言规范重载一个方法要求:1.方法名一样;2特征签名与原方法不同。

方法的特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合。

方法的特征签名包括方法名称,参数的个数,类型,顺序。却不包含返回值。所以仅仅返回值不同是不能构成重载的。

  • Class文件的构成8:属性表的集合 

限制宽松,不要求各个属性表具有严格顺序。在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。

8.1 结构简介

对于每一个属性,有大致的表结构为:

类型

名称

数量

u2

attibute_name_index

1

u4

attribute_length

1

u1

info

attribute_length

attribute_name_index u2 : 名字,指向常量池UTF-8类型常量(一个字符串)

attribute_length u4 : 长度

info[attribute_length] u1 :内容  其实就是指定了长为attribute_length字节的内存

attribute本身也可以包含其他attribute

8.2 部分属性介绍

8.2.1 Deprecated——用于描述字段和方法或者类被废弃

Deprecated属性结构:

类型

名称

描述

u2

attibute_name_index

指向常量池一个包含“Deprecated”字符串的UTF-8常量

u4

attribute_length

值恒为0

8.2.2 ConstantValue

类中的一个常量定义为“publicstatic int sid=99;”

类型

名称

描述

u2

attibute_name_index

指向常量池一个包含“ConstantValue”字符串的UTF-8常量

u4

attribute_length

值恒为2

u2

constantvalue_index u2

指向常量池中的常量值 UTF-8、Float、Double等

8.2.3 Code属性

用一个结构体描述


举例:


如果0到5字节码出现java/lang/IllegalStateException异常,就跳转到第8个字节码去处理

catch_type指向常量池的第48号字符串是java/lang/IllegalStateException

LineNumberTable属性


localVariableTable属性

 

8.2.4 Exceptions属性

表示方法抛出的异常

结构:

类型

名称

描述

u2

attibute_name_index

指向常量池“Exceptions”字符串

u4

attribute_length

后面还有多少字节长度

u2

number_of_exceptions

抛了多少个异常

u2

exception_index_table[number_of_exceptions]

异常索引表

8.2.5 SourceFile属性

结构:

类型

名称

描述

u2

attibute_name_index

指向常量池“SourceFile”字符串

u4

attribute_length

固定为2

u2

soucefile_index

指向常量池某个UTF-8型常量的类名字符串

猜你喜欢

转载自blog.csdn.net/fantalee/article/details/80846217