Java虚拟机(四):Class文件结构及字节码指令

时间:2022-11-30 19:53:51
一、Class文件结构
    Java class文件是对Java程序二进制文件格式的精确定义。每一个Java class文件都对一个Java类或者Java接口作出了全面描述。一个class文件中只能包含一个类或者接口。

1、平台无关性
    尽管class文件与Java语言结构相关,但它并不一定必须与Java语言相关。如下图,可以使用其他语言来编写程序,然后将其编译为class文件,或者把Java程序编译为另一种不同的二进制文件格式。

       Java虚拟机(四):Class文件结构及字节码指令                 Java虚拟机(四):Class文件结构及字节码指令  
 
                     (注:如Scala、Groovy、JRuby等基于JVM的语言)
 
    Java class文件是8位字节的二进制流。数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使class文件紧凑。占据多个字节空间的项按照高位在前的顺序分为几个连续的字节存放。
    在class文件中,可变长度项的大小和长度位于其实际数据之前。这个特性使得class文件流可以从头到尾被顺序解析,首先读出项的大小,然后读出项的数据。
 
2、class文件的内容
    Java class文件中包含了Java虚拟机所需知道的、关于类或接口的所有信息。

       class文件“基本类型”

类 型

描 述

u1

1个字节,无符号类型

u2

2个字节,无符号类型

u4

4个字节,无符号类型

u8

8个字节,无符号类型

 
    可变长度的ClassFile表中的项,按照它们在class文件中出现的顺序列出了主要部分。

ClassFile表的格式

类 型

名 称

数 量

u4

magic

1

u2

minor_version

1

u2

major_version

1

u2

constant_pool_count

1

cp_info

constant_pool

constant_pool_count-1

u2

access_flags

1

u2

this_class

1

u2

super_class

1

u2

interfaces_count

1

u2

interfaces

interfaces_count

u2

fields_count

1

field_info

fields

fields_count

u2

methods_count

1

method_info

methods

methods_count

u2

attributes_count

1

attribute_info

attributes

attributes_count

Class文件组织结果:
  Java虚拟机(四):Class文件结构及字节码指令Java虚拟机(四):Class文件结构及字节码指令
1)magic(魔数)
    每个Java class文件的前4个字节被称为它的魔数(magic number):0xCAFEBABE。魔数的作用在于,可以轻松地分辨出Java class文件和非Java class文件。
 
2)minor_version和major_version
    class文件的下面4个字节包含了主、次版本号。对于Java虚拟机来说,版本号确定了特定的class文件格式,通常只有给定主版本号和一系列次版本号后,Java虚拟机才能够读取class文件。
 
3)constant_pool_count和constant_pool
    魔数和版本号后面的是常量池。常量池包含了与文件中类和接口相关的常量。常量池中存储了诸如文字字符串、final变量值、类名和方法名的常量。Java虚拟机把常量池组织为入口列表的形式。在实际列表constant_pool之前,是入口在列表中的计数constant_pool_count。
    每个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型。
    每一个标志都有一个相对应的表,表名通过在标志名后加上“_info”后缀来产生。

常量池标志

入 口 类 型

标 志 值

描 述

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

对一个字段或方法的部分符号引用

 
    除了字面常量(或者说直接量)值以外,常量池还可以容纳下面几种符号引用:
  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
4)access_flags
    紧接常量池后的两个字节称为access_flags,它展示了文件中定义的类或接口的几段信息。

access_flags项的标志位

标 志 名

设置后的含义

设 置 者

ACC_PUBLIC

0x0001

public类型

类和接口

ACC_FINAL

0x0010

类为final类型

只有类

ACC_SUPER

0x0020

使用新型的invokespecial语义

类和接口

ACC_INTERFACE

0x0200

接口类型,不是类类型

所有的接口,没有类

ACC_ABSTRACT

0x0400

abstract类型

所有的接口,部分类

    在access_flags中所有未使用的位都必须由编译器置0,而且Java虚拟机必须忽略它。
 
5)this_class
    接下来的两个字节为this_class项,它是一个对常量池的索引。在this_class位置的常量池入口必须为CONSTANT_Class_info表。该表由两个部分组成——标签和name_index。标签部分是一个具有CONSTANT_Class值的常量,在name_index位置的常量池入口为一个包含了类或接口全限定名的CONSTANT_Utf8_info表。

Java虚拟机(四):Class文件结构及字节码指令
 
6)super_class
    紧接在this_class之后的是super_class项,它是一个两个字节的常量池索引。在super_class位置的常量池入口是一个指向该类超类全限定名的CONSTANT_Class_info入口。
 
7)interfaces_count和interfaces
    紧接着super_class是interfaces_count。此项的含义为:在文件中由该类直接实现或者由接口所扩展的父接口的数量。在这个计数的后面,是名为interfaces的数组,它包含了对每个由该类或者接口直接实现(注:只包含直接出现在implements、extends子句中的父接口)的父接口的常量池索引。
 
8)fields_count和fields
    紧接在interfaces后面的是对在该类或者接口中所声明的字段的描述。首先是名为fields_count的计数,它是类变量和实例变量的字段的数量总和。在这个计数后面的是不同长度的field_info表的序列(fields_count指出了序列中有多少个field_info表)。在fields列表中,不列出从超类或者父接口继承而来的字段。
 
9)methods_count和methods
    紧接着fields后面的是对在该类或者接口中所声明的方法的描述。只包括在该类或者接口中显式定义的方法。
 
10)attributes_count和attributes
    class文件中最后的部分是属性(attribute),它给出了在该文件中类或者接口所定义的属性的基本信息。
 
  • 特殊字符串
    常量池中容纳的符号引用包括三种特殊的字符串:全限定名、简单名称和描述符。
 
  • 全限定名
    当常量池入口指向类或者接口时,它们给出该类或者接口的全限定名。在class文件中,全限定名中的点用斜线取代了。
 
  • 简单名称
    字段名和方法名以简单名称(非全限定名)形式出现在常量池入口中。
 
  • 描述符
    字段的描述符给出了字段的类型;方法描述符给出了方法的返回值和方法参数的数量、类型以及顺序。
    字段和方法的描述符由如下所示的上下文无关语法定义。该语法中非终结符号用斜体字标出,如FieldType;终结符号使用等宽度字体标出,如BV;星号代表紧接在它前面的符号(中间没有空格)将会出现0次或者多次。
 
FieldDescriptor:  FieldType
ComponentType: FieldType
FieldType:  BaseType、  ObjectType、  ArrayType
BaseType:  B、 C、  D、 F、  I、  J、 S、 Z
ObjectType:、 L<classname>;
ArrayType:、 [ ComponentType
MethodDescriptor:、 ( ParameterDescriptor * ) ReturnDescriptor
ParameterDescriptor:、   FieldType
ReturnDescriptor:、   FieldType、   V
 
V        表示方法返回值为void类型
L;      对象类型终结符
[         数组类型终结符
()      方法描述符终结符
 

基本类型终结符

终 结 

类 

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean


字段描述符示例

描 述 符

字 段 声 明

I

int i;

[[J

long[][] windingRoad;

[Ljava/lang/Object;

java.lang.Object[] stuff;

Ljava/util/Hashtable;

java.util.Hashtable ht;

[[[Z

boolean[][][] isReady;


方法描述符示例

描 述 

方 法 声 

()I

int getSize();

()Ljava/lang/String;

String toString();

([Ljava/lang/String;)V

void main(String[] args);

()V

void wait();

(JI)V

void wait(long timeout, int nanos);

(ZILjava/lang/String;II)Z

boolean regionMatches(boolean ignoreCase, int toOffset, String other, int offset, int len);

([BII)I

int read(byte[] b, int off, int len);


3、常量池
    常量池是一个可变长度cp_info表的有序序列。cp_info表一共有11种类型。

cp_info表的通常形式

类 

名 

数 

描 

u1

tag

1

表的类型和格式

u1

info

根据tag值决定



1)CONSTANT_Utf8_info表
    可变长度的CONSTANT_Utf8_info表使用一种UTF-8格式的变体来存储一个常量字符串。这种类型的表可以存储多种字符串,包括:
    - 文字字符串,如String对象。
    - 被定义的类和接口的全限定名。
    - 被定义的类的超类(如果有的话)的全限定名。
    - 被定义的类和接口的父接口的全限定名。
    - 由类或者接口声明的任意字段的简单名称和描述符。
    - 由类或者接口声明的任意方法的简单名称和描述符。
    - 任何引用的类和接口的全限定名。
    - 任何引用的字段的简单名称和描述符。
    - 任何引用的方法的简单名称和描述符。
    - 与属性相关的字符串。

CONSTANT_Utf8_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Utf8(1)

u2

length

1

bytes项的长度(字节数)

u1

bytes

length

按照变体UTF-8格式存储的字符串中的字符


2)CONSTANT_Integer_info表的格式

CONSTANT_Integer_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Integer(3)

u4

bytes

1

按照高位在前的格式存储int类型值


3)CONSTANT_Float_info表的格式

CONSTANT_Float_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Float(4)

u4

bytes

1

按照高位在前的格式存储float类型值


4)CONSTANT_Long_info表的格式

CONSTANT_Long_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Long(5)

u8

bytes

1

按照高位在前的格式存储long类型值


5)CONSTANT_Double_info表的格式

CONSTANT_Double_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Double(6)

u8

bytes

1

按照高位在前的格式存储double类型值


6)CONSTANT_Class_info表的格式

CONSTANT_Class_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Class(7)

u2

name_index

1

包含类或者接口全限定名的CONSTANT_Utf8_info表的索引


7)CONSTANT_String_info表的格式

CONSTANT_String_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_String(8)

u2

string_index

1

包含文字字符串值的CONSTANT_Utf8_info表的索引


8)CONSTANT_Fieldref_info表的格式

CONSTANT_Fieldref_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Fieldref(9)

u2

class_index

1

声明被引用字段的类或者接口的CONSTANT_Class_info入口的索引

u2

name_and_type_index

1

提供了CONSTANT_NameAndType_info入口的索引,该入口提供了字段的简单名称以及描述符


9)CONSTANT_Methodref_info表的格式

CONSTANT_Methodref_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_Methodref(10)

u2

class_index

1

声明被引用方法的类的CONSTANT_Class_info入口的索引

u2

name_and_type_index

1

提供了CONSTANT_NameAndType_info入口的索引,该入口提供了方法的简单名称以及描述符


10)CONSTANT_InterfaceMethodref_info表的格式

CONSTANT_InterfaceMethodref_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_InterfaceMethodref(11)

u2

class_index

1

声明被引用方法的接口的CONSTANT_Class_info入口的索引

u2

name_and_type_index

1

提供了CONSTANT_NameAndType_info入口的索引,该入口提供了方法的简单名称以及描述符


11)CONSTANT_NameAndType_info表的格式

CONSTANT_NameAndType_info表的格式

类 

名 

数 

描 

u1

tag

1

值为CONSTANT_NameAndType(12)

u2

name_index

1

给出了CONSTANT_Utf8_info入口的索引,该入口给出了字段或者方法的名称

u2

descriptor_index

1

提供了CONSTANT_Utf8_info入口的索引,该入口提供了字段或者方法的描述符

 
4、字段
    在类或者接口中声明的每一个字段(类变量或者实例变量)都由class文件中的一个名为field_info的可变长度的表进行描述。

field_info表的格式

类 

名 

数 

描 

u2

access_flags

1

见下方的表

u2

name_index

1

提供了给出字段简单名称(不是全限定名)的CONSTANT_Utf8_info入口的索引

u2

descriptor_index

1

提供了给出字段描述符的CONSTANT_Utf8_info入口的索引

u2

atrributes_count

1

attributes_count指出列表中attribute_info表的数量

attribute_info

atrributes

atrributes_count

由多个attribute_info表组成的列表


field_info表中access_flags项的标志

标 志 名 

设 定 含 

设 定 

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_TRANSIENT

0x0080

字段设为transient

只有类

 
    类(不包括接口)中声明的字段,只能拥有ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED这三个标志中的一个。ACC_FINAL和ACC_VOLATILE不能同时设置。所有接口中声明的字段必须有且只能有ACC_PUBLIC、ACC_STATIC和ACC_FINAL这三种标志。
 
5、方法
    在class文件中,每个在类和接口中声明的方法,或者由编译器产生的方法,都由一个可变长度的method_info表来描述。

method_info表的格式

类 

名 

数 

描 

u2

access_flags

1

见下方的表

u2

name_index

1

提供了给出方法简单名称(不是全限定名)的CONSTANT_Utf8_info入口的索引

u2

descriptor_index

1

提供了给出方法描述符的CONSTANT_Utf8_info入口的索引

u2

atrributes_count

1

attributes_count指出列表中attribute_info表的数量

attribute_info

atrributes

atrributes_count

由多个attribute_info表组成的列表


method_info表中access_flags项的标志

标 志 名 

设 定 含 

设 定 

ACC_PUBLIC

0x0001

方法设为public

类和所有的接口方法

ACC_PRIVATE

0x0002

方法设为private

只有类

ACC_PROTECTED

0x0004

方法设为protected

只有类

ACC_STATIC

0x0008

方法设为static

只有类

ACC_FINAL

0x0010

方法设为final

只有类

ACC_SYNCHRONIZED

0x0020

方法设为synchronized

只有类

ACC_NATIVE

0x0100

方法设为native

只有类

ACC_ABSTRACT

0x0400

方法设为abstract

类和所有的接口方法

ACC_STRICT

0x0800

方法设为strictFP

类和接口的<clinit>方法


    属性在Java class文件中多处出现。它们可以出现在ClassFile、field_info、method_info和Code_attribute表中。Code_attribute表本身即为一个属性。
    Java虚拟机规范定义了9种属性。为了正确地解释Java class文件,所有Java虚拟机实现都必须能够识别下列三种属性:Code,ConstantValue和Exception。为了正确地实现Java和Java 2平台类库,虚拟机实现必须能够识别InnerClasses和Synthetic属性,但可以自主选择究竟是识别还是忽略其他一些预定义的属性。

由规范定义的attribute_info表的类型

名 

使 用 

描 

Code

method_info

方法的字节码和其他数据

ConstantValue

field_info

final变量的值

Deprecated

field_info、method_info

字段或者方法被禁用的指示符

Exceptions

method_info

方法可能抛出的可被检测的异常

InnerClasses

ClassFile

内部、外部类的列表

LineNumberTable

Code_attribute

方法的行号与字节码的映射

LocalVariableTable

Code_attribute

方法的局部变量的描述

SourceFile

ClassFile

源文件名

Synthetic

field_info、method_info

编译器产生的字段或者方法的指示符


二、Java字节码指令               
1、
加载与存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输:

    - 将一个局部变量加载到操作栈的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
    - 将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
    - 将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
    - 扩充局部变量表的访问索引的指令:wide

        访问对象的字段或数组元素的指令也同样会与操作数栈传输数据。

        上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如 iload_<n>),这些指令助记符实际上是代表了一组指令(例如 iload_<n>,它代表了 iload_0、iload_1、iload_2 和 iload_3 这几条指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都是在指令中隐含的。除此之外,他们的语义与原生的通用指令完全一致(例如 iload_0 的语义与操作数为 0 时的 iload 指令语义完全一致)。在尖括号之间的字母制定了指令隐含操作数的数据类型,<i>代表是 int 形数据,<l>代表 long 型,<f>代表 float 型,<d>代表 double型。在操作 byte、char 和 short 类型数据时,也用 int 类型表示。

这种指令表示方法,在整个《Java 虚拟机规范》之中都是通用的。

2、运算指令

        算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是那种算术指令,都是使用 Java 虚拟机的数字类型的。数据没有直接支持 byte、short、char 和 boolean 类型的算术指令,对于这些数据的运算,都是使用操作 int 类型的指令。

整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为,所有的算术指令包括:

    - 加法指令:iadd、ladd、fadd、dadd
    - 减法指令:isub、lsub、fsub、dsub
    - 乘法指令:imul、lmul、fmul、dmul
    - 除法指令:idiv、ldiv、fdiv、ddiv
    - 求余指令:irem、lrem、frem、drem
    - 取反指令:ineg、lneg、fneg、dneg
    - 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
    - 按位或指令:ior、lor
    - 按位与指令:iand、land
    - 按位异或指令:ixor、lxor
    - 局部变量自增指令:iinc
    - 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
3、 类型转换指令
     窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。
4、对象创建与操作指令

    虽然类实例和数组都是对象,但 Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:

    - 创建类实例的指令:new
    - 创建数组的指令:newarray,anewarray,multianewarray
    - 访问类字段(static 字段,或者称为类变量)和实例字段(非 static 字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic
    - 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
    - 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
    - 取数组长度的指令:arraylength
    - 检查类实例类型的指令:instanceof、checkcas

5、操作数栈管理指令

    Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap。

6、控制转移指令

    控制转移指令可以让 Java 虚拟机有条件或无条件地从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括有:

    - 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
    - 复合条件分支:tableswitch、lookupswitch
    - 无条件分支:goto、goto_w、jsr、jsr_w、ret
7、方法调用和返回指令

    以下四条指令用于方法调用:

    - invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
    - invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
    - invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
    - invokestatic 指令用于调用类方法(static 方法)。

    而方法返回指令则是根据返回值的类型区分的,包括有 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn,另外还有一条 return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。

8、抛出异常

    在程序中显式抛出异常的操作会由 athrow 指令实现,除了这种情况,还有别的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。

9、同步指令
    同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持。