JVM(四):类文件结构

Java程序运行在Java虚拟机上,实现平台无关性。
其它语言的应用程序也可以运行在Java虚拟机上,实现语言无关性。
平台无关性和语言无关性的基础就是虚拟机字节码(Class文件)。

Java语言中的各种变量、关键字和运算符号的定义最终都是由多条字节码命令组合而成,因此字节码命令提供的语义描述能力比Java语言本身更强大。

一. Class类文件结构

任何一个Class文件都对应唯一的一个类或接口定义,但类或接口不一定都得定义在文件里(类加载器可直接生成类或接口)。

Class文件是一组以8位字节为基础单位二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。当某个数据项需要占用8位字节以上的空间时,则按照高位在前的方式分隔成若干个8位字节进行存储。

Class文件中定义了两种数据类型:

  • 无符号数
    无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串值,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数。


  • 表用于描述有层次关系的复合结构类型,由多个无符号数或其他表作为数据项构成的复合数据类型,习惯性以“_info”结尾。

1. 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

2. 说明

  • 魔数(magic)

Class文件的前4个字节,用来确定这个文件是否为一个能被虚拟机接受的Class文件。
Java的Class文件魔数为0xCAFEBABE


  • 次版本号(minor_version)、主版本号(major_version)

随JDK发行而不断增加。JDK能向下兼容以前版本的Class文件,但是不能运行之后版本的Class文件。


  • 常量池(constant_pool)

常量池是Class文件中的资源仓库,是占用Class文件空间最大的数据项目之一。

常量池中常量的数量是不固定的,因此需要在常量池的入口处加入一个u2类型的数据来表示常量池中常量个数。
常量池中的第0项特意被空出来不存放常量,当要表达“不引用任何一个常量池项目”时,就可以将索引值置为0来表示了。因此常量池中存放的常量是从第1项开始的,所以常量池中常量个数为constant_pool_count-1。

常量池中存放的常量主要包括:
(1)字面量:文本字符串、声明为final的常量值等。
(2)符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

这些常量都是表,被分别定义为如下14中表结构:

类型 标志 描述
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 动态方法调用点

这些表结构的第一位都是一个u1的标志位,根据此标志位值可以确定该常量的表结构是什么样的,然后才能正确解析。

javap -verbose ClassName可以解析名为ClassName的class文件字节码内容。


  • 访问标志(access_flags)

访问标志用于识别类/接口层次的访问信息,例如这个Class是类还是接口,是否定义为public,是否为abstract类型等。


  • 类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)

Class文件中这三项数据用于确定这个类的继承关系。
类索引:确定这个类的全限定名;
父类索引:确定这个类的父类的全限定名;
索引集合:描述这个类实现了哪些接口;

查找全限定名的过程:


  • 字段表集合(fields)

字段表用于描述接口或类中声明的变量,包括类级变量、实例级变量,但不包括方法内部声明的局部变量。
字段表集合中不会列出从超类或者父接口中继承而来的字段。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

(1)access_flags用于描述字段作用域(public、private、protected)、实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)等信息。
(2)name_index是对常量池的引用,常量池中存放实际的字段的简单名。
(3)descriptor_index也是对常量池的引用,常量池中存放实际的字段的描述符。

描述符:

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型
[ 数组

举例:
java.lang.String[][] 描述为[[Ljava/lang/String;
int[] 描述为[I


  • 方法表集合(methods)

结构与字段表集合类似。
父类方法在子类中没有被重载,那么方法表集合中就没有来自父类的方法信息。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

为什么Java中不能根据返回值不同来重载方法?
由方法表集合结构可知,Java的方法特征签名中只有方法名、参数顺序和参数类型,不包括返回值,所以不能用返回值对方法进行重载。

  • 属性表集合(attributes)

在Class文件中,属性表集合用于描述某些场景专有的信息。
字段表和方法表的最后就是使用属性表来描述一些额外信息的。

属性表结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
自定义 info 1

attribute_name_index是一个指向CONSTANT_Utf8_info型常量的索引,表示属性名。
attribute_length指示了属性值占用的位数。
info是属性值的结构,完全自定义。

举例:
(1)Code属性
Java程序的方法体内的代码经过javac编译后,变为字节码指令存储在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_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index:指向CONSTANT_Utf8_info型常量索引(值固定为“Code”)。
max_stack:操作数栈深度的最大值,该方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行时根据此值分配栈帧中的操作栈深度。
max_locals:局部变量表所需的存储空间。
code:存储Java源程序编译后生成的字节码指令。
exception_table:方法的显式异常处理表。

(2)Exceptions属性
Exceptions属性用于列举出方法中可能抛出的受查异常,即throws关键字后面列举的异常。

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

二、字节码指令

字节码指令 = 操作码 + 操作数
操作码:一个字节长度的、代表某种特定含义的数字;
操作数:跟随在操作码后的零至多个参数。

操作码只有一个字节(0-255),所以最多只有256个操作码。

对于大部分与数据类型相关的字节码指令,操作码助记符都有特殊字符表示:

类型 助记符
int i
long l
short s
byte b
char c
float f
double d
reference a
  • 1. 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
(1)将一个局部变量加载到操作数栈:iload、iload_<n>、lload、lload_<n>等。
(2)将一个数值从操作数栈存储到局部变量表:istore、istore_<n>等。
(3)将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w等。
(4)扩充局部变量表的访问索引:wide。

注:iload_<n>是指在操作码后直接加上了操作数,省去了取操作数的动作。
例如:iload_0 等价于操作数为0时的iload指令。

  • 2. 运算指令

运算或算术指令用于对操作数栈上的两个值进行某种特定运算,并把结果重新存入到操作数栈顶。
(1)加法指令:iadd、ladd、fadd、dadd。
(2)减法指令:isub、lsub、fsub、dsub。
(3)乘法指令:imul、lmul、fmul、dmul。
(4)除法指令:idiv、ldiv、fdiv、ddiv。
(5)求余指令:irem、lrem、frem、drem。
(6)取反指令:ineg、lneg、fneg、dneg。
(7)位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
(8)按位或指令:ior、lor。
(9)按位与指令:iand、land。
(10)按位异或指令:ixor、lxor。
(11)局部变量自增指令:iinc。
(12)比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

  • 3. 类型转换指令

类型转换指令可以将两种不同的数值类型进行互相转换。
Java虚拟机直接支持宽化类型转换,无需显式的转换指令:
int --> long、float、double
long --> float、double
float --> double

对于窄化类型转换,需要显式使用转换指令:
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。

  • 4. 对象创建和访问指令

(1)创建类实例指令:new。
(2)创建数组指令:newarray、anewarray、multianewarray。
(3)访问类字段和实例字段:getfield、putfield、getstatic、putstatic
(4)把一个数组元素加载到操作数栈的指令:baload、caload、saload等。
(5)将一个操作数栈的值存储到数组元素中:bastore、castore、sastore等。
(6)取数组长度的指令:arraylength。
(7)检查类实例类型的指令:instanceof、checkcast。

  • 5. 操作数栈管理指令

(1)将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
(2)复制栈顶一个或两个元素并将复制值重新压入栈顶:dup、dup2、dup_x1等。
(3)将栈最顶端的两个数值互换:swap。

  • 6. 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件的去指定的指令位置处执行程序,而不是默认的在下一条指令位置处执行程序。

概念模型上来说,控制转移指令就是有条件或无条件的修改PC(Program Counter)寄存器的值。

(1)条件分支:ifeq、iflt、ifle、ifne等。
(2)复合条件分支:tableswitch、lookupswitch。
(3)无条件分支:goto、goto_w、jsr、jsr_w、ret。

  • 7. 方法调用和返回指令

(1)invokevirtual:调用对象的实例方法,根据对象的实际类型进行分派。
(2)invokeinterface:调用接口方法,运行时搜索实现了这个接口方法的对象,找出合适的方法调用。
(3)invokespecial:调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
(4)invokestatic:调用类方法。
(5)invokedynamic:在运行时动态解析出调用点限定符所引用的方法。

  • 8. 异常处理指令

Java程序中显示抛出异常的操作(throw)都是有athrow指令实现的。

  • 9. 同步指令

Java虚拟机支持方法级的同步和方法内部一段指令序列的同步。

方法级同步无须通过字节码指令控制,虚拟机可以从方法表的ACC_SYNCHRONIZED访问标志得知该方法是否为同步方法,如果设置了ACC_SYNCHRONIZED标志,则执行线程要求先成功持有管程(Monitor),管程同时只能被一个线程持有,然后才能执行方法,方法完成(无论是正常完成还是非正常完成)后释放管程。

同步一段指令集需要使用monitorenter和monitorexit两条指令完成。编译器必须确保无论这个方法是正常结束还是异常结束,调用过的每条monitorenter指令必须执行对应的monitorexit指令。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341

推荐阅读更多精彩内容