目录
这篇文章很不错,尤其是讲到类加载阶段那一块的时候:(没有本人这篇博文好,哈哈)

根据 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;
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];
}

反编译命令:javap -v Xxx.class
- package org.wuya.test;
-
- /**
- * 演示 字节码指令 和 操作数栈、常量池的关系
- */
- public class Demo3_1 {
- public static void main(String[] args) {
- int a = 10;
- int b = Short.MAX_VALUE + 1;
- int c = a + b;
- System.out.println(c);
- }
- }
Classfile /D:/JavaTools/springbootRedisDemo/springbootRedisDemo/target/classes/org/wuya/test/Demo3_1.class
Last modified 2024-4-21; size 641 bytes
MD5 checksum 87a2a6a3e3f7d22289041a5d50f4c0dd
Compiled from "Demo3_1.java"
public class org.wuya.test.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."":()V
#2 = Class #27 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #30.#31 // java/io/PrintStream.println:(I)V
#6 = Class #32 // org/wuya/test/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lorg/wuya/test/Demo3_1;
#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 MethodParameters
#24 = Utf8 SourceFile
#25 = Utf8 Demo3_1.java
#26 = NameAndType #8:#9 // "":()V
#27 = Utf8 java/lang/Short
#28 = Class #34 // java/lang/System
#29 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#30 = Class #37 // java/io/PrintStream
#31 = NameAndType #38:#39 // println:(I)V
#32 = Utf8 org/wuya/test/Demo3_1
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (I)V
{
public org.wuya.test.Demo3_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/wuya/test/Demo3_1;public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
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 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
MethodParameters:
Name Flags
args
}
SourceFile: "Demo3_1.java"

(stack=2,locals=4)

bipush 10
istore_1
ldc #3 // int 32768
istore_2
iload_1
iload_2
iadd
istore_3
getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
iload_3
invokevirtual #5 // Method java/io/PrintStream.println:(I)V
return
说明:具体每一步的图片看讲义。非常明晰。
bipush 10
将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池。
- bipush 10 将一个 byte 压入操作数栈(其长度会补齐 4 个字节)
- istore_1 将操作数栈顶数据弹出,存入局部变量表的 slot 1,结果就是a被赋值为10 【可见,bipush 10和istore_1就对应java源代码中的int a = 10;操作】
(接下来就是对b变量赋值:由于b变量对应的值是32768,已经超过了short的范围了,所以它存储的位置是运行时常量池中)
- ldc 从常量池加载#3数据到操作数栈(注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的)
- istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,结果就是b被赋值为32767
(接下来该执行a+b赋值给c了。局部变量表中不能执行a+b这个操作,它一定是在操作数栈中完成,所以执行引擎要对a、b这两个变量进行读取)
- iload_1 把局部变量1槽位的值读入操作数栈上
- iload_2 把局部变量2槽位的值读入操作数栈上
- iadd 相加,把结果存入操作数栈
- istore_3 将操作数栈顶数据弹出,存入局部变量表的slot 3
- getstatic 从常量池#4找成员变量(System类的out成员变量)的引用,把堆中System.out 对象的引用值放入操作数栈
(接下来该进行打印,打印的话,需要一些参数)
- iload_3 把局部变量2槽位的值32768读入操作数栈上
- invokevirtual 分析见下面
- return 完成main方法调用,弹出main栈帧,程序结束
invokevirtual #5


1)源码如下:
- package org.wuya.test;
-
- /**
- * 从字节码角度分析 a++ 相关题目
- */
- public class Demo3_2 {
- public static void main(String[] args) {
- int a = 10;
- int b = a++ + ++a + a--;
- System.out.println(a);
- System.out.println(b);
- }
- }
2)编译后的字节码文件:
重点看下面这段
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1 表示对1号槽位自增,自增1
7: iinc 1, 1
10: iload_1
11: iadd 表示将操作数栈中的两个数(10+12)进行了相加,把结果22保存在了操作数栈
12: iload_1 执行a--操作,将局部变量表中slot 1的值12先读入操作数栈,再自减
13: iinc 1, -1
16: iadd 将22+12的结果34保存在操作数栈
17: istore_2 将操作数栈顶数据弹出,存入局部变量表的slot 2,即将34赋值给了b
18: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
21: iload_1
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_2
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
LineNumberTable:
line 8: 0
line 9: 3
line 10: 18
line 11: 25
line 12: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
3 30 1 a I
18 15 2 b I
MethodParameters:
Name Flags
args
}
分析:
a++ 是先iload再自增(iinc),++a 是先自增(iinc)再iload。
具体分析见上面。图解见讲义。
讲义有一张指令代表的含义,略。
几点说明:
源码:
- package org.wuya.test;
-
- public class Demo3_3 {
- public static void main(String[] args) {
- int a = 0;
- if(a == 0) {
- a = 10;
- } else {
- a = 20;
- }
- }
- }
关键的字节码:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12 ifne是判断是否 != 0,如果不等于0的话就跳到12行,否则顺序执行
6: bipush 10
8: istore_1
9: goto 15 goto表示跳到15行执行
12: bipush 20
14: istore_1
15: return
其实循环控制还是前面介绍的那些指令,例如 while 循环,do while 循环,字节码略。
- public class Demo3_4 {
- public static void main(String[] args) {
- int a = 0;
- while (a < 10) {
- a++;
- }
- }
- }
-
-
-
- public class Demo3_5 {
- public static void main(String[] args) {
- int a = 0;
- do {
- a++;
- } while (a < 10);
- }
- }
再看看 for 循环,比较 while 和 for 的字节码,你发现它们是一模一样的。
请从字节码角度分析,下列代码运行的结果:
- package org.wuya.test;
-
- public class Demo3_6_1 {
- public static void main(String[] args) {
- int i = 0;
- int x = 0;
- while (i < 10) {
- x = x++;
- i++;
- }
- System.out.println(x);// 结果是 0
- }
- }
关键字节码:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: bipush 10
7: if_icmpge 21
10: iload_2
11: iinc 2, 1
14: istore_2 关键是这个赋值操作,将操作数栈中的0又赋值给了x
15: iinc 1, 1
18: goto 4
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
源码:
- package org.wuya.test;
-
- public class Demo3_8_1 {
-
- static {
- i = 20;
- }
-
-
- static {
- i = 30;
- }
-
- static int i = 10;
-
- }

反编译后的字节码:
Classfile /D:/JavaTools/.../src/main/java/org/wuya/test/Demo3_8_1.class
Last modified 2024-4-21; size 305 bytes
MD5 checksum 88505d650a05ea6c2869fd5eae46f394
Compiled from "Demo3_8_1.java"
public class org.wuya.test.Demo3_8_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."":()V
#2 = Fieldref #3.#15 // org/wuya/test/Demo3_8_1.i:I
#3 = Class #16 // org/wuya/test/Demo3_8_1
#4 = Class #17 // java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8
#12 = Utf8 SourceFile
#13 = Utf8 Demo3_8_1.java
#14 = NameAndType #7:#8 // "":()V
#15 = NameAndType #5:#6 // i:I
#16 = Utf8 org/wuya/test/Demo3_8_1
#17 = Utf8 java/lang/Object
{
static int i;
2: putstatic #2 // Field i:I
5: bipush 30
7: putstatic #2 // Field i:I
10: bipush 10
12: putstatic #2 // Field i:I
15: return
LineNumberTable:
line 6: 0
line 11: 5
line 14: 10
}
SourceFile: "Demo3_8_1.java"
编译器会按(源码中)从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法
static int i;
2: putstatic #2 // Field i:I
5: bipush 30
7: putstatic #2 // Field i:I
10: bipush 10
12: putstatic #2 // Field i:I
15: return
源码:
- package org.wuya.test;
-
- public class Demo3_8_2 {
-
-
- private String a = "s1";
-
- {
- b = 20;
- }
-
- private int b = 10;
-
- {
- a = "s2";
- }
-
- public Demo3_8_2(String a, int b) {
- this.a = a;
- this.b = b;
- }
-
- public static void main(String[] args) {
- Demo3_8_2 d = new Demo3_8_2("s3", 30);
- System.out.println(d.a);
- System.out.println(d.b);
- }
- }

下面是反编译后的字节码:
- PS D:\JavaTools\...\target\classes\org\wuya\test> javap -v .\Demo3_8_2.class
- Classfile /D:/JavaTools/.../target/classes/org/wuya/test/Demo3_8_2.class
- Last modified 2024-4-21; size 859 bytes
- MD5 checksum 987873efec13bf5b024a1af1a2d71201
- Compiled from "Demo3_8_2.java"
- public class org.wuya.test.Demo3_8_2
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #12.#32 // java/lang/Object."
":()V - #2 = String #33 // s1
- #3 = Fieldref #6.#34 // org/wuya/test/Demo3_8_2.a:Ljava/lang/String;
- #4 = Fieldref #6.#35 // org/wuya/test/Demo3_8_2.b:I
- #5 = String #36 // s2
- #6 = Class #37 // org/wuya/test/Demo3_8_2
- #7 = String #38 // s3
- #8 = Methodref #6.#39 // org/wuya/test/Demo3_8_2."
":(Ljava/lang/String;I)V - #9 = Fieldref #40.#41 // java/lang/System.out:Ljava/io/PrintStream;
- #10 = Methodref #42.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #11 = Methodref #42.#44 // java/io/PrintStream.println:(I)V
- #12 = Class #45 // java/lang/Object
- #13 = Utf8 a
- #14 = Utf8 Ljava/lang/String;
- #15 = Utf8 b
- #16 = Utf8 I
- #17 = Utf8
- #18 = Utf8 (Ljava/lang/String;I)V
- #19 = Utf8 Code
- #20 = Utf8 LineNumberTable
- #21 = Utf8 LocalVariableTable
- #22 = Utf8 this
- #23 = Utf8 Lorg/wuya/test/Demo3_8_2;
- #24 = Utf8 MethodParameters
- #25 = Utf8 main
- #26 = Utf8 ([Ljava/lang/String;)V
- #27 = Utf8 args
- #28 = Utf8 [Ljava/lang/String;
- #29 = Utf8 d
- #30 = Utf8 SourceFile
- #31 = Utf8 Demo3_8_2.java
- #32 = NameAndType #17:#46 // "
":()V - #33 = Utf8 s1
- #34 = NameAndType #13:#14 // a:Ljava/lang/String;
- #35 = NameAndType #15:#16 // b:I
- #36 = Utf8 s2
- #37 = Utf8 org/wuya/test/Demo3_8_2
- #38 = Utf8 s3
- #39 = NameAndType #17:#18 // "
":(Ljava/lang/String;I)V - #40 = Class #47 // java/lang/System
- #41 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
- #42 = Class #50 // java/io/PrintStream
- #43 = NameAndType #51:#52 // println:(Ljava/lang/String;)V
- #44 = NameAndType #51:#53 // println:(I)V
- #45 = Utf8 java/lang/Object
- #46 = Utf8 ()V
- #47 = Utf8 java/lang/System
- #48 = Utf8 out
- #49 = Utf8 Ljava/io/PrintStream;
- #50 = Utf8 java/io/PrintStream
- #51 = Utf8 println
- #52 = Utf8 (Ljava/lang/String;)V
- #53 = Utf8 (I)V
- {
- public org.wuya.test.Demo3_8_2(java.lang.String, int);
- descriptor: (Ljava/lang/String;I)V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=3, args_size=3
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()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
- LineNumberTable:
- line 18: 0
- line 6: 4
- line 9: 10
- line 12: 16
- line 15: 22
- line 19: 28
- line 20: 33
- line 21: 38
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 39 0 this Lorg/wuya/test/Demo3_8_2;
- 0 39 1 a Ljava/lang/String;
- 0 39 2 b I
- MethodParameters:
- Name Flags
- a
- b
-
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=4, locals=2, args_size=1
- 0: new #6 // class org/wuya/test/Demo3_8_2
- 3: dup
- 4: ldc #7 // String s3
- 6: bipush 30
- 8: invokespecial #8 // Method "
":(Ljava/lang/String;I)V - 11: astore_1
- 12: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
- 15: aload_1
- 16: getfield #3 // Field a:Ljava/lang/String;
- 19: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
- 25: aload_1
- 26: getfield #4 // Field b:I
- 29: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
- 32: return
- LineNumberTable:
- line 24: 0
- line 25: 12
- line 26: 22
- line 27: 32
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 33 0 args [Ljava/lang/String;
- 12 21 1 d Lorg/wuya/test/Demo3_8_2;
- MethodParameters:
- Name Flags
- args
- }
- SourceFile: "Demo3_8_2.java"
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
【从字节码可以看出来,新的构造方法是Demo3_8_2(java.lang.String, int)】
在这个构造方法中,aload_0 表示把this加载到操作数栈;
- package org.wuya.test;
-
- public class Demo3_9 {
- public Demo3_9() { }
-
- private void test1() { }
-
- private final void test2() { }
-
- public void test3() { }
-
- public static void test4() { }
-
- }
则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:
- {
- public org.wuya.test.Demo3_9();
- descriptor: ()V
- flags: ACC_PUBLIC
-
- public static void test4();
- descriptor: ()V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=0, locals=0, args_size=0
- 0: return
- LineNumberTable:
- line 12: 0
- }
可以看到在类加载阶段,只加载了构造方法和static方法。
2)如果源代码是这样的:
- package org.wuya.test;
-
- public class Demo3_9 {
- public Demo3_9() { } //构造方法
-
- private void test1() { } //私有方法
-
- private final void test2() { } //final方法
-
- public void test3() { } //public方法
-
- public static void test4() { } //静态方法
-
- @Override
- public String toString() {
- return super.toString();
- }
-
- public static void main(String[] args) {
- Demo3_9 d = new Demo3_9();
- d.test1();
- d.test2();
- d.test3();
- d.test4();//通过对象调用的静态方法
- Demo3_9.test4();//通过类名调用的静态方法
- d.toString();
- }
-
- }
则javac命令编译后,再执行javap反编译后,方法表集合的字节码是这样的:
- {
- public org.wuya.test.Demo3_9();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()V - 4: return
- LineNumberTable:
- line 4: 0
-
- public void test3();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=0, locals=1, args_size=1
- 0: return
- LineNumberTable:
- line 10: 0
-
- public static void test4();
- descriptor: ()V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=0, locals=0, args_size=0
- 0: return
- LineNumberTable:
- line 12: 0
-
- public java.lang.String toString();
- descriptor: ()Ljava/lang/String;
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #2 // Method java/lang/Object.toString:()Ljava/lang/String;
- 4: areturn
- LineNumberTable:
- line 16: 0
-
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=2, args_size=1
- 0: new #3 // class org/wuya/test/Demo3_9
- 3: dup
- 4: invokespecial #4 // Method "
":()V - 7: astore_1
- 8: aload_1
- 9: invokespecial #5 // Method test1:()V
- 12: aload_1
- 13: invokespecial #6 // Method test2:()V
- 16: aload_1
- 17: invokevirtual #7 // Method test3:()V
- 20: aload_1
- 21: pop
- 22: invokestatic #8 // Method test4:()V
- 25: invokestatic #8 // Method test4:()V
- 28: aload_1
- 29: invokevirtual #9 // Method toString:()Ljava/lang/String;
- 32: pop
- 33: return
- LineNumberTable:
- line 20: 0
- line 21: 8
- line 22: 12
- line 23: 16
- line 24: 20
- line 25: 25
- line 26: 28
- line 27: 33
- }
final的方法,不管修饰符是private,还是public,还是什么,对应的字节码指令是一样的。
可以看到:
其中,invokestatic和invokespecial都属于静态绑定,也就是在.class字节码指令生成的时候,就知道如何找到哪个类的哪个方法了。构造方法、私有方法、静态方法都是能唯一确定的;只有普通的public方法在编译期间并不能确定是调用哪个类的哪个方法,也许是子类的也许是父类的,所以invokevirtual叫动态绑定,在运行的时候确定。
前面介绍了虚拟机指令中的方法调用的多种方式,其中有一种方式是invokevirtual ,它是实现一个方法的多态调用,它的工作方法更为复杂,这里研究invokevirtual指令的执行流程,也就是多态的原理。
- package org.wuya.test;
-
- import java.io.IOException;
-
- /**
- * 演示多态原理,注意加上下面的 JVM 参数,禁用指针压缩
- * -XX:-UseCompressedOops -XX:-UseCompressedClassPointers
- */
- public class Demo3_10 {
-
- public static void test(Animal animal) {
- animal.eat();
- System.out.println(animal.toString());
- }
-
- public static void main(String[] args) throws IOException {
- test(new Cat());
- test(new Dog());
- System.in.read();
- }
- }
-
- abstract class Animal {
- public abstract void eat();
-
- @Override
- public String toString() {
- return "我是" + this.getClass().getSimpleName();
- }
- }
-
- class Dog extends Animal {
-
- @Override
- public void eat() {
- System.out.println("啃骨头");
- }
- }
-
- class Cat extends Animal {
-
- @Override
- public void eat() {
- System.out.println("吃鱼");
- }
- }
Dog - public void eat() @0x000000001b7d3fa8Animal - 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;
源码:
- package org.wuya.test;
-
- public class Demo3_11_1 {
-
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- } catch (Exception e) {
- i = 20;
- }
- }
- }
运行上面的代码,生成.class文件,再执行反编译命令后的关键的字节码摘录:
(不运行代码直接javac编译,与运行代码编译,反编译后的字节码格式上有一点差异)
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1 这两行执行try中的代码块,如果发生异常,怎么进入catch的呢?
5: goto 12
8: astore_2 把那些异常对象的引用地址存入到局部变量表2号槽位上,槽位名字是e
9: bipush 20
11: istore_1
12: return
Exception table: Exception table对上面[2,5)行代码进行监控,一旦有异常发生,会进行匹配,如果与声明的java/lang/Exception类型一致或者是它的子类,则进入到target指示的第8行执行。
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable:
line 6: 0
line 8: 2
line 11: 5
line 9: 8
line 10: 9
line 12: 12
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
MethodParameters:
Name Flags
args

这是JDK1.7后的新语法
源码:
- package org.wuya.test;
-
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
-
- public class Demo3_11_3 {
-
- public static void main(String[] args) {
- try {
- Method test = Demo3_11_3.class.getMethod("test");
- test.invoke(null);
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- e.printStackTrace();
- }
- }
-
- public static void test() {
- System.out.println("ok");
- }
- }

- package org.wuya.test;
-
- public class Demo3_11_4 {
-
- public static void main(String[] args) {
- int i = 0;
- try {
- i = 10;
- } catch (Exception e) {
- i = 20;
- } finally {
- i = 30;
- }
- }
- }

分析字节码知道:finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。
finally 出现了 return
问:下面程序输出结果是什么?


finally 中出现了 return,会吞掉异常,所以日常编码一定不要在finally中写return代码!
- package org.wuya.test;
-
- public class Demo3_12_1 {
- public static void main(String[] args) {
- int result = test();
- System.out.println(result);
- }
-
- public static int test() {
- try {
- //int i = 1/0;//虽然有异常,但该程序执行时不会发生任何异常,因为吞掉了。
- return 10;
- } finally {
- return 20;
- }
- }
- }
- package org.wuya.test;
-
- 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;
- }
- }
- }

分析字节码知道:如果在try中进行了return,之后又在finally中进行了修改,是不会影响返回结果的。因为它在return之前先做了暂存,然后执行了finally中的代码,然后再把暂存的值恢复到栈顶,返回的还是暂存的这个值。所以结果已经在return时确定了,再在finally中修改已经无效了。
延伸:

从字节码角度分析:为什么synchronized能够保证加锁/释放锁的成对出现呢?
- package org.wuya.test;
-
- public class Demo3_13 {
- public static void main(String[] args) {
- Object lock = new Object();
- synchronized (lock) {
- System.out.println("ok");
- }
- }
- }
- public static void main(java.lang.String[]);
- descriptor: ([Ljava/lang/String;)V
- flags: ACC_PUBLIC, ACC_STATIC
- Code:
- stack=2, locals=4, args_size=1
- 0: new #2 // class java/lang/Object
- 3: dup
- 4: invokespecial #1 // Method java/lang/Object."
":()V - 7: astore_1
- 8: aload_1
- 9: dup
- 10: astore_2
- 11: monitorenter
- 12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
- 15: ldc #4 // String ok
- 17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 20: aload_2
- 21: monitorexit
- 22: goto 30
- 25: astore_3
- 26: aload_2
- 27: monitorexit
- 28: aload_3
- 29: athrow
- 30: return
- Exception table:
- from to target type
- 12 22 25 any
- 25 28 25 any
- LineNumberTable:
- line 5: 0
- line 6: 8
- line 7: 12
- line 8: 20
- line 9: 30
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 31 0 args [Ljava/lang/String;
- 8 23 1 lock Ljava/lang/Object;
- StackMapTable: number_of_entries = 2
- frame_type = 255 /* full_frame */
- offset_delta = 25
- locals = [ class "[Ljava/lang/String;", class java/lang/Object, class java/lang/Object ]
- stack = [ class java/lang/Throwable ]
- frame_type = 250 /* chop */
- offset_delta = 4
- MethodParameters:
- Name Flags
- args

注意:方法级别的 synchronized 不会在字节码指令中有所体现。
- package org.wuya.test;
-
- public class Candy2 {
- public static void main(String[] args) {
- Integer x = 1;
- int y = x;
- }
- }

- public static void main(String[] args) {
- Integer x = Integer.valueOf(1);
- int y = x.intValue();
- }
- package org.wuya.test;
-
- public class Candy4 {
- public static void foo(String... args) {
- String[] array = args; // 直接赋值
- System.out.println(array);
- }
- public static void main(String[] args) {
- foo("hello", "world");
- }
- }
- public class Candy4 {
- public static void foo(String[] args) {
- String[] array = args; // 直接赋值
- System.out.println(array);
- }
- public static void main(String[] args) {
- foo(new String[]{"hello", "world"});
- }
- }
注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去。
注意foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中Iterable 用来获取集合的迭代器( Iterator )
- package org.wuya.test;
-
- enum Sex {
- MALE, FEMALE;
- }
- public class Candy7 {
- public static void foo(Sex sex) {
- switch (sex) {
- case MALE:
- System.out.println("男"); break;
- case FEMALE:
- System.out.println("女"); break;
- }
- }
- }
- public class Candy7 {
- /**
- * 定义一个合成类(仅 jvm 使用,对我们不可见)
- * 用来映射枚举的 ordinal 与数组元素的关系
- * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
- * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
- */
- static class $MAP {
- // 数组大小即为枚举元素个数,里面存储case用来对比的数字
- static int[] map = new int[2];
- static {
- map[Sex.MALE.ordinal()] = 1;
- map[Sex.FEMALE.ordinal()] = 2;
- }
- }
- public static void foo(Sex sex) {
- int x = $MAP.map[sex.ordinal()];
- switch (x) {
- case 1:
- System.out.println("男");
- break;
- case 2:
- System.out.println("女");
- break;
- }
- }
- }
- enum Sex {
- MALE, FEMALE
- }
- public final class Sex extends Enum
{ - public static final Sex MALE;
- public static final Sex FEMALE;
- private static final Sex[] $VALUES;
- static {
- MALE = new Sex("MALE", 0);
- FEMALE = new Sex("FEMALE", 1);
- $VALUES = new Sex[]{MALE, FEMALE};
- }
- /**
- * Sole constructor. Programmers cannot invoke this constructor.
- * It is for use by code emitted by the compiler in response to
- * enum type declarations.
- *
- * @param name - The name of this enum constant, which is the identifier
- * used to declare it.
- * @param ordinal - The ordinal of this enumeration constant (its position
- * in the enum declaration, where the initial constant is
- assigned
- */
- private Sex(String name, int ordinal) {
- super(name, ordinal);
- }
- public static Sex[] values() {
- return $VALUES.clone();
- }
- public static Sex valueOf(String name) {
- return Enum.valueOf(Sex.class, name);
- }
- }
枚举类的本质也是一个Class,它里面的 MALE, FEMALE 实际上就是Class的两个实例对象。枚举类和普通类的最大区别就是,普通类的实例对象可以是无穷多个,而枚举类的实例对象是有限的,比如该例中实例对象只有MALE, FEMALE两个。通过上面的字节码可以分析出来。
jdk1.7新增,用于简化资源的关闭。语法:
- try (资源变量 =创建资源对象) {
-
- } catch () {
-
- }
- public class Candy9 {
- public static void main(String[] args) {
- try(InputStream is = new FileInputStream("d:\\1.txt")) {
- System.out.println(is);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- public class Candy9 {
- public Candy9() {
- }
-
- public static void main(String[] args) {
- try {
- InputStream is = new FileInputStream("d:\\1.txt");
- Throwable t = null;
- try {
- System.out.println(is);
- } catch (Throwable e1) {
- // t 是我们代码出现的异常
- t = e1;
- throw e1;
- } finally {
- // 判断了资源不为空
- if (is != null) {
- // 如果我们代码有异常
- if (t != null) {
- try {
- is.close();
- } catch (Throwable e2) {
- // 如果 close 出现异常,作为被压制异常添加
- t.addSuppressed(e2);
- }
- } else {
- // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
- is.close();
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
引用局部变量的匿名内部类,源代码:
- 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;
- 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属性没有机会再跟着一起变化。
三大步:加载;链接(验证、准备、解析);初始化。
注意
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中
- 可以通过前面介绍的 HSDB 工具查看

1)验证
验证类是否符合 JVM规范,安全性检查。
为 static 变量分配空间,设置默认值;



3)解析
将常量池中的符号引用解析为直接引用。(举例说明的)
初始化即调用
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
- public class Load4 {
- public static void main(String[] args) {
- System.out.println(E.a);//不会
- System.out.println(E.b);//不会
- System.out.println(E.c);//会
- }
- }
- class E {
- public static final int a = 10;
- public static final String b = "hello";
- public static final Integer c = 20;
- }
- public final class Singleton {
- private Singleton() { }
- // 内部类中保存单例
- private static class LazyHolder {
- static final Singleton INSTANCE = new Singleton();
- }
- // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
- public static Singleton getInstance() {
- return LazyHolder.INSTANCE;
- }
- }
静态内部类的好处:它可以访问外部类的资源。
以上的实现特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的

- public static void main(String[] args) throws ClassNotFoundException {
- Class> aClass = Class.forName("java.lang.String");
- ClassLoader classLoader = aClass.getClassLoader();
- System.out.println(classLoader);//null
- }
代码如上,如果获取某个类的类加载器是null时,表示该类是由启动类加载器Bootstrap加载的。因为Bootstrap加载器的代码是c语言写的,java直接获取不到。
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则。
1)启动类加载器优先级最高。
2)如我们自己写的类(classpath路径下),首先经由AppClassLoader时,必须经过【上级】加载器的允许,即会逐层向上询问,上级的两个加载器没有加载时,才能轮到AppClassLoader加载。(确保了类的唯一性、安全性)
3)首先检查本类加载器有没有加载过,如果还没有,就看它有没有上级,有上级就调用递归方法委派给上级,源码如下:(老师用的断点跟踪,非常清晰)
- protected Class> loadClass(String name, boolean resolve)
- throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- // 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) {
- }
-
- if (c == null) {
- long t1 = System.nanoTime();
- // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
- c = findClass(name);
-
- // 5. 记录耗时
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }

我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver")也是可以让 com.mysql.jdbc.Driver 正确加载的,怎么做的呢?
- ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
- Iterator<接口类型> iter = allImpls.iterator();
- while(iter.hasNext()) {
- iter.next();
- }
- public static
ServiceLoader load(Class service) { - // 获取线程上下文类加载器
- ClassLoader cl = Thread.currentThread().getContextClassLoader();
- return ServiceLoader.load(service, cl);
- }
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器AppClassLoader ,它内部又是由 Class.forName 调用了线程上下文类加载器完成类加载,具体代码略。
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
1. 继承 ClassLoader 父类;
2. 要遵从双亲委派机制,重写 findClass 方法;
注意不是重写 loadClass 方法,否则不会走双亲委派机制
3. 读取类文件的字节码;
4. 调用父类的 defineClass 方法来加载类;
5. 使用者调用该类加载器的 loadClass 方法;
JVM 将执行状态分成了 5 个层次:
profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
- import java.io.IOException;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
-
- public class Reflect1 {
- public static void foo() {
- System.out.println("foo...");
- }
- public static void main(String[] args) throws Exception {
- Method foo = Reflect1.class.getMethod("foo");
- for (int i = 0; i <= 16; i++) {
- System.out.printf("%d\t", i);
- foo.invoke(null);
- }
- System.in.read();
- }
- }
foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现
- class NativeMethodAccessorImpl extends MethodAccessorImpl {
- private final Method method;
- private DelegatingMethodAccessorImpl parent;
- private int numInvocations;
-
- NativeMethodAccessorImpl(Method var1) {
- this.method = var1;
- }
-
- public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
- // inflationThreshold 膨胀阈值,默认 15
- if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
- // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
- MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
- this.parent.setDelegate(var3);
- }
-
- // 调用本地native实现,效率低下
- return invoke0(this.method, var1, var2);
- }
-
- void setParent(DelegatingMethodAccessorImpl var1) {
- this.parent = var1;
- }
-
- private static native Object invoke0(Method var0, Object var1, Object[] var2);
- }
当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1
可以使用阿里的 arthas 工具:
java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1
注意
通过查看 ReflectionFactory 源码可知,sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
sun.reflect.inflationThreshold 可以修改膨胀阈值。