JVM 字节码与类的加载
案例分析
非静态成员变量赋值顺序:默认初始化 -> 父类
1 | public class Demo16 { |
以上代码对应字节码如下:
1 | // Father 的 <init> |
Class 文件结构
按顺序依次为:
- magic number(u4):0xCAFEBABE
- Class 文件版本(u4):副版本 + 主版本。向下兼容。
- 常量池:count(u2)、具体数组(长度为 count - 1)
- 访问标志(u2)
- 类索引(u2)、父类索引(u2)
- 接口索引集合:count(u2)、具体数组
- 字段表集合:count(u2)、具体数组
- 方法表集合:count(u2)、具体数组
- 属性表集合: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 |
方法表
描述当前类或接口声明的方法和编译器添加的方法(如
长度 | 含义 |
---|---|
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_0
、xload_1
、xload_2
、xload_3
(其中 x 表示此次操作的数据类型,为 i、l、f、d、a(引用))
1 | void load(int num, Object obj, long count, boolean flag, int[] arr) { |
常量入栈指令
将常量加载到操作数栈。
特定的常量入栈:
iconst_m1
(表示 -1)、iconst_0
、iconst_1
、iconst_2
、iconst_3
、iconst_4
、iconst_5
lconst_0
、lconst_1
fconst_0
、fconst_1
、fconst_2
dconst_0
、dconst_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 | void pushConstLdc() { |
出栈装入局部变量表指令
将一个数值从操作数栈弹出后存储到指定索引的局部变量表位置。
xstore
、xstore_0
、xstore_1
、xstore_2
、xstore_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 | public static void main(String[] args) { |
所有的算术指令包括
- 加法指令:
iadd
、ladd
、fadd
、dadd
- 减法指令:
isub
、lsub
、fsub
、dsub
- 乘法指令:
imu
、lmu
、fmul
、dmul
- 除法指令:
idiv
、ldiv
、fdiv
、ddiv
- 求余指令:
irem
、lrem
、frem
、drem
(remainder:余数) - 取反指令:
ineg
、lneg
、fneg
、dneg
- 自增指令:
iinc
(long 类型的 ++ 实际上是 + 1 的语法糖,字节码是一样的) - 位运算指令:
- 位移指令:
ishl
、ishr
、iushr
、lshl
、lshr
、lushr
- 按位或指令:
ior
、lor
- 按位与指令:
iand
、land
- 按位异或指令:
ixor
、lxor
- 位移指令:
- 比较指令:
dcmpg
、dcmp1
、fcmpg
、fcmp1
、lcmp
1 | public void method() { |
前++与后++的问题
如果不涉及赋值操作,从字节码角度看是一样的
1 | public void method6() { |
涉及到赋值操作时
- i++ 是先赋值后运算
- ++i 是先运算后赋值
1 | public void method7() { |
比较指令
用于比较栈顶两个元素的大小,并将比较结果入栈。
比较指令有:dcmpg
,dcmpl
、fcmpg
、fcmpl
、lcmp
。
对于 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
对应的指令为:i2l
、i2f
、i2d
、l2f
、l2d
、f2d
。
代码中的 byte、short、char 转 int 实际上仅仅只是复制,而 byte、short、char 转 long 、 float 、double 实际上是 int 转 long 、 float 、double 。
int 、long 转 float 可能丢失几个最低有效位的值,但不会抛异常。
1 | void wideningNumericConversions(int i) { |
窄化类型转换
double -> float -> long -> int(byte、short、char)
对应的指令为:d2
、d2l
、d2f
、f2i
、f2l
、l2i
。
从 int 类型至 byte、short、char 类型以及 byte、short、char 之间互相转换,对应的指令为:i2b
、i2c
、i2s
。
尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是永远不可能导致虚拟机抛出运行时异常。
1 | void narrowingNumericConversion(int i, long l) { |
将 float
或 double
窄化转换为整数类型 int
或 long
时
- 如果浮点值是 NaN,那么转换为
int
或long
类型的 0 - 如果浮点值不是无穷大的话,丢掉小数位取整,若得到的整数超出了目标类型表示范围,将转换为目标类型能表示的最大或者最小整数
将 double
窄化转换为 float
时,通过向最接近数舍入模式舍入一个可以使用 float 类型表示的数字
- 如果转换结果的绝对值太小而无法使用 float 来表示,将返回 float 类型的正负零。
- 如果转换结果的绝对值太大而无法使用 float 来表示,将返回 float 类型的正负无穷大。
- 对于double 类型的 NaN 值将按规定转换为 float 类型的 NaN 值。
1 | void d2i() { |
对象创建与访问指令
对象和数组创建
new
:接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。newarray
:创建基本类型数组anewarray
:创建引用类型数组multianewarray
:创建多维数组
1 | public void newArray() { |
字段访问
getstatic
、putstatic
:访问类字段(static 字段)getfield
、putfield
:访问类实例字段(非 static 字段)
get 即入栈,put 即出栈
1 | public class NewTest { |
数组操作
baload
(byte 和 boolean)、caload
、saload
、iaload
、laload
、faload
、daload
、aaload
:把数组元素加载到操作数栈bastore
(byte 和 boolean)、castore
、sastore
、iastore
、lastore
、fastore
、dastore
、aastore
:将操作数栈的值存储到数组元素中arraylength
:弹出栈顶的数组元素,获取数组的长度,将长度压入栈。
1 | public void setArray() { |
类型检查指令
instanceof
:判断给定对象是否是某一个类的实例,并将判断结果压入操作数栈
checkcast
:检查类型强制转换是否可以进行。如果可以进行,那么 checkcast 指令不会改变操作数栈,否则抛出ClassCastException
异常
1 | String checkCast(Object obj) { |
方法调用与返回指令
方法调用
- invokevirtual:调用所有的虚方法和
final
方法(final
方法是非虚方法) - invokestatic:调用 static 方法
- invokespecial:调用 private 方法、constructor 方法、super 关键字调用的方法
- invokeinterface:调用接口方法
- invokedynamic
1 | public class InterfaceMethodTest { |
方法返回
ireturn
(用于 boolean、byte、char、 short 和 int 类型)、lreturn
、freturn
、dreturn
和areturn
return
用于 void 的方法、实例初始化方法以及类和接口的类初始化方法
1 | double returnF(int i) { |
操作数栈指令
pop
、pop2
:将 一个 slot(4 字节)或两个 slot(8 字节,2 个 4 字节的数据或一个 8 字节的数据)的元素从栈顶弹出丢弃dup
、dup2
:复制栈顶一个或两个 slot 的数据dup_x1
、dup2_x1
、dup_x2
、dup2_x2
:复制栈顶一个或两个 slot 的数据并将其插入到栈顶下的某个位置dup_x1
:放在栈顶的 2 个 slot 下面dup2_x1
:放在栈顶的 3 个 slot 下面dup_x2
:放在栈顶的 3 个 slot 下面dup2_x2
:放在栈顶的 4 个 slot 下面
swap
:将栈最顶端的两个 slot 交换。虚拟机没有提供交换两个 64 位数据类型数值的指令nop
:字节码为0x00,表示什么都不做。这条指令一般可用于调试、占位等
1 | long add() { |
控制转移指令
条件跳转
常与比较指令配合使用。
ifeq
,iflt
,ifle
,ifne
,ifgt
,ifge
,ifnull
,ifnonnull
:这些指令都接收两个字节的操作数,用于计算跳转的位置(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 | public void compare() { |
比较跳转
比较指令和跳转指令的结合体。
if_icmpeq
、if_icmpne
、if_icmplt
、if_icmpgt
、if_icmple
、 ificmpge
、if_acmpeq
、if_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 | public int switch1(int select) { |
lookupswitch
用于 case 值不连续,会自动对 case 值进行排序,逐一匹配。
用 String 类型作为 case 时,先进行哈希值的比较,若哈希值相同,再进行 equals() 比较。
1 | public int switch2(int select) { |
1 | public void switch3(String s) { |
无条件跳转
goto
:接收两个字节的操作数,组成带符号整数,用于指定指令的偏移量
goto_w
:接收四个宇节的操作数,可以表示更大的地址范围
jsr
、jsr_w
、ret
:主要用于 try-finally 语句,且已经被虚拟机逐渐废弃
1 | public void whileInt() { |
异常处理指令
抛出异常
athrow
:显式抛出异常 (throw 语句)。在抛异常时,虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
1 | public void throwOne(int i) { |
处理异常
处理异常(catch
语句)不是由字节码指令来实现的(早期使用 jsr
、ret
指令),而是采用异常表来完成的。定义了一个 try-catch 或者 try-finally 的异常处理,就会创建一个异常表项,try-catch 语句创建的异常表项匹配相应的异常,try-finally 语句创建的异常表项匹配所有异常。
产生异常时,根据异常表寻找一个匹配的处理,如果没有找到,强制结束当前方法并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程(如 main 线程)里抛出,将会导致 JVM 自己终止。
finally
的实现原理是根据不发生异常、发生异常并被 catch
语句捕获、发生异常未被 catch
语句捕获三种情况在适当的位置插入重复的字节码保证 finally
语句的指令一定被执行。
异常表
- 起始位置
- 结束位置
- 处理代码的位置
- 被捕获的异常类在常量池中的索引
1 | public void testThrow() { |
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 | public static String func() { |
同步指令
方法级的同步
隐式存在,无须通过字节码指令来控制,虚拟机根据方法的 ACC_SYNCHRONIZED 访问标志来判断加锁。
方法内指定指令序列的同步
monitorenter
和 monitorexit
在虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
使用 monitorenter
指令请求进入临界区时,如果当前对象的监视器计数器为 0 ,或监视器计数器为 1 且持有当前监视器的线程为自己,则进入,否则进行等待。
使用 monitorexit
退出临界区。发生异常的时候务必确保要释放锁,因此会自动添加异常表处理异常。
1 | private final Object obj = new Object(); |
对应产生两项异常表,一项用以处理临界区代码,一项用以处理异常处理代码
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 的实例。
- 获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
- 创建
java.lang.Class
类的实例,封装类位于方法区内的数据结构。作为方法区这个类的各种数据的访问入口
注: java.lang.Class
的构造方法私有,由 JVM 调用。
链接(Linking)
验证(Verification)
格式检查:在 Loading 阶段执行。包括验证是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等
语义检查:是否所有的类都有父类的存在(除了
Object
类)、是否一些被定义为final
的方法或者类被重写或继承、非抽象类是否实现了所有抽象方法或者接口方法、 是否存在签名相同的方法等字节码验证:是否会跳转到一条不存在的指令、函数的调用是否传递了正确类型的参数、变量的赋值是不是给了正确的数据类型等。栈映射帧(StackMapTable),用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。
符号引用验证:在 Resolution 阶段执行。检查常量池中指向的类或者方法是否存在
准备(Preparation)
为类的 static
变量分配内存,并将其初始化为默认值,若字段属性表中存在 ConstantValue 属性(即被 static final
修饰且赋值为字面量),则被初始化为 ConstantValue 属性所指定的初始值。此环节不会执行具体的代码。
1 | public static int a = 12; |
解析(Resolution)
执行完初始化之后再执行。将类、接口、字段和方法的符号引用转为直接引用。
初始化(Inititalization)
- 线程安全性
执行类的 static
成员的显式赋值语句以及 static
代码块合并而成。
虚拟机会保证一个类的
1 | class StaticA { |
- 主动使用与被动使用
一个类或接口在初次主动使用前,进行初始化。
主动使用:
- 创建一个类的实例,如使用
new
关键字、反射(**Class.forName()**)、克隆、反序列化 - 调用类的静态方法,即字节码执行
invokestatic
- 使用类、接口的静态字段(
static final
修饰且赋值为字面量的除外)时,即字节码执行getstatic
或putstatic
- 初始化子类时,若其父类还未进行初始化,则初始化父类(并不会初始化它所实现的接口,即一个父接口并不会因为它的子接口或者实现类的初始化而初始化)
- 如果一个接口定义了
default
方法,那么直接或者间接实现该接口的类的初始化时,会先初始化该接口 - 当虚拟机启动时,会先初始化主类(用户指定执行的包含 main() 方法的类)
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)
1 | class AnyClass { |
被动使用:主动使用之外的情况都是被动使用,不会引起类的初始化。如子类引用父类的静态变量不会导致子类初始化、通过数组定义类引用不会触发此类的初始化、引用常量不会触发此类或接口的初始化、调用 ClassLoader 类的 loadClass() 方法加载一个类等。
使用(Using)
卸载(Unloading)
启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm 和 jls 规范)。
被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接访问的到,其达到 unreachable 的可能性极小。
被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。
因此,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。
类加载器
任何类都由加载它的类加载器和这个类本身共同确定唯一性,即不同的类加载器可以存在类的包名完全相同的两个类。
引导类加载器(由 C/C++ 实现):启动类加载器
自定义加载器(Java 语言实现,继承于 ClassLoader
类):扩展类加载器、系统类加载器、用户自定义类加载器
逻辑上启动类加载器是扩展类加载器的父类加载器,扩展类加载器是系统类加载器的父类加载器,但物理上并不是继承关系,而是通过在 构造器中传入的名为 parent 的字段进行维护。
ClassLoader 类
ClassLoader 是一个抽象类,但其内部没有抽象方法,主要方法如下:
1 | // 返回该类加载器的超类加载器 |
Class.forName() 是主动使用,会初始化类;classLoader.loadClass() 是被动使用,不会导致类的初始化。
双亲委派的优劣
一个类加载器收到类加载请求,会递归交给父加载器,直至到达顶层的启动类加载器。引导类加载器可以完成类加载,则成功返回,否则递归交给子加载器尝试加载。重写 loadClass() 可以破坏双亲委派机制。自定义类加载器、系统类加载器、扩展类加载器最终都必须调用 java.lang.ClassLoader.defineClass() 方法,该方法会执行 preDefineClass() 接口以保护 JDK 核心类库。
优势:
- 避免类的重复加载,确保一个类的全局唯一性
- 保护程序安全,防止核心 API 被任意篡改
弊端:
- 顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(基础的类由上层的加载器进行加载,但若基础类型要调用回用户的代码就会出现问题)
破坏双亲委派机制:由于顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类,故有时需要破坏,有三种方法:
JDK1.2 之前有 loadClass(),但没有双亲委派机制
线程上下文类加载器,通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,使得父类加载器以其为中介,能够请求子类加载器完成类加载,违背了双亲委派模型的一般性原则
用户对程序动态性的追求。如:代码热替换(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 | public class MyClassLoader extends ClassLoader { |
JDK 9 的变化
扩展机制被移除。扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader)。可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取。
平台类加载器和应用程序类加载器都不再继承自 java.net.URLClassLoader,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader。
启动类加载器由 Java 代码实现(以前是 C++ 实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到 BootClassLoader 实例。
JDK9 时基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数十个 JMOD 文件),当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。