Class字节码文件结构总结

目录

一、什么是字节码指令

二、解读字节码指令的方式

三、Class字节码文件结构

四、魔数:Class文件的标志

五、Class文件的版本号

六、常量池:存放所有常量

七、访问标识(access_flag)

八、类索引、父类索引、接口索引集合

九、字段表集合

十、方法表集合

十一、属性表集合

十二、总结


一、什么是字节码指令

Java源代码经过编译器编译(javac)之后便会生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码(这也是C执行效率高的原因之一)。

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

  • 比如:操作码(操作数)
aload_0
aload_1
iconst_0
.....

二、解读字节码指令的方式

解读字节码指令主要有如下三种方式:

方式一:一个一个二进制的看,可以使用Notepad++,安装一个HEX一Editor插件,或者使用Binary Viewer;

方式二:使用javap指令:jdk自带的反解析工具,终端输入以下指令:

  • javap -v Test.class
  • 写入文件 javap -v Test.class > test.txt

方式三:使用IDEA插件,jclasslib或jclasslib bytecode viewer客户端工具;

三、Class字节码文件结构

JDK官方文档对Class字节码文件结构的介绍如下:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

Class类的本质:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在,我们还可以从网络中获取Class文件,所以说Class文件是一组以8位字节为基础单位的二进制流更为合适。

Class文件格式:

  • Class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
  • Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表
    1. 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8编码构成字符串值。
    2. 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。

Class文件的结构大体如下:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

翻译过来就是说Class文件内部结构主要包含如下表所示的几个部分:

类型

名称

说明

长度

数量

u4

magic

魔数,识别Class文件格式

4个字节

1

u2

minor_version

副版本号(小版本)

2个字节

1

u2

major_version

主版本号(大版本)

2个字节

1

u2

constant_pool_count

常量池计数器

2个字节

1

cp_info

constant_pool

常量池表

n个字节

constant_pool_count-1

u2

access_flags

访问标识

2个字节

1

u2

this_class

类索引

2个字节

1

u2

super_class

父类索引

2个字节

1

u2

interfaces_count

接口计数

2个字节

1

u2

interfaces

接口索引集合

2个字节

interfaces_count

u2

fields_count

字段计数器

2个字节

1

field_info

fields

字段表

n个字节

fields_count

u2

methods_count

方法计数器

2个字节

1

method_info

methods

方法表

n个字节

methods_count

u2

attributes_count

属性计数器

2个字节

1

attribute_info

attributes

属性表

n个字节

attributes_count

下面我们详细说明其中各项的含义,基于下述简单的代码进行分析:

public class Demo {
    private int num = 1;

    public int add() {
        num = num + 2;
        return num;
    }
}

四、魔数:Class文件的标志

  • 每个 Class 文件开头的4个字节的无符号整数称为魔数(Magic Number)
  • 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。
  • 魔数值固定为0xCAFEBABE,不会改变。
  • 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 1885430635 in classfile StringTest
  • 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

五、Class文件的版本号

  • 紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。
  • 它们共同构成了class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就确定为M.m;

版本号和Java编译器的对应关系如下表:

主版本(十进制)

副版本(十进制)

编译器版本

45

3

1.1

46

0

1.2

47

0

1.3

48

0

1.4

49

0

1.5

50

0

1.6

51

0

1.7

52

0

1.8

53

0

1.9

54

0

1.10

55

0

1.11

Java的版本号是从45开始的,JDK1.1之后的每个IDK大版本发布主版本号向上加1。

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

比如说,你在JDK7下使用JDK8的新特性,到时候运行就会报出UnsupportedClassVersionError版本不兼容的错误。

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

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

六、常量池:存放所有常量

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

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

类型

名称

数量

u2(无符号数)

constant_pool_count

1

cp_info(表)

constant_pool

constant_pool_count - 1

数据类型

定义

说明

无符号数

无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。

其中无符号数属于基本的数据类型。 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节

表是由多个无符号数或其他表构成的复合数据结构。

所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。

由上表可见,Class文件使用了一个前置的容量计数器(constant_pool_count),再加若干个连续的数据项(constant_pool)的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。

常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

(一)、常量池计数器:constant_pool_count

  • 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。
  • 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项,例如Demo的值为:

其值为0x0016,转换为十进制就是22。 需要注意的是,这实际上只有21项常量。索引为范围是1一21。为什么呢? 通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

(二)、常量池表: constant_pool [] (常量池)

constant_pool是一种表结构,以1 ~ (constant_pool_count - 1)为索引,表明了后面有多少个常量项。

常量池主要存放两大类常量:字面量(Literal) 和符号引用(Symbolic References)

  • 字面量:基本数据类型,字符串类型常量等;
  • 符号引用:类、字段、方法、接口等的符号引用;

它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。

(三)、常量类型和结构

常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte(标记字节、标签字节)。 

类型

标志(或标识)

描述

CONSTANT_utf8_info

1

UTF-8编码的字符串

CONSTANT_Integer_info

3

整型字面量

CONSTANT_Float_info

4

浮点型字面量

CONSTANT_Long_info

5

长整型字面量

CONSTANT_Double_info

6

双精度浮点型字面量

CONSTANT_Class_info

7

类或接口的符号引用

CONSTANT_String_info

8

字符串类型字面量

CONSTANT_Fieldref_info

9

字段的符号引用

CONSTANT_Methodref_info

10

类中方法的符号引用

CONSTANT_InterfaceMethodref_info

11

接口中方法的符号引用

CONSTANT_NameAndType_info

12

字段或方法的符号引用

CONSTANT_MethodHandle_info

15

表示方法句柄

CONSTANT_MethodType_info

16

标志方法类型

CONSTANT_InvokeDynamic_info

18

表示一个动态方法调用点

(四)、字面量和符号引用

常量池主要存放两大类常量:字面量(Literal) 和符号引用(Symbolic References)。如下表:

常量

具体的常量

字面量

文本字符串

声明为final的常量值

符号引用

类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

  • 全限定名

com/wsh/jvm/test/Demo这个就是类的全限定名,仅仅是把包名的"."替换成"/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

  • 简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。

  • 描述符

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、 char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:

标志符

含义

B

基本数据类型byte

C

基本数据类型char

D

基本数据类型double

F

基本数据类型float

I

基本数据类型int

J

基本数据类型long

S

基本数据类型short

Z

基本数据类型boolean

V

代表void类型

L

对象类型,比如:Ljava/lang/Object;

[

数组类型,代表一维数组。比如:double[][][] is [[[D

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法java.lang.String toString()的描述符为() Ljava/lang/String;,方法int abc(int【】x, int y)的描述符为(【II)I。 - 如下图,基本类型与引用类型的区别:

public class Demo {
    public static void main(String[] args) {
        Object[] objects = new Object[10];
        System.out.println(objects);

        String[] strings = new String[10];
        System.out.println(strings);

        long[][] longs = new long[10][];
        System.out.println(longs);
    }
}

运行结果:

[Ljava.lang.Object;@13221655
[Ljava.lang.String;@2f2c9b19
[[J@31befd9f

虚拟机加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻详到具体的内存地址中。这里说明下符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中

(五)、常见类型和结构细节

下面我们解析Demo类中常量池中所有的常量:

比如第一项0a,转换为十进制就是10,对照上面表格,10表示这是一个方法符号引用,可以通过字节码文件对照得出:

(六)、常量池表项数据的总结

我们发现上述18种常量中并没有出现byte、short、char,boolean,原因:他们编译之后都可以理解为Integer。

总结一:

  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。
  • 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。
  • 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过UTF-8编码,就可以知道其长度。

总结二:

  • 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

常量池中为什么要包含这些内容?

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中

七、访问标识(access_flag)

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

标志名称

标志值

含义

ACC_PUBLIC

0x0001

标志为public类型

ACC_FINAL

0x0010

标志被声明为final,只有类可以设置

ACC_SUPER

0x0020

标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)

ACC_INTERFACE

0x0200

标志这是一个接口

ACC_ABSTRACT

0x0400

是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型为假

ACC_SYNTHETIC

0x1000

标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)

ACC_ANNOTATION

0x2000

标志这是一个注解

ACC_ENUM

0x4000

标志这是一个枚举

类的访问权限通常为ACC_开头的常量

每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。

使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。

补充说明

  1. 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。
    • 1)如果一个class文件被设置了ACC_INTERFACE 标志,那么同时也得设置ACC_ABSTRACT 标志。同时它不能再设置ACC_FINAL、ACC_SUPER或ACC_ENUM 标志
    • 2)如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外,这两个标志不得同时设置。
  1. ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志。对于Java SE 8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。
    • 1) ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER标志在由JDK 1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么oracle的Java虚拟机实现会将其忽略。
  1. ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的
  2. 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
  3. ACC_ENUM标志表明该类或其父类为枚举类型。

比如Demo.class 中,这个值为21,就是ACC_PUBLIC(1)和 ACC_SUPER(20)相加的和,如下图:

八、类索引、父类索引、接口索引集合

  • 在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:

长度

含义

u2

this_class

u2

super_class

u2

interfaces_count

u2

interfaces[interfaces_count]

  • 这三项数据来确定这个类的继承关系。
    1. 类索引用于确定这个类的全限定名。
    2. 父类索引用于确定这个类的父类的全限定名。由于 Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.0bject 之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
    3. 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。

(一)、this_class (类索引)

2字节无符号整数,指向常量池的索引。它提供了类的全限定名。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口。

(二)、super_class (父类索引)

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

super_class指向的父类不能是final。

(三)、interfaces

指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。

由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。

  • 3.1 interfaces_count (接口计数器)

interfaces_count项的值表示当前类或接口的直接超接口数量。

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

interfaces []中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_count。每个成员interfaces[i]必须为CONSTANT_Class_info结构,其中0 <= i < interfaces_count。在 interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces【0】对应的是源代码中最左边的接口。

例如,Demo类的类索引、父类索引、接口索引如下图:

九、字段表集合

fields字段表集合:

  • 用于描述接口或类中声明的变量。字段(field)包括类变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。

注意事项

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

(一)、fields_count (字段计数器)

fields_count的值表示当前class文件fields表的成员个数。使用两个字节来表示。 fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。

(二)、fields[](字段表)

  • fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
  • 一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。
    1. 作用域(public、 private、 protected修饰符)
    2. 是实例变量还是类变量(static修饰符)
    3. 可变性(final)
    4. 并发可见性(volatile修饰符,是否强制从主内存读写)
    5. 可否序列化 (transient修饰符)
    6. 字段数据类型(基本数据类型、对象、数组)
    7. 字段名称
  • 字段表结构

字段表作为一个表,同样有自己的结构: 类型 | 名称 | 含义 | 数量 ---|---|---|--- u2 | access_flags | 访问标志 | 1 u2 | name_index | 字段名索引 | 1 u2 | descriptor_index | 描述符索引 | 1 u2 | attrubutes_count | 属性计数器 | 1 attribute_info | attributes | 属性集合 | attributes_count

【a】字段访问标识

我们知道,一个字段可以被各种关键宇去修饰,比如:作用域修饰符(public、private、 protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

字段是否为public

ACC_PRIVATE

0x0002

字段是否为private

ACC_PROTECTED

0x0004

字段是否为protected

ACC_STATIC

0x0008

字段是否为static

ACC_FINAL

0x0010

字段是否为final

ACC_VOLATILE

0x0040

字段是否为volatile

ACC_TRANSTENT

0x0080

字段是否为transient

ACC_SYNCHETIC

0x1000

字段是否为由编译器自动产生

ACC_ENUM

0x4000

字段是否为enum

【b】字段名索引

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

【c】描述符索引

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示,如下所示:

字符

类型

含义

B

byte

有符号字节型树

C

char

Unicode字符,UTF-16编码

D

double

双精度浮点数

F

float

单精度浮点数

I

int

整型数

J

long

长整数

S

short

有符号短整数

Z

boolean

布尔值true/false

V

void

代表void类型

L Classname

reference

一个名为Classname的实例

[

reference

一个一维数组

【d】属性表集合

一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中,属性具体内容存放在attributes数组中。

以常量属性为例,结构为:

ConstantValue_attribute{
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
}

说明:对于常量属性而言,attribute_length值恒为2。

例如,我们将num修改为final类型,重新编译后,查看其字节码,发现常量属性的 attribute_length值恒为2。如下图所示:

十、方法表集合

methods:指向常量池索引集合,它完整描述了每个方法的签名

  • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public、private或protected),方法的近回值类型以及方法的参数信息等。
  • 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来。
  • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法《clinit》()和实例初始化方法《init》())。

使用注意事项:

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

也就是说,尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码文件中却怡怡允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同。

【a】methods_count (方法计数器)

methods_count的值表示当前class文件methods表的成员个数,使用两个字节来表示。

methods表中每个成员都是一个method_info结构。

【b】methods[] (方法表)

  • methodsmethod表中的每个成员都必须是一个_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info结构access的_flags项既没有设置ACC_NATIVE标志也没有设置CC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的Java虚拟机指令。
  • method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法。

方法表的结构实际跟字段表是一样的,方法表结构如下:

类型

名称

含义

数量

u2

access_flags

访问标志

1

u2

name_index

字段名索引

1

u2

descriptor_index

描述符索引

1

u2

attrubutes_count

属性计数器

1

attribute_info

attributes

属性集合

attributes_count

【c】方法表访问标志

跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

字段是否为public

ACC_PRIVATE

0x0002

字段是否为private

ACC_PROTECTED

0x0004

字段是否为protected

ACC_STATIC

0x0008

字段是否为static

ACC_FINAL

0x0010

字段是否为final

ACC_VOLATILE

0x0040

字段是否为volatile

ACC_TRANSTENT

0x0080

字段是否为transient

ACC_SYNCHETIC

0x1000

字段是否为由编译器自动产生

ACC_ENUM

0x4000

字段是否为enum

例如,Demo类中两个方法的方法表集合信息如下图所示: 

十一、属性表集合

  • 方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class 文件的源文件的名称,以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。
  • 此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
  • 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

【a】attributes_ count(属性计数器)

attributes_countclass的值表示当前文件属性表的成员个数。属性表中每一项都是一个 attribute_info结构。

【b】attributes [](属性表)

属性表的每个项的值必须是attribute_info结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。

1、属性的通用格式

类型

名称

数量

含义

u2

attribute_name_index

1

属性名索引

u4

attribute_length

1

属性长度

u1

info

attribute_length

属性表

即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。

2、属性类型

属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,Java8里面定义了23种属性。下面这些是虚拟机中预定义的属性:

属性名称

使用位置

含义

Code

方法表

Java代码编译成的字节码指令

ConstantValue

字段表

final关键字定义的常量池

Deprecated

类、方法、字段表

被声明为deprecated的方法和字段

Exceptions

方法表

方法抛出的异常

EnclosingMethod

类文件

仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法

InnerClass

类文件

内部类列表

LineNumberTable

Code属性

Java源码的行号与字节码指令的对应关系

LocalVariableTable

Code属性

方法的局部变量描述

StackMapTable

Code属性

JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配

Signature

类,方法表,字段表

用于支持泛型情况下的方法签名

SourceFile

类文件

记录源文件名称

SourceDebugExtension

类文件

用于存储额外的调试信息

Syothetic

类,方法表,字段表

标志方法或字段为编泽器自动生成的

LocalVariableTypeTable

使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加

RuntimeVisibleAnnotations

类,方法表,字段表

为动态注解提供支持

RuntimelnvisibleAnnotations

类,方法表,字段表

用于指明哪些注解是运行时不可见的

RuntimeVisibleParameterAnnotation

方法表

作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法

RuntirmelnvisibleParameterAnniotation

方法表

作用与RuntimelnvisibleAnnotations属性类似,作用对象哪个为方法参数

AnnotationDefauit

方法表

用于记录注解类元素的默认值

BootstrapMethods

类文件

用于保存invokeddynanic指令引用的引导方式限定符

【c】部分属性详解

①ConstantValue属性

ConstantValue属性表示一个常量字段的值。位于 field_info结构的属性表中。 

ConstantValue_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 constantvalue_index;
    //字段值在常量池中的索引,常量池在该索引处的项给出该属性表示的常量值。
    //(例如,值是long型的,在常量池中便是CONSTANT_Long)
}

②Deprecated 属性

Deprecated属性是在JDK1.1为了支持注释中的关键词@deprecated而引入的。

 Deprecated_ attribute{
    u2 attribute_name_ index; 
    u4 attribute_length;
}

③Code属性

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

类型

名称

数量

含义

u2

attribute_name_index

1

属性名索引

u4

attribute_length

1

属性长度

u2

max_stack

1

操作数栈深度的最大值

u2

max_locals

1

局部变量表所需的存续空间

u4

code_length

1

字节码指令的长度

u1

code

code_length

存储字节码指令

u2

exception_table_length

1

异常表长度

exception_info

exception_table

exception_length

异常表

u2

attributes_count

1

属性集合计数器

attribute_info

attributes

attributes_count

属性集合

可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。

④InnerClasses属性

为了方便说明特别定义一个表示类或接口的 Class 格式为C。如果C的常量池中包含某个CONSTANT_Class_info成员,且这个成员所表示的类或接口不属于任何一个包,那么C的ClassFile结构的属性表中就必须含有对应的 InnerClasses属性。InnerClasses属性是在JDK 1.1 中为了支持内部类和内部接口而引入的,位于 ClassFile结构的属性表。

⑤LineNumber Table 属性

  • LineNumberTable属性是可选变长属性,位于 Code结构的属性表。
  • LineNumberTable属性是来描述Java源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。
    • start_pc,即字节码行号;line_number,即Java源代码行号。
  • 在 Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现,此外,多个 LineNumberTable属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable属性不需要与源文件的行一一对应。

LineNumberTable属性表结构

LineNumberTable_attribute {
    u2 attribute_name_index:
    u4 attribute_length:
    u2 line_number_table_length;
    {u2 start_pc:
        u2 line_number:
    } line_number_table[line_number_table_length]

如Demo类的LineNumberTable属性如下图所示:

⑥LocalVariableTable属性

LocalVariableTable是可选变长属性,位于Code属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息

  • 在 Code属性的属性表中,LocalVariableTable属性可以按照任意顺序出现。Code属性中的每个局部变量最多只能有一个LocalVariableTable属性。
    • start pc + length:表示这个变量在字节码中的生命周期起始和结束的偏移位置(this生命周期从头0到结尾10)
    • index:就是这个变量在局部变量表中的槽位(槽位可复用)
    • name:就是变量名称
    • Descriptor:表示局部变量类型描述

LocalVariableTable属性表结构

LocalVariableTable_attribute {
    u2 attribute_name_index:
    u4 attribute_length:
    u2 local_variable_table_length:
    { u2 start_pc:
        u2 length:
        u2 name_index;
        u2 descriptor_index;
        u2 index;
    } local_variable_table[local_variable_table_length]

如Demo类的LocalVariableTable属性表如下图所示:

⑦ Signature 属性

Signature 属性是可选的定长属性,位于 ClassFile, field_info 或 method_info结构的属性表中。在 Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。

⑧SourceFile属性

属性的通用格式:

类型

名称

数量

含义

u2

attribute_name_index

1

属性名索引

u4

attribute_length

1

属性长度

u1

info

attribute_length

属性表

可以看到,其总长度总是固定的8个字节

十二、总结

本文主要总结了Class字节码文件的内部结构,包括魔数、版本号、常量池、访问标识、类索引、字段表、方法表、属性表等结构,我们只需要大概了解各个部分大概存放了什么内容即可,能够帮助我们以后在查看类对应的字节码信息时知道哪个部分代表什么意思。最后附上Demo类详细的Class内存结构分析图(宋红康老师yyds):

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java虚拟机可以读取字节码文件并将其转换成可执行的代码。字节码文件Java源代码编译后生成的二进制文件,它包含了一系列指令,这些指令被Java虚拟机解释和执行。通过这种方式,Java程序可以在不同的硬件平台和操作系统上运行,实现了"Write Once, Run Anywhere"的目标。 Java虚拟机读取字节码文件的过程可以简单概括为以下几个步骤: 1. 加载:Java虚拟机通过类加载器加载字节码文件,将其转换为运行时的类对象。类加载器负责查找并加载类文件,并将其转换为内存中的类对象。 2. 验证:在加载字节码文件后,Java虚拟机会对字节码文件进行验证,确保其符合Java语言规范和虚拟机规范。验证过程包括对字节码文件结构、语义和安全性进行检查。 3. 准备:在验证通过后,Java虚拟机会为类变量(静态变量)分配内存,并设置默认初始值。此时,还没有执行任何Java代码。 4. 解析:在准备阶段之后,Java虚拟机会对字节码文件中的符号引用进行解析,将其转换为直接引用。这个过程将类或接口的符号引用解析为实际的内存地址。 5. 初始化:在准备阶段之后,Java虚拟机会执行类的初始化操作,包括执行静态初始化块和静态变量的赋值操作。在这个阶段,Java程序的主方法会被调用,程序开始执行。 通过以上步骤,Java虚拟机可以读取字节码文件并执行其中的指令,实现Java程序的运行。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java 进阶:实例详解 Java 虚拟机字节码指令](https://blog.csdn.net/m0_54853420/article/details/126104672)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值