目录
三.类加载与字节码技术
1.类文件结构
2.字节码指令
2.1 javap工具
Oracle 提供了 javap 工具来反编译 class 文件
[root@localhost ~]# javap -v 类名.class
2.2 图解方法执行流程
1)java代码
package cn.itcast.jvm.t3.bytecode;
/**
* 演示 字节码指令 和 操作数栈、常量池的关系
*/
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);
}
}
2)编译后的字节码文件
[root@localhost ~]# javap -v Demo3_1.class
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#26 // java/lang/Object."<init>":()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 // cn/itcast/jvm/t3/bytecode/Demo3_1
#7 = Class #33 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcn/itcast/jvm/t3/bytecode/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 // "<init>":()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 cn/itcast/jvm/t3/bytecode/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 cn.itcast.jvm.t3.bytecode.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."
<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t3/bytecode/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
3)常量池载入运行时常量池
4)方法字节码载入方法区
5)main 线程开始运行,分配栈帧内存
(stack=2,locals=4)
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
}
3)常量池载入运行时常量池
java代码被执行时,是由类加载器将main方法所在的类的class字节数据加载到内存中。
常量池中的数据会放到运行时常量池中。
注意:运行时常量池是方法区的一部分
比较小的数字(小于short的数字)并不会存放在常量池中,他们会和字节码指令存放在一起
4)方法字节码载入方法区
方法的字节码会放在方法区
5)main线程开始运行,分配栈帧内存
main方法运行之前会先分配栈帧内存
栈帧:局部变量表、操作数栈、动态链接、方法出口
(stack=2,locals=4):局部变量表是4,操作数栈是2
执行引擎开始执行字节码
bipush 10
- 将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈(从常量池中拿取)
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池
istore 1
- 将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中
- 对应代码中的 a = 10
ldc #3
- 读取运行时常量池中 #3 ,即 32768 (超过 short 最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
- 注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的。
istore 2
将操作数栈中的元素弹出,放到局部变量表的 2 号位置
iload1 iload2
将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
iadd
将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中。
istore 3
将操作数栈中的元素弹出,放入局部变量表的3号位置。
getstatic #4
在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中
iload 3
将局部变量表中 3 号位置的元素压入操作数栈中。
invokevirtual #5
- 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法(System.out.println(c);)
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
- 执行完毕,弹出栈帧
- 清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
2.3 练习分析a++
从字节码角度分析a++相关题目
package cn.itcast.jvm.t3.bytecode;
/**
* 从字节码角度分析 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);
}
}
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
分析:
- 注意 iinc 指令是直接在局部变量 slot 上进行运算
- a++ 和 ++a 的区别是先执行 iload 还是 先执行 iinc
步骤分析:
- bipush 10 将10 压入栈
- istore_1 放入1槽(slot)中
- iload_1 取出1对应的10
- iinc 1, 1 将1中的数据加1(在slot中进行)
- iinc 1, 1 将1中的数据加1(在slot中进行)
- iload_1 将1中的数据取出
- iadd 进行add操作
- iload_1 将1中的数据取出
- iinc 1, -1 将1中的数据减1(在slot中进行)
- iadd 进行add操作
- istore_2 将结果放进2槽中
图解:
- bipush 10 将10 压入栈
- istore_1 放入1槽(slot)中
- iload_1 取出1对应的10
- iinc 1, 1 将1中的数据加1(在slot中进行)
- iinc 1, 1 将1中的数据加1(在slot中进行)
- iload_1 将1中的数据取出
- iadd 进行add操作
- iload_1 将1中的数据取出
- iinc 1, -1 将1中的数据减1(在slot中进行)
- iadd 进行add操作
- istore_2 将结果放进2槽中
练习2
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x);
}
分析:
Code:
stack=2, locals=3, args_size=1 // 操作数栈分配2个空间,局部变量表分配 3 个空间
0: iconst_0 // 准备一个常数 0
1: istore_1 // 将常数 0 放入局部变量表的 1 号槽位 i = 0
2: iconst_0 // 准备一个常数 0
3: istore_2 // 将常数 0 放入局部变量的 2 号槽位 x = 0
4: iload_1 // 将局部变量表 1 号槽位的数放入操作数栈中
5: bipush 10 // 将数字 10 放入操作数栈中,此时操作数栈中有 2 个数
7: if_icmpge 21 // 比较操作数栈中的两个数,如果下面的数大于上面的数,
就跳转到 21 。这里的比较是将两个数做减法。因为涉及运算操作,
所以会将两个数弹出操作数栈来进行运算。运算结束后操作数栈为空
10: iload_2 // 将局部变量 2 号槽位的数放入操作数栈中,放入的值是 0
11: iinc 2, 1 // 将局部变量 2 号槽位的数加 1 ,自增后,槽位中的值为 1
14: istore_2 //将操作数栈中的数放入到局部变量表的 2 号槽位,2 号槽位的值又变为了0
15: iinc 1, 1 // 1 号槽位的值自增 1
18: goto 4 // 跳转到第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
JVM层面:
- x++:是先将x放进操作数栈,然后在栈中的局部变量 slot 上进行++运算操作,最后=是将操作数栈上的数据赋值给x(放进局部变量 slot )
- ++x:是先在栈中的局部变量 slot 上进行++运算操作,然后将x放进操作数栈,最后=是将操作数栈上的数据赋值给x(放进局部变量 slot )
代码层面:
- x++:是先进行自增然后把x自增前的值赋值(0)给x
- ++x:是先进行自增然后把x自增后的值赋值(0)给x
2.4 构造方法
cinit()V:类的初始化
public class Code_12_CinitTest {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i); // 30
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
init()V:对象初始化
public class Code_13_InitTest {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Code_13_InitTest(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Code_13_InitTest d = new Code_13_InitTest("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后.
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
// 原始构造方法在最后执行
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
代码层面讲解:类加载
2.5 方法调用
public class Code_14_MethodTest {
public Code_14_MethodTest() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Code_14_MethodTest obj = new Code_14_MethodTest();
obj.test1();
obj.test2();
obj.test3();
Code_14_MethodTest.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
- 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令
- 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定(编译期间不能确定有没有调用重载或重写的方法)
- 静态方法在调用时使用 invokestatic 指令
Code:
stack=2, locals=2, args_size=1
0: new #2 //
3: dup // 复制一份对象地址压入操作数栈中,相当于两个,
其中一个在invokespecial之后就会删除处理
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: invokestatic #7 // Method test4:()V
23: return
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈
- dup 是赋值操作数栈栈顶的内容,本例即为【对象引用】,为什么需要两份引用呢,一个是要配合 invokespecial 调用该对象的构造方法 “init”: ()V (会消耗掉栈顶一个引用),另一个要 配合 astore_1 赋值给局部变量
- 终方法(final),私有方法(private),构造方法都是由 invokespecial 指令来调用,属于静态绑定
- 普通成员方法是由 invokevirtual 调用,属于动态绑定,即支持多态 成员方法与静态方法调用的另一个区别是,执行方法前是否需要【对象引用】
静态方法被对象调用时候同样会执行类调用的字节码指令,所以建议直接使用类调用。
2.6多态的原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的 Class
- Class 结构中有 vtable
- 查询 vtable 找到方法的具体地址
- 执行方法的字节码
多态的某个方法是子类方法还是父类方法,是直接体现在vtable中的,vtable会根据具体地址直接直接指向对性的字节码文件,也就直接找到了所在的对象。
2.7异常处理
try-catch
public class Code_15_TryCatchTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应的字节码指令
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
finally
public class Code_17_FinallyTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
对应字节码
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
// try块
2: bipush 10
4: istore_1
// try块执行完后,会执行finally
5: bipush 30
7: istore_1
8: goto 27
// catch块
11: astore_2 // 异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
// catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
// 出现异常,但未被 Exception 捕获,
会抛出其他异常,这时也需要执行 finally 块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow // 抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
finally中的return
public class Code_18_FinallyReturnTest {
public static void main(String[] args) {
int i = Code_18_FinallyReturnTest.test();
// 结果为 20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0
3: iload_0
4: istore_1 // 暂存返回值
5: bipush 20
7: istore_0
8: iload_0
9: ireturn // ireturn 会返回操作数栈顶的整型值 20
// 如果出现异常,还是会执行finally 块中的内容,没有抛出异常
10: astore_2
11: bipush 20
13: istore_0
14: iload_0
15: ireturn // 这里没有 athrow 了,也就是如果在 finally
块中如果有返回操作的话,且 try 块中出现异常,会吞掉异常!
Exception table:
from to target type
0 5 10 any
- 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以finally的为准
- 至于字节码中第 2 行,似乎没啥用,且留个伏笔,看下个例子
- 跟上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常
- 所以不要在finally中进行返回操作
被吞掉的异常
public static int test() {
int i;
try {
i = 10;
// 这里应该会抛出异常
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
会发现打印结果为 20 ,并未抛出异常
finally 不带 return
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
对应字节码
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 // 赋值给i 10
3: iload_0 // 加载到操作数栈顶
4: istore_1 // 加载到局部变量表的1号位置
5: bipush 20
7: istore_0 // 赋值给i 20
8: iload_1 // 加载局部变量表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
返回的是10
2.8Synchronized
public class Code_19_SyncTest {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
对应字节码
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
8: aload_1 // 加载到操作数栈
9: dup // 复制一份,放到操作数栈,用于加锁时消耗
10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 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
3. 编译期处理(语法糖)
待学习。。。。。。
4. 类加载阶段
4.1 加载
- 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
- instanceKlass保存在方法区。JDK8之后,方法区位于元空间中,而元空间由位于本地内存中
- _java_mirror是保存在堆内存中
- instanceKlass和*.class(java镜像)互相保存了对方的地址
- 类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceClass,从而获取类的各种信息
4.2 链接
4.2.1 验证
验证类是否复核jvm规范,安全性检查
4.2.2 准备
为static分配空间,设默认值
- static变量在JDK7之前是存储在instanceKlass末尾。但在JDK7之后就存储在_java_mirror末尾了
- static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型,以及字符串常量,那么编译期间就能确定了,赋值就会在准备阶段完成
- 如果static变量是final的,但是属于引用类型(对象),那么赋值就会在初始化阶段完成
4.2.3 解析
将常量池中的符号引用解析为直接引用
- 未解析时常量池中看到的对象仅是符号,未真正的存在于内存中,解析之前不会知道这些符号对应的具体值
public class Demo1 {
public static void main(String[] args) throws IOException,
ClassNotFoundException {
ClassLoader loader = Demo1.class.getClassLoader();
//只加载不解析
Class<?> c = loader.loadClass("com.nyima.JVM.day8.C");
//用于阻塞主线程
System.in.read();
}
}
class C {
D d = new D();
}
class D {
}
loader不会触发解析,也不会触发初始化。
使用HSDB查看
- 此时只加载了类C
查看类C的常量池,可以看到类D未被解析,只是存在于常量池中的符号
- 解析以后(new C),会将常量池中的符号引用解析为直接引用
- 可以看到,此时已加载并解析了类C和类D
附:HSDB的使用
- 先获得要查看的进程ID
jps
Copy
- 打开HSDB
java -cp F:\JAVA\JDK8.0\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
Copy
- 运行时可能会报错,是因为缺少一个.dll的文件,我们在JDK的安装目录中找到该文件,复制到缺失的文件下即可
- 定位需要的进程
4.3 初始化
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的构造方法的线程安全
- clinit方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的
注意
编译器手机的顺序是由语句在源文件中出现的顺序决定的,惊天语句块中只能访问到定义的静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但不能访问
类的初始化是懒惰的,以下情况不会初始化
- 访问类的static final 静态变量(基本类型和字符串)
- 类.class不会触发初始化
- 创建该类对象的数组
- 类加载器的.loadClass方法
- Class.forNamed的参数2为false时
以下情况会初始化
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化会先初始化父类
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new一个对象
验证类是否被初始化,可以看该类的静态代码块是否被执行
5. 类加载器
java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”的动作放到了java虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器
5.1 类与类加载器
类加载器虽然只用于实现类的加载过程,但是它在java程序中起到的作用却远超类加载阶段
对于任意一个类,都必须由加载他的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。通俗讲就是:比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义。否则,即使两个类来源于同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类也必定不相等。
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(拓展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
启动类加载器
可以通过在控制台输入指令,使得类被启动类加载器加载
拓展类加载器
如果classpath和JAVA_HOME/jre/lib/ext 下有同类名,加载时会使用拓展类加载器加载。当应用程序类加载器会发现拓展类加载已将该同名类加载过了,则不会再次加载。
5.2 双亲委派模式
双亲委派模式,即调用类加载器ClassLoader的loadClass方法时,查找类的规则
loadClass源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先查找该类是否已经被该类加载器加载过了
Class<?> c = findLoadedClass(name);
//如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,
但它显示为null
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) {
//如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,
如果还是没找到,就抛出异常
//然后让应用类加载器去找classpath下找该类
long t1 = System.nanoTime();
c = findClass(name);
// 记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
步骤:
- 首先该类是否已经被类加载器加载过了
- 如果没有被加载过
- 看是否被它的上级加载器加载过 Extension的上级是Bootstarp。但它显示为null
- 看是否被启动类加载器加载过
- 有try。。。catch捕获异常但是不作处理
- 如果还是没有找到,先让拓展类加载器调用findClass方法找到该类,如果没有找到,就抛出异常
- 然后让应用类加载器去classpath下找该类
5.3 自定义类加载器
使用场景:
- 想加载非classpath,随意路径中的类文件
- 通过接口实现,希望解耦时,常用在框架设计
- 这些类希望得到隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承classloder父类
- 要遵从双亲委派机制,重写findClass方法。不重写loadClass方法就不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throw ClassNotFoundException {
String path = "e:\\myclasspath\\"+name+".class";
try{
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Path.get(path), os);
//得到字节组
byte[] bytes = os.toByteArray();
//byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e){
e.printStackTrace();
throw new ClassNotFoundException("类文件没有找到",e);
}
}
}
public class Test {
pubulic static void main(String[] args) throws Exception {
MyClassloader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("test1");
Class<?> c2 = classLoader.loadClass("test1");
System.out.println(c1==c2);
MyClassloader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("test1");
System.out.println(c1==c3);
}
}
ture
false
6. 运行期优化
6.1 分层编译
JVM将执行状态分为5个层次
- 0层:解释执行,用解释器将字节码翻译成为机器码
- 1层:使用c1即时编译器编译执行(不带profiling)
- 2层:使用c1即时编译器编译执行(带基本的profiling)
- 3层:使用c1即时编译器编译执行(带完全的profiling)
- 4层:使用c2即时编译器编译执行
普通代码的执行都是0层执行,但是部分代码被多次调用之后会自动上升为1层。
c2和c1比较,c2即时编译器优化更彻底。
profiling是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等
即时编译器和解释器的区别
解释器:
- - 将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
- - 将字节码解释为针对所有平台都能通用的机器码
即时编译器:
- - 将一些字节码编译成为机器码,并存入Code Cache ,下次遇到相同的代码,直接执行,无需编译
- - 根据平台类型,生成平台特定的机器码
对于大部分的不常用的代码,通常是直接采用解释执行的方式运行;对于仅占据小部分的热点代码,我们则可以将其编译成成机器码,以达到理想的运行速度。执行效率上简单比较是 interpreter<c1<c2 ,总的目标是发现热点代码(hotsport名称的由来)并优化这些热点代码
逃逸分析
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
逃逸分析的 JVM 参数如下:
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
6.2 方法内联
JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。
第二个原因则更重要:方法内联
如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:
private int add4(int x1, int x2, int x3, int x4) {
//这里调用了add2方法
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
private int add4(int x1, int x2, int x3, int x4) {
//被替换为了方法本身
return x1 + x2 + x3 + x4;
}
6.3 反射优化
7. java 内存模型 (JMM)
java内存模型和java内存结构是不同的,java内存模型是定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、原子性的规则和保障
7.1 原子性
7.1.1 问题解析
提出问题:两个线程对初始值是0的静态变量一个做自增,一个做自减,各5000次,结果是0吗?
public class Demo1 {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i++;
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
答案是不一定,结果可能是正数 、零 、负数。因为java中对静态变量的自增,自减并不是原子操作。
其中 i++产生的JVM字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i
i--产生的字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i
而java的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换。
所以在多线程下就会出现字节码交错执行
正常执行顺序:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic i // 线程1-获取静态变量i的值 线程内i=1
iconst_1 // 线程1-准备常量1
isub // 线程1-自减 线程内i=0
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=0
出现负数情况:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
出现正数:
// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
7.1.2 解决办法
synchronized(同步关键字)
语法:
synchronized( 对象 ) {
要作为原子操作代码
}
解决问题:
public class Demo1 {
static int i = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i++;
}
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 50000; j++) {
synchronized (obj) {
i--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);//输出为0
}
}
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter 和monitorexit指令实现的。 monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
7.2 可见性
7.2.1 退不出的循环
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
当主线程对静态变量run修改后,t 线程也没有停止
原因:
- 初始状态,t 线程刚开始时从内存读取run到工作区内存
- 后来发现t 线程要频繁从主内存中读取run的值,JIT编译期就会将run的值缓存至自己工作内存中的告诉缓存中,减少对主内存中run的访问,提高效率
- 再后来,main线程修改了run的值,并同步到了主内存中,但是t 线程是从自己的工作内存中的高速缓存中读取的run变量,所以此次修改没有造成t 线程的停止。
7.2.2 解决办法
增加易变关键字 volatile [ˈvɑːlətl]
volatile可以用来修饰成员变量和静态变量,它可以避免线程从自己的工作缓存中查找变量值,必须到主内存中获取它的值,线程操作volatile修饰的变量都是直接从主内存中获取值的,保证了共享变量的可见性,但是并不能保证原子性。
public class Demo1 {
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// ....
}
});
t.start();
Thread.sleep(1000);
run = false; // 线程t不会如预想的停下来
}
}
注意:
-
synchronized
语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但 缺点是synchronized
是属于重量级操作,性能相对更低 - 如果在前面示例的死循环中加入
System.out.println()
会发现即使不加 volatile 修饰符,线程 t 也 能正确知道 run 变量的修改了
是因为println方法
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
可以看出加了synchronized
,保证了每次run
变量都会从主存中获取
7.3 有序性
7.3.1 问题
先看一个例子:
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
其实这里会有三种情况出现:
- 情况一:线程1先执行,这是ready是false,就会走到else中结果是1(r是返回的值)
- 情况二:线程2先执行但是在还没有执行到ready=true时候,线程1开始执行,这时同上,结果是1
- 情况三:线程3线程2先执行,线程1在执行,这时,进入到if分支,结果是4(已经执行了num=2)
其实还有可能出现为0的情况!
- 情况四:先执行线程2,但是并没有先对num赋值,而是执行的ready=true。然后就切换到线程1,进入if分支,相加为0.然后切换到线程2,执行num=2.
这种现象就是指令重排。(出现概率小)。
7.3.2 解决办法
volitile修饰的变量,可以禁用指令重排
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;//可以禁用指令重排
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
7.3.3 有序性的理解
同一线程内,jvm会在不影响正确性的前提下,可以调整语句的执行顺序,例如:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;
可以看到先执行i,还是先执行j对结果没有影响,所以,上面代码真正执行时可以是
i = ...; // 较为耗时的操作
j = ...;
也可以是
j = ...;
i = ...; // 较为耗时的操作
这种特性称为指令重排,多线程下指令重排会影响正确性,例如著名的 double-checked locking
模式实现单例
可能会发生指令重排
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
//实例没创建,才会进入内部的 synchronized 代码块
if (INSTANCE == null) {
//可能第一个线程在synchronized 代码块还没创建完对象时,
第二个线程已经到了这一步,所以里面还需要加上判断
synchronized (Singleton.class) {
//也许有其他线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
以上的实现特点是:
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
上面的代码看似已经很完美了,但是在多线程环境下还是会有指令重排问题!
INSTANCE = new Singleton()
对应的字节码为:
0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field INSTANCE:Lcn/itcast/jvm/t4/Singleton;
其中4 7 两步顺序不是固定的,也许 jvm 会优化为:先将引用地址赋值给 INSTANCE 变量后,再执行构造方法,如果两个线程 t1,t2 按如下时间顺序执行:
时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null(7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != null(synchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将 是一个未初始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才 会真正有效
7.4 happens-before
happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见(线程1对x上锁m,改变值,解锁后,上锁m的线程2对x可见)
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start()
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用
t1.isAlive()
或t1.join()
等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通 过
t2.interrupted
或t2.isInterrupted
)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);//0
break;
}
}
},"t2");
t2.start();
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);//0
}
-
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
-
具有传递性,如果
x hb-> y
并且y hb-> z
那么有x hb-> z
以上变量都是指共享变量即成员变量或静态资源变量
8. CAS与原子类
8.1 CAS
CAS即compare and swap,它体现的是一种乐观锁的思想
比如多个线程要对一个共享的整型变量执行+1操作:
// 需要不断尝试
while(true) {
int 旧值 = 共享变量 ; // 比如拿到了当前值 0
int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
/*
这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
compareAndSwap 返回 false,重新尝试,直到:
compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
*/
if( compareAndSwap ( 旧值, 结果 )) {
// 成功,退出循环
}
//不一样,继续循环尝试
}
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈。多核cpu的场景下。
- 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
CSA底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,下面是直接使用Unsafe对象进行线程安全保护的一个例子:
public class TestCAS {
public static void main(String[] args) throws InterruptedException {
DataContainer dc = new DataContainer();
int count = 5;
Thread t = new Thread(() -> {
for (int i = 0; i < count; i++) {
dc.increase();
}
});
t.start();
t.join();
System.out.println(dc.getData());
}
}
class DataContainer {
private volatile int data;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
try {
// Unsafe 对象不能直接调用,只能通过反射获得
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
try {
// data 属性在 DataContainer 对象中的偏移量,
// 用于 Unsafe 直接访问该属性
DATA_OFFSET =
unsafe.objectFieldOffset(DataContainer.class.getDeclaredField("data"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public void increase() {
int oldValue;
while (true) {
// 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
oldValue = data;
// cas 尝试修改 data 为 旧值 + 1,如果期间旧值被别的线程改了,返回 false
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue +
1)) {
return;
}
}
}
public void decrease() {
int oldValue;
while (true) {
oldValue = data;
if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue -
1)) {
return;
}
}
}
public int getData() {
return data;
}
}
8.2 乐观锁和悲观锁
- CAS是基于乐观锁的思想:不怕别的线程来修改共享变量,即使做了修改,就会自己再次重试。
- Synchrnized是基于悲观锁思想:默认为线程都会修改共享变量,所以只有放开锁之后才会被别的线程修改。
8.3 原子操作类
juc(java.util.concurrent)中提供了原子操作类,可以供线程安全的操作,例如:AtomicIteger、AtomicBoolean等,两个类的底层都是采用了CAS技术和volitile来实现的。可以使用Atomiclnteger改写之前的例子:
public class TestCAS {
//创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndIncrement(); //获取并且自增 i++
}
});
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
i.getAndDecrement(); //获取并且自减 i--
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);//0
}
}
9. synchronized 优化
java HotStop虚拟机中,每个对象都有对象头(包括class指针和mark word)。mark word 平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就会根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容
9.1 轻量级锁
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),那么可以使用轻量级锁来优化。这就好比:
学生(线程 A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明有竞争,继续上他的课。 如果这期间有其它学生(线程 B)来了,会告知(线程A)有并发访问,线程A 随即升级为重量级锁,进入重量级锁的流程。
而重量级锁就不是那么用课本占座那么简单了,可以想象线程 A 走之前,把座位用一个铁栅栏围起来,假设有两个方法同步块,利用同一个对象加锁
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
9.2锁膨胀
如果尝试加轻量级锁的过程中,CAS操作无法成功,这种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
9.3重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候锁线程已经退出了同步块,释放了锁),这时线程就可以避免阻塞。
在java6之后自旋锁是适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性高,就会自旋多次;反之,就少自旋甚至不自旋,总之,比较智能。
9.4偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS
- 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
- 访问对象的hashC也会撤销偏向锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重新偏向会重置对象的Thread ID
- 撤销偏向和重偏向都是批量进行的,以类为单位
- 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
- 可以主动使用 -xx
:-UseBiasedLocking
禁用偏向锁
9.5 其他优化
(1)减少上锁时间
同步代码块中尽量短
(2)减少锁的粒度
将一个锁拆成多个锁提高并发度,例如:
- ConcurrentHashMap
- LongAdder 分为 base 和 cells 两部分。没有并发争用的时候或者是 cells 数组正在初始化的时候,会使用 CAS 来累加到base,有并发争用,会初始化 cells 数组,数组有多少个 cell,就允许有多少线程并行修改,最后将数组中每个 cell 累加,再加上 base 就是最终的值
- LinkedBlockingQueue 入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高
(3)锁粗化
多次循环进入同步块不如同步块内多次循环,另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)
new StringBuffer().append("a").append("b").append("c");
(4)锁消除
JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候就会被即时编译器忽略掉所有同步操作。
(5)读写分离
如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
- 可以主动使用
-XX:-UseBiasedLocking
禁用偏向锁