置顶

深入理解JVM

深入理解JVM

1.什么是JVM?

定义:Java Virtual Machine-java程序的运行环境(java二进制码的运行环境)

好处

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界检查
  • 多态

比较:JVM、JRE、JDK

93

2.常见的JVM

94

3.学习路线

95

4.内存结构

程序计数器

96

定义:Program Counter Register,是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

作用:在jvm指令的执行过程中,记住下一条jvm指令的地址。

97

流程:jvm指令 -> 解释器 -> 机器码 -> cpu

特点:是线程私有的;不会存在内存溢出的区(OutOfMemoryError)

虚拟机栈

98

先进后出(FILO)

99

当每个方法被调用到执行完毕的过程,就对应一个栈帧这在虚拟机栈中从入栈到出栈的过程。

定义:Java Virtual Machine Stacks

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

示例:

100

问题辨析:

1.垃圾回收是否涉及栈内存?

2.栈内存分配越大越好吗?

3.方法内的局部变量是否线程安全?

  • 如果方法内的局部变量没有逃离方法作用范围,它是线程安全的。
  • 如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全问题。

栈内存溢出

  • 栈帧过多导致栈内存溢出(方法的递归调用,可以设置VM options=-Xss256k)。
  • 栈帧过大导致栈内存溢出。

线程运行诊断

  • cpu占用过多

    用top定位哪个进程对cpu的占用过高。

    ps H -eo pid,tid,%cpu | grep进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)。

    jstack进程id:可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号。

  • 程序运行时间很长。

本地方法栈

给本地方法的运行提供内存空间。是为虚拟机使用的本地方法服务。

定义

  • 通过new关键字,创建对象都会使用堆内存。
  • 它是线程共享的,在虚拟机启动时创建,堆中对象都需要考虑线程安全的问题。
  • 有垃圾回收机制。

堆内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/21 - 16:37
*/

import java.util.ArrayList;
import java.util.List;

/**
* 堆内存溢出
* VM options: -Xmx8m
*/
public class Demo1 {

public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String s = "hello";
while (true){
list.add(s);
s = s + s;
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}

效果:

1
2
3
4
5
6
7
8
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.tzd.Demo1.main(Demo1.java:23)
22

堆内存诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/21 - 16:51
*/
/**
* 演示堆内存
*/
public class Demo2 {

public static void main(String[] args) throws InterruptedException{
System.out.println("1......");
Thread.sleep(20000);
byte[] array = new byte[1024*1024*10];
System.out.println("2....");
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}

}

jps工具:查看当前系统中有哪些java进程—–jps

jmap工具:查看堆内存占用情况—-jmap -heap 进程id

jconsole工具:图形界面的,多功能的检测工具,可以连续检测—–jconsole

案例

垃圾回收后,内存占用仍然很高

方法区

定义

组成

img1

方法区内存溢出

1.8以前会导致永久代内存溢出

1
2
java.lang.OutOfMemoryError:PermGen space
-XX:MaxPermSize=8m

1.8以后会导致元空间内存溢出

1
2
java.lang.OutOfMemoryError:Metaspace
-XX:MaxMetaspaceSize=8m

场景

  • spring
  • mybatis

运行时常量池

1
2
3
4
5
6
7
package xxxxxx;
//二进制字节码(类基本信息,常量池,类定义方法,包含虚拟机指令)
public class HelloWorld{
public static void main(String[] args){
System.out.println("hello world");
}
}
1
2
//反编译
javap -c HelloWorld.class
  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息。
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

StringTable

img2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 15:48
*/
//hashtable结构,不能扩容
public class Demo3 {
//常量池中的信息,都会被加载到运行时常量池,这时a,b,ab都是常量池中的符号,还没有变为java字符串对象
//ldc #2会把a符号变为“a”字符串对象
//ldc #3会把a符号变为“b”字符串对象
//ldc #4会把a符号变为“ab”字符串对象
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
E:\Java\jdk1.8.0_66\bin\javap.exe -verbose com.tzd.Demo3
Classfile /G:/jvm/out/production/jvm/com/tzd/Demo3.class
Last modified 2022-4-25; size 483 bytes
MD5 checksum 7836650fdf343fc671c54dc6807c6c47
Compiled from "Demo3.java"
public class com.tzd.Demo3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab
#5 = Class #28 // com/tzd/Demo3
#6 = Class #29 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/tzd/Demo3;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 s1
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 s2
#21 = Utf8 s3
#22 = Utf8 SourceFile
#23 = Utf8 Demo3.java
#24 = NameAndType #7:#8 // "<init>":()V
#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab
#28 = Utf8 com/tzd/Demo3
#29 = Utf8 java/lang/Object
{
public com.tzd.Demo3();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/tzd/Demo3;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 s1 Ljava/lang/String;
6 4 2 s2 Ljava/lang/String;
9 1 3 s3 Ljava/lang/String;
}
SourceFile: "Demo3.java"

Process finished with exit code 0

拼接1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 15:48
*/
public class Demo3 {
//常量池中的信息,都会被加载到运行时常量池
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").toString()
System.out.println(s3 == s4);//false,这时两个对象,s3存放在常量池,s4存放在堆里
System.out.println(s3.equals(s4));//true
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
E:\Java\jdk1.8.0_66\bin\javap.exe -verbose com.tzd.Demo3
Classfile /G:/jvm/out/production/jvm/com/tzd/Demo3.class
Last modified 2022-4-25; size 667 bytes
MD5 checksum c054a85e2cde0118cd48bc0ce5d665bb
Compiled from "Demo3.java"
public class com.tzd.Demo3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
#5 = Class #33 // java/lang/StringBuilder
#6 = Methodref #5.#29 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#35 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #36 // com/tzd/Demo3
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/tzd/Demo3;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 SourceFile
#28 = Utf8 Demo3.java
#29 = NameAndType #11:#12 // "<init>":()V
#30 = Utf8 a
#31 = Utf8 b
#32 = Utf8 ab
#33 = Utf8 java/lang/StringBuilder
#34 = NameAndType #38:#39 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #40:#41 // toString:()Ljava/lang/String;
#36 = Utf8 com/tzd/Demo3
#37 = Utf8 java/lang/Object
#38 = Utf8 append
#39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#40 = Utf8 toString
#41 = Utf8 ()Ljava/lang/String;
{
public com.tzd.Demo3();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/tzd/Demo3;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 9
line 14: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
29 1 4 s4 Ljava/lang/String;
}
SourceFile: "Demo3.java"

Process finished with exit code 0

拼接2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 15:48
*/
public class Demo3 {
//常量池中的信息,都会被加载到运行时常量池
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";//javac在编译期间的优化,结果已经在编译期间确定为ab
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
E:\Java\jdk1.8.0_66\bin\javap.exe -verbose com.tzd.Demo3
Classfile /G:/jvm/out/production/jvm/com/tzd/Demo3.class
Last modified 2022-4-25; size 690 bytes
MD5 checksum 29cb4a3211d139fd5fcfdce40e137c25
Compiled from "Demo3.java"
public class com.tzd.Demo3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = String #31 // a
#3 = String #32 // b
#4 = String #33 // ab
#5 = Class #34 // java/lang/StringBuilder
#6 = Methodref #5.#30 // java/lang/StringBuilder."<init>":()V
#7 = Methodref #5.#35 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #5.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Class #37 // com/tzd/Demo3
#10 = Class #38 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/tzd/Demo3;
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 s1
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 s2
#25 = Utf8 s3
#26 = Utf8 s4
#27 = Utf8 s5
#28 = Utf8 SourceFile
#29 = Utf8 Demo3.java
#30 = NameAndType #11:#12 // "<init>":()V
#31 = Utf8 a
#32 = Utf8 b
#33 = Utf8 ab
#34 = Utf8 java/lang/StringBuilder
#35 = NameAndType #39:#40 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#36 = NameAndType #41:#42 // toString:()Ljava/lang/String;
#37 = Utf8 com/tzd/Demo3
#38 = Utf8 java/lang/Object
#39 = Utf8 append
#40 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = Utf8 toString
#42 = Utf8 ()Ljava/lang/String;
{
public com.tzd.Demo3();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/tzd/Demo3;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 9
line 14: 29
line 16: 33
LocalVariableTable:
Start Length Slot Name Signature
0 34 0 args [Ljava/lang/String;
3 31 1 s1 Ljava/lang/String;
6 28 2 s2 Ljava/lang/String;
9 25 3 s3 Ljava/lang/String;
29 5 4 s4 Ljava/lang/String;
33 1 5 s5 Ljava/lang/String;
}
SourceFile: "Demo3.java"

Process finished with exit code 0

StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象

  • 利用串池的机制,来避免重复创建字符串对象

  • 字符串变量拼接的原理是StringBuilder【1.8】

  • 字符串常量拼接的原理是编译期优化

  • 可以使用Intern方法,主动将串池中还没有的字符串对象放入串池

    • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池的对象返回。
    • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.tzd;

    /**
    * @author tianzedeng
    * @date 2022/4/25 - 17:09
    */
    public class Demo4 {

    //["a","b","ab"]常量池中
    public static void main(String[] args) {
    String s = new String("a") + new String("b");//new String("ab")
    //堆 new String("a") new String("b") new String("ab")
    String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回

    System.out.println(s2 == "ab");//true
    System.out.println(s == "ab");//true
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    E:\Java\jdk1.8.0_66\bin\javap.exe -verbose com.tzd.Demo4
    Classfile /G:/jvm/out/production/jvm/com/tzd/Demo4.class
    Last modified 2022-4-25; size 921 bytes
    MD5 checksum 2fb9c3469df09bca2ff3346b40c872b7
    Compiled from "Demo4.java"
    public class com.tzd.Demo4
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
    #1 = Methodref #15.#36 // java/lang/Object."<init>":()V
    #2 = Class #37 // java/lang/StringBuilder
    #3 = Methodref #2.#36 // java/lang/StringBuilder."<init>":()V
    #4 = Class #38 // java/lang/String
    #5 = String #39 // a
    #6 = Methodref #4.#40 // java/lang/String."<init>":(Ljava/lang/String;)V
    #7 = Methodref #2.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    #8 = String #42 // b
    #9 = Methodref #2.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
    #10 = Methodref #4.#44 // java/lang/String.intern:()Ljava/lang/String;
    #11 = Fieldref #45.#46 // java/lang/System.out:Ljava/io/PrintStream;
    #12 = String #47 // ab
    #13 = Methodref #48.#49 // java/io/PrintStream.println:(Z)V
    #14 = Class #50 // com/tzd/Demo4
    #15 = Class #51 // java/lang/Object
    #16 = Utf8 <init>
    #17 = Utf8 ()V
    #18 = Utf8 Code
    #19 = Utf8 LineNumberTable
    #20 = Utf8 LocalVariableTable
    #21 = Utf8 this
    #22 = Utf8 Lcom/tzd/Demo4;
    #23 = Utf8 main
    #24 = Utf8 ([Ljava/lang/String;)V
    #25 = Utf8 args
    #26 = Utf8 [Ljava/lang/String;
    #27 = Utf8 s
    #28 = Utf8 Ljava/lang/String;
    #29 = Utf8 s2
    #30 = Utf8 StackMapTable
    #31 = Class #26 // "[Ljava/lang/String;"
    #32 = Class #38 // java/lang/String
    #33 = Class #52 // java/io/PrintStream
    #34 = Utf8 SourceFile
    #35 = Utf8 Demo4.java
    #36 = NameAndType #16:#17 // "<init>":()V
    #37 = Utf8 java/lang/StringBuilder
    #38 = Utf8 java/lang/String
    #39 = Utf8 a
    #40 = NameAndType #16:#53 // "<init>":(Ljava/lang/String;)V
    #41 = NameAndType #54:#55 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    #42 = Utf8 b
    #43 = NameAndType #56:#57 // toString:()Ljava/lang/String;
    #44 = NameAndType #58:#57 // intern:()Ljava/lang/String;
    #45 = Class #59 // java/lang/System
    #46 = NameAndType #60:#61 // out:Ljava/io/PrintStream;
    #47 = Utf8 ab
    #48 = Class #52 // java/io/PrintStream
    #49 = NameAndType #62:#63 // println:(Z)V
    #50 = Utf8 com/tzd/Demo4
    #51 = Utf8 java/lang/Object
    #52 = Utf8 java/io/PrintStream
    #53 = Utf8 (Ljava/lang/String;)V
    #54 = Utf8 append
    #55 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
    #56 = Utf8 toString
    #57 = Utf8 ()Ljava/lang/String;
    #58 = Utf8 intern
    #59 = Utf8 java/lang/System
    #60 = Utf8 out
    #61 = Utf8 Ljava/io/PrintStream;
    #62 = Utf8 println
    #63 = Utf8 (Z)V
    {
    public com.tzd.Demo4();
    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 7: 0
    LocalVariableTable:
    Start Length Slot Name Signature
    0 5 0 this Lcom/tzd/Demo4;

    public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=4, locals=3, args_size=1
    0: new #2 // class java/lang/StringBuilder
    3: dup
    4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
    7: new #4 // class java/lang/String
    10: dup
    11: ldc #5 // String a
    13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    19: new #4 // class java/lang/String
    22: dup
    23: ldc #8 // String b
    25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
    28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    34: astore_1
    35: aload_1
    36: invokevirtual #10 // Method java/lang/String.intern:()Ljava/lang/String;
    39: astore_2
    40: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
    43: aload_2
    44: ldc #12 // String ab
    46: if_acmpne 53
    49: iconst_1
    50: goto 54
    53: iconst_0
    54: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
    57: return
    LineNumberTable:
    line 11: 0
    line 13: 35
    line 15: 40
    line 16: 57
    LocalVariableTable:
    Start Length Slot Name Signature
    0 58 0 args [Ljava/lang/String;
    35 23 1 s Ljava/lang/String;
    40 18 2 s2 Ljava/lang/String;
    StackMapTable: number_of_entries = 2
    frame_type = 255 /* full_frame */
    offset_delta = 53
    locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]
    stack = [ class java/io/PrintStream ]
    frame_type = 255 /* full_frame */
    offset_delta = 0
    locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]
    stack = [ class java/io/PrintStream, int ]
    }
    SourceFile: "Demo4.java"

    Process finished with exit code 0

    StringTable位置

    img3

StringTable垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 20:22
*/

/**
* 演示StringTable垃圾回收
* -Xmx10m(堆的最大参数) -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo5 {

public static void main(String[] args) {
int i = 0;
try {
for (int j = 0; j <100 ; j++) {
String.valueOf(j).intern();
i++;
}catch (Throwable e){
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

0
Heap
def new generation total 3072K, used 1462K [0x04a00000, 0x04d50000, 0x04d50000)
eden space 2752K, 53% used [0x04a00000, 0x04b6d9d8, 0x04cb0000)
from space 320K, 0% used [0x04cb0000, 0x04cb0000, 0x04d00000)
to space 320K, 0% used [0x04d00000, 0x04d00000, 0x04d50000)
tenured generation total 6848K, used 0K [0x04d50000, 0x05400000, 0x05400000)
the space 6848K, 0% used [0x04d50000, 0x04d50000, 0x04d50200, 0x05400000)
Metaspace used 1920K, capacity 2280K, committed 2368K, reserved 4480K
SymbolTable statistics:
Number of buckets : 20011 = 80044 bytes, avg 4.000
Number of entries : 12610 = 151320 bytes, avg 12.000
Number of literals : 12610 = 548304 bytes, avg 43.482
Total footprint : = 779668 bytes
Average bucket size : 0.630
Variance of bucket size : 0.628
Std. dev. of bucket size: 0.793
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 1009 = 4036 bytes, avg 4.000
Number of entries : 1658 = 19896 bytes, avg 12.000
Number of literals : 1658 = 131192 bytes, avg 79.127
Total footprint : = 155124 bytes
Average bucket size : 1.643
Variance of bucket size : 1.571
Std. dev. of bucket size: 1.254
Maximum bucket size : 6

Process finished with exit code 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

100
Heap
def new generation total 3072K, used 1628K [0x04a00000, 0x04d50000, 0x04d50000)
eden space 2752K, 59% used [0x04a00000, 0x04b97368, 0x04cb0000)
from space 320K, 0% used [0x04cb0000, 0x04cb0000, 0x04d00000)
to space 320K, 0% used [0x04d00000, 0x04d00000, 0x04d50000)
tenured generation total 6848K, used 0K [0x04d50000, 0x05400000, 0x05400000)
the space 6848K, 0% used [0x04d50000, 0x04d50000, 0x04d50200, 0x05400000)
Metaspace used 1991K, capacity 2280K, committed 2368K, reserved 4480K
SymbolTable statistics:
Number of buckets : 20011 = 80044 bytes, avg 4.000
Number of entries : 12957 = 155484 bytes, avg 12.000
Number of literals : 12957 = 560000 bytes, avg 43.220
Total footprint : = 795528 bytes
Average bucket size : 0.647
Variance of bucket size : 0.648
Std. dev. of bucket size: 0.805
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 1009 = 4036 bytes, avg 4.000
Number of entries : 1782 = 21384 bytes, avg 12.000
Number of literals : 1782 = 135696 bytes, avg 76.148
Total footprint : = 161116 bytes
Average bucket size : 1.766
Variance of bucket size : 1.719
Std. dev. of bucket size: 1.311
Maximum bucket size : 6

Process finished with exit code 0

StringTable性能调优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 20:42
*/

import java.io.*;

/**
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics -XX:StringTableSize=20000(桶的个数)//调整参数大小
*/
public class Demo6 {

public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"),"utf-8"))){
String line = null;
long start = System.nanoTime();
while (true){
line = reader.readLine();
if (line == null){
break;
}
line.intern();
}
System.out.println("cost:" + (System.nanoTime() - start)/1000000);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

}

直接内存

定义

  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

img4

img5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 21:27
*/

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
* 直接内存溢出
*/
public class Demo7 {

public static final int _100Mb = 1024 * 1024 * 100;

public static void main(String[] args) {

List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
}finally {
System.out.println(i);
}

}

}
1
2
3
4
5
6
7
8
2
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:658)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.tzd.Demo7.main(Demo7.java:25)

Process finished with exit code 1

分配和回收原理

分配内存和释放内存通过unsafe对象来管理的。并且回收需要主动调用freeMemory方法。

1
2
3
4
5
6
7
8
9
Unsafe unsafe = getUnsafe();
//分配内存
long base = unsafe.allocateMemory(静态成员变量);
unsafe.setMemory(base,静态成员变量,(byte)0);
System.in.read();

//释放内存
unsafe.freeMemory(base);
System.in.read();

ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.tzd;

/**
* @author tianzedeng
* @date 2022/4/25 - 21:58
*/

import java.nio.ByteBuffer;

/**
* -XX:+DisableExplicitGC 显式的
*/
public class Demo8 {

public static int _1Gb = 1024 * 1024 * 1024;

public static void main(String[] args) throws Exception{
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕。。。。。");
System.in.read();
System.out.println("开始释放。。。。。");
byteBuffer = null;
System.gc();//显式的垃圾回收
System.in.read();
}

}

5.垃圾回收

如何判断对象可以回收

引用计数法

可达性分析算法

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

  • 哪些对象可以作为GC Root?(Eclipse Memory Analyzer工具)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.tzd;

    /**
    * @author tianzedeng
    * @date 2022/4/26 - 16:22
    */

    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;

    /**
    * 演示GC Roots
    */

    public class Demo9 {

    public static void main(String[] args) throws InterruptedException, IOException {

    List<Object> list = new ArrayList<>();
    list.add("a");
    list.add("b");
    System.out.println(1);
    System.in.read();

    list = null;
    System.out.println(2);
    System.in.read();
    System.out.println("end..........");

    }

    }
1
2
3
4
5
6
7
G:\jvm>jmap -dump:format=b,live,file=1.bin 6340
Dumping heap to G:\jvm\1.bin ...
Heap dump file created

G:\jvm>jmap -dump:format=b,live,file=2.bin 6340
Dumping heap to G:\jvm\2.bin ...
Heap dump file created

img6

img7

四种引用

img8

  1. 强引用
    • 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
  2. 软引用
    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象。
    • 可以配合引用队列来释放软引用自身。
  3. 弱引用
    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。
    • 可以配合引用队列来释放弱引用自身。
  4. 虚引用(Cleaner)
    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
  5. 终结器引用
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能被回收。

垃圾回收算法

标记清除(Mark Sweep)

  • 速度较快
  • 会造成内存碎片

img9

标记整理(Mark Compact)

  • 速度慢
  • 没有内存碎片

img10

复制(Copy)

  • 不会有内存碎片
  • 需要占双倍的内存空间

img12

img13

img11

分代垃圾回收

img4

img15

  • 对象首先分配在伊甸园区域。
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄加1,并且交换from to。
  • minor gc会引发stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程恢复运行。
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc。STW的时间更长。

相关VM参数

img16

垃圾回收器

串行

  • 单线程
  • 堆内存较小,适合个人电脑

img17

吞吐量优先

  • 多线程

  • 堆内存较大的场景,多核cpu支持

  • 让单位时间内,STW的时间最短

    img18

响应时间优先

  • 多线程
  • 堆内存较大的场景,多核cpu支持
  • 尽可能让单次的STW的时间最短

img19

Garbage First

适用场景

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间是复制算法

相关参数

-XX:+UseG1GC

-XX:G1HeapRegionSize=size

-XX:MaxGCPauseMills=time

G1垃圾回收阶段

img20

Young Collection

  • 会STW

    img21

img22

img23

Young Collection + Concurrent Mark(CM)

  • 在Young GC时会进行GC Root的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
  • -XX:InitiatingHeapOccupancyPercent=percent(默认45%)

img24

Mixed Collection

会对E、S、O进行全面垃圾回收

  • 最终标记(Remark)会STW
  • 拷贝存活(Evacuation)会STW
  • -XX:MaxGCPuaseMills=ms

img25

Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集-minor gc
    • 老年代内存不足发生的垃圾回收-full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集-minor gc
    • 老年代内存不足发生的垃圾回收-full gc
  • CMS
    • 新生代内存不足发生的垃圾收集-minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集-minor gc
    • 老年代内存不足

Young Collection跨代引用

  • 新生代回收的跨代引用(老年代引用新生代)问题

img26

Remark

  • pre-write barrier(写屏障) + satb_mark_queue

img

JDK 8u20字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了cpu时间,新生代回收时间略微增加
  • -XX:+UseStringDeduplication
1
2
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果他们值一样,让他们引用同一个cha[]
  • 注意,与String.intern()不一样,String.intern()关注的是字符串对象,而字符串去重关注的是char[],在JVM内部,使用了不同的字符串表

JDK 8u40并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。

-XX:+ClassUnloadingWithConcurrentMark默认启用

JDK 8u60回收巨型对象

  • 一个对象大于region的一半时,称之为巨型对象
  • G1不会对巨型对象拷贝
  • 回收时会被优先考虑
  • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JDK 9并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为FullGC
  • JDK 9之前需要使用-XX:InitiatingHeapOccupancyPercent
  • JDK 9可以动态调整
    • -XX:InitiatingHeapOccupancyPercent用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档时间

垃圾回收调优

1
2
//查看虚拟机运行参数
"E:\Java\jdk1.8.0_66\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
uintx AutoGCSelectPauseMillis = 5000 {product}
bool BindGCTaskThreadsToCPUs = false {product}
uintx CMSFullGCsBeforeCompaction = 0 {product}
uintx ConcGCThreads = 0 {product}
bool DisableExplicitGC = false {product}
bool ExplicitGCInvokesConcurrent = false {product}
bool ExplicitGCInvokesConcurrentAndUnloadsClasses = false {product}
uintx G1MixedGCCountTarget = 8 {product}
uintx GCDrainStackTargetSize = 64 {product}
uintx GCHeapFreeLimit = 2 {product}
uintx GCLockerEdenExpansionPercent = 5 {product}
bool GCLockerInvokesConcurrent = false {product}
uintx GCLogFileSize = 8192 {product}
uintx GCPauseIntervalMillis = 0 {product}
uintx GCTaskTimeStampEntries = 200 {product}
uintx GCTimeLimit = 98 {product}
uintx GCTimeRatio = 99 {product}
bool HeapDumpAfterFullGC = false {manageable}
bool HeapDumpBeforeFullGC = false {manageable}
uintx HeapSizePerGCThread = 67108864 {product}
uintx MaxGCMinorPauseMillis = 4294967295 {product}
uintx MaxGCPauseMillis = 4294967295 {product}
uintx NumberOfGCLogFiles = 0 {product}
intx ParGCArrayScanChunk = 50 {product}
uintx ParGCDesiredObjsFromOverflowList = 20 {product}
bool ParGCTrimOverflow = true {product}
bool ParGCUseLocalOverflow = false {product}
uintx ParallelGCBufferWastePct = 10 {product}
uintx ParallelGCThreads = 0 {product}
bool ParallelGCVerbose = false {product}
bool PrintClassHistogramAfterFullGC = false {manageable}
bool PrintClassHistogramBeforeFullGC = false {manageable}
bool PrintGC = false {manageable}
bool PrintGCApplicationConcurrentTime = false {product}
bool PrintGCApplicationStoppedTime = false {product}
bool PrintGCCause = true {product}
bool PrintGCDateStamps = false {manageable}
bool PrintGCDetails = false {manageable}
bool PrintGCID = false {manageable}
bool PrintGCTaskTimeStamps = false {product}
bool PrintGCTimeStamps = false {manageable}
bool PrintHeapAtGC = false {product rw}
bool PrintHeapAtGCExtended = false {product rw}
bool PrintJNIGCStalls = false {product}
bool PrintParallelOldGCPhaseTimes = false {product}
bool PrintReferenceGC = false {product}
bool ScavengeBeforeFullGC = true {product}
bool TraceDynamicGCThreads = false {product}
bool TraceParallelOldGCTasks = false {product}
bool UseAdaptiveGCBoundary = false {product}
bool UseAdaptiveSizeDecayMajorGCCost = true {product}
bool UseAdaptiveSizePolicyWithSystemGC = false {product}
bool UseAutoGCSelectPolicy = false {product}
bool UseConcMarkSweepGC = false {product}
bool UseDynamicNumberOfGCThreads = false {product}
bool UseG1GC = false {product}
bool UseGCLogFileRotation = false {product}
bool UseGCOverheadLimit = true {product}
bool UseGCTaskAffinity = false {product}
bool UseMaximumCompactionOnSystemGC = true {product}
bool UseParNewGC = false {product}
bool UseParallelGC = false {product}
bool UseParallelOldGC = false {product}
bool UseSerialGC = false {product}
java version "1.8.0_66"
Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
Java HotSpot(TM) Client VM (build 25.66-b17, mixed mode)

调优领域

  • 内存
  • 锁竞争
  • cpu竞争
  • IO

确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • CMS,G1,ZGC(低延迟)
  • ParallelGC(高吞吐量)
  • Zing

最快的GC是不发生GC

  • 查看FullGC前后的内存占用,考虑下面几个问题
  • 数据是不是太多?(resultSet = statement.executeQuery(“select * from table limit n”))
  • 数据表示是否太臃肿?(对象图、对象大小)16 Integer 24 int 4
  • 是否存在内存泄漏?(第三方缓存实现redis,软引用,弱引用)

新生代调优

  • 新生代的特点

    所有的new操作的内存分配非常廉价

    TLAB thread-local allocation buffer

  • 死亡对象的回收代价是零

  • 大部分对象用过即死

  • Minor GC的时间远远低于Full GC

  • 新生代能容纳所有【并发量*(请求-响应)】的数据

  • 幸存区大到能保留【当前活跃对象+需要晋升对象】

  • 晋升阈值配置得当,让长时间存活对象尽快晋升

    -XX:MaxTenuringThreshold=threshold

    -XX:+PrintTenuringDistribution

    img28

老年代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC那么已经……,否则先尝试调优新生代
  • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
  • -XX:CMSInitiatingOccupancyFraction=percent

案例

  • 案例1:Full GC和Minor GC频繁
  • 案例2:请求高峰期发生Full GC,单次暂停时间特别长(CMS)
  • 案例3:老年代充裕情况下,发生Full GC(1.7)

类文件结构与字节码技术

详情见《深入理解java虚拟机》第三版。周志明。

当执行invokevirtual指令时,

  1. 先通过栈帧中的对象引用找到对象
  2. 分析对象头,找到对象的实际class
  3. class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
  4. 查表得到方法的具体地址
  5. 执行方法的字节码

编译期处理

  • 默认构造器
  • 自动拆装箱
  • 泛型集合取值
  • 可变参数
  • foreach循环
  • switch字符串
  • switch枚举
  • 枚举类
  • try-with-resources
  • 方法重写时的桥接方法
  • 匿名内部类

类加载阶段

加载

将字节码载入方法区中,内部采用C++的InstanceKlass描述Java类,它的重要field有:

  • _java_mirror即java的类镜像,例如对String来说,就是String.class,作用就是把Klass暴露给java使用
  • _super即父类
  • _fields即成员变量
  • _methods即方法
  • _constants即常量池
  • _class_loader即类加载器
  • _vtable虚方法表
  • _itable接口方法表

如果这个类还有父类没有加载,先加载父类。

加载和链接可能是交替运行的。

注意:

  • instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中。
  • 可以通过前面介绍的HSDB工具查看

img29

链接

  • 验证:验证类是否符合JVM规范,安全性检查。用UE等支持二进制的编辑器修改HelloWorld.class的魔数,在控制台运行。

img30

  • 准备:为static变量分配空间,设置默认值
    • static变量在JDK7之前存储在InstanceKlass末尾,从JDK7开始,存储在_java_mirror末尾
    • static变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果static变量是final的基本类型以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
  • 解析:将常量池中的符号引用解析为直接引用

img31

初始化

()v方法

初始化即调用()v,虚拟机会保证这个类的构造方法的线程安全

发生的时机

概括的说,类初始化是懒惰的

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new会导致初始化

不会初始化的情况

  • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化
  • 创建类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2为false时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.tzd;

/**
* @author tianzedeng
* @date 2022/5/6 - 16:49
*/
public class Demo10 {

static{
System.out.println("main init");
}

public static void main(String[] args) throws ClassNotFoundException{
/**
* 不会触发初始化
*/
//静态常量不会触发初始化
System.out.println(B.b);
//类对象.class不会触发初始化
System.out.println(B.class);
//创建该类的数组不会触发初始化
System.out.println(new B[0]);
//不会初始化类B,但会加载B、A
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("cn.itcast.jvm.t3.load.B");
//不会初始化类B,但会加载B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("cn.itcast.jvm.t3.load.B",false,c2);

/**
* 触发初始化
*/
//首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
//子类初始化,如果父类还没有初始化,会触发
System.out.println(B.c);
//子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
//会初始化类B,并先初始化A
Class.forName("cn.itcast.jvm.t3.load.B");
}

}

class A{
static int a = 0;
static {
System.out.println("a init");
}
}

class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}

练习

从字节码分析,使用a,b,c这三个常量是否会导致E初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tzd;

/**
* @author tianzedeng
* @date 2022/5/6 - 17:12
*/
public class Demo11 {

public static void main(String[] args) {
System.out.println(E.a);//不会导致E初始化
System.out.println(E.b);//不会导致E初始化
System.out.println(E.c);//会导致E初始化
}

}

class E{
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;//包装类型,Integer.valueOf(20)
static {
System.out.println("init E");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10

public static final java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String hello

public static final java.lang.Integer c;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 20
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #3 // Field c:Ljava/lang/Integer;
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String init E
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 20: 0
line 22: 8
line 23: 16
}

典型应用-完成懒惰初始化单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.tzd;

/**
* @author tianzedeng
* @date 2022/5/6 - 17:24
*/
public class Demo12 {

public static void main(String[] args) {
//Singleton.test();
Singleton.getInstance();
}

}

class Singleton{

public static void test(){
System.out.println("test");
}

private Singleton(){}

private static class LazyHolder{
private static Singleton SINGLETON = new Singleton();
static {
System.out.println("lazy holder init");
}
}

public static Singleton getInstance(){
return LazyHolder.SINGLETON;
}
}

类加载器

img32

启动类加载器

用BootStrap类加载器加载类:

1
2
3
4
5
public class F{
static{
System.out.println("bootstrap F init");
}
}

执行:

1
2
3
4
5
6
public class Demo{
public static void main(String[] args) throws ClassNotFoundException{
Class<?> aClass = Class.forName("包名.F");
System.out.println(aClass.getClassLoader());
}
}

输出:

1
2
bootstrap F init
null

扩展类加载器

使用Extension加载器加载类:

1
2
3
4
5
public class G{
static{
System.out.println("classpath G init");
}
}

执行:

1
2
3
4
5
6
public class Demo{
public static void main(String[] args) throws ClassNotFoundException{
Class<?> aClass = Class.forName("包名.G");
System.out.println(aClass.getClassLoader());
}
}

输出:

1
2
classpath G init
sun misc Launcher$AppClassLoader@18b4aac2

双亲委派模式

所谓双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则

注意:这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
}

线程上下文类加载器

我们在使用JDBC时,都需要加载Driver驱动,不知道你注意没有,不写

1
Class.forName("com.mysql.jdbc.Driver");

也可以让com.mysql.jdbc.Driver正确加载。为什么?

追踪源码:

1
2
3
4
5
6
7
8
9
10
11
public class DriverManager{
//注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();

//初始化驱动
static{
loadInitialDriver();
println("JDBC DriverManager initialized");
}
}

先不看别的,看看DriverManager的类加载器:

1
System.out.println(DriverManager.class.getClassLoader());

打印null,表示它的类加载器是Bootstrap ClassLoader,会到JAVA_HOME/jre/lib下搜索类,但JAVA_HOME/jre/lib下显然没有mysql-connector-java-版本号.jar包,那么,在DriverManager的静态代码块中,怎么能正确加载com.mysql.jdbc.Driver?——-打破双亲委派机制

继续看loadInitialDrivers()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static void loadInitialDrivers(){
String drivers;
try{
drivers = AccessController.doPrivileged(new PrivilegedAction<String>(){
public String run(){
return System.getProperty("jdbc.Driver");
}
});
}catch(Exception ex){
drivers = null;
}
//1.使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(new PrivilegedAction<Void>(){
public void run(){
ServiceLoader<Driver> loadDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driverIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()){
driversIterator.next();
}
}catch(){
//Do nothing
}
return null;
}
});
println("DriverManager.initialize:jdbc.drivers = " + drivers);

//2.使用jdbc.drivers定义驱动名加载驱动
if(drivers == null || drivers.equals("")){
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for(String aDriver:driversList){
try{
println("DriverManager.Initialize: loading" + aDriver);
//这里的ClassLoader.getSystemClassLoader()就是应用程序类加载器
Class.forName(aDriver,true,ClassLoader.getSystemClassLoader());
}catch(Exception ex){
println("DriverManager.Initialize: load failed:" + ex);
}
}
}

自定义类加载器

什么时候需要自定义类加载器?

  • 想加载菲classpath随意路径中的文件
  • 都是通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

步骤:

  1. 继承ClassLoader父类
  2. 要遵循双亲委派机制,重写findClass方法。注意不是重写loadClass方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的defineClass方法来加载类
  5. 使用者调用该类加载器的loadClass方法

6.内存模型

java内存模型(Java Memory Model)

JMM定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障。

原子性

问题提出:两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Demo{
static int i = 0;

public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() - > {
for(int j = 0;j < 5000;j++){
i++;
}
});

Thread t2 = new Thread(() - > {
for(int j = 0;j < 5000;j++){
i--;
}
});
t1.start();
t2.start();

t1.join();
t2.join();
System.out.println(i);
}
}

问题分析

以上结果可能是正数、负数、零,为什么?因为java中对静态变量的自增,自减并不是原子操作。

例如,对于i++而言(i是静态变量),实际会产生如下的JVM字节码指令:

1
2
3
4
getstatic	i	//获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i

而对应i–也是类似:

1
2
3
getstatic	i	//获取静态变量i的值
iconst_1 //准备常量1
isub //自减

而java的内存模型如下,完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:

img33

如果是单线程以上8行代码是顺序执行(不会交错)没有问题:

1
2
3
4
5
6
7
8
9
//假设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
isub //线程1-自减 线程内i=0
putstatic i //线程1-将修改后的值存入静态变量i 静态变量i=0

但多线程下这8行代码可能出现交错运行

出现负数的情况:

1
2
3
4
5
6
7
8
9
//假设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

出现正数的情况:

1
2
3
4
5
6
7
8
9
//假设i的初始值为0
getstatic i //线程1-获取静态变量i的值 线程内i=0
getstatic i //线程2-获取静态变量i的值 线程内i=0
iconst_1 //线程1-准备常量1
iadd //线程1-自增 线程内i=1
iconst_1 //线程2-准备常量1
isub //线程2-自减 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i //线程1-将修改后的值存入静态变量i 静态变量i=1

解决方法

synchronized(同步关键字)

语法

1
2
3
synchronized(对象){
要作为原子操作代码
}

用synchronized解决并发问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo{
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 < 5000;j++){
synchronized(obj){
i++;
}
}
});

Thread t2 = new Thread(() - > {
for(int j = 0;j < 5000;j++){
synchronized(obj){
i--;
}
}
});
t1.start();
t2.start();

t1.join();
t2.join();
System.out.println(i);
}
}

运行结果为:0

可见性

退不出的循环

先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
static boolean run = true;

public static void main(String[] args){
Thread t = new Thread(() - >{
while(run){
//.....
}
} );
t.start();

Thread.sleep(1000);
run = false;//线程t不会如预想的停下来
}

分析:

  1. 初始状态,t线程刚开始从主内存读取了run的值到工作内存。

    img34

  2. 因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。

    img35

  3. 1秒后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

    img36

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

可见性

前面的例子体现的实际上就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程情况:

上个例子从字节码理解是这样的:

1
2
3
4
5
6
getstatic	run	//线程t获取run true
getstatic run //线程t获取run true
getstatic run //线程t获取run true
getstatic run //线程t获取run true
getstatic run //线程main修改run为false,仅此一次
getstatic run //线程t获取run false

比较一下之前的两个线程i++和i–

1
2
3
4
5
6
7
8
9
//假设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

注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对较低。

有序性

诡异的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

I_Result是一个对象,有一个属性r1用来保存结果,可能的结果有几种?

情况1:线程1先执行,这时ready = false,所以进入else分支结果为1;

情况2:线程2先执行num = 2,但是没来得及执行ready = true,线程1执行,还是进入else分支,结果为1;

情况3:线程2执行到ready = true,线程1执行,这回进入if分支,结果为4(因为num已经执行过了);

情况4:线程2执行ready = true,切换到线程1,进入if分支,相加为0,再切回线程2执行num = 2。

这种现象叫做指令重排,是JIT编译器在运行时的一些优化,这个现象需要大量测试才能复现。

解决方法(volatile修饰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@JCStressTest
@Outcome(id = {"1","4"},expect = Expect.ACCEPTABLE,desc = "ok")
@Outcome(id = "0",expect = Expect.ACCEPTABLE_INTERESTING,desc = "!!!!")
@State
public class ConcurrentTest{
int num = 0;
volatile boolean ready = false;

//线程1执行此方法
@Actor
public void actor1(I_Result r){
if(ready){
r.r1 = num + num;
}else{
r.r1 = 1;
}
}

//线程2执行此方法
@Actor
public void actor2(I_Result r){
num = 2;
ready = true;
}
}

执行

1
2
mvn clean install
java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

img37

有序性理解

指令重排—>double-checked locking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE = null;
public static Singleton getInstance(){
//实例没创建,才会进入内部的synchronized代码块
if(INSTANCE == null){
synchronized(Singleton.class){
//也许有其它线程已经创建实例,所以再判断一次
if(INSATNCE == null){
INSTANCE = new Sinleton();
}
}
}
return INSTANCE;
}
}

以上实现特点是:

  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁

但在多线程环境下,上面的代码是有问题的,INSTANCE = new Singleton()对应的字节码为:

1
2
3
4
0:new			#2
3:dup
4:invokespecial #3
7:putstatic #4

happens-before

happens-before规定了哪些写操作对其他线程的读操作性可见,它是可见性与有序性的一套规则总结:

  • 线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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变量的写,对接下来其它线程对该变量的读可见
1
2
3
4
5
6
7
8
9
volatile static int x;

new Thread(() - >{
x = 10;
},"t1").start();

new Thread(() - >{
System.out.println(x);
},"t2").start();
  • 线程start前对变量的写,对该线程开始后对该变量的读可见
1
2
3
4
5
6
7
static int x;

x = 10;

new Thread(() - > {
System.out.println(x);
},"t2").start();
1
2
3
4
5
6
7
static int x;

x = 10;

new Thread(()->{
System.out.println(x);
},"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)
1
2
3
4
5
6
7
8
9
static int x;

Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();

t1.join();
System.out.println(x);
  • 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int x;

public static void main(String[] args){
Thread t2 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.prinln(x);
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);
}

CAS与原子类

CAS

CAS即Compare and Swap,它体现的一种乐观锁的思想,比如多个线程要对一个共享的整型变量执行+1操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//需要不断尝试
while(true){
int 旧值 = 共享变量;//比如拿到当前值0
int 结果 = 旧值 + 1//在旧值0的基础上增加1,正确结果是1

/*
这时候如果别的线程把共享变量改成了5,本线程的正确结果就作废了,这时候
CAS返回false,重新尝试,知道:
CAS返回true,表示本线程做修改的同时,别的线程没有干扰
*/
if(compareAndSwap(旧值,结果)){
//成功,退出循环
}
}

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,使用于竞争不激烈,多核CPU的场景下。

  • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的原因
  • 但如果竞争激烈,可以想到重试必须频繁发生,反而效率会受影响

CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,下面是直接使用Unsafe对象进行线程安全保护的例子。

乐观锁与悲观锁

  • CAS是基于乐观锁思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试就好了。
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

原子操作类

7.synchronized优化

Java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark World)。Mark World平时存储这个对象的哈希码、分带年龄,当加锁时,这些信息就根据情况被替代为标记位、线程锁记录指针、重量级锁指针、线程ID等内容。

轻量级锁

如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。这就好比:

学生(线程A)用课本占座,上了半节课,出门了(CPU时间到),回来一看,发现课本没变,说明没有竞争,继续上他的课。

如果这期间有其它学生(线程B)来了,会告知(线程A)有并发访问,线程A随即升级为重量级锁,进入重量级锁流程。

而重量级锁就不是那么用课本占座那么简单了,可以想象线程A走之前,把座位用一个铁栅栏围起来

假设有两个方法同步块,利用同一个对象加锁

1
2
3
4
5
6
7
8
9
10
11
12
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark World。

img38

img39

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变成重量级锁。

1
2
3
4
5
6
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块
}
}

img40

img41

重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这个时候锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火相当于阻塞(等待时间长了划算)
  • Java7之后不能控制是否开启自旋功能

自旋重试成功的情况

img42

自旋重试失败的情况

img43

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark World头,之后发现这个线程ID是自己的就表示没有竞争,不用重新使用CAS。

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有的线程需要暂停(STW)
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,之后重置对象的线程ID
  • 撤销偏向和重偏向都是批量进行的,以类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用-XX:-UseBiasedLocking禁用偏向锁

假设有两个方法同步块,利用同一对象加锁

1
2
3
4
5
6
7
8
9
10
11
12
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}

其它优化

  1. 减少上锁时间

    同步代码块中尽量短

  2. 减少锁的粒度

    将一个锁拆分为多个锁提高并发度,例如:

    • ConcurrentHashMap
    • LongAdder分为base和cells两部分。没有并发争用的时候或是cells数组正在初始化的时候,会使用CAS来累加值到base,有并发争用,会初始化cells数组,数组有多少个cell,就允许有多少个线程并行修改,最后将数组中每个cells累加,再加上base就是最终的值。
    • LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高。
  3. 锁粗化

    多次循环进入同步块不如同步块内多次循环

    另外JVM可能会做如下优化,把多次append的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)

    1
    new StringBuffer().append("a").append("b").append("c");
  4. 锁消除

    JVM会进行代码的逃逸分析,例如某个加锁对象时方法内局部变量,不会被其它线程访问到 ,这时候就会被即时编译器忽略掉所有同步操作。

  5. 读写分离

    CopyOnWriteArrayList

    ConvOnWriteSet

-------------本文结束感谢您的阅读-------------