目 录CONTENT

文章目录

类的文件结构

FatFish1
2025-04-07 / 0 评论 / 0 点赞 / 40 阅读 / 0 字 / 正在检测是否收录...

class文件的优势和基本结构

class是java虚拟机直接运行的媒介,java虚拟机可以运行java、JRuby、JPython、Scala、Groovy等语言就是因为可以通过不同的编译器将这些语言编译为class文件标准。

使用winHex编辑器可以阅读class文件内容。使用javap工具 -verbose参数可以反编译class

javap -verbose TestClass

class文件结构具有很好的稳定性。class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑第排列在文件种。当遇到需要占用8个字节以上空间的数据项时,会按照大端序的方式分割成若干个8字节进行存储。class文件格式是一种伪结构,只有两种数据类型:无符号数和表

  • 无符号数:基本数据类型,以u1、u2、u4、u8代表1字节、2字节、4字节、8字节。可以描述数字、索引引用、数量值或按UTF-8编码构成字符串值

  • 表是由多个无符号数或其他表做为数据项构成的符合数据类型,为了便于区分,都习惯性以_info结尾。表用于描述有层次关系的复合结构数据。整个class文件本质上也是一张表,这个表顺序严格不能变,甚至数据存储的字节序(class是Big-Endian大端序)也是严格限定的,全都不许改变。

为什么要这么设计?

因为Class没有任何分隔符,因此其中二进制数据的顺序、数量、字节序都绝对不能变

Class表中各个长度的数据项含义如下:

类型

名称

数量

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文件最开头4个字节存储的内容就也是魔数,值是0xCAFEBABE,即16进制下的CAFEBABE

其他的常见格式文件头例如:

JPEG (jpg), 文件头:FFD8FF 文件尾:FF D9
PNG (png), 文件头:89504E47 文件尾:AE 42 60 82
GIF (gif), 文件头:47494638 文件尾:00 3B
ZIP Archive (zip), 文件头:504B0304 文件尾:504B0506
TIFF (tif), 文件头:49492A00 文件尾:
Windows Bitmap (bmp), 文件头:424D 文件尾:
CAD (dwg), 文件头:41433130 文件尾:
Adobe Photoshop (psd), 文件头:38425053 文件尾:
Rich Text Format (rtf), 文件头:7B5C727466 文件尾:
XML (xml), 文件头:3C3F786D6C 文件尾:
HTML (html), 文件头:68746D6C3E
Email [thorough only] (eml), 文件头:44656C69766572792D646174653A
Outlook Express (dbx), 文件头:CFAD12FEC5FD746F
Outlook (pst), 文件头:2142444E
MS Word/Excel (xls.or.doc), 文件头:D0CF11E0
MS Access (mdb), 文件头:5374616E64617264204A
WordPerfect (wpd), 文件头:FF575043
Adobe Acrobat (pdf), 文件头:255044462D312E
Quicken (qdf), 文件头:AC9EBD8F
Windows Password (pwl), 文件头:E3828596
RAR Archive (rar), 文件头:52617221
Wave (wav), 文件头:57415645
AVI (avi), 文件头:41564920
Real Audio (ram), 文件头:2E7261FD
Real Media (rm), 文件头:2E524D46
MPEG (mpg), 文件头:000001BA
MPEG (mpg), 文件头:000001B3
Quicktime (mov), 文件头:6D6F6F76
Windows Media (asf), 文件头:3026B2758E66CF11
MIDI (mid), 文件头:4D546864

class版本

魔数后面的4个字节存储的是class版本,其中前两个字节是次版本号,后两个字节是主版本号

java低版本jvm环境不能运行高版本class文件就是由此判断的

jDK版本从java1.1是45版本,到JDK6 是50版本,JDK8是52版本,依次类推,例如下图:

代表版本号的是0x0034,即10进制的52,即该class基于JDK8开发

常量池

版本号后面是常量池,通常在class文件中占空间最大

因为每个类中常量数量是不确定的,因此常量池的前两个字节定死了是一个u2数据,代表常量池的长度,例如下图:

0x13,即10进制19,说明常量池中有18个常量,其中第0位是空,如果有方法指向这个位,代表不引用任何常量,然后后面1-18位都是常量

常量池存放内容主要两大类:字面量、符号引用。

  • 字面量:文本字符串、被声明为final的常量值

  • 符号引用:被模块导出或开放的包、类和接口的全限定名、字段名称的描述符、方法的名称和描述符、方法句柄和类型、动态调用点和动态常量。

常量表和常量的解读

常量池中的每个常量都是一个表,表结构第一位是一个u1的tag,代表该常量类型,JDK13为止有17种常量类型:

每个常量除了u1的tag,后面都是自己独特的格式,分别是:

类型

项目

类型

描述

CONSTANT_Utf8_info

tag

u1

值为1

length

u2

utf-8缩略编码字符串占用字节数

bytes

u1

长度为length的utf-8缩略编码字符串

CONSTANT_Integer_info

tag

u1

值为3

bytes

u4

按照高位在前储存的int值

CONSTANT_Float_info

tag

u1

值为4

bytes

u4

按照高位在前储存的float值

CONSTANT_Long_info

tag

u1

值为5

bytes

u8

按照高位在前储存的long值

CONSTANT_Double_info

tag

u1

值为6

bytes

u8

按照高位在前储存的double值

CONSTANT_Class_info

tag

u1

值为7

index

u2

指向全限定名常量项的索引

CONSTANT_String_info

tag

u1

值为8

index

u2

指向字符串字面量的索引

CONSTANT_Fieldref_info

tag

u1

值为9

index

u2

指向声明字段的类或接口描述符CONSTANT_Class_info的索引项

index

u2

指向字段描述符CONSTANT_NameAndType_info的索引项

CONSTANT_Methodref_info

tag

u1

值为10

index

u2

指向声明方法的类描述符CONSTANT_Class_info的索引项

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

CONSTANT_InterfaceMethodref_info

tag

u1

值为11

index

u2

指向声明方法的接口描述符CONSTANT_Class_info的索引项

index

u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

CONSTANT_NameAndType_info

 

 

tag

u1

值为12

index

u2

指向该字段或方法名称常量项的索引

index

u2

指向该字段或方法描述符常量项的索引

 

CONSTANT_MethodHandle_info

tag

u1  

值为15

refrence_kind

u1

值必须在1-9之间,决定了方法句柄的类型,方法句柄的类型的值表示方法句柄字节码的行为

refrence_index

u2

值必须是对常量池的有效索引

CONSTANT_MethodType_info

tag

u1

值为16

descriptor_index

u2

值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示方法的描述符

CONSTANT_Dynamic_info

tab

u1

值为17

bootstrap_method_attr_index

u2

值必须对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引

name_and_type_index

u2

值必须对当前常量池的有效索引,常量池中在该索引出的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

CONSTANT_InvokeDynamic_info

tag 

u1

值为18

bootstrap_method_attr_index

u2

值必须对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引

name_and_type_index

u2

值必须对当前常量池的有效索引,常量池中在该索引出的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

CONSTANT_Module_info

tag

u1

值为19

name_index

u2

值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示模块名

CONSTANT_Package_info

tag

u1

值为20

name_index

u2

值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示包名

想要解析.class文件中的常量,必须先分析tag找到对应的常量类型,然后根据类型对应的结构分析具体内容

例如一个常量池的16进制结果为:00 16 07 00 02 01 00 1D ...

首先0x0016代表常量池中的常量数量为21个,07是第一个常量的tag位,对应CONSTANT_Class_info,而这个常量的结构是tag后面有一个u2的index指向常量池中全限定名常量项的索引,即后面的0x0002 ,代表指向常量池索引为2的常量,索引为2的常量是01 00 1D ,代表其tag为是01,对应的是CONSTANT_Utf8_info常量,其结果在tag后面有一个u2的length,即0x001D ,即长度为29字节,因此后面29个字节都是该CONSTANT_Utf8_info常量的内容,这样就可以根据ascii码表翻译出其中的具体内容

访问标志

常量池后面的u2数据占两个字节,代表访问标志access_flag,识别类或接口层次的访问信息,例如public、abstract、final等,标志位枚举值如下:

字段的访问权限

Flag Name

Value

Remarks

ACC_PUBLIC

0x0001

pubilc,包外可访问。

ACC_PRIVATE

0x0002

private,只可在类内访问。

ACC_PROTECTED

0x0004

protected,类内和子类中可访问。

ACC_STATIC

0x0008

static,静态。

ACC_FINAL

0x0010

final,常量。

ACC_VOILATIE

0x0040

volatile,直接读写内存,不可被缓存。不可和ACC_FINAL一起使用。

ACC_TRANSIENT

0x0080

transient,在序列化中被忽略的字段。

ACC_SYNTHETIC

0x1000

synthetic,由编译器产生,不存在于源代码中。

ACC_ENUM

0x4000

enum,枚举类型字段

ACC_MODULE

0x8000

标识这是一个模块

可以共存的标志位会一起出现做位运算,例如使用jdk1.2以后编译的话,ACC_SUPER为真,且是public的,因此标志位值为0x0001|0x0020=0x0021

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

访问标志后面是3个u2数据,分别是类索引this_class、父类索引super_class、接口索引数量interfaces_count,跟在接口索引数量后面的是接口索引集合interfaces,其中每个索引是一个u2数据,而其数量由前面的数量决定

因为u2的父类索引就一个值,所以java只能单继承,而接口索引集合是多个数据,因此java可以实现多个接口

类索引和父类索引指向的都是全限定名,各自指向一个CONSTANT_Class_info常量,进而再指向一个CONSTANT_Utf8_info字符串常量,从而找到实际的类的全限定名。

接口索引集合第一个u2是接口计数器,表示索引表容量,如果没有实现任何接口,计数器值为0,后面索引表不占字节长度。

字段表集合

然后是字段表集合

字段表field_info用于描述接口或类中声明的变量。字段信息包括字段修饰符(作用域修饰符public等、static修饰符、可变性final、并发可见性volatile、可否被序列号transient修饰符)、字段数据类型、字段名称。

字段表最前面有一个u2的容量计数器,后面这些信息组成的格式如下:

  • 字段修饰符access_flags:与类的access_flags很相似,是u2格式,也有可同时出现或不可同时出现的规则,也是做位运算,标志位如下:

  • name_index和:分别代表字段的简单名称以及方法/字段的描述符,它们都是通过指向常量池缩印来确定具体值的

    • 简单名称:没有任何修饰的方法或字段名称,例如public void getAllStudent()方法的简单名称就是getAllStudent

    • 方法/字段的描述符:包括数据类型、方法参数列表、返回值等。普通数据类型见下表。基本数据类型直接用标识符,对象类型用L加上对象的全限定名,例如String类型记录成Ljava/lang/String如果是数组类型,每一维在常量池里面常量前面加个[,例如String[][]记录成[[Ljava/lang/String

    • 描述顺序:先括号扩起参数列表、后返回值。例如void inc()描述符为()VtoString()方法描述符为()Ljava/lang/String ,而更复杂的int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targeCount, int fromIndex)的描述符为([CII[CIII)I

  • attributes_count和attributes_info:属性表集合,存储额外信息,如果属性表计数为0,代表没有额外描述信息,但是如果字段有初始值123,在属性表中就会存储一个ConstantValue的属性,指向常量123.

注意:字段表集合中不会列出父类或父接口中继承而来的字段,但有可能出现原本java代码中不存在的字段,例如编译器自动添加的public或protect

看一个例子:一个类的字段表二进制数据如下:00 01 00 02 00 05 00 06

首先00 01 代表字段表计数器为1,即00 02 00 05 00 06是这个具体字段内容,只有三个u2,即access_flags、name_index、decriptor_index,没有初始属性

  • 00 02是0x0002,即access_flags=private

  • 00 05是0x0005,即指向常量池第5个常量,假如为foo,即name_index=foo

  • 00 06是0X0006,即指向常量池第6个常量,假如为I,即decriptor_index为int类型

翻译过来就是private int foo()

方法表集合

方法表和字段表内容大体一致,格式也差不多,包括访问标志access_flags、名称索引name_index、描述符索引descriptor_index、属性表数量attributes_count、属性集合attributes

但方法表的access_flags与字段表肯定不一样,主要区别是:方法表的访问标志不包括transient、volatile,但增加了synchronized、native、strictfp、abstract这几类。有如下值:

需要注意以下几点:

  1. 方法代码不存储在方法表集合,而是编译成字节码存储在属性表集合中的code属性中

  2. 另外,java中的多态运行方法在参数不同的情况下同名,但仅返回值不同是不允许的,因此不能仅依靠返回值不同来重载方法。但class文件中只要方法描述符不同就认为是不完全一致,即两个方法名称、入参相同,但返回值不同,就可以共存。这是一点小差异

  3. class字节码不存直接继承父类的方法,除非子类重写。但是也会添加编译器自动生成的方法,例如类构造器<clinit>()方法和实例构造器<init>()方法

属性表集合

结构

Class文件对其他的数据项目要求严格的顺序、长度和内容不同,而属性表没有Class文件那么严格,不要求各个属性表具有严格的顺序,甚至允许只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息

另外,属性表中各个属性也不是严格紧凑存放在一起的,而是分散在不同的其他表里面,例如方法表中有一项attribute_info属性集合,字段表中也有字段的attribute_info属性集合本质上都是属性表内容

所有地方的attribute_info结构都是一样的,包括属性名称索引、属性长度,以及具体内容如下:

其中:

  • attribute_name_index是一个指向CONSTANT_Utf8_info常量的索引

  • attribute_length是属性值长度

  • 索引加长度一共6字节,因此属性值长度即属性表长度减6

另外,属性表在javaSE12中已经有29项了,虚拟机运行时会忽略掉他不认识的属性。以下是一些常用属性举例:

code

方法代码段经过javac编译后,变为字节码存储在code属性中。Code属性出现在方法表的属性集合中,但并非所有方法都一定有code属性,比如抽象类或接口。

code属性结构如下:

其中max_stack到attributes就是attribute_info中的info具体内容

  • code的attribute_name_index虽然是一个索引,但是不管在哪个类,其找到的结果一定是“Code”

  • max_stack是操作数栈深度的最大值。虚拟机运行时根据这个值分配栈帧的操作栈深度。这里不够分配,就会导致java栈溢出

  • max_locals代表局部变量表所需的存储空间,单位是变量槽slot。变量槽是虚拟机为局部变量分配内存所使用的最小单位。不超过32位的基本数据类型占1个槽,double、long是64位的占两个槽。方法参数(包括隐藏参数this)、显示异常处理程序的参数(catch块中的异常)、方法体中的局部变量都依赖局部变量表存放。但并不是方法中用了多少局部变量,算占的槽之和,虚拟机会重用槽,优化性能

  • code_length和code存储的是具体的字节码指令,其中code_length代表字节码长度,而code存储的是字节码指令流。

    • 之所以叫字节码,是因为一个字节就可以表达一条指令,通过数值就可以表达,1个字节存储数据是28数据,即162数据,两个16进制数,存储的最大值是0x00~0xFF,或者说是0~255。而虚拟机目前定义了约200条字节码,即每个字节码都有其唯一编号。只需要查到code每个字节对应的int数值,就可以推导出其对应的字节码指令了

    • code_length虽然是u4长度,但是实际上一个方法最多不超过65535条字节码指令,因此不建议一个方法搞太大

举个例子:从max_stack开始看,一段class文件的二进制记录如下:00 01 00 01 00 00 00 05 2A B7 00 0A B1

最大栈深度为0x0001即1个,本地变量表槽是0x0001占1个,字节码长度为0x00000005,即长度为5,即后面5个u12A B7 00 0A B1都是字节码。对照字节码指令表就可以知道是什么指令

  • B1为常见的指令return

  • B7为invokespecial,这条指令左右是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或它的父类的方法。这个方法后面有一个u2类型的参数00 0A指向一个CONSTANT_Methodref_info常量。代表这个方法参数是谁。

但是看本地变量表大小是1,却没有参数,这是真的吗?构造一个测试类:

public class ByteCodeTest {
	private int m;
	
	public int inc() {
        return m+1;
    }
}

然后在idea中在类上点击打开于-终端,执行javac ByteCodeTest.java 然后再执行 javap -verbose .\ByteCodeTest.class ,可以看到字节码如下:

public class com.huawei.cbc.udrmetricservice.jvmtest.ByteCodeTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/huawei/cbc/udrmetricservice/jvmtest/ByteCodeTest.m:I
   #3 = Class              #17            // com/huawei/cbc/udrmetricservice/jvmtest/ByteCodeTest
   #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               ByteCodeTest.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               com/huawei/cbc/udrmetricservice/jvmtest/ByteCodeTest
  #18 = Utf8               java/lang/Object
{
  public com.huawei.cbc.udrmetricservice.jvmtest.ByteCodeTest();
    descriptor: ()V
    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();
    descriptor: ()I
    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 8: 0
}
SourceFile: "ByteCodeTest.java"

可以看到inc方法中确实生成了args_size=1这是因为java里面可以通过this关键字访问所属对象,尽管java不需要主动写this关键字,编译器会在类里面自动加一个本地变量this,指向的对象就是类对象本身。如果写静态方法,就没有这个this本地变量了

  • exception_table_length和exception_table分别是异常表的长度和异常表,是用来存储方法中显示用try语句捕捉的异常的,格式如下:

    • start_pc和end_pc分别记录try语句的起止行数

    • catch_type记录显示捕捉的异常类型

    • handler_pc记录处理异常语句的行数

    • 当start_pc和end_pc行数之间出现了catch_type记录的异常,就会捕捉到,代码就会跳转到handler_pc对应的行数处理

举个例子,如下方法:

public int inc() {
    try {
        return m+1;
    } catch (NumberFormatException e) {
        e.printStackTrace();
    } catch (NullPointerException e) {
        e.printStackTrace();
    }
    return 0;
}

编译-反编译后可以看到如下exception_table:

Exception table:
   from    to  target type
       0     6     7   Class java/lang/NumberFormatException
       0     6    15   Class java/lang/NullPointerException

可知捕获两种异常,分别跳转执行的行号为7和15

7: astore_1
……
15: astore_1

exception属性

要注意一点,这里的exception属性和code里面的异常表不是一码事

exception属性是列举出方法中可能抛出的受查异常,即方法描述时在throws关键字后面列举的异常。其格式如下:

  • attribute_name_index(u2)、attribute_length(u4):固定的两个元素

  • number_of_exceptoins:u2数据,数量1,表示方法抛出异常数量

  • exception_index_table:u2数据,数量由number_of_exceptoins决定,表示异常信息

还是以上面为例,在inc方法后面加上异常类型,重新编译-反编译:

public int inc() throws NumberFormatException, NullPointerException {
    ……
}
  public int inc() throws java.lang.NumberFormatException, java.lang.NullPointerException;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      ……
    Exceptions:
      throws java.lang.NumberFormatException, java.lang.NullPointerException

最后得到的Exceptions项目就是方法的exception属性

LineNumberTable属性

非必须属性,描述Java源码行号与字节码行号对应关系。可以使用javac -g:nonejavac -g:lines可以取消生成。主要作用就是抛出异常时堆栈可以显示出错行号,且调试需要行号支持。

结构如下:

LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable描述栈帧中局部变量表变量与Java源码之间定义变量关系,可以用g:noneg:vars取消,如果参数名丢失,IDE将会使用arg0、arg1这些代替原有参数名,调试时也无法获取参数值。LocalVariableTypeTable用于描述特征签名。

SourceFile及SourceDebugExtension属性

SourceFile记录Class文件的源码文件名称,可使用g:noneg:source关闭。不生成的话堆栈不显示出错代码的文件名。这个属性是定长的。

SourceDebugExtension存储代码调试信息。

ConstantValue属性

通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才有这个属性。也是一个定长属性,其格式如下:

attribute_length值固定为2,constantvalue_index代表常量池一个字面量的引用,字面量可选范围仅有CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info

InnerClass属性

描述内部类与宿主类之间的关系,结构如下:

inner_class_access_flags字段与类的访问标识符类似。

Deprecated及Synthetic属性

值都是布尔类型。Deprecated表示某个类标注@deprecated注解,即不推荐使用。Synthetic代表此字段或方法不是由Java源码直接产生。如果是字段也可能会设置ACC_SYNTHETIC标志位。

StackTableMap属性

在Code属性表中,这个属性会在类加载的字节码验证阶段被新类型检查验证器使用,验证字节码的行为逻辑行为的合法性。

Signature属性

Signature属性负责记录泛型签名信息。Java的泛型采用的是擦除法,字节码Code中泛型信息在编译后都被擦除掉,这样运行时可以节省内存。但无法像C#那样支持真泛型,将泛型类型与用户定义的普通类型同等对待。例如反射无法获得泛型信息。Signature属性就是弥补这一点的。反射获取的泛型类型最终数据也来源于这个属性。结构如下:

类型

名称

数量

u2

attribute_name_index

1

u4

attribute_length

1

u2

signature_index

1

BootstrapMethods属性

用于保持invokedynamic指令引用的引导方法的限定符。该指令的原理见后续。

MethodParameters属性

用于记录方法的各个形参名称和信息。方法参数也会生成在上面的LocalVariableTable中,但这个属性是code的子属下,没有方法体,自然就没有局部变量表。甚至对于接口、抽象方法而言,没有方法体很正常。所以JDK8新增这个属性,可以将方法参数名称写入Class中。编译时使用-parameter参数即可。

模块化相关属性

JDK9具备模块化功能。模块描述文件存储在module-info.java,jdk9扩展了Module、ModulePackages和ModuleMainClass三个属性支持模块化功能。

运行时注解相关属性

存储源码中的注解信息。包括RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations、RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations六个属性,其中RuntimeVisibleAnnotations是最常用的代表属性。该属性中存在annotations表结构,表内有elemet_value_pairs表结构,存储的是键值对,表示注解参数和值。

字节码

字节码表

字节码的长度就是1个字节,因此只有0~255,所以指令集的总操作码不超过256条。

大多数指令都其操作的数据类型信息,通过操作类型前缀+指令构成。

比如iload_1指令,load指令就是把局部变量表中的变量加载入栈,而iload指令就是从局部变量表汇总加载int类型变量入栈,iload_1就是加载局部变量1中加载int变量入栈

其中类型及其前缀对应为:

类型

前缀

类型

前缀

byte

b

float

f

short

s

double

d

int

i

char

c

long

l

reference

a

而全量字节码如下:

可以发现,大部分指令不支持byte、shourt、char,甚至没有指令支持boolean。因为编译器会在编译期或运行期间将shrot、byte类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。对这四类型的数组,也会转换为int类型的字节码操作

扩展是为了让不同位的二进制进行运算,比如1111和11001100进行运算,必须将1111补到8位

  • 零扩展:高位补0,即1111补到00001111。

  • 符合扩展:如果数据是带符号的二进制,即1111是负数,补0变成00001111变成了正数。需要补最高位符号位,即11111111。符号扩展并不影响数据结果,例如0111+1000,补8位是00000111+11111000,运算后还是00000111

字节码指令分类梳理

加载和存储指令

  • 将一个局部变量加载到操作栈: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

其中以<n>结尾代表操作数,例如iload_0、iload_1等,iload_0与操作数为0的iload语义完全一致。

运算指令

  • 加法运算:iadd,ladd,fadd,dadd。

  • 减法运算:isub,lsub,fsub,dsub。

  • 乘法运算:imul,lmul,fmul,dmul。

  • 除法运算:idiv,ldiv,fdiv,ddiv。

  • 求余指令:irem,lrem,frem,drem。

  • 取反指令:imeg,lmeg,fmeg,dmeg。

  • 位移指令:ishl,ishr,iushr,lshl,lshr,lushr。

  • 按位或指令:ior,lor。

  • 按位与指令:iand,land。

  • 按位异或指令:ixor,lxor。

  • 局部变量自增指令:iinc。

  • 比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp。

注:只有在除法指令(idiv,ldiv)和求余指令(irem,lrem)当出现除数为零时会导致虚拟机抛出AirtmeticException异常,其余整形和浮点型运算场景都不会抛出异常

类型转换指令

可以将两种不同数值类型进行相互转换。虚拟机天然支持基本数据类型的宽化类型转换,例如int到long、flost、double等。对于窄化数据类型转化则必须用显示的转换指令,如下:

i2b(int -> boolean)
i2c(int -> char)
i2s(int -> short)
l2i(long -> int)
f2i(float -> int)
f2l(float -> long)
d2i(double -> int)
d2l(double -> long)
d2f(double -> float)
  • int/long 类型窄化转换为整数类型T时,转换过程为丢弃除最低位N(T的数据类型长度)个字节以外的内容。

  • 浮点值窄化转换为整数类型T(int/long)时,向0舍入取整

  • double类型转float类型时,向最近数舍入模式舍入

转换会发生上限溢出、下限溢出、精度丢失,但是不会抛异常

对象创建和访问指令

  • 创建类实例的指令:new

  • 创建数组的指令:newarray、anewarray、multianewarray

  • 访问类字段(static字段)和实例字段(非static字段)的指令:getfield、putfield、getstatic、putstatic

  • 将一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、faload、daload、aaload

  • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、iastore、sastore、fastore、fastore、dastore、aastore

  • 取数组长度的指令:arraylength

  • 检查类实例类型的指令:instanceof、checkcast

操作数栈管理指令

  • 将一个操作数栈的栈顶一个或两个元素出栈:pop、pop2

  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

  • 将栈顶端的两个数值交换:swap

控制转移指令

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

  • 条件分支:ifeq、ifit、ifle、ifgt、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq、if_acmpne

  • 复合条件分支:tableswitch、lookupswitch

  • 无条件分支:gosto、goto_w、jsr、jsr_w、ret

方法调用和返回指令

  • invokevirtua:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)。

  • invokeinterface:用于调用接口方法,它在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

  • invokespecial:用于调用一些需要特殊处理的实例方法,包括实例的初始化方法,私有方法和父类方法。

  • invokestatic:用于调用类方法(static方法)

  • invokedynamic:用于运行时动态解析出调用点限定符所应用的方法,并执行该方法。(前面的分派逻辑都固化在虚拟机内部,而该指令的分派逻辑是由用户自定义)。

  • 方法返回指令:ireture(返回类型是int,short,byte,char,boolean时)、lreturn、freturn、dreturn、areturn,还有一条return供void方法、实例/类/接口的初始化方法使用

0

评论区