1. 类文件结构
package com.company;
public class Test{
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Test.java 代码编译后,得到的 Test.class 文件内容如下:
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 .......8."......
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
00000060: 0004 7468 6973 0100 124c 636f 6d2f 636f ..this...Lcom/co
00000070: 6d70 616e 792f 5465 7374 3b01 0004 6d61 mpany/Test;...ma
00000080: 696e 0100 1628 5b4c 6a61 7661 2f6c 616e in...([Ljava/lan
00000090: 672f 5374 7269 6e67 3b29 5601 0004 6172 g/String;)V...ar
000000a0: 6773 0100 135b 4c6a 6176 612f 6c61 6e67 gs...[Ljava/lang
000000b0: 2f53 7472 696e 673b 0100 0a53 6f75 7263 /String;...Sourc
000000c0: 6546 696c 6501 0009 5465 7374 2e6a 6176 eFile...Test.jav
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a...............
000000e0: 0c48 656c 6c6f 2057 6f72 6c64 2107 001f .Hello World!...
000000f0: 0c00 2000 2101 0010 636f 6d2f 636f 6d70 .. .!...com/comp
00000100: 616e 792f 5465 7374 0100 106a 6176 612f any/Test...java/
00000110: 6c61 6e67 2f4f 626a 6563 7401 0010 6a61 lang/Object...ja
00000120: 7661 2f6c 616e 672f 5379 7374 656d 0100 va/lang/System..
00000130: 036f 7574 0100 154c 6a61 7661 2f69 6f2f .out...Ljava/io/
00000140: 5072 696e 7453 7472 6561 6d3b 0100 136a PrintStream;...j
00000150: 6176 612f 696f 2f50 7269 6e74 5374 7265 ava/io/PrintStre
00000160: 616d 0100 0770 7269 6e74 6c6e 0100 1528 am...println...(
00000170: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e Ljava/lang/Strin
00000180: 673b 2956 0021 0005 0006 0000 0000 0002 g;)V.!..........
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 ............./..
000001a0: 0001 0000 0005 2ab7 0001 b100 0000 0200 ......*.........
000001b0: 0a00 0000 0600 0100 0000 0300 0b00 0000 ................
000001c0: 0c00 0100 0000 0500 0c00 0d00 0000 0900 ................
000001d0: 0e00 0f00 0100 0900 0000 3700 0200 0100 ..........7.....
000001e0: 0000 09b2 0002 1203 b600 04b1 0000 0002 ................
000001f0: 000a 0000 000a 0002 0000 0006 0008 0007 ................
00000200: 000b 0000 000c 0001 0000 0009 0010 0011 ................
00000210: 0000 0001 0012 0000 0002 0013 0a .............
根据 JVM 规范,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 |
1.1. 魔数
【magic】:魔数(第1~4字节)。
唯一作用是判断该文件是否为一个能被虚拟机接受的 Class 文件。值固定为 0xcafebabe
。
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 …8."…
1.2. 文件版本
【minor_version】:副版本号(第5~6字节)。
【major_version】:主版本号(第7~8字节)。
主副版本号共同构成了 Class 文件的格式版本号,高版本的虚拟机支持低版本的编译器编译的 Class 文件结构,反之则不行。
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 …8."…
1.3. 常量池
【constant_pool_count】:常量池计数器(第8~9字节),值等于 constant_pool 表中的成员数加 1。
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 …8."…
0022
十进制为34,表示常量池有#1~#33项,#0为空。
【constant_pool】:常量池。
表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的。(0 表示不引用常量池的任一项)
常量类型 | 值 |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 …8."…
第#1项:0a
表示 Method 信息,00 06
(6)和 00 14
(20),表示引用了常量池中#6和#20项来获得这个方法的所属类和方法名。
00000000: cafe babe 0000 0038 0022 0a00 0600 1409 …8."…
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 …
第#2项:09
表示 Field 信息,00 15
(21)和 00 16
(22),表示引用了常量池中#21和#22项来获得这个成员变量的所属类和成员变量名。
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 …
第#3项:08
表示 String 信息,00 17
(23),表示引用了常量池中#23项。
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 …
第#4项:0a
表示 Method 信息,00 18
(20)和 00 19
(21),表示引用了常量池中#20和#21项来获得这个成员变量的所属类和方法名。
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 …
第#5项:07
表示 Class 信息,00 1a
(26),表示引用了常量池中#26项。
00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 …
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 ……()
第#6项:07
表示 Class 信息,00 1b
(27),表示引用了常量池中#27项。
00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 ……()
第#7项:01
表示 Utf8 串,00 06
(6)表示长度,3c 696e 6974 3e
是字符串 <init>
。
00000020: 001b 0100 063c 696e 6974 3e**01 0003 2829 ** ……()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V…Code…LineN
第#8项:01
表示 Utf8 串,00 03
(3)表示长度,2829 56
是字符串 ()V
,表示无参无返回值。
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V…Code…LineN
第#9项:01
表示 Utf8 串,00 04
(4)表示长度,436f 6465
是字符串 Code
。
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V…Code…LineN
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable…Loc
第#10项:01
表示 Utf8 串,00 0f
(15)表示长度,4c 696e 654e 756d 6265 7254 6162 6c65
是字符串 LineNumberTable
。
00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable…Loc
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
第#11项:01
表示 Utf8 串,00 12
(18)表示长度,4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65
是字符串 LocalVariableTable
。
00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
00000060: 0004 7468 6973 0100 124c 636f 6d2f 636f …this…Lcom/co
第#12项:01
表示 Utf8 串,00 04
(4)表示长度,7468 6973
是字符串 this
。
00000060: 0004 7468 6973 0100 124c 636f 6d2f 636f …this…Lcom/co
00000070: 6d70 616e 792f 5465 7374 3b01 0004 6d61 mpany/Test;…ma
第#13项:01
表示 Utf8 串,00 12
(18)表示长度,4c 636f 6d2f 636f 6d70 616e 792f 5465 7374 3b
是字符串 Lcom/company/Test;
。
00000070: 6d70 616e 792f 5465 7374 3b01 0004 6d61 mpany/Test;…ma
00000080: 696e 0100 1628 5b4c 6a61 7661 2f6c 616e in…([Ljava/lan
第#14项:01
表示 Utf8 串,00 04
(4)表示长度,6d61 696e
是字符串 main
。
00000080: 696e 0100 1628 5b4c 6a61 7661 2f6c 616e in…([Ljava/lan
00000090: 672f 5374 7269 6e67 3b29 5601 0004 6172 g/String;)V…ar
第#15项:01
表示 Utf8 串,00 16
(22)表示长度,28 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56
是字符串 ([Ljava/lang/String;)V
,其实就是参数为字符串数组,无返回值。
00000090: 672f 5374 7269 6e67 3b29 5601 0004 6172 g/String;)V…ar
000000a0: 6773 0100 135b 4c6a 6176 612f 6c61 6e67 gs…[Ljava/lang
第#16项:01
表示 Utf8 串,00 04
(4)表示长度,6172 6773
是字符串 args
。
000000a0: 6773 0100 135b 4c6a 6176 612f 6c61 6e67 gs…[Ljava/lang
000000b0: 2f53 7472 696e 673b 0100 0a53 6f75 7263 /String;…Sourc
第#17项:01
表示 Utf8 串,00 13
(19)表示长度,5b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b
是字符串 [Ljava/lang/String;
。
000000b0: 2f53 7472 696e 673b 0100 0a53 6f75 7263 /String;…Sourc
000000c0: 6546 696c 6501 0009 5465 7374 2e6a 6176 eFile…Test.jav
第#18项:01
表示 Utf8 串,00 0a
(10)表示长度,53 6f75 7263 6546 696c 65
是字符串 SourceFile
。
000000c0: 6546 696c 6501 0009 5465 7374 2e6a 6176 eFile…Test.jav
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a…
第#19项:01
表示 Utf8 串,00 09
(9)表示长度,5465 7374 2e6a 6176 61
是字符串 Test.java
。
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a…
第#20项:0c
表示 NameAndType 信息,00 07
(7)和 00 08
(8),表示引用了常量池中#7和#8项。
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a…
第#21项:07
表示 Class 信息,00 1c
(30),表示引用了常量池中#30项。
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a…
第#22项:0c
表示 NameAndType 信息,00 1d
(29)和 00 1e
(30),表示引用了常量池中#29和#30项。
000000d0: 610c 0007 0008 0700 1c0c 001d 001e 0100 a…
000000e0: 0c48 656c 6c6f 2057 6f72 6c64 2107 001f .Hello World!..
第#23项:01
表示 Utf8 串,00 0c
(12)表示长度,48 656c 6c6f 2057 6f72 6c64 21
是字符串 Hello World!
。
000000e0: 0c48 656c 6c6f 2057 6f72 6c64 2107 001f .Hello World!..
第#24项:07
表示 Class 信息,00 1f
(31),表示引用了常量池中#31项。
000000f0: 0c00 2000 2101 0010 636f 6d2f 636f 6d70 … .!..com/comp
第#25项:0c
表示 NameAndType 信息,00 20
(32)和 00 21
(33),表示引用了常量池中#32和#33项。
000000f0: 0c00 2000 2101 0010 636f 6d2f 636f 6d70 … .!..com/comp
00000100: 616e 792f 5465 7374 0100 106a 6176 612f any/Test…java/
第#26项:01
表示 Utf8 串,00 10
(16)表示长度,636f 6d2f 636f 6d70 616e 792f 5465 7374
是字符串 com/company/Test
。
00000100: 616e 792f 5465 7374 0100 106a 6176 612f any/Test…java/
00000110: 6c61 6e67 2f4f 626a 6563 7401 0010 6a61 lang/Object…ja
第#27项:01
表示 Utf8 串,00 10
(16)表示长度,6a 6176 612f 6c61 6e67 2f4f 626a 6563 74
是字符串 java/lang/Object
。
00000110: 6c61 6e67 2f4f 626a 6563 7401 0010 6a61 lang/Object…ja
00000120: 7661 2f6c 616e 672f 5379 7374 656d 0100 va/lang/System…
第#28项:01
表示 Utf8 串,00 10
(16)表示长度,6a61 7661 2f6c 616e 672f 5379 7374 656d
是字符串 java/lang/System
。
00000120: 7661 2f6c 616e 672f 5379 7374 656d 0100 va/lang/System…
00000130: 036f 7574 0100 154c 6a61 7661 2f69 6f2f .out…Ljava/io/
第#29项:01
表示 Utf8 串,00 03
(3)表示长度,6f 7574
是字符串 out
。
00000130: 036f 7574 0100 154c 6a61 7661 2f69 6f2f .out…Ljava/io/
00000140: 5072 696e 7453 7472 6561 6d3b 0100 136a PrintStream;…j
第#30项:01
表示 Utf8 串,00 15
(21)表示长度,4c 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d3b
是字符串 Ljava/io/PrintStream;
。
00000140: 5072 696e 7453 7472 6561 6d3b 0100 136a PrintStream;…j
00000150: 6176 612f 696f 2f50 7269 6e74 5374 7265 ava/io/PrintStre
00000160: 616d 0100 0770 7269 6e74 6c6e 0100 1528 am…println…(
第#31项:01
表示 Utf8 串,00 13
(19)表示长度,6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d
是字符串 java/io/PrintStream
。
00000160: 616d 0100 0770 7269 6e74 6c6e 0100 1528 am…println…(
第#32项:01
表示 Utf8 串,00 07
(7)表示长度,70 7269 6e74 6c6e
是字符串 println
。
00000160: 616d 0100 0770 7269 6e74 6c6e 0100 1528 am…println…(
00000170: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e Ljava/lang/Strin
00000180: 673b 2956 0021 0005 0006 0000 0000 0002 g;)V.!..
第#33项:01
表示 Utf8 串,00 15
(21)表示长度,28 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956
是字符串 (Ljava/lang/String;)V
。
1.4. 访问标识与继承信息
用于识别一些类或者接口层次的访问信息:
标记名 | 值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 类,可以被包的类外访问。 |
ACC_FINAL | 0x0010 | final 类,不允许有子类。 |
ACC_SUPER | 0x0020 | 当用到 invokespecial 指令时,需要特殊处理的父类方法。 |
ACC_INTERFACE | 0x0200 | 标识定义的是 interface 而不是 class。 |
ACC_ABSTRACT | 0x0400 | abstract 类,不能被实例化。 |
ACC_SYNTHETIC | 0x1000 | 标识并非 Java 源码生成的代码。 |
ACC_ANNOTATION | 0x2000 | 标识注解类型 |
ACC_ENUM | 0x4000 | 标识枚举类型 |
【access_flags】:在常量池结束之后,紧接着的两个字节代表访问标志。
00000180: 673b 2956 0021 0005 0006 0000 0000 0002
0021
表示该 class 是一个类,且访问权限为 public。
【this_class】:类索引,用于确定本类的全限定名。
0005
(5) 表示在常量池中#5代表本类全限定名。
【super_class】:父类索引,用于确定父类的全限定名。
0006
(6) 表示在常量池中#6代表父类全限定名。
【interfaces_count】:接口数量。
0000
表示本类的接口数量(数量为0)。
【interfaces】:接口索引集合,接口列表。
1.5. 字段信息
【fields_count】:接口或类中声明的变量的数量。
00000180: 673b 2956 0021 0005 0006 0000 0000 0002
0000
表示本类的字段数量(数量为0)。
【fields】:用于描述接口或类中声明的变量。包括静态变量和成员变量,但不包括方法局部变量。
一个字段的信息包括:作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。这些修饰符都是布尔值,要么有,要么没有。
字段结构如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
字段 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_SYNTHETIC | 0x1000 | 表示字段由编译器自动产生。 |
ACC_ENUM | 0x4000 | enum,表示字段为枚举类型 |
数据类型:
字符 | 类型 | 含义 |
---|---|---|
B | byte | 有符号字节型数 |
C | char | Unicode 字符, UTF-16 编码 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 整型数 |
J | long | 长整数 |
S | short | 有符号短整数 |
Z | boolean | 布尔值 true/false |
L Classname; | reference | 一个名为 Classname 的实例 |
[ | reference | 一个一维数组 |
- 对于数组类型,每一个维度用一个前置的
[
字符来描述,如定义个int[][]
类型的二维数组,记录为:[[I
。 - 用描述符来描述方法时,按照先参数列表后返回值的顺序描述。参数裂变按照参数顺序放在
()
内,如方法void login()
描述符为()V
,方法java.lang.String toString()
的描述符为()Ljava.lang.String
。
1.6. 方法信息
如果子类没有覆写父类方法,则方法列表中就不会有父类方法。但由编译器自动添加的方法(类构造器 <clinit>
和实例构造器 <init>
)就有可能会出现。
【methods_count】:方法数量。
00000180: 673b 2956 0021 0005 0006 0000 0000 0002
0002
表示本类的方法数量(数量为2),默认构造方法,和 main
方法。
【methods】:方法列表,其中每一个方法信息又由多个部分组成。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法 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_BRIDGE | 0x0040 | bridge,方法由编译器产生 |
ACC_VARARGS | 0x0080 | 表示方法带有变长参数 |
ACC_NATIVE | 0x0100 | native,方法引用非 java 语言的本地方法 |
ACC_ABSTRACT | 0x0400 | abstract,方法没有具体实现 |
ACC_STRICT | 0x0800 | strictfp,方法使用 FP-strict 浮点格式 |
ACC_SYNTHETIC | 0x1000 | 方法在源文件中不出现,由编译器产生 |
【<init>
构造方法】
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 …/…
0001
表示方法的访问修饰为 public
。
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 …/…
0007
(7)表示方法的名称引用了常量池中#7。
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 …/…
0008
(8)表示方法的参数信息引用了常量池中#8。
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 …/…
0001
(1)表示方法的属性数量为1。
00000190: 0001 0007 0008 0001 0009 0000 002f 0001 …/…
000001a0: 0001 0000 0005 2ab7 0001 b100 0000 0200 …*…
000001b0: 0a00 0000 0600 0100 0000 0300 0b00 0000 …
000001c0: 0c00 0100 0000 0500 0c00 0d00 0000 0900 …
这段代表方法的属性信息:
0009
(9)表示引用了常量池#9项,即Code
属性。0000 002f
(47)表示此属性的长度是47。0001
表示操作数栈最大深度。0001
表示局部变量表最大槽数(slot)。0000 0005
(5)表示字节码长度是5。2ab7 0001 b1
是方法内的字节码指令。00 0000 02
(2)表示方法细节属性数量是2。00 0a
(10)表示引用了常量池#10项,即LineNumberTable
属性。00 0000 06
(6)表示此属性的长度是6。00 01
(1)表示 LineNumberTable 的长度是1。00 00
(0)表示字节码行号为0,对应00 03
(3)表示 java 源码行号为3。
00 0b
(11)表示引用了常量池#11项,即LocalVariableTable
属性。00 0000 0c
(12)表示此属性的长度是12。00 01
(1)表示LocalVariableTable
的长度是1。00 00
(0)表示局部变量生命周期开始,相对于字节码的偏移量。00 05
(5)表示局部变量覆盖的范围长度为5。00 0c
(12)表示局部变量名称,引用了常量池#12项,即this
。00 0d
(13)表示局部变量的类型,引用了常量池#13项,即Lcom/company/Test;
。00 00
(0)表示局部变量占有的槽位编号(slot)是0。
【<mian>
主方法】
000001c0: 0c00 0100 0000 0500 0c00 0d00 0000 0900 …
00 09
表示方法的访问修饰为 public static
。
000001c0: 0c00 0100 0000 0500 0c00 0d00 0000 0900 …
000001d0: 0e00 0f00 0100 0900 0000 3700 0200 0100 …7…
00 0e
(14)表示方法的名称引用了常量池中#14。
000001d0: 0e00 0f00 0100 0900 0000 3700 0200 0100 …7…
00 0f
(15)表示方法的参数信息引用了常量池中#15。
000001d0: 0e00 0f00 0100 0900 0000 3700 0200 0100 …7…
0001
(1)表示方法的属性数量为1。
000001d0: 0e00 0f00 0100 0900 0000 3700 0200 0100 …7…
000001e0: 0000 09b2 0002 1203 b600 04b1 0000 0002 …
000001f0: 000a 0000 000a 0002 0000 0006 0008 0007 …
00000200: 000b 0000 000c 0001 0000 0009 0010 0011 …
00000210: 0000 0001 0012 0000 0002 0013 0a …
这段代表方法的属性信息:
00 09
(9)表示引用了常量池#9项,即Code
属性。00 0000 37
(55)表示此属性的长度是55。00 02
表示操作数栈最大深度。00 01
表示局部变量表最大槽数(slot)。00 0000 09
(9)表示字节码长度是9。b2 0002 1203 b600 04b1
是方法内的字节码指令。0000 0002
(2)表示方法细节属性数量是2。000a
(10)表示引用了常量池#10项,即LineNumberTable
属性。0000 000a
(10)表示此属性的长度是10。0002
(2)表示 LineNumberTable 的长度是2。0000
(0)表示字节码行号为0,对应0006
(6)表示 java 源码行号为6。0008
(8)表示字节码行号为8,对应0007
(7)表示 java 源码行号为7。
000b
(11)表示引用了常量池#11项,即LocalVariableTable
属性。0000 000c
(12)表示此属性的长度是12。0001
(1)表示LocalVariableTable
的长度是1。0000
(0)表示局部变量生命周期开始,相对于字节码的偏移量。0009
(9)表示局部变量覆盖的范围长度为5。0010
(16)表示局部变量名称,引用了常量池#16项,即args
。0011
(17)表示局部变量的类型,引用了常量池#17项,即[Ljava/lang/String;
。0000
(0)表示局部变量占有的槽位编号(slot)是0。
1.7. 属性信息
Class 文件、字段信息、方法信息中都可以携带自己的属性信息,以用于描述某些场景专有的信息。 Java 虚拟机运行时会忽略掉它不认识的属性。
【attributes_count】:附加属性的数量。
00000210: 0000 0001 0012 0000 0002 0013 0a …
0001
(1)表示附加属性的数量是1。
【attributes】:属性列表,其中每一个属性信息又由多个部分组成。
属性的通用格式:
attribute_info {
u2 attribute_name_index; //属性名索引
u4 attribute_length; //属性长度
u1 info[attribute_length]; //属性的具体内容
}
00000210: 0000 0001 0012 0000 0002 0013 0a …
0012
(18)表示引用了常量池#18项,即 SourceFile
。
00000210: 0000 0001 0012 0000 0002 0013 0a …
0000 0002
(2)表示此属性的长度是2。
00000210: 0000 0001 0012 0000 0002 0013 0a …
0013
(19)表示引用了常量池#19项,即 Test.java
。
2. 字节码指令
2.1. 入门
public com.company.Test();
构造方法的字节码指令为 2ab7 0001 b1
2a
:aload_0
,加载 slot 0 的局部变量,即this
。b7
:invokespecial
,预备调用构造方法,即.
。0001
:引用常量池#1项,即Method java/lang/Object."<init>":()V
,表示Object
类的构造方法。b1
:return
,表示方法返回。
public static void main(java/lang/String[] args);
主方法的字节码指令为 b2 0002 1203 b600 04b1
-
b2
:getstatic
,加载静态变量。 -
0002
:引用常量池#2项,即Field java/lang/System.out:Ljava/io/PrintStream;
。 -
12
:ldc
,加载参数。 -
03
:引用常量池#3项,即String HelloWorld!
。 -
b6
:invokespecial
,预备调用成员方法,即.
。 -
00 04
:引用常量池#4项,即Method java/io/PrintStream.println:(Ljava/lang/String;)V
。 -
b1
:return
,表示方法返回。
2.2. Javap 工具
原始 java 代码:
package com.company;
public class Test {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
编译后的 class 代码,再使用JDK 的 javap 工具反编译 class文件:javap -v Test.class
Classfile /.../com/company/Test.class
Last modified 2021年2月23日; size 598 bytes
MD5 checksum 0d028b42346027580fe220acb3a33c3f
Compiled from "Test.java"
public class com.company.Test
minor version: 0
major version: 56
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #6 // com/company/Test
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // com/company/Test
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/company/Test;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Test.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/company/Test
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.company.Test();
descriptor: ()V
flags: (0x0001) 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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/company/Test;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10 // 【数字较小,就和字节码指令在一起】
2: istore_1
3: ldc #3 // 【int 32768 数字较大,就存放在常量池,32768=32767+1】
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 5: 0
line 6: 3
line 7: 6
line 8: 10
line 9: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Test.java"
2.3. 方法执行流程
Class 加载过程中,class 文件常量池(Constant pool)会进入运行时常量池,方法的代码(Code)会进入方法区。
【main 方法执行流程】
- 主线程启动:调用 main 方法是产生一个栈帧,包含局部变量表(LocalVariableTable)和操作数栈。
bipush 10
:将一个 byte 压入操作数栈(会补齐4个字节),类似的:ldc
将一个 int 压入操作数栈;ldc2_w
将一个 long 压入操作数栈(要分两次)。istore_1
:将操作数栈顶数据弹出,存入局部变量表的 slot 1。即对 a 赋值:a = 10
。ldc #3
:从运行时常量池中加载#3项的数据到操作数栈。istore_2
:将操作数栈顶数据弹出,存入局部变量表的 slot 2。即对 b 赋值:b = 32768
。iload_1
:从局部变量表的 slot 1 读取数据,并压入操作数栈。即读取 a 的值:10
。iload_2
:从局部变量表的 slot 2 读取数据,并压入操作数栈。即读取 b 的值:32768
。iadd
:弹出操作数栈中的两个变量,执行加法运算,并把结果(32778
)压入操作数栈。istore_3
:将操作数栈顶数据弹出,存入局部变量表的 slot 3。即对 c 赋值:c = 32778
。getstatic #4
:从运行时常量池#4项中找到成员变量的引用,在从堆中根据引用找到对象地址,并压入操作数栈。即System.out
对象。iload_3
:从局部变量表的 slot 3 读取数据,并压入操作数栈。即读取 c 的值:32778
。invokevirtual #5
:从运行时常量池#5项中找到方法,并定位到方法区中的该方法,创建新的栈帧,并传递参数,即System.out.println(c);
。return
:方法运行完毕,弹出栈帧,程序结束。
2.4. 条件判断指令
指令码 | 操作码(助记符) | 描述 |
---|---|---|
0x99 | ifeq | 若栈顶 int 类型值为 0 则跳转。 |
0x9a | ifne | 若栈顶 int 类型值不为 0 则跳转。 |
0x9b | iflt | 若栈顶 int 类型值小于 0 则跳转。 |
0x9e | ifle | 若栈顶 int 类型值小于等于 0 则跳转。 |
0x9d | ifgt | 若栈顶 int 类型值大于 0 则跳转。 |
0x9c | ifge | 若栈顶 int 类型值大于等于 0 则跳转。 |
0x9f | if_icmpeq | 若栈顶两 int 类型值相等则跳转。 |
0xa0 | if_icmpne | 若栈顶两 int 类型值不相等则跳转。 |
0xa1 | if_icmplt | 若栈顶两 int 类型值前小于后则跳转。 |
0xa4 | if_icmple | 若栈顶两 int 类型值前小于等于后则跳转。 |
0xa3 | if_icmpgt | 若栈顶两 int 类型值前大于后则跳转。 |
0xa2 | if_icmpge | 若栈顶两 int 类型值前大于等于后则跳转。 |
0xa5 | if_acmpeq | 若栈顶两引用类型值相等则跳转。 |
0xa6 | if_acmpne | 若栈顶两引用类型值不相等则跳转。 |
java 源码:
package com.company;
public class Test {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}
class 字节码指令部分:
0: iconst_0 // 【创建一个常量0。注意-1~5的数字就创建常量】
1: istore_1 // 【存放到局部变量表槽位1】
2: iload_1 // 【读取局部变量表槽位1的值,并压入操作数栈】
3: ifne 12 // 【比较栈顶元素,如果不为0,则跳转执行第12行指令】
6: bipush 10 // 【把10压入操作数栈】
8: istore_1 // 【把栈顶元素10,存放到局部变量表槽位1】
9: goto 15 // 【跳转执行第15行指令】
12: bipush 20 // 【把20压入操作数栈】
14: istore_1 // 【把栈顶元素20,存放到局部变量表槽位1】
15: return // 【程序运行结束】
2.5. 循环控制指令
循环控制指令就是条件判断指令 + 无条件跳转指令组成。
java 源码:
package com.company;
public class Test {
public static void main(String[] args) {
int a = 0;
while (a < 0) {
a++;
}
}
}
class 字节码指令部分:
0: iconst_0 // 【创建一个常量0。注意-1~5的数字就创建常量】
1: istore_1 // 【存放到局部变量表槽位1】
2: iload_1 // 【读取局部变量表槽位1的值,并压入操作数栈】
3: bipush 10 // 【把10压入操作数栈】
5: if_icmpge 14 // 【比较栈顶的两个元素,前大于等于后,则跳转执行第14行指令】
8: iinc 1, 1 // 【自增。局部变量表槽位1的元素直接增加1】
11: goto 2 // 【跳转执行第2行指令,产生循环】
14: return // 【程序运行结束】
2.6. 为什么 x=x++ 无效
package com.company;
public class Test {
public static void main(String[] args) {
int x = 0;
x = x++;
System.out.println(x); // 最终x=0
}
}
class 字节码指令部分:
0: iconst_0 // 【创建一个常量0。注意-1~5的数字就创建常量】
1: istore_1 // 【存放到局部变量表槽位1】
2: iload_1 // 【读取局部变量表槽位1的值,并压入操作数栈】
3: iinc 1, 1 // 【自增。局部变量表槽位1的元素直接增加1】
6: istore_1 // 【把栈顶元素0,存放到局部变量表槽位1】
7: return // 【程序运行结束】
由字节码指令可以看出,x = x++
的运行步骤为:
- 局部变量表
槽位1
存放数字0
。(即int x = 0;
) - 局部变量表
槽位1
中的数字复制一份到操作数栈中。(即x = x++;
,等号右边的x
的读取过程) - 局部变量表
槽位1
存放的数字0
自增为1
。(即++
,重点:iinc
指令是直接修改局部变量表中的值) - 操作数栈中的数字
0
存放到局部变量表槽位1
。(即x = x++;
中的=
赋值操作)
经过以上4个步骤,局部变量表槽位1
中的数字由0
自增到1
之后,又被覆盖成了0
,使得最终读取的时候,还是最初的值0
。
【延伸问题】
从字节码的角度,比较 x++
、++x
、x += 1
、x = x+1
的效率。
正确答案,在效率上: x++
= ++x
= x += 1
> x = x+1
。
x++
、++x
、x += 1
编译后的字节码相同:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: return
他们都是直接操作局部变量表,只需要一条字节码指令 iinc 1, 1
。此外,可以将 x++
或 ++x
看成是 x += 1
的特例(自增量为1)的进一步简写。
x = x+1
编译后的字节码:
0: iconst_0
1: istore_1
2: iload_1 // 读取局部变量表中的x的值到操作数栈
3: iconst_1 // 准备一个常量1
4: iadd // 让栈顶的x和1相加
5: istore_1 // 将栈顶的x移动并覆盖到局部变量表中的x
6: return
这种方式明显比上面的方式要麻烦一些,因此效率相对较低。
2.7. 构造方法
2.7.1. <cinit>()V 方法
public class Test {
static int i = 0;
static {
i = 1;
}
static {
i = 2;
}
}
编译器会按从上至下的顺序,收集所有 static
静态代码块和静态成员赋值的代码,合并成一个特殊的方法 <cinit>()V
。
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
<cinit>()V
方法会在类的初始化阶段被调用。
2.7.2. <init>()V 方法
public class Test {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Test(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Test test = new Test("s3", 30);
System.out.println(test.a); // s3
System.out.println(test.b); // 30
}
}
编译器会按从上至下的顺序,收集所有 {}
代码块和成员赋值的代码,合并成一个新的构造方法 <init>()V
,并且原始构造方法方法内的代码总会放到最后。
public com.company.Test(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Lcom/company/Test;
0 39 1 a Ljava/lang/String;
0 39 2 b I
2.8. 方法调用
Java 源代码:
package com.company;
public class Test {
public Test() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Test test = new Test();
test.test1();
test.test2();
test.test3();
test.test4();
Test.test4();
}
}
class 字节码指令部分:
0: new #2 // class com/company/Test 在堆中分配对象所需要的内存,并把引用压入操作数栈
3: dup // 把栈顶地址复制一份,并把引用压入操作数栈
4: invokespecial #3 // Method "<init>":()V 栈顶对象调用构造方法创建对象(调用会被消耗,所以复制)
7: astore_1 // 把栈顶的对象引用弹出,保存到局部变量表槽位1中
8: aload_1 // 读取局部变量表槽位1的值,并压入操作数栈
9: invokevirtual #4 // Method test1:()V 栈顶对象调用方法test1
12: aload_1 // 读取局部变量表槽位1的值,并压入操作数栈
13: invokevirtual #5 // Method test2:()V 栈顶对象调用方法test2
16: aload_1 // 读取局部变量表槽位1的值,并压入操作数栈
17: invokevirtual #6 // Method test3:()V 栈顶对象调用方法test3
20: aload_1 // 读取局部变量表槽位1的值,并压入操作数栈
21: pop // 把栈顶的对象引用弹出
22: invokestatic #7 // Method test4:()V 直接调用静态方法test4
25: invokestatic #7 // Method test4:()V 直接调用静态方法test4
28: return
2.9. 异常处理
2.9.1. try-catch-finally
public class Test {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
class 字节码部分:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 【i=0】
-------------------------------try
2: bipush 10
4: istore_1 // 【i=10】
-------------------------------finally
5: bipush 30
7: istore_1 // 【i=30】
8: goto 27
-------------------------------catch
11: astore_2
12: bipush 20
14: istore_1 // 【i=20】
-------------------------------finally
15: bipush 30
17: istore_1 // 【i=30】
18: goto 27
21: astore_3 // 【没有名字的局部变量表槽位3】
-------------------------------finally
22: bipush 30
24: istore_1 // 【i=30】
-------------------------------error
25: aload_3
26: athrow // 【抛出Error】
27: return
Exception table:
// 【监测字节码第[from,to)行的运行,抛出异常后跳转至target行】
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 【监测try块的其他异常类型,比如Error】
11 15 21 any // 【监测catch块的其他异常类型,比如Error】
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
2.9.2. finally 出现 return
public class Test {
public static void main(String[] args) {
System.out.println(test()); // 20
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
class 字节码部分:
public static int test();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
-------------------------------try
0: bipush 10 // 【10放入栈顶】
2: istore_0 // 【10从栈移除,放入槽位0】
-------------------------------finally
3: bipush 20 // 【20放入栈顶】
5: ireturn // 【💡返回栈顶的20】
-------------------------------catch
6: astore_1 // 【把异常对象放入槽位1】
-------------------------------finally
7: bipush 20 // 【20放入栈顶】
9: ireturn // 【返回栈顶的20】
Exception table:
from to target type
0 3 6 any
finally
中的 ireturn
指令被出入到了所有的可能流程,所以结果肯定是以 finally
为准。
2.9.3. finally 对返回值的影响
public class Test {
public static void main(String[] args) {
System.out.println(test()); // 10
}
public static int test() {
int i = 10;
try {
// return之前先暂存,待finally执行完毕后,再返回暂存值
return i;
} finally {
i = 20;
}
}
}
class 字节码部分:
public static int test();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // 【10放入栈顶】
2: istore_0 // 【10从栈移除,放入槽位0】
-------------------------------try
3: iload_0 // 【10从槽位0读取,放入栈顶】
4: istore_1 // 【10从栈移除,放入槽位1。暂存,固定返回值】
-------------------------------finally
5: bipush 20 // 【20放入栈顶】
7: istore_0 // 【20从栈移除,放入槽位0】
-------------------------------return
8: iload_1 // 【10从槽位1读取,放入栈顶】
9: ireturn // 【💡返回栈顶的10】
-------------------------------catch
10: astore_2 // 【把异常对象放入槽位2】
-------------------------------finally
11: bipush 20
13: istore_0
-------------------------------error
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
2.10. synchronized
synchronized 必须要保证,加锁之后,如果抛出异常,也要正确解锁。
public class Test {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("OK");
}
}
}
class 字节码部分:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // 【在堆中分配对象所需要的内存,并把引用压入操作数栈】
3: dup // 【把栈顶地址复制一份,并把引用压入操作数栈】
4: invokespecial #1 // 【栈顶对象调用构造方法创建对象(调用会被消耗,所以复制)】
7: astore_1 // 【把栈顶对象lock弹出,并放入局部变量表槽位1】
-------------------------------synchronized
8: aload_1 // 【把局部变量表槽位1的对象lock弹出,压入操作数栈】
9: dup // 【把栈顶元素的引用复制一份,并把引用压入操作数栈】
10: astore_2 // 【把栈顶对象lock弹出,并放入局部变量表槽位2】
11: monitorenter // 【对栈顶对象lock加锁(其实是槽位1的那份)】
-------------------------------print
12: getstatic #7
15: ldc #13
17: invokevirtual #15
20: aload_2 // 【把局部变量表槽位2的对象lock弹出,压入操作数栈】
21: monitorexit // 【对栈顶对象lock解锁】
22: goto 30 // 【结束】
-------------------------------error
25: astore_3 // 【把异常对象放入槽位2】
26: aload_2 // 【把局部变量表槽位2的对象lock弹出,压入操作数栈】
27: monitorexit // 【对栈顶对象lock解锁】
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
3. 类加载
https://www.jianshu.com/p/d432a94be182
Java 类加载分为 5 个过程,分别为:加载,链接(验证、准备、解析),初始化,使用,卸载。
这些阶段按顺序开始,但不是按顺序进行或顺序完成,通常是交叉进行的(一个阶段执行中激活另外一个阶段)。
3.1. 加载
3.1.1. 非数组类
通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个 Class 对象。
- 使用类加载器通过完全限定名找到字节码文件,将其以二进制流的形式读入内存。
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构(包含 class 文件常量池进入运行时常量池的过程)。
- 在内存中生成一个该类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
3.1.2. 数组类
本身不通过类加载器创建,而是由 Java 虚拟机直接创建,但数组类的元素类型使用类加载器创建。
- 如果数组的组件类型是引用类型(如
Integer[]
),那就递归去加载这个组件类型,数组类将在加载该组件类型的类加载器的类名称空间上被标识。 - 如果数组的组件类型不是引用类型(如
int[]
),Java 虚拟机将会把数组类标记为与引导类加载器关联。 - 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性默认为 public。
3.2. 验证
确保 Class 文件的字节流中包含信息符合当前虚拟机规范。
3.2.1. 文件格式验证
验证字节流是否符合 Class 文件格式的规范,且能被当前版本的虚拟机处理。
只有通过后,字节流才会进入内存的方法区中进行存储。以后的验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流。
-
是否以 0xCAFEBABE 开头。
-
主、次版本号是否在当前虚拟机的处理范围之内。
-
常量池中的常量是否有不被支持的常量类型。
-
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
-
Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。
-
其他验证…
3.2.2. 元数据验证
对字节码描述的信息进行语义分析,保证其描述的信息符合 Java 语言规范的要求。
- 这个类是否有除了
java.lang.Object
之外的父类。 - 这个类的父类是否继承了不允许被继承的类(被
final
修饰的类)。 - 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾。
- 其他验证…
3.2.3. 字节码验证
对类的方法体进行校验分析,保证方法在运行时不会危害虚拟机安全。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的。
3.2.4. 符号引用验证
发生在解析阶段,其对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行。
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性是否可被当前类访问。
3.3. 准备
为类变量(static 变量)分配内存并且设置该类变量的初始值,这些内存都将分配在方法区中。不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
通常是数据类型默认的初始值,而不是被在 Java 代码中被显式地赋予的值。
- 基本数据类型的类变量和全局变量,默认为零值(
0
、false
、''
),而局部变量在使用前必须显式地赋值,否则编译不通过。 - 引用数据类型的变量,默认为零值(
null
)。 - 被
static
和final
修饰的常量,必须在声明的时候显式地赋值,否则编译不通过,因为final
在编译的时候就会分配了。 - 被
final
修饰的常量,没有默认零值,既可以在声明时显式地赋值,也可以在类初始化时(构造方法中)显式地赋值。 - 在数组初始化时,数组中的各元素将根据对应数据类型被赋予默认值。
public class Test {
// 基本数据类型,准备阶段值创建变量
static int a;
// 准基本数据类型,准备阶段值创建变量,初始化阶段<cinit>中赋值
static int b = 10;
// 基本数据类型,准备阶段值创建变量并赋值
static final int c = 20;
// String类型,准备阶段值创建变量并赋值
static final String d = "OK";
// 引用数据类型,准备阶段值创建变量,初始化阶段<cinit>中赋值
static final Object e = new Object();
}
class 字节码:
{
static int a;
descriptor: I
flags: (0x0008) ACC_STATIC
// 【没有值】
static int b;
descriptor: I
flags: (0x0008) ACC_STATIC
// 【没有值,初始化阶段<cinit>中赋值】
static final int c;
descriptor: I
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: int 20 // 【编译阶段确定值,准备阶段直接赋值】
static final java.lang.String d;
descriptor: Ljava/lang/String;
flags: (0x0018) ACC_STATIC, ACC_FINAL
ConstantValue: String OK // 【编译阶段确定值,准备阶段直接赋值】
static final java.lang.Object e;
descriptor: Ljava/lang/Object;
flags: (0x0018) ACC_STATIC, ACC_FINAL
// 【没有值,初始化阶段<cinit>中赋值】
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=2, locals=0, args_size=0
--------------------------【对b赋初始值10】
0: bipush 10
2: putstatic #7 // Field b:I
--------------------------【调用Object的构造方法,对e赋初始值】
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #13 // Field e:Ljava/lang/Object;
15: return
}
3.4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程,有类或接口的解析,字段解析,类方法解析,接口方法解析等。
- 符号引用:一组符号来描述所引用的目标,可以是任何字面量(简单而言就是代码)。
- 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄(简单而言就是内存地址)。
package com.company;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Test.class.getClassLoader();
// 只做A类的加载,不会做A类的解析及后续流程,因此内部的B类也不会加载
Class<?> aClass = classLoader.loadClass("com.company.A");
// 会做A类的加载,且会执行解析及后续流程,因此内部的B类也会加载
A a = new A();
}
}
class A {
B b = new B();
}
class B {
}
3.5. 初始化
对类的静态变量赋真正的初始值,对成员变量赋初始值。即执行 <cinit>()V
方法,并且虚拟机会保证线程安全。
// 主要有两种方式:
// ① 声明类变量时指定初始值;
static int i = 5;
// ② 使用静态代码块为类变量指定初始值。
static int i;
static { i = 5; }
3.5.1. 初始化顺序
- 假如类还没有被加载和连接,则程序先加载并连接该类。
- 假如类的直接父类还没有被初始化,则先初始化其直接父类。
- 假如类中有初始化语句,则系统依次执行这些初始化语句。
3.5.2. 主动引用(初始化时机)
只有当对类主动引用的时候才会导致类的初始化。
- JVM 启动时的启动类(包含
main()
方法的类)。 - 首次访问类/接口的静态变量,调用类的静态方法。
- 子类初始化时,其父类也会被初始化;子接口初始化时,其父接口不会被初始化。
- 创建类的实例(
new
、反射、clone
、反序列化)。 - 使用
java.lang.reflect
包中的方法对类进行反射调用。
3.5.3. 被动引用(不会初始化)
当对类被动引用的时候不会导致类的初始化。
【通过子类引用父类的静态变量】
只会触发父类的初始化,而不会触发子类的初始化。
// 父类
public class SuperClass {
static {
System.out.println("父类正在初始化");
}
public static String name = "这是父类";
}
// 子类
public class SubClass extends SuperClass {
static {
System.out.println("子类正在初始化");
}
}
// 测试类
public class InitTest {
public static void main(String[] args) {
// 通过子类引用父类中定义的静态变量,不会初始化子类
System.out.println(SubClass.name);
// 运行结果:
// 父类正在初始化
// 这是父类
}
}
【通过数组定义集合定义来引用类】
JVM 会自动生成生成一个继承于 Object
的子类,并由字节码指令 newarray
创建。
// 测试类
public class InitTest {
public static void main(String[] args) {
SuperClass[] arrays = new SuperClass[10];
ArrayList<SubClass> list = new ArrayList<>();
System.out.println("数组元素个数:" + arrays.length);
System.out.println("列表元素个数:" + list.size());
// 运行结果:
// 数组元素个数:10
// 列表元素个数:0
}
}
【静态常量】
静态常量在编译阶段就会存入调用类的常量池中,因此不会触发初始化。
// 常量类
public class ConstClass {
static {
System.out.println("有常量的类正在初始化");
}
public static final String NAME = "这是常量类";
}
// 测试类
public class InitTest {
public static void main(String[] args) {
System.out.println(ConstClass.NAME);
// 运行结果:
// 这是常量类
}
}
3.5.4. 初始化总结
① 判断使用 a、b、c 这三个常量,是否会导致类 A 初始化。
public class Test {
public static void main(String[] args) {
System.out.println(A.a); // 不会导致A初始化
System.out.println(A.b); // 不会导致A初始化
System.out.println(A.c); // 会导致A初始化
}
}
class A {
public static final int a = 10; // 静态基本类型常量,不会初始化
public static final String b = "Hello"; // 静态字符串类型常量,不会初始化
public static final Integer c = 20; // 静态引用类型常量,会初始化
}
② 静态内部类单例模式(懒惰初始化)
public class Singleton {
private Singleton() {
}
// 静态内部类保存单例对象
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 首次调用该方法,才会导致静态内部类初始化,并初始化静态成员
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
4. 类加载器
类加载器负责加载所有的类,在内存中生成一个实例对象。并且采用按需加载模式。
4.1. 类加载器的分类
名称 | 加载类的位置 | 说明 |
---|---|---|
Bootstrap Classloader | $JAVA_HOME/jre/lib/rt.jar | 不能直接访问使用 |
Extension Classloader | $JAVA_HOME/jre/lib/ext | 上级为 Bootstrap |
Application Classloader | $classpath | 上级为 Extension |
自定义 Classloader | 自定义 | 上级为 Application |
4.1.1. 启动类加载器(Bootstrap Classloader)
由 C++ 语言实现,并不继承自 java.lang.ClassLoader
,是虚拟机自身的一部分,主要加载 JVM 自身需要的类。
- 加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 和 sun.boot.class.path 路径下的内容),加载包名为 java、javax、sun 等开头的类。
- 加载扩展类加载器和应用程序类加载器,并指定他们的父类加载器。
4.1.2. 扩展类加载器(Extension Classloader)
由 Java 语言实现,父类加载器为 null。
- 用来加载 Java 的扩展库 (JAVA_HOME/jre/ext/*.jar,或 java.ext.dirs 路径下的内容) 。
- 由 ExtClassLoader 实现。
4.1.3. 应用程序类加载器(Application Classloader)
由 Java 语言实现,父类加载器为 ExtClassLoader。也称为系统类加载器。
- 它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说 Java 应用的类都是它加载的。
- 由 AppClassLoader 实现。
4.1.4. 自定义类加载器
需要用到自定义类加载器的情况:
- 想加载非 classpath 随意路径中的类文件。
- 都是通过接口来使用实现,希望解耦时,常用在框架设计。
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器。
继承 java.lang.ClassLoader 类,实现自己的类加载器。
- 遵守双亲委派模型:继承 ClassLoader,重写
findClass()
方法。 - 破坏双亲委派模型:继承 ClassLoader,重写
loadClass()
方法。
自定义代码:
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 固定加载某个目录下的class文件
String path = "E:\\myclasspath\\" + name + ".class";
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Files.copy(Paths.get(path), outputStream);
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name, e);
}
}
}
使用:
MyClassLoader classLoader1 = new MyClassLoader();
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c1 = classLoader1.findClass("Test");
Class<?> c2 = classLoader1.findClass("Test");
Class<?> c3 = classLoader2.findClass("Test");
System.out.println(c1 == c2); // true。 全限定名和类加载器都相同,只加载1次
System.out.println(c1 == c3); // false。 类加载器不相同,会加载2次
4.2. 类加载步骤
- 检测此 Class 是否加载过(缓冲区中是否有此 Class),如果有直接进入第 8 步,否则进入第 2 步。
- 如果没有上级加载器,则要么 Parent 是根类加载器,要么本身就是根类加载器,则跳到第 4 步,如果上级加载器存在,则进入第 3 步。
- 请求使用上级加载器去载入目标类,如果载入成功则跳至第 8 步,否则接着执行第 5 步。
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第 8 步,否则跳至第 7 步。
- 当前类加载器尝试寻找 Class 文件,如果找到则执行第 6 步,如果找不到则执行第 7 步。
- 从文件中载入 Class,成功后跳至第 8 步。
- 抛出
ClassNotFountException
异常。 - 返回对应的
java.lang.Class
对象。
4.3. 类加载机制
-
全盘负责:同一个类加载器负责加载该 Class,以及所依赖和引用的其他 Class,除非显式指定其他类加载器。
-
双亲委派:先让上级加载器尝试加载该 Class,只有在上级加载器无法加载该类时,自己才尝试加载该类。
-
缓存机制:程序需要使用某个 Class 时,类加载器先从缓存区中获取,只有不存在时才执行加载并存入缓冲区。因此动态修改了 Class 后,必须重启 JVM 后修改才会生效。
4.4. 双亲委派
4.4.1. 执行流程
- 上浮:一个类加载器收到加载请求,他并不会去加载该类,而是把这个请求委派给上级加载器(层层往上),因此所有的类加载请求最终都会传送到启动类加载器。
- 下沉:只有当上级加载器在其搜索范围内无法找到所需的类,下级加载器会尝试去自己加载(层层往下)。
// 抽象类 ClassLoader.java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先检查这个类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果有上级加载器,则先交给上级加载器去加载
c = parent.loadClass(name, false);
} else {
// 如果没有上级加载器,自己检查这个类是否已经加载
// 如果没有加载,则说明向上委托流程中没有加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 向上和向下流程都走完了
if (c == null) {
// 如果仍然没有找到,则调用自身的findClass方法去查找该类
// findClass是抽象方法,由具体的ClassLoader继承类去实现
long t1 = System.nanoTime();
c = findClass(name);
// 记录加载耗时信息
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
4.4.2. 双亲委派的优点
- 避免重复加载。相同的 class 文件被不同的类加载器加载就是不同的两个类,上级加载器已经加载过该类,下级加载器没必要再加载一次。
- 提高安全性。防止用户自己编写的类动态替换 Java 的一些核心类。(比如
java.lang.Integer
,而启动类加载器找到了这个名字且该类已被加载,所以并不会重新加载其他的java.lang.Integer
,而是直接返回已加载过的 Integer.class)
4.4.3. 破坏双亲委派
- 双亲委派模型不是一种强制性约束,它是一种 Java 设计者推荐使用类加载器的方式。
- 有时必须违反这个约束。例如 SPI(Service Provider Interface), 是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
- 线程上下文类加载器(Thread Context ClassLoader)可以通过
java.lang.Thread
类的setContextClassLoader()
方法设置,默认从父线程中继承(父线程默认为应用程序类加载器)。