1.Class文件结构

一、概述

1.1、Java语言:跨平台的语言

1.当java源代码成功编译成字节码后,如果想在不同的平台运行,无需再次编译

2.这个优势不再那么吸引人,Python

3.跨平台似乎已经快成为一门语言必选的特性

1.2、java虚拟机:跨语言的平台

1.Java虚拟机不和包括Java在内的任何语言绑定,它只与.class文件这种特定的二进制文件格式所关联

2.无论使用何种语言进行软件开发, 只要能将源文件编译为正确的.class文件,那么这种语言就可以在Java虚拟机上执行,可以说统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁

3.所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的, 这样一来字节码文件可以在各种JVM上进行

1.3、想要让一个Java程序正确地运行在JVM中,Java源码就是必须要被编译为符合JVM规范的字节码

1.4、透过字节码看代码指令

1.有些问题需要从字节码的角度来看待问题,知道了字节码才知道程序到底是怎么运行的

1.4.1、例子1:

比较两个Integer的大小

  • 有些问题需要从字节码的角度来看待问题,知道了字节码才知道程序到底是怎么运行的

  • 通过反编译得到的字节码

  • java.lang.Integer.valueof(int i)方法源码

  • low就是-128,high就是127,可以看到jvm内部对-128至127进行了缓存,所以如果在-128至127之间一定是true,否则就是false

1.4.2、例子2:

比较new String("a") + new String("b")和new String("ab")是否相等

  • 反编译后的字节码

二、虚拟机的基石:Class文件

2.1、字节码文件里是什么?

源代码经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成本地机器码

2.2、什么是字节码指令(byte code)?

  • Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的操作码 (opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成

  • 虚拟机中许多指令并不包含操作数,只有一个操作码

  • 由于指令只有一个字节大小,所以最多只有256个,对于添加新的指令要非常小心,超过256个可就完蛋了

2.3、如何解读供虚拟机解释执行的二进制字节码?

1. 使用Notepad++查看,安装HEX-Editor插件后进行查看

  1. 使用javap指令,JDK自带的反解析工具

3. 使用IDEA插件,jclasslib或jclasslib bytecode viewer客户端工具

三、Class文件结构

3.1、Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说Class文件实际上它并不一定以磁盘文件形式存在。Class文件是一组以字节为基础单位的二进制流,在内存中的表现形式为字节数组

3.2、Class文件格式

  • Class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变

  • Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明

3.3、Class文件格式图

3.4、Class文件结构概述

3.4.1、魔数(Class文件的标志)

类似于PE文件的标志 50 45;;;;

1.Magic Number(魔数)

2.每个Class文件开头的4个字节的无符号整数称为魔数(Magic Number)

3.它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符

4.魔数值固定为0xCAFEBABE(咖啡北鼻),不会改变、

5.如果一个 Class 文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:

3.4.2、Class文件版本号

1.紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version

2.它们共同构成了Class文件的格式版本号。譬如某个Class文件的主版本号为M,服版本号为m,那么这个Class文件的格式版本号就确定为M.m
Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1

3.不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常(向下兼容)

4.在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境的JDK版本是否一致

5.虚拟机JDK版本为1.k (k >= 2)时,对应的Class文件格式版本号的范围为45.0 - 44 + k.0(含两端)

3.4.3、常量池(存放所有常量)

  • 常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用

  • 随着Java虚拟机的不断发展,常量池的内容也日渐丰富,可以说常量池是整个Class文件的基石

  • 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项

  • 常量池中常量的数量是不固定的所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count),与Java中语言习惯不一样的是,这个容量计数是从1而不是 0 开始的

  • 常量池表是一种表结构,索引为1 - constant_pool_count - 1

  • 常量池中主要存放两大常量,字面量和符号引用:

字面量:使用""引起来的字符串、使用final修饰的常量
符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

  • 常量项大概有14种,结构都是标记字节(1字节) + 其他。比较常见的有CONSTANT_utf8_info、CONSTANT_class_info、CONSTANT_Methodref_info、CONSTANT_Fieldref_info等
  • 常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一

3.4.4、访问标识

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等

3.4.5、类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces_count、interfaces[])

  • 这三项数据来确定这个类的继承关系以及实现的接口,就是用来描述类的全限定名、父类是谁、实现了多个个接口,具体是哪些接口

  • 三个数据的格式

  • 类索引(this_class)

类索引用于确定这个类的全限定名,2字节无符号整数,指向常量池的索引
常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个Class文件所定义的类或接口

  • 父类索引(super_class)

2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名
如果我们没有继承任何类,其默认继承的是java/lang/Object 类。同时,由 于Java不支持多继承,所以其父类只有一个

  • 接口索引集合(interfaces_count、interfaces[])

interfaces_count项的值表示当前类或接口的直接超接口数量。
interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长 度为interfaces_count:::

每个成员interfaces[i]必须为CONSTANT_Class_info结构,其中0 <= i < interfaces_count。。。
在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口

3.4.6、字段表集合

3.4.6.1、概述

  • 字段表集合包含字段计数器以及字段表

  • 字段表用于描述接口类中声明的变量

  • 字段(field)包括类级变量以及实例级变量, 但是不包括方法内部、代码块内部声明的局部变量

  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述

  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final 修饰符)等

  • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段

  • 在 Java 语言中字段是无法重载的,两个字段的数据类型、修饰符不管 是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个 字段的描述符不一致,那字段重名就是合法的

3.4.6.2、字段计数器

fields_count的值表示当前Class文件fields表的成员个数。使用两个字节来表示

3.4.6.3、字段表

1.fields表中的每个成员都必须是一个 fields_info 结构的数据项,用于表示当 前类或接口中某个字段的完整描述

2.字段表结构

3.字段访问标志(access_flag)

4.字段名索引(name_index)

根据字段名索引的值,查询常量池中的指定索引项即可

5.描述符索引:字段的数据类型(基本数据类型、引用数据类型和数组)

6.属性表集合(属性个数和属性表数组)

1.一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、 一些注释信息等
2.属性个数存放在attribute_count中
3.属性具体内容存放在attributes数组中
4.以常量属性为例,结构为:


3.4.7、方法表集合

1.分为方法表计数器以及方法表数组

2.方法表计数器:有多少个方法表

3.方法表:用于表示当前类 或接口中某个方法的完整描述

1.方法表结构
2.方法表访问标识
3.方法名索引:对应常量池中CONSTANT_Uft8_info方法的名称
4.描述符索引:对应常量池中符号引用方法的返回值和参数列表

3.4.8、属性表集合

3.4.8.1、属性计数器:有多少个属性表

3.4.8.2、属性表

java8中定义了23张属性表

常见的code属性表

Code属性就是存放方法体里面的代码,但是并非所有方法表都有Code属 性,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了,code属性表结构::

常见的LineNumberTable

1.LineNumberTable属性是可选变长属性,位于Code结构的属性表
2.LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系,这个属性可以用来在调试的时候定位代码执行的行数
3.LineNumberTable属性表结构

常见的LocalVariableTable

1.LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息
2.LocalVariableTable属性表结构
注意:attribute_name_index:属性表名称常量池表索引
注意:attribute_length:属性表长度
注意:local_variable_table_length:局部变量个数
--------->
start_pc + length:这个变量在方法内的作用于
name_index:变量名在常量池表的索引
descriptor_index:局部变量数据类型在常量池表的索引
index:变量在局部变量表中的槽位

3.4.9、总结

  • Class文件其实就是对类的整体描述

  • 类有哪些字段?有哪些方法?类的全限定名?类的父类是谁?类实现的接口有哪些?方法中具体的方法体是什么?类的访问权限和修饰符等

  • Class文件中有一块非常重要的内容就是常量池,通过上面的分析可以得知,常量池就是各种字面量的引用,进行复用,最后都定位到字面量

3.5、小结

随着Java平台的不断发展,在将来,Class文件的内容也一定会做进一步的扩充,但是其基本的格式和结构不会做重大调整 从 Java 虚拟机的角度看,通过 Class 文件,可以让更多的计算机语言支持 Java 虚拟机平台。因此,Class 文件结构不仅仅是 Java 虚拟机的执行入口,更 是 Java 生态圈的基础和核心

四、解析举例

4.1、原始字节码文件

cafe babe 0000 0034 0016 0a00 0400 1209  
  
0003 0013 0700 1407 0015 0100 036e 756d  
  
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 184c 636f 6d2f  
  
6174 6775 6967 752f 6a61 7661 312f 4465  
  
6d6f 3b01 0003 6164 6401 0003 2829 4901  
  
000a 536f 7572 6365 4669 6c65 0100 0944  
  
656d 6f2e 6a61 7661 0c00 0700 080c 0005  
  
0006 0100 1663 6f6d 2f61 7467 7569 6775  
  
2f6a 6176 6131 2f44 656d 6f01 0010 6a61  
  
7661 2f6c 616e 672f 4f62 6a65 6374 0021  
  
0003 0004 0000 0001 0002 0005 0006 0000  
  
0002 0001 0007 0008 0001 0009 0000 0038  
  
0002 0001 0000 000a 2ab7 0001 2a04 b500  
  
02b1 0000 0002 000a 0000 000a 0002 0000  
  
0007 0004 0008 000b 0000 000c 0001 0000  
  
000a 000c 000d 0000 0001 000e 000f 0001  
  
0009 0000 003d 0003 0001 0000 000f 2a2a  
  
b400 0205 60b5 0002 2ab4 0002 ac00 0000  
  
0200 0a00 0000 0a00 0200 0000 0b00 0a00  
  
0c00 0b00 0000 0c00 0100 0000 0f00 0c00  
  
0d00 0000 0100 1000 0000 0200 11

4.2、原始代码

4.3、代码解析图

posted @ 2021-07-18 15:46  宋佳强  阅读(292)  评论(0编辑  收藏  举报