根据 JVM 规范,类文件结构如下
ClassFile {
u4 magic;
u2 minor_version; // 小版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池
cp_info constant_pool[constant_pool_count-1];
u2 access_flags; // 访问修饰 public project private
u2 this_class; // 包名 类名
u2 super_class; // 父类信息
u2 interfaces_count; // 接口信息
u2 interfaces[interfaces_count];
u2 fields_count; //类中的成员变量,静态变量
field_info fields[fields_count];
u2 methods_count; // 类中成员方法,静态方法
method_info methods[methods_count];
u2 attributes_count; // 附加的属性信息
attribute_info attributes[attributes_count];
}
0~3 字节,表示它是否是【class】类型的文件
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
(咖啡宝贝?:)
4~7 字节,表示类的版本 00 34(52) 表示是 Java 8
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
8-9字节表示常量池的长度(constant_pool_count)
00 23 (35)表示常量池有 #1~#34项,#0项不计入,也没有值;
第一个字节表示常量类型,一共有 17 种类型:每种类型有着完全独立的数据结构
第#1项 0a 表示一个 Method 信息,00 06 和 00 15(21) 表示它引用了常量池中 #6 和 #21 项来获得这个方法的【所属类】和【方法名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
第#2项 09 表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和 # 23 项来获得这个成员变量的【所属类】和【成员变量名】
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项
0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【<init>
】表示构造方法
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【()V】其实就是表示无参、无返回值
0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
第#21项 0c 表示一个 【名+类型】,00 07 00 08 引用了常量池中 #7 #8 两项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#22项 07 表示一个 Class 信息,00 1d(29) 引用了常量池中 #29 项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
第#23项 0c 表示一个 【名+类型】,00 1e(30) 00 1f (31)引用了常量池中 #30 #31 两项
0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
第#28项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/Object】
0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
第#29项 01 表示一个 utf8 串,00 10(16) 表示长度,是【java/lang/System】
0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
第#30项 01 表示一个 utf8 串,00 03 表示长度,是【out】
0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
第#31项 01 表示一个 utf8 串,00 15(21) 表示长度,是【Ljava/io/PrintStream;】
0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
21 表示该 class 是一个类,公共的
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
05 表示根据常量池中 #5 找到本类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
06 表示根据常量池中 #6 找到父类全限定名
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示接口的数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示成员变量数量,本类为 0
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
字节码中表示类型信息的方法
表示方法数量,本类为 2
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
一个方法由 访问修饰符,名称,参数描述,方法属性数量,方法属性组成
00 01 表示附加属性数量
00 13 表示引用了常量池 #19 项,即【SourceFile】
00 00 00 02 表示此属性的长度
00 14 表示引用了常量池 #20 项,即【HelloWorld.java】
0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
0001120 00 00 02 00 14
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
public cn.itcast.jvm.t5.HelloWorld(); 构造方法的字节码指令
2a b7 00 01 b1
另一个是 public static void main(java.lang.String[]); 主方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
自己分析类文件结构太麻烦了,Oracle 提供了 javap 工具来反编译 class 文件:
javap -v class文件路径
D:\CodeProject\Java\java_virtual_machine\target\classes\com\rainsun\d3_class_structure> javap -v .\d1_HelloWorld.class
Classfile /D:/CodeProject/Java/java_virtual_machine/target/classes/com/rainsun/d3_class_structure/d1_HelloWorld.class
Last modified 2023年12月25日; size 604 bytes
SHA-256 checksum f4c26de1e0291f2f0984d894624592a6287a89c87805cc57f0b2e658b5a796c7
Compiled from "d1_HelloWorld.java"
public class com.rainsun.d3_class_structure.d1_HelloWorld
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // com/rainsun/d3_class_structure/d1_HelloWorld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // hello world
#14 = Utf8 hello world
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // com/rainsun/d3_class_structure/d1_HelloWorld
#22 = Utf8 com/rainsun/d3_class_structure/d1_HelloWorld
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/rainsun/d3_class_structure/d1_HelloWorld;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 d1_HelloWorld.java
{
public com.rainsun.d3_class_structure.d1_HelloWorld();
descriptor: ()V
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "d1_HelloWorld.java"
package com.rainsun.d3_class_structure;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
public class d2_method_runflow {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
D:\CodeProject\Java\java_virtual_machine\target\classes\com\rainsun\d3_class_structure> javap -v .\d2_method_runflow.class
Classfile /D:/CodeProject/Java/java_virtual_machine/target/classes/com/rainsun/d3_class_structure/d2_method_runflow.class
Last modified 2023年12月26日; size 675 bytes
SHA-256 checksum 090cee1c8efae1a0d3b54af310b799f33efc90463f218ea25bdb4cea5aef56ef
Compiled from "d2_method_runflow.java"
public class com.rainsun.d3_class_structure.d2_method_runflow
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #22 // com/rainsun/d3_class_structure/d2_method_runflow
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/lang/Short
#8 = Utf8 java/lang/Short
#9 = Integer 32768
#10 = Fieldref #11.#12 // java/lang/System.out:Ljava/io/PrintStream;
#11 = Class #13 // java/lang/System
#12 = NameAndType #14:#15 // out:Ljava/io/PrintStream;
#13 = Utf8 java/lang/System
#14 = Utf8 out
#15 = Utf8 Ljava/io/PrintStream;
#16 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#17 = Class #19 // java/io/PrintStream
#18 = NameAndType #20:#21 // println:(I)V
#19 = Utf8 java/io/PrintStream
#20 = Utf8 println
#21 = Utf8 (I)V
#22 = Class #23 // com/rainsun/d3_class_structure/d2_method_runflow
#23 = Utf8 com/rainsun/d3_class_structure/d2_method_runflow
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 LocalVariableTable
#27 = Utf8 this
#28 = Utf8 Lcom/rainsun/d3_class_structure/d2_method_runflow;
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 args
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 a
#34 = Utf8 I
#35 = Utf8 b
#36 = Utf8 c
#37 = Utf8 SourceFile
#38 = Utf8 d2_method_runflow.java
{
public com.rainsun.d3_class_structure.d2_method_runflow();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rainsun/d3_class_structure/d2_method_runflow;
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 #9 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #16 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 10
line 12: 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: "d2_method_runflow.java"
首先加载main方法所在的类,加载类需要将类中的常量池放入加载到运行时常量池
(stack=2,locals=4)
绿色:局部变量表,有 4 个槽
蓝绿色:操作数栈,深度为 2
将操作数栈顶数据弹出,存入局部变量表的 slot 1
将操作数栈顶数据弹出,存入局部变量表的 slot 2
加载 slot 1 中的数据到操作数栈
执行加法
将栈顶的执行结果存入局部变量表的 slot 3
getstatic获取一个成员变量的引用,将该对象加载到堆中,并将堆中的引用放入操作数栈
将操作数变量表中的 slot 3 位置的变量放入操作数栈,传递给out对象
调用println函数
public class Demo3_9 {
public Demo3_9() { } // 构造方法
private void test1() { } // 私有方法
private final void test2() { } // final 方法
public void test3() { } // 普通 public 成员方法
public static void test4() { } // 静态方法
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}
字节码:
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
Demo3_9 d = new Demo3_9();
<init>
”😦)V (会消耗掉栈顶一个引用),另一个要配合 astore_1 赋值给局部变量test1
test2
最终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
test3
:
普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态
成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
比较有意思的是 d.test4(); 是通过【对象引用】调用一个静态方法,可以看到在调用 invokestatic 之前执行了 pop 指令,把【对象引用】从操作数栈弹掉了
还有一个执行 invokespecial 的情况是通过 super 调用父类方法
(HSDB工具的使用)
对象的内存结构是由基础的16字节加上存储属性所花费的字节组成的
16字节中,前8字节为 MarkWord 用于计算类的 hashcode,后 8 字节为对象的Class指针
查看该对象的 class 指针指向的内存地址,可以找到其中关联一个 vtable(虚函数表),里面存储着虚方法。从 Class 的起始地址开始算,偏移 0x1b8 就是 vtable 的起始地址
通过 Tools -> Class Browser 查看每个类的方法定义,比较可知
Dog - public void eat() @0x000000001b7d3fa8
Animal - public java.lang.String toString() @0x000000001b7d35e8;
Object - protected void finalize() @0x000000001b3d1b10;
Object - public boolean equals(java.lang.Object) @0x000000001b3d15e8;
Object - public native int hashCode() @0x000000001b3d1540;
Object - protected native java.lang.Object clone() @0x000000001b3d1678;
当执行 invokevirtual 指令时,
try catch 原理:
多个 single-catch 块的情况:
因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
监听[2, 5)之间的字节码,如果出现了异常就转到对应的 target 行进行处理
multi-catch 的情况
catch 多个不同类型的异常,那么 target 跳转的行就会相同
finally原理:
finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
finally 出现了 return
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
字节码的角度分析:
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> slot 0 (从栈顶移除了)
3: bipush 20 // <- 20 放入栈顶
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
因为 finally 块中的代码被插入了所有可能的流程(当然包括 try 流程)。所以在try 中代码执行后,执行了 finally 中的代码,由于最后执行的 finally 代码,finally 中的 20 被放入栈顶了,最后 return 就是栈顶的 20。
这也说明了 finally 的代码是在 return 前插入的
public class Demo3_12_1 {
public static void main(String[] args) {
int result = test();
System.out.println(result);//20
}
public static int test() {
try {
int i = 1/0;
return 10;
} finally {
return 20;
}
}
}
finally 对返回值影响
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result); // 10
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
字节码:
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-> slot 0
3: iload_0 // <- slot 0 的10加载到栈顶
4: istore_1 // 栈顶10暂存到 solt 1,目的是为了固定返回值
5: bipush 20 // 20 放入栈顶
7: istore_0 // 20 放入solt 0
8: iload_1 // solt 1 里的 10 加载到栈顶
9: ireturn // 返回栈顶的 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable:
line 9: 0
line 11: 3
line 13: 5
line 11: 8
line 13: 10
line 14: 14
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ int ]
stack = [ class java/lang/Throwable ]
}
如果 finally 中没有 return,对返回值的修改是没有影响的,因为在return前会对返回值进行暂存
同时由于 finally 中没有 return 抛出异常的 athrow 也不会被吞掉
public class Demo3_13 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
monitorenter 指令对对象进行加锁
monitorexit 指令对对象进行解锁,在正常执行和异常处理的部分都会放置一份,确保即使出现异常也可以解锁成功
所谓的 语法糖,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成和转换的一些代码
编译器转换的结果直接就是 class 字节码,只是为了便于阅读,这里给出了 几乎等价 的 java 源码方式,
原本的源码->优化->源码优化后生成的字节码->反编译生成优化后的源码
public class Candy1 {
}
//编译成class后的代码:
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
}
}
从JDK 5开始:
之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
//1:
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
// 2:
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue;
}
}
泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:
public class Candy3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是 List.add(Object e)
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}
擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息
使用反射,仍然能够获得这些信息:
public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
System.out.println("原始类型 - " + parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < arguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
}
}
}
输出:
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
方法重写时对返回值分两种情况:
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
对于子类,java 编译器会做如下处理:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,
// 源代码:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
// 转换后:额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
引用局部变量的匿名内部类,源代码:
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
转换后代码:
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x; // 外部的局部变量变成了类的成员变量,无法感知外部的变化,所以引用的外部变量必须是 final 的
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建
Candy11$1
对象时,将 x 的值赋值给了 Candy11$1
对象的 val$x
属性,所以 x 不应该再发生变化了,如果变化,那么 val$x
属性没有机会再跟着一起变化
通过类加载器将类的字节码载入方法区,内部采用 C++的 instanceKlass 描述 java 类,它的重要的 field(域)有:
如果这个类还有父类没有加载,则加载父类
加载和连接可能是交替运行的
注意
验证字节码是否符合JVM的规范,进行安全性检查
为 static 变量分配空间,设置默认值
将常量池中的符号引用解析为直接引用
没解析之前仅仅是一组符号,而解析后,这些符号被替换成了具体的地址
初始化即调用 <cinit>()V
,虚拟机会保证这个类的『构造方法』的线程安全
<clinit>()
并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物;
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
<clinit>()
方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()
方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()
方法的类型肯定是java.lang.Object
初始化发生的时机:
概括得说,类初始化是【懒惰的】
不会导致类初始化的情况
利用类的类加载机制,JVM保证了类加载过程是线程互斥的;
通过静态内部类实现的多线程环境中的单例模式:
package com.rainsun.d3_class_structure;
public class d4_lazySingleton {
public static void main(String[] args) {
Singleton.test(); // 仅仅输出 test
// Singleton.getInstance();
}
}
class Singleton{
private Singleton(){}
public static void test(){
System.out.println("test");
}
private static class LazyHolder{
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("Lazy Holder init");
}
}
public static Singleton getInstance(){
return LazyHolder.SINGLETON;
}
}
保证了实例只会被创建一次,而且也只会在第一次调用的时候使用互斥机制,避免了每次加锁的低效问题。
JVM有不同层级的类加载器,加载不同类型的类,JDK 8 为例:
启动类加载器->扩展类加载器->应用程序加载器
如果上级类加载器都没有加载,下级才可以加载
加载JAVA_HOME/jre/lib 目录下的类
可以使用 java -Xbootclasspath:<new bootclasspath>
追加路径,交给启动类加载器加载
<new bootclasspath>
加载 JAVA_HOME/jre/ext 目录下的类
它的类加载器是 sun.misc.Launcher$ExtClassLoader
所谓双亲委派就是指调用类加载器的loadClass方法时,查找类的规则:首先委派上级优先进行类的加载,上级没有这个类才有本级进行加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级进行 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 没有上级了,说明当前类加载器是 ExtClassLoader,则委派 BootstrapClassLoader
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();
// 4. 如果上一层找不到,则调用 findClass方法在本层寻找
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
public class d5_classLoad {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(d5_classLoad.class.getClassLoader());
Class<?> aClass = d5_classLoad.class.getClassLoader().loadClass("com.rainsun.d3_class_structure.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
sun.misc.Launcher$AppClassLoader
//1 处, 开始查看已加载的类,结果没有sun.misc.Launcher$AppClassLoader
// 2 处,委派上级sun.misc.Launcher$ExtClassLoader.loadClass()
sun.misc.Launcher$ExtClassLoader
// 1 处,查看已加载的类,结果没有sun.misc.Launcher$ExtClassLoader
// 3 处,没有上级了,则委派 BootstrapClassLoader
查找BootstrapClassLoader
是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有sun.misc.Launcher$ExtClassLoader
// 4 处,调用自己的 findClass
方法,是在sun.misc.Launcher$AppClassLoader
的 // 2 处sun.misc.Launcher$AppClassLoader
// 4 处,调用它自己的 findClass 方法,在 classpath 下查找,找到了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
package com.rainsun.d3_class_structure;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class d6_classLoader {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("H");
Class<?> aClass1 = classLoader.loadClass("H");
System.out.println(aClass1 == aClass1); // true
aClass1.newInstance(); // 初始化了类,执行了static 代码块的语句:H init
}
}
class MyClassLoader extends ClassLoader{
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "D:\\CodeProject\\Java\\"+ name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// 字节数据 -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}