案例分析

非静态成员变量赋值顺序:默认初始化 -> 父类 -> 显式初始化 -> 构造器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Demo16 {
public static void main(String[] args) {
FatherClass f = new SonClass();
// 属性无多态性,当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
System.out.println(f.x);
}
}

class Father {
int x = 10;

public Father() {
this.print();
x = 20;
}

public void print() {
System.out.println("Father.x = " + x);
}
}

class Son extends Father {
int x = 30;

public Son() {
this.print();
x = 40;
}

public void print() {
System.out.println("Son.x = " + x);
}
}

// 输出:
// Son.x = 0
// Son.x = 30
// 20

以上代码对应字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Father 的 <init>
0 aload_0
1 invokespecial #1 <java/lang/Object.<init> : ()V>
4 aload_0
5 bipush 10
7 putfield #2 <com/pl/Father.x : I>
10 aload_0
11 invokevirtual #3 <com/pl/Father.print : ()V>
14 aload_0
15 bipush 20
17 putfield #2 <com/pl/Father.x : I>
20 return

// Son 的 <init>
0 aload_0
1 invokespecial #1 <com/pl/Father.<init> : ()V>
4 aload_0
5 bipush 30
7 putfield #2 <com/pl/Son.x : I>
10 aload_0
11 invokevirtual #3 <com/pl/Son.print : ()V>
14 aload_0
15 bipush 40
17 putfield #2 <com/pl/Son.x : I>
20 return

Class 文件结构

按顺序依次为:

  1. magic number(u4):0xCAFEBABE
  2. Class 文件版本(u4):副版本 + 主版本。向下兼容。
  3. 常量池:count(u2)、具体数组(长度为 count - 1)
  4. 访问标志(u2)
  5. 类索引(u2)、父类索引(u2)
  6. 接口索引集合:count(u2)、具体数组
  7. 字段表集合:count(u2)、具体数组
  8. 方法表集合:count(u2)、具体数组
  9. 属性表集合:count(u2)、具体数组
类型 名称 说明 长度
u4 magic 魔数,识别 Class 文件格式 4 个字节
u2 minor_version 副版本号 2 个字节
u2 major_version 主版本号 2 个字节
u2 constant_pool_count 常量池计算器 2 个字节
cp_info constant_pool 常量池 n 个字节
u2 access_flags 访问标志 2 个字节
u2 this_class 类索引 2 个字节
u2 super_class 父类索引 2 个字节
u2 interfaces_count 接口计数器 2 个字节
u2 interfaces 接口索引集合 2 个字节
u2 fields_count 字段个数 2 个字节
field_info fields 字段集合 n 个字节
u2 methods_count 方法计数器 2 个字节
method_info methods 方法集合 n 个字节
u2 attributes_count 附加属性计数器 2 个字节
attribute_info attributes 附加属性集合 n 个字节

常量池

常量池表项主要存放字面量(literal)和符号引用(symbolic references),计数从 1 开始,即常量池计数器为 n,则实际有 n - 1 项常量,索引 0 保留用作空指向。

常量 具体类型
字面量 文本字符串
声明为 final 的常量值
符号引用 类和接口的全限定名(如 com/pl/demo1;)
字段的名称和描述符
方法的名称和描述符

描述符包括字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。如:Object m(int i, double d, Thread t) {…} 描述为 (IDLjava/lang/Thread;)Ljava/lang/Object;

标志符 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
[ 一维数组类型。如:[[[D 表示 double[][][]
L 对象类型。如:Ljava/lava/Object;

符号引用与内存布局无关,直接引用与内存布局相关。

虚拟机加载 class 文件时进行动态链接,在解析阶段将符号引用替换为直接引用,并翻译到具体的内存地址中。

常量池中的项有 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_MothodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

访问标志

识别类或接口的访问信息。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否被声明为 final,只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令,JDK1.2 之后编译出来的类的这个标志为 true,主要为了兼容旧的编译器
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为 abstract 类型,对于接口或抽象类来说,此标志值为 true,其他值为 false
ACC_SYNTHETIC 0x1000 标志这个类并非由用户产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

如果设置了 ACC_INTERFACE,那么同时也得设置 ACC_ABSTRACT,且不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 。

如果没有设置 ACC_INTERFACE,那么可以具有上表中除 ACC_ANNOTATION 外的其他所有标志。当然,ACC_FINAL 和
ACC_ABSTRACT 这类互斥的标志除外。

如果设置了 ACC_ANNOTATION,那么同时也得设置 ACC_INTERFACE。

类索引、父类索引、接口索引

长度 含义
u2 this_class
u2 super_class
u2 interfaces_count
n * u2 interfaces[interfaces_count]

字段表

当前类或接口定义和编译器添加的成员变量。在 Java 源代码中,不允许字段重名,在字节码文件中,两个字段的描述符不相同,则允许重名

长度 含义
u2 字段计数器

字段表结构

类型 名称 含义
u2 access_flags 访问标志
u2 name_index 字段名索引
u2 descriptor_index 描述符索引
u2 attributes_count 属性计数器
attribute_info attributes 属性集合

字段访问标志

标志名称 标志值 含义
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

方法表

描述当前类或接口声明的方法和编译器添加的方法(如 ()、())。在 Java 源代码中,方法的特征签名为参数类型,在字节码文件中,方法的特征签名为参数类型和返回值类型。

长度 含义
u2 方法计数器

方法表结构

类型 名称 含义
u2 access_flags 访问标志
u2 name_index 方法名索引
u2 descriptor_index 描述符索引
u2 attributes_count 属性计数器
attribute_info attributes 属性集合

如果访问标志项没有设置 ACC_NATIVE 和 ACC_ABSTRACT,则包含方法的虚拟机指令

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否 public
ACC_PRIVATE 0x0002 方法是否 private
ACC_PROTECTED 0x0004 方法是否 protected
ACC_STATIC 0x0008 方法是否 static
ACC_FINAL 0x0010 方法是否为 final
ACC_SYNCHRONIZED 0x0040 方法是否 sychronized
ACC_BRIDGE 0x0080 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x1000 方法是否接受不定参数
ACC_NATIVE 0x4000 方法是否为 native
ACC_ABSTRACT 0x0080 方法是否为 Abstract
ACC_STRICT 0x1000 方法是否为 strictfp
ACC_SYNTHETIC 0x4000 方法是否有编译器自动产生

Code 属性表

类型 名称 含义
u2 attribute_name_index 属性名索引
u4 attribute_length 属性长度
u2 max_stack 操作数栈最大深度
u2 max_locals 局部变量表所需空间
u4 code_length 字节码指令长度
u1 code 存储字节码指令
u2 exception_table_length 异常表长度
exception_info exception_table 异常表
u2 attributes_count 属性集合计数器
attribute_info attributes 属性集合

属性表

描述 class 文件携带的辅助信息

字节码指令

虚拟机指令包括一个字节长度的操作码和 0 或多个操作数

大致可分为:

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

加载与存储指令

将数据从栈帧的局部变量表和操作数栈之间来回传递

局部变量压栈指令

将指定索引的局部变量复制一份加载到操作数栈:

xload n

xload_0xload_1xload_2xload_3(其中 x 表示此次操作的数据类型,为 i、l、f、d、a(引用))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void load(int num, Object obj, long count, boolean flag, int[] arr) {
System.out.println(this);
System.out.println(num);
System.out.println(obj);
System.out.println(count);
System.out.println(flag);
System.out.println(arr);
}

// 对应字节码为
0 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
3 aload_0
4 invokevirtual #19 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
7 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #20 <java/io/PrintStream.println : (I)V>
14 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
17 aload_2
18 invokevirtual #19 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
21 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
24 lload_3
25 invokevirtual #21 <java/io/PrintStream.println : (J)V>
28 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
31 iload 5
33 invokevirtual #22 <java/io/PrintStream.println : (Z)V>
36 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload 6
41 invokevirtual #19 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
44 return

常量入栈指令

将常量加载到操作数栈。

特定的常量入栈:

  • iconst_m1(表示 -1)、iconst_0iconst_1iconst_2iconst_3iconst_4iconst_5
  • lconst_0lconst_1
  • fconst_0fconst_1fconst_2
  • dconst_0dconst_1
  • aconst_null(表示 null)

非特定的部分常量入栈:

  • bipush(8 位整数, [-128, 127])
  • sipush(16 位整数, [-32768, -127] 和 [128,32767])

其他:

  • ldc(接收 8 位参数,指向常量池中的 int、float、String 索引,将指定内容压入栈)
  • ldc_w(接收两个 8 位参数,支持的索引范围更大)、ldc2_w(压入的元素为 double 或 long)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void pushConstLdc() {
int i = -1;
int a = 5;
int b = 6;
int c = 127;
int d = 128;
int e = 32767;
int f = 32768;
}

void constLdc() {
long a1 = 1;
long a2 = 2;
float f1 = 2;
float f2 = 3;
double d1 = 1;
double d2 = 2;
Date d = null;
}

// 对应字节码为
0 iconst_m1
1 istore_1
2 iconst_5
3 istore_2
4 bipush 6
6 istore_3
7 bipush 127
9 istore 4
11 sipush 128
14 istore 5
16 sipush 32767
19 istore 6
21 ldc #23 <32768>
23 istore 7
25 return

0 lconst_1
1 lstore_1
2 ldc2_w #24 <2>
5 lstore_3
6 fconst_2
7 fstore 5
9 ldc #26 <3.0>
11 fstore 6
13 dconst_1
14 dstore 7
16 ldc2_w #27 <2.0>
19 dstore 9
21 aconst_null
22 astore 11
24 return

出栈装入局部变量表指令

将一个数值从操作数栈弹出后存储到指定索引的局部变量表位置。

xstorexstore_0xstore_1xstore_2xstore_3(其中 x 为 i、l、f、d、a)

xastore(操作数组元素,其中 x 为 i、1、f、d、a、b、c、s)

扩充局部变量表的访问索引的指令

wide(扩展局部变量数,将 8 位的索引扩展到 16 位)

算术指令

算术指令用于将操作数栈上的值取出进行特定运算,并把计算结果重新压入操作数栈。

虚拟机规范并未明确规定整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0 时会导致虚拟机抛出异常 ArithmeticException

当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用 NaN 值来表示。而且所有使用 NaN 值作为操作数的算术操作,结果都会返回 NaN

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
int i = -10;
double j = i / 0.0;
System.out.println(j); // -Infinity

double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2); // NaN
}

所有的算术指令包括

  • 加法指令:iaddladdfadddadd
  • 减法指令:isublsubfsubdsub
  • 乘法指令:imulmufmuldmul
  • 除法指令:idivldivfdivddiv
  • 求余指令:iremlremfremdrem(remainder:余数)
  • 取反指令:ineglnegfnegdneg
  • 自增指令:iinc(long 类型的 ++ 实际上是 + 1 的语法糖,字节码是一样的)
  • 位运算指令:
    • 位移指令:ishlishriushrlshllshrlushr
    • 按位或指令:iorlor
    • 按位与指令:iandland
    • 按位异或指令:ixorlxor
  • 比较指令:dcmpgdcmp1fcmpgfcmp1lcmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void method() {
int i = 100;
i = i + 10;
i += 10;
}

// 对应的字节码
0 bipush 100
2 istore_1
3 iload_1
// i = i + 10 即压入 10,弹出 i 和 10 相加后压入栈,再将压入栈的和存入局部变量表
4 bipush 10
6 iadd
7 istore_1
// i += 10 直接使用自增指令
8 iinc 1 by 10
11 return


public void negation() {
int i = 100;
int j = ~i;
}

0 bipush 100
2 istore_1
3 iload_1
// 取反操作被优化成与 -1 做异或运算
4 iconst_m1
5 ixor
6 istore_2
7 return

前++与后++的问题

如果不涉及赋值操作,从字节码角度看是一样的

1
2
3
4
5
6
7
8
9
10
11
public void method6() {
int i = 10;
i++;
++i;
}

0 bipush 10
2 istore_1
3 iinc 1 by 1
6 iinc 1 by 1
9 return

涉及到赋值操作时

  • i++ 是先赋值后运算
  • ++i 是先运算后赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void method7() {
int i = 10;
int a = i++;

int j = 20;
int b = ++j;
}

0 bipush 10
2 istore_1
3 iload_1
// i 自增
4 iinc 1 by 1
// 将自增前的 i 存入 a
7 istore_2

8 bipush 20
10 istore_3
// j 自增
11 iinc 3 by 1
// 将自增后的 j 放入操作数栈
14 iload_3
// 从操作数栈中将自增后的 j 存到 b
15 istore 4
17 return

比较指令

用于比较栈顶两个元素的大小,并将比较结果入栈。

比较指令有:dcmpgdcmplfcmpgfcmpllcmp

对于 double 和 float 类型的数据,由于 NaN 的存在,各有两个版本的比较指令,它们的区别在于在数字比较时,若遇到NaN 值,处理结果不同。

指令 lcmp 针对 long 型整数,由于 long 型整数没有 NaN 值,故无需准备两套指令。

例如:

指令 fcmpg fcmpl 都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为 v2,栈顶顺位第 2 位的元素为 v1,若v1 = v2,则压入 0;若v1 > v2 则压入 1;若v1 < v2 则压入 -1。

如果遇到 NaN 值,fcmpg 会压入 1,而 fcmpl 会压入 -1。

类型转换指令

宽化类型转换

int(byte、short、char) -> long -> float -> double

对应的指令为:i2li2fi2dl2fl2df2d

代码中的 byte、short、char 转 int 实际上仅仅只是复制,而 byte、short、char 转 long 、 float 、double 实际上是 int 转 long 、 float 、double 。

int 、long 转 float 可能丢失几个最低有效位的值,但不会抛异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void wideningNumericConversions(int i) {
byte b = 1; // char、short 与 byte 在宽化类型转换相同
int bi = b;
long bl = b;

long l = i;
float f = i;
double d = i;

float f1 = l;
double d1 = l;

double d2 = f1;
}

// 对应的字节码
0 iconst_1
1 istore_2
2 iload_2
3 istore_3
4 iload_2
5 i2l
6 lstore 4

8 iload_1
9 i2l
10 lstore 6
12 iload_1
13 i2f
14 fstore 8
16 iload_1
17 i2d
18 dstore 9

20 lload 6
22 l2f
23 fstore 11
25 lload 6
27 l2d
28 dstore 12

30 fload 11
32 f2d
33 dstore 14
35 return

窄化类型转换

double -> float -> long -> int(byte、short、char)

对应的指令为:d2d2ld2ff2if2ll2i

从 int 类型至 byte、short、char 类型以及 byte、short、char 之间互相转换,对应的指令为:i2bi2ci2s

尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是永远不可能导致虚拟机抛出运行时异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void narrowingNumericConversion(int i, long l) {
byte b = (byte) i;
short s = (short) i;
char c = (char) i;

int i1 = (int) l;
byte b1 = (byte) l;
}

// 字节码
0 iload_1
1 i2b
2 istore 4
4 iload_1
5 i2s
6 istore 5
8 iload_1
9 i2c
10 istore 6
12 lload_2
13 l2i
14 istore 7
16 lload_2
17 l2i
18 i2b
19 istore 8
21 return

floatdouble 窄化转换为整数类型 intlong

  • 如果浮点值是 NaN,那么转换为 intlong 类型的 0
  • 如果浮点值不是无穷大的话,丢掉小数位取整,若得到的整数超出了目标类型表示范围,将转换为目标类型能表示的最大或者最小整数

double 窄化转换为 float 时,通过向最接近数舍入模式舍入一个可以使用 float 类型表示的数字

  • 如果转换结果的绝对值太小而无法使用 float 来表示,将返回 float 类型的正负零。
  • 如果转换结果的绝对值太大而无法使用 float 来表示,将返回 float 类型的正负无穷大。
  • 对于double 类型的 NaN 值将按规定转换为 float 类型的 NaN 值。
1
2
3
4
5
6
7
8
9
void d2i() {
int i1 = (int) (0.0d / 0.0); // 0
int i2 = (int) 23.99; // 23
int i3 = (int) -21.99; // -21
int i4 = (int) 2147483997.3; // 2147483647
int i5 = (int) -2147483997.3; // -2147483648
float f1 = (float) Double.POSITIVE_INFINITY; // Infinity
float f2 = (float) Double.NaN; // NaN
}

对象创建与访问指令

对象和数组创建

  • new:接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。

  • newarray:创建基本类型数组

  • anewarray:创建引用类型数组

  • multianewarray:创建多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void newArray() {
int[] intArray = new int[99];
Object[] objArray = new Object[98];
int[][] mintArray = new int[97][96];
String[][] strArray1 = new String[95][];
String[][] strArray = new String[94][93];
}

// 字节码指令
0 bipush 99
2 newarray 10 (int)
4 astore_1
5 bipush 98
7 anewarray #21 <java/lang/Object>
10 astore_2
11 bipush 97
13 bipush 96
15 multianewarray #22 <[[I> dim 2
19 astore_3
// new String[95][] 实际上只创建了一个一维数组
20 bipush 95
22 anewarray #23 <[Ljava/lang/String;>
25 astore 4
27 bipush 94
29 bipush 93
31 multianewarray #24 <[[Ljava/lang/String;> dim 2
35 astore 5
37 return

字段访问

  • getstaticputstatic:访问类字段(static 字段)
  • getfieldputfield:访问类实例字段(非 static 字段)

get 即入栈,put 即出栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class NewTest {
public void setOrderId() {
Order order = new Order();
order.id = 1001;
System.out.println(order.id);

Order.name = "ORDER";
System.out.println(Order.name);
}
}

class Order {
int id;
static String name;
}

// 字节码
0 new #11 <Order>
3 dup
4 invokespecial #12 <Order.<init>>
7 astore_1
8 aload_1
9 sipush 1001
12 putfield #13 <Order.id>
15 getstatic #8 <java/lang/System.out>
18 aload_1
19 getfield #13 <Order.id>
22 invokevirtual #14 <java/io/PrintStream.println>
25 ldc #15 <ORDER>
27 putstatic #16 <Order.name>
30 getstatic #8 <java/lang/System.out>
33 getstatic #16 <Order.name>
36 invokevirtual #10 <java/io/PrintStream.println>
39 return

数组操作

  • baload(byte 和 boolean)、caloadsaloadialoadlaloadfaloaddaloadaaload:把数组元素加载到操作数栈
  • bastore(byte 和 boolean)、castoresastoreiastorelastorefastoredastoreaastore:将操作数栈的值存储到数组元素中
  • arraylength:弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void setArray() {
int[] intArray = new int[99];
intArray[3] = 98;
System.out.println(intArray[1]);

boolean[] arr = new boolean[97];
arr[1] = false;

System.out.println(arr.length);
}

// 字节码
0 bipush 99
2 newarray 10 (int)
4 astore_1
5 aload_1
6 iconst_3
7 bipush 98
9 iastore
10 getstatic #10 <java/lang/System.out : Ljava/io/PrintStream;>
13 aload_1
14 iconst_1
15 iaload
16 invokevirtual #12 <java/io/PrintStream.println : (I)V>

19 bipush 97
21 newarray 4 (boolean)
23 astore_2
24 aload_2
25 iconst_1
26 iconst_0
27 bastore

28 getstatic #10 <java/lang/System.out : Ljava/io/PrintStream;>
31 aload_2
32 arraylength
33 invokevirtual #12 <java/io/PrintStream.println : (I)V>
36 return

类型检查指令

instanceof:判断给定对象是否是某一个类的实例,并将判断结果压入操作数栈

checkcast:检查类型强制转换是否可以进行。如果可以进行,那么 checkcast 指令不会改变操作数栈,否则抛出ClassCastException 异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String checkCast(Object obj) {
if(obj instanceof String) {
return ((String) obj);
} else {
return null;
}
}

// 字节码
0 aload_1
1 instanceof #25 <java/lang/String>
4 ifeq 12 (+8)
7 aload_1
8 checkcast #25 <java/lang/String>
11 areturn
12 aconst_null
13 areturn

方法调用与返回指令

方法调用

  • invokevirtual:调用所有的虚方法和 final 方法final 方法是非虚方法)
  • invokestatic:调用 static 方法
  • invokespecial:调用 private 方法、constructor 方法、super 关键字调用的方法
  • invokeinterface:调用接口方法
  • invokedynamic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
aa.method2();

// 只能通过类调用
AA.method1();
}
}

interface AA {
public static void method1() {}

public default void method2() {}
}

class BB implements AA {
}

// 字节码
0 new #2 <BB>
3 dup
4 invokespecial #3 <BB.<init>>
7 astore_1
8 aload_1
9 invokeinterface #4 <AA.method2> count 1
14 invokestatic #5 <AA.method1>
17 return

方法返回

  • ireturn (用于 boolean、byte、char、 short 和 int 类型)、lreturnfreturndreturnareturn
  • return 用于 void 的方法、实例初始化方法以及类和接口的类初始化方法
1
2
3
4
5
6
7
8
double returnF(int i) {
return i;
}

// 字节码
0 iload_1
1 i2f
2 freturn

操作数栈指令

  • poppop2:将 一个 slot(4 字节)或两个 slot(8 字节,2 个 4 字节的数据或一个 8 字节的数据)的元素从栈顶弹出丢弃

  • dupdup2:复制栈顶一个或两个 slot 的数据

  • dup_x1dup2_x1dup_x2dup2_x2:复制栈顶一个或两个 slot 的数据并将其插入到栈顶下的某个位置

    • dup_x1:放在栈顶的 2 个 slot 下面
    • dup2_x1:放在栈顶的 3 个 slot 下面
    • dup_x2:放在栈顶的 3 个 slot 下面
    • dup2_x2:放在栈顶的 4 个 slot 下面
  • swap:将栈最顶端的两个 slot 交换。虚拟机没有提供交换两个 64 位数据类型数值的指令

  • nop:字节码为0x00,表示什么都不做。这条指令一般可用于调试、占位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
long add() {
return i++;
}
private long i = 0;

// 字节码
0 aload_0
1 dup
2 getfield #2 <Test.i : J>
5 dup2_x1
6 lconst_1
7 ladd
8 putfield #2 <Test.i : J>
11 lreturn

控制转移指令

条件跳转

常与比较指令配合使用。

ifeqifltifleifneifgtifgeifnullifnonnull:这些指令都接收两个字节的操作数,用于计算跳转的位置(16 位符号整数作为当前位置的 offset)。

指令 说明
ifeq equals 当栈顶 int 类型数值等于 0 时跳转
ifne not equals 当栈顶 int 类型数值不等于 0 时跳转
iflt lower than 当栈顶 int 类型数值小于 0 时跳转
ifle lower or equals 当栈顶 int 类型数值小于等于 0 时跳转
ifgt greater than 当栈顶 int 类型数组大于 0 时跳转
ifge greater or equals 当栈顶 int 类型数值大于等于 0 时跳转
ifnull 为 null 时跳转
ifnonnull 不为 null 时跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void compare() {
int i1 = 10;
long l1 = 20;
System.out.println(i1 < l1);
}

// 字节码
0 bipush 10
2 istore_1
3 ldc2_w #6 <20>
6 lstore_2
7 getstatic #4 <java/lang/System.out>
10 iload_1
11 i2l
12 lload_2
13 lcmp
14 ifge 21 (+7)
17 iconst_1
18 goto 22 (+4)
21 iconst_0
22 invokevirtual #5 <java/io/PrintStream.println>
25 return

比较跳转

比较指令和跳转指令的结合体。

if_icmpeqif_icmpneif_icmpltif_icmpgtif_icmpleificmpgeif_acmpeqif_acmpne:i 开头的指令针对 int型整数操作(包括 short 和 byte),以字符 a 开头的指令表示对象引用的比较。

前者为栈顶元素的下一个元素,后者为栈顶元素。

指令 说明
if_icmpeq 比较栈顶两 int 类型数值大小,当前者等于后者时跳转
if_icmpne 比较栈顶两 int 类型数值大小,当前者不等于后者时跳转
if_icmplt 比较栈顶两 int 类型数值大小,当前者小于后者时跳转
if_icmple 比较栈顶两 int 类型数值大小,当前者小于等于后者时跳转
if_icmpgt 比较栈顶两 int 类型数值大小,当前者大于后者时跳转
if_icmpge 比较栈顶两 int 类型数值大小,当前者大于等于后者时跳转
if_acmpeq 比较栈顶两引用类型数值,当结果相等时跳转
if_acmpne 比较栈顶两引用类型数值,当结果不相等时跳转

多条件分支跳转

对应源代码的 switch-case 语句。

tableswitch

用于 case 值连续的情况,会自动对 case 值进行排序,只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public int switch1(int select) {
int num;
switch (select) {
case 7:
num = 10;
break;
case 9:
num = 20;
case 8:
num = 30;
default:
num = 40;
}
return num;
}

// 字节码
0 iload_1
1 tableswitch 7 to 9
7: 28 (+27)
8: 37 (+36)
9: 34 (+33)
default: 40 (+39)
28 bipush 10
30 istore_2
31 goto 43 (+12)
34 bipush 20
36 istore_2
37 bipush 30
39 istore_2
40 bipush 40
42 istore_2
43 iload_2
44 ireturn

lookupswitch

用于 case 值不连续,会自动对 case 值进行排序,逐一匹配。

用 String 类型作为 case 时,先进行哈希值的比较,若哈希值相同,再进行 equals() 比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public int switch2(int select) {
int num;
switch (select) {
case 70:
num = 10;
break;
case 90:
num = 20;
case 80:
num = 30;
default:
num = 40;
}
return num;
}

// 字节码
0 iload_1
1 lookupswitch 3
70: 36 (+35)
80: 45 (+44)
90: 42 (+41)
default: 48 (+47)
36 bipush 10
38 istore_2
39 goto 51 (+12)
42 bipush 20
44 istore_2
45 bipush 30
47 istore_2
48 bipush 40
50 istore_2
51 iload_2
52 ireturn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void switch3(String s) {
switch (s) {
case "hi":
break;
case "hello":
break;
case "bye":
break;
}
}

// 字节码
0 aload_1
1 astore_3
2 iconst_m1
3 istore 4
5 aload_3
6 invokevirtual #2 <java/lang/String.hashCode : ()I>
9 lookupswitch 2
3329: 36 (+27)
99162322: 51 (+42)
default: 63 (+54)
36 aload_3
37 ldc #3 <hi>
39 invokevirtual #4 <java/lang/String.equals : (Ljava/lang/Object;)Z>
42 ifeq 63 (+21)
45 iconst_0
46 istore 4
48 goto 63 (+15)
51 aload_3
52 ldc #5 <hello>
54 invokevirtual #4 <java/lang/String.equals : (Ljava/lang/Object;)Z>
57 ifeq 63 (+6)
60 iconst_1
61 istore 4
63 iload 4
65 lookupswitch 2
0: 92 (+27)
1: 98 (+33)
default: 104 (+39)
92 bipush 10
94 istore_2
95 goto 107 (+12)
98 bipush 20
100 istore_2
101 goto 107 (+6)
104 bipush 40
106 istore_2
107 iload_2
108 ireturn

无条件跳转

goto:接收两个字节的操作数,组成带符号整数,用于指定指令的偏移量

goto_w:接收四个宇节的操作数,可以表示更大的地址范围

jsrjsr_wret:主要用于 try-finally 语句,且已经被虚拟机逐渐废弃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void whileInt() {
int i = 0;
while (i < 100) {
i++;
}
}

// 字节码
0 iconst_0
1 istore_1
2 iload_1
3 bipush 100
5 if_icmpge 14 (+9)
8 iinc 1 by 1
11 goto 2 (-9)
14 return

异常处理指令

抛出异常

athrow:显式抛出异常 (throw 语句)。在抛异常时,虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void throwOne(int i) {
if (i == 1) {
throw new RuntimeException();
}
}

// 字节码
0 iload_1
1 iconst_1
2 if_icmpne 13 (+11)
5 new #9 <java/lang/RuntimeException>
8 dup
9 invokespecial #10 <java/lang/RuntimeException.<init> : ()V>
12 athrow
13 return

处理异常

处理异常(catch 语句)不是由字节码指令来实现的(早期使用 jsrret 指令),而是采用异常表来完成的。定义了一个 try-catch 或者 try-finally 的异常处理,就会创建一个异常表项,try-catch 语句创建的异常表项匹配相应的异常,try-finally 语句创建的异常表项匹配所有异常。

产生异常时,根据异常表寻找一个匹配的处理,如果没有找到,强制结束当前方法并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程(如 main 线程)里抛出,将会导致 JVM 自己终止。

finally 的实现原理是根据不发生异常、发生异常并被 catch 语句捕获、发生异常未被 catch 语句捕获三种情况在适当的位置插入重复的字节码保证 finally 语句的指令一定被执行。

异常表

  • 起始位置
  • 结束位置
  • 处理代码的位置
  • 被捕获的异常类在常量池中的索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void testThrow() {
try {
throwOne(1);
}
catch (MailException | MappingException e) {
}
finally {
int a = 66;
}
}

// 字节码
0 aload_0
1 iconst_1
2 invokevirtual #9 <com/pl/Demo18.throwOne : (I)V>
// 正常情况不发生异常
5 bipush 66
7 istore_1
8 goto 24 (+16)
// 发生异常并被 catch 语句捕获
11 astore_1
12 bipush 66
14 istore_1
15 goto 24 (+9)
// 发生异常未被 catch 语句捕获
18 astore_2
19 bipush 66
21 istore_3
22 aload_2
23 athrow
24 return
Nr. Start PC End PC Handler PC Catch Type
0 0 5 11 cp_info #10
org/springframework/mail/MailException
1 0 5 11 cp_info #10
org/springframework/data/MappingException
2 0 5 18 cp_info #0
any

若在 try 语句中涉及 return ,虚拟机会先将返回值暂存在局部变量表中,然后执行 finally 子句,再从局部变量表中取出暂存的返回值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static String func() {
String str = "old";
try {
return str;
} finally {
str = "new";
}
}

// 字节码
0 ldc #2 <old>
2 astore_0
3 aload_0
// 保存返回值到 1 的位置
4 astore_1
5 ldc #3 <new>
7 astore_0
// 从 1 的位置加载并返回
8 aload_1
9 areturn
10 astore_2
11 ldc #3 <new>
13 astore_0
14 aload_2
15 athrow

同步指令

方法级的同步

隐式存在,无须通过字节码指令来控制,虚拟机根据方法的 ACC_SYNCHRONIZED 访问标志来判断加锁。

方法内指定指令序列的同步

monitorentermonitorexit

在虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

使用 monitorenter 指令请求进入临界区时,如果当前对象的监视器计数器为 0 ,或监视器计数器为 1 且持有当前监视器的线程为自己,则进入,否则进行等待。

使用 monitorexit 退出临界区。发生异常的时候务必确保要释放锁,因此会自动添加异常表处理异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private final Object obj = new Object();
private int i = 0;
public void sy() {
synchronized (obj) {
i++;
}
}

// 字节码
0 aload_0
1 getfield #3 <com/pl/Demo18.obj : Ljava/lang/Object;>
4 dup
5 astore_1
// 将监视器计数器置 1
6 monitorenter
7 aload_0
8 dup
9 getfield #4 <com/pl/Demo18.i : I>
12 iconst_1
13 iadd
14 putfield #4 <com/pl/Demo18.i : I>
17 aload_1
// 将监视器计数器置 0
18 monitorexit
19 goto 27 (+8)
22 astore_2
23 aload_1
// 将监视器计数器置 0,确保发生异常也要释放锁
24 monitorexit
25 aload_2
26 athrow
27 return

对应产生两项异常表,一项用以处理临界区代码,一项用以处理异常处理代码

Nr. Start PC End PC Handler PC Catch Type
0 7 19 22 cp_info #0
any
1 22 25 22 cp_info #0
any

类的加载

基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

Loading -> Linking(Verification -> Preparation -> Resolution) -> Inititalization -> Using -> Unloading

加载(Loading)

将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象,即查找并加载类的二进制数据,生成 Class 的实例。

  1. 获取类的二进制数据流
  2. 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  3. 创建 java.lang.Class 类的实例,封装类位于方法区内的数据结构。作为方法区这个类的各种数据的访问入口

注: java.lang.Class 的构造方法私有,由 JVM 调用。

链接(Linking)

验证(Verification)

  1. 格式检查:在 Loading 阶段执行。包括验证是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等

  2. 语义检查:是否所有的类都有父类的存在(除了 Object 类)、是否一些被定义为 final 的方法或者类被重写或继承、非抽象类是否实现了所有抽象方法或者接口方法、 是否存在签名相同的方法等

  3. 字节码验证:是否会跳转到一条不存在的指令、函数的调用是否传递了正确类型的参数、变量的赋值是不是给了正确的数据类型等。栈映射帧(StackMapTable),用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。

  4. 符号引用验证:在 Resolution 阶段执行。检查常量池中指向的类或者方法是否存在

准备(Preparation)

类的 static 变量分配内存,并将其初始化为默认值,若字段属性表中存在 ConstantValue 属性(即被 static final 修饰且赋值为字面量),则被初始化为 ConstantValue 属性所指定的初始值。此环节不会执行具体的代码。

1
2
3
4
5
public static int a = 12;
public static final int b = 22;
public static final String s1 = "hello";
public static final String s2 = new String("hi");
// b 和 s1 有 ConstantValue 属性

解析(Resolution)

执行完初始化之后再执行。将类、接口、字段和方法的符号引用转为直接引用。

初始化(Inititalization)

  • 线程安全性

执行类的 方法,它由类 static 成员的显式赋值语句以及 static 代码块合并而成。

虚拟机会保证一个类的 方法在多线程环境中被正确地加锁、同步。可能造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class StaticA {
static {
try {
// 此时会对 StaticA 类的 <cinit> 方法加锁
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}

class StaticB {
static {
try {
// 此时会对 StaticB 类的 <clinit> 方法加锁
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}


public class StaticDeadLockMain extends Thread {
private char flag;

public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}

@Override
public void run() {
try {
Class.forName("Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}

public static void main(String[] args) throws InterruptedException {
// 造成死锁
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}

  • 主动使用与被动使用

一个类或接口在初次主动使用前,进行初始化。

主动使用:

  1. 创建一个类的实例,如使用 new 关键字、反射(**Class.forName()**)、克隆、反序列化
  2. 调用类的静态方法,即字节码执行 invokestatic
  3. 使用类、接口的静态字段(static final 修饰且赋值为字面量的除外)时,即字节码执行 getstaticputstatic
  4. 初始化子类时,若其父类还未进行初始化,则初始化父类(并不会初始化它所实现的接口,即一个父接口并不会因为它的子接口或者实现类的初始化而初始化)
  5. 如果一个接口定义了 default 方法,那么直接或者间接实现该接口的类的初始化时,会先初始化该接口
  6. 当虚拟机启动时,会先初始化主类(用户指定执行的包含 main() 方法的类)
  7. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class AnyClass {
public static void main(String[] args) {
int i1 = SubClass.a; // 没有输出
System.out.println("------");
int i2 = SubClass.b; // 输出 Interface1
System.out.println("------");
int i3 = SubClass.sba; // 输出 SuperClass 、Interface2 和 SubClass
}
}

class SuperClass {
static int spa = 1;
static final int bb = new Random().nextInt();
static {
System.out.println("SuperClass");
}
}

class SubClass extends SuperClass implements Interface1 {
static int sba = 1;
static {
System.out.println("SubClass");
}
}

interface Interface1 {
AnyClass s = new AnyClass() {
{
System.out.println("Interface1");
}
};
int a = 1;
int b = new Random().nextInt();
}

被动使用:主动使用之外的情况都是被动使用,不会引起类的初始化。如子类引用父类的静态变量不会导致子类初始化、通过数组定义类引用不会触发此类的初始化、引用常量不会触发此类或接口的初始化、调用 ClassLoader 类的 loadClass() 方法加载一个类等。

使用(Using)

卸载(Unloading)

启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm 和 jls 规范)。

被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接访问的到,其达到 unreachable 的可能性极小。

被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。

因此,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的

类加载器

JVM ClassLoader

任何类都由加载它的类加载器和这个类本身共同确定唯一性,即不同的类加载器可以存在类的包名完全相同的两个类。

引导类加载器(由 C/C++ 实现):启动类加载器

自定义加载器(Java 语言实现,继承于 ClassLoader 类):扩展类加载器、系统类加载器、用户自定义类加载器

逻辑上启动类加载器是扩展类加载器的父类加载器,扩展类加载器是系统类加载器的父类加载器,但物理上并不是继承关系,而是通过在 构造器中传入的名为 parent 的字段进行维护。

ClassLoader 类

ClassLoader 是一个抽象类,但其内部没有抽象方法,主要方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 返回该类加载器的超类加载器
public final classLoader getParent() {......}

// 采用双亲委派模式加载指定的类
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) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

// 当前加载器的父类递归加载失败
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

// 在 loadClass() 被调用,查找指定名称的类,由子类重写此方法。一般情况下,在自定义类加载器时,会重写 findClass() 方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用 defineClass() 方法生成类的 Class 对象
protected Class<?> findclass(String name) throws ClassNotFoundException {}

// 将 byte 字节流解析成 JVM 能够识别的 Class 对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len){}

Class.forName() 是主动使用,会初始化类;classLoader.loadClass() 是被动使用,不会导致类的初始化。

双亲委派的优劣

一个类加载器收到类加载请求,会递归交给父加载器,直至到达顶层的启动类加载器。引导类加载器可以完成类加载,则成功返回,否则递归交给子加载器尝试加载。重写 loadClass() 可以破坏双亲委派机制。自定义类加载器、系统类加载器、扩展类加载器最终都必须调用 java.lang.ClassLoader.defineClass() 方法,该方法会执行 preDefineClass() 接口以保护 JDK 核心类库。

优势:

  • 避免类的重复加载,确保一个类的全局唯一性
  • 保护程序安全,防止核心 API 被任意篡改

弊端:

  • 顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(基础的类由上层的加载器进行加载,但若基础类型要调用回用户的代码就会出现问题)

破坏双亲委派机制:由于顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类,故有时需要破坏,有三种方法:

  1. JDK1.2 之前有 loadClass(),但没有双亲委派机制

  2. 线程上下文类加载器,通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,使得父类加载器以其为中介,能够请求子类加载器完成类加载,违背了双亲委派模型的一般性原则

  3. 用户对程序动态性的追求。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

    代码热替换的实现:创建新的 classLoader 加载替换后的类

沙箱安全机制

JDK1.0 时期:将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信任的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的 Java 实现中,安全依赖于沙箱机制。

JDK1.1 时期:JDK1.0 中严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限。

JDK1.2 时期:再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

JDK1.6 时期:当前最新的安全机制实现,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限 。存在于域中的类文件具有了当前域的全部权限,

沙箱安全机制

自定义类的加载器

作用

  • 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包。再比如:Tomcat 这类 Web 应用服务器,内部自定义了好几种类加载器,用于隔离同一个 Web 应用服务器上的不同应用程序。(类的仲裁→类冲突)

  • 修改类加载的方式:类的加载模型并非强制,除 Bootstrap 外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

  • 扩展加载源:比如从数据库、网络、甚至是电视机机顶盒进行加载

  • 防止源码泄漏:Java 代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

注意

在做类型转换时,只有两个类型都是由同一个加载器所加载,才能进行类型转换,否则转换时会发生异常。

实现

  • 方式一:重写 loadClass() 方法(不推荐)
  • 方式二:重写 findClass() 方法(推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class MyClassLoader extends ClassLoader {
private String byteCodePath;

public MyClassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}

public MyClassLoader(ClassLoader parent, String byteCodePath) {
super(parent);
this.byteCodePath = byteCodePath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
// 获取字节码文件的完整路径
String fileName = byteCodePath + name + ".class";
// 获取一个输入流
bis = new BufferedInputStream(new FileInputStream(fileName));
// 获取一个输出流
baos = new ByteArrayOutputStream();
// 具体读入数据并写出的过程
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
// 获取内存中的完整的字节数组的数据
byte[] byteCodes = baos.toByteArray();
// 调用defineClass(),将字节数组的数据转换为Class的实例。
Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}

JDK 9 的变化

扩展机制被移除。扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader)。可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取。

平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。

启动类加载器由 Java 代码实现(以前是 C++ 实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到 BootClassLoader 实例。

JDK9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件),当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。