JVM相关知识点

程序计数器(Program Counter Register)

程序计数器里面存放的是下一条 JVM 指令代码的执行地址,概念上有点类似数据库结果集遍历时的游标,不断指向下一条,物理上是通过寄存器实现的,寄存区在 CPU 上是非常快的单元

在 JVM 的规范中,每条线程都有自己的程序计数器,所以程序计数器是线程私有的,因为当发生线程切换的时候,需要知道切换回来后下一步应该执行什么操作,如果不是线程私有则会造成混乱

程序计数器是 JVM 中唯一不会存在 OOM 的区域,这一点是 JVM 规范上要求的


虚拟机栈

说虚拟机栈之前先说栈,栈的特点就是先进后出或后进先出

虚拟机栈是线程运行时需要的内存空间,线程运行的目的是为了执行代码,代码由一个个方法组成,当线程运行的时候,每个方法需要的内存空间就是栈帧(参数,局部变量,返回地址),每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存,调用方法时入栈,执行完出栈,每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(栈顶部),栈是有一定的深度的,当发生递归死循环时,一直有方法入栈但不出栈,导致栈溢出,抛出 java.lang.StackOverflowError

IDEA debug 时左下角的 Frame 就是栈帧,比较直观,从上到下,右边还可以看到栈内的局部变量值

垃圾回收是否涉及栈内存

不会

栈内存随着出栈就释放掉,垃圾回收主要用来回收堆内存

栈内存分配越大越好吗

不是

-Xss size(比如-Xss1m,没有等号)

Windows 下取决于虚拟内存的设置,其他主流操作系统上默认为 1024KB(1MB)

物理内存的大小是固定的,栈内存分配设置得越大,占用越多内存,由于栈是线程私有的,所以线程占用的内存就会变大,那么操作系统可以创建的总线程数就会变小

比如操作系统 10M 内存(只是举例),本来一个线程默认占用 1M 内存,那么可以创建 10 个线程,但设置-Xss2m 后,只能创建 5 个线程了

-Xss 设置得大,只会增加方法递归调用的次数,一般不会提升性能,就是不容易爆栈,但死循环递归该爆还得爆

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

每个线程都有私有的栈帧,里面会有各自独立的变量,互不干扰。线程安全问题出现在共享变量上,如果方法内局部变量没有逃离方法的作用范围,那么这个局部变量就是线程安全的

线上诊断方法

通常都是十八般武艺配合着来用,包括但不限于:

  • top 命令,查看 %CPU,配合 grep java
  • ps H -eo pid,tid,%cpu(小写 cpu) | grep 进程号
  • jstack 进程 id,会列出所有线程,有线程号 nid(16 进制),printf ‘%x’ 10 进制 id
  • jstack 查看是否出现死锁

本地方法栈

native 地方法运行时所需要的空间,其他的每什么好说的了,不懂


通过 new 关键字创建的对象都会使用堆内存
堆里面的对象是线程共享的,所以一般都需要考虑线程安全的问题,堆有垃圾回收机制,并且会发生内存溢出 java.lang.OutOfMemoryError: Java heap sapce

JVM 参数常用的有两个,用于设置内存占用大小
-Xms:堆的初始大小
-Xmx:堆的最大占用

为什么一般都会将这两个参数设置成一样?
因为当堆内存不足需要进行扩容的时候,会发生内存抖动,对程序运行的稳定性会有一定的影响

线上诊断方法

包括但不限于:

  • jps:查看系统中存在的 Java 进程
  • jmap:查看堆内存占用情况,jmap -head pid
  • jconsole:图形界面的,多功能监测工具,可以连续监测(可测出是否发生死锁)
  • jvisualvm:堆 dump

方法区

线程共享,存放一些类的基本信息(类,类加载器,运行时常量池),逻辑上是堆的一部分,不同厂商有不同的实现

HotSpot 在 1.7 之前用永久代(堆),1.8 后用元空间(操作系统内存)

方法区是规范,永久代或者元空间是其中一种实现,方法区会发生内存溢出
1.8 以前会导致永久代内存溢出:-XX:MaxPermSize=m(OutOfMemory:PermGen space)
1.8 之后会导致元空间内存溢出:-XX:MaxMetaspaceSize=m(OutOfMemory:Metaspace)


字符串常量池,串池(StringTable)

常量池:是一张表,维护类名,方法名,参数类型,字面量等信息

运行时常量池:当类被加载时,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

字符串常量池:字符串是日常开发中使用得非常多的类,池化技术某种程度上会有性能上的提升

如何判断字符串是在堆还是在串池中

1
2
3
4
5
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
}

javap -v x.class 查看二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)

不同的 JDK 版本可以看到反编译出来的不一样

JDK 1.8

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
Classfile /home/linweiyuan/Temp/test/Test.class
Last modified Sep 24, 2022; size 759 bytes
MD5 checksum 1eeb9a3b24c5c4f6a213e62ca4ee1691
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
#2 = String #19 // a
#3 = String #20 // b
#4 = Class #21 // java/lang/StringBuilder
#5 = Methodref #4.#18 // java/lang/StringBuilder."<init>":()V
#6 = Methodref #4.#22 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #4.#23 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #24 // Test
#9 = Class #25 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 a
#20 = Utf8 b
#21 = Utf8 java/lang/StringBuilder
#22 = NameAndType #26:#27 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#23 = NameAndType #28:#29 // toString:()Ljava/lang/String;
#24 = Utf8 Test
#25 = Utf8 java/lang/Object
#26 = Utf8 append
#27 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#28 = Utf8 toString
#29 = Utf8 ()Ljava/lang/String;
{
public Test();
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 1: 0

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: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: return
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
line 7: 25
}
SourceFile: "Test.java"

JDK 9+,我这里用的是 JDK 17,但其实从 9 开始就变了,之前有试过,最近整理笔记,电脑上 JDK 9 已经没有了

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
Classfile /home/linweiyuan/Temp/test/Test.class
Last modified Sep 24, 2022; size 759 bytes
SHA-256 checksum 1f929ea79bba2fa30f55910a61ea7627d430b3dff13f7bdb8cd275e901a0343d
Compiled from "Test.java"
public class Test
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #15 // Test
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 3
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // a
#8 = Utf8 a
#9 = String #10 // b
#10 = Utf8 b
#11 = InvokeDynamic #0:#12 // #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#12 = NameAndType #13:#14 // makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#13 = Utf8 makeConcatWithConstants
#14 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#15 = Class #16 // Test
#16 = Utf8 Test
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 SourceFile
#22 = Utf8 Test.java
#23 = Utf8 BootstrapMethods
#24 = MethodHandle 6:#25 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#25 = Methodref #26.#27 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#26 = Class #28 // java/lang/invoke/StringConcatFactory
#27 = NameAndType #13:#29 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#28 = Utf8 java/lang/invoke/StringConcatFactory
#29 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#30 = String #31 // \u0001\u0001
#31 = Utf8 \u0001\u0001
#32 = Utf8 InnerClasses
#33 = Class #34 // java/lang/invoke/MethodHandles$Lookup
#34 = Utf8 java/lang/invoke/MethodHandles$Lookup
#35 = Class #36 // java/lang/invoke/MethodHandles
#36 = Utf8 java/lang/invoke/MethodHandles
#37 = Utf8 Lookup
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: ldc #7 // String a
2: astore_1
3: ldc #9 // String b
5: astore_2
6: aload_1
7: aload_2
8: invokedynamic #11, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: return
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
line 7: 14
}
SourceFile: "Test.java"
BootstrapMethods:
0: #24 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#30 \u0001\u0001
InnerClasses:
public static final #37= #33 of #35; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

得出结论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JDK 1.8
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2; // new StringBuilder().append("a").append("b").toString();

// JDK 1.9 -> StringConcatFactory.makeConcatWithConstants()
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // StringConcatFactory.makeConcatWithConstants()
String s5 = "a" + "b"; // 编译期优化

System.out.println(s3 == s4); // false,因为 s3 存在串池中,s4 是 new 出来的对象在堆中
System.out.println(s3 == s5); // true,因为 ab 已经存在串池中了(s3),对于确定的字符串(不是变量),Java 会进行编译期优化

StringTable 特性

  • 常量池中的字符串只是符号,第一次用到时才变为对象
  • 利用串池的机制,可以避免重复创建字符串对象
  • 字符串常量拼接的原理是编译期优化
  • 用 intern() 可以主动把串池中还没有的字符串对象放入串池(尝试放入串池,有则不会放入,没有则放入,无论放入是否成功,返回的都是串池中的对象)

这里有一个要注意的地方,JDK 1.8 intern() 将自己放入串池(一个对象),JDK 1.6 把自己复制一份放进串池(两个对象)

1
2
3
4
5
String s = new String("a") + new String("b"); // 此时 s 在堆中
String s2 = s.intern(); // s 放入了串池,并且返回了串池中的对象 ab

System.out.println(s2 == "ab"); // true,因为 s2 就是串池中返回的对象,和串池中的对象 ab 相比,相等
System.out.println(s == "ab"); // true, 因为串池中还没有,intern() 放入了后,s 此时在串池中
1
2
3
4
5
6
String x = "ab"; // 串池中已经有了 ab
String s = new String("a") + new String("b"); // 此时 s 在堆中
String s2 = s.intern(); // s 放入串池失败,因为已经有了,返回了串池中的对象 ab 给 s2,但是 s 还在堆中

System.out.println(s2 == "ab"); // true,因为 s2 就是串池中返回的对象,和串池中的对象 ab 相比,相等
System.out.println(s == "ab"); // false, 因为串池放入失败,s还在堆中
1
2
3
4
5
6
String s = new String("a") + new String("b"); // 此时 s 在堆中
System.out.println(s == "ab"); // false, ab 此时在串池中,s 在堆中
String s2 = s.intern(); // s 放入串池失败,因为已经有了,返回了串池中的对象 ab 给 s2,但是 s 还在堆中

System.out.println(s2 == "ab"); // true,因为 s2 就是串池中返回的对象,和串池中的对象 ab 相比,相等
System.out.println(s == "ab"); // false, 因为串池放入失败,s 还在堆中

StringTable 位置

  • 1.6:常量池的一部分,位于永久代中(永久代 Full GC 才会触发,字符串用的场景很多,但是好多都是可以回收的,内存占用太多会造成永久代内存不足)(PermGen space)
  • 1.8:堆中的一部分(堆 Minor GC 就会触发)(Heap space)
  • -XX:-UseFCOverheadLimit(+启用,-禁用,默认启用):如果 98%的时间花在了垃圾回收上,但是只有 2% 的堆内存回收了,此时 JVM 直接抛出 OutOfMemoryError:GC overhead limit exceeded

StringTable 性能调优

StringTable 本质上是一个哈希表,哈希表性能与桶的个数有关

  • 桶个数太大,元素分散,哈希碰撞机率减少,检索速度较快
  • 桶个数太小,哈希碰撞机率增加,链表就会变长,查找速度就会降低

可以通过 JVM 参数调整桶的个数,-XX:StringTableSize=12345(最小值 1009)


垃圾回收

最快的 GC 是不发生 GC

如何判断对象是否能回收

  • 引用计数法(有循环引用问题,A 引用 B,B 引用 A,都不会被回收,JVM 不是这种)
  • 可达性分析算法(JVM 采用这种,GC Roots)
    • 哪种可作为 Roots?(可以用 Eclipse Memory Analyzer 查看,jps -> jmap -dump:format=b,live,file=m.bin pid)
      • System Class(核心的类,Object, HashMap, String 等)
      • Native Stack(操作系统方法执行时引用的 Java 对象)
      • Thread(活动线程)
      • Busy Monitor(被加锁的对象,回收了,就无法解锁)
  • 四种引用
    • 强引用(new,赋值,沿着 GC Roots 能找到,就不会被回收)
    • 软引用(没有强引用时,发生垃圾回收,如果内存不足,再执行一次,被回收,可配合引用队列使用释放自身)
      • SoftReference
      • ReferenceQueue, new SoftReference(obj, queue)
      • 当 obj 被回收时,进入队列
      • queue.poll() 获取自身,遍历 remove()
    • 弱引用(没有强引用时,只要发生了垃圾回收,被回收,可配合引用队列使用释放自身)
      • WeakReference
    • 虚引用(必须配合引用队列使用,用于 NIO 中,由 Reference Handler 处理)
      • PhantomReference
    • 终结器引用(必须配合引用队列使用,finalize() 后进入队列,由 Finalizer 处理)

垃圾回收算法

  • 标记清除:速度较快,容易产生内存碎片(空间不连续,数组放不下)
  • 标记整理:速度较慢,没有内存碎片,标记 -> 清除 -> 整理(会移动对象,因此对象的引用地址会变,需要做额外的工作来更新)
  • 复制:没有内存碎片,需要占用双倍内存空间,将内存分成大小相等的两块(from, to)发生垃圾回收时,交换位置

分代垃圾回收

新生代(朝生夕死):

  • eden
  • from
  • to

垃圾回收过程(GC 都会 Stop The World,时间上新生代短一点,老年代长一点,暂停其他用户线程,让垃圾回收线程先执行完)

  • 新对象默认存放在 eden
  • 当空间不足时,触发 Minor GC
  • 标记-清除-复制,把存活的复制到 to(注意不是到 from),寿命加 1
  • 交换 from to 位置(谁是空的谁就是 to)
  • 继续放对象
  • eden 存活的复制到 to,寿命加 1
  • 原来在 from 的移到存活后移到 to,寿命加 1
  • 交换 from to 位置
  • 寿命超过阈值(15,保存在对象头,4bit,最高 1111 -> 15),晋升到老年代
  • 如果老年代也放不下,先尝试 Minor GC, 内存还不够,触发 Full GC,再不够,抛出 OOM
  • 大对象在新生代空间不够,但老年代空间够的情况下,直接晋升

相关 JVM 参数

参数含义
-Xms初始堆大小
-Xmx / -XX:MaxHeapSize=size堆最大大小
-Xmn / -XX:NewSize=size, -XX:MaxNewSize=size新生代大小
-XX:InitialSurvivorRatio=ratio, -XX:+UseAdaptiveSizePolicy幸存区比例(动态)
-XX:SurvivorRatio=ratio幸存区比例
-XX:MaxTenuringThreshold=threshold晋升阈值
-XX:+PrintTenuringDistribution晋升详情
-XX:+PrintGCDetails -verbose:gcGC 详情
-XX:+ScavengeBeforeFullGCFull GC 前 Minor GC

线程内的 OOM 不会导致整个 JVM 挂掉

垃圾回收器

  • 串行
    • 单线程
    • 适合堆内存较小,个人电脑,CPU 个数少(因为是单线程)
    • -XX:+UseSerialGC=Serial+SerialOld
  • 吞吐量优先(1.8 默认)
    • 多线程
    • 适合堆内存较大,多核 CPU
    • 让单位时间内,STW 的时间最短
    • -XX:+UseParallelGC
    • -XX:+UseParallelOldGC
    • -XX:+UseAdaptiveSizePolicy
    • -XX:GCTimeRatio=99 (1 / 1 + ratio, 默认 99,0.01,100 分钟内只能一分钟用于垃圾回收,因为上一行配置了动态调整,所以一般会自动增加堆的大小,大小增加了,就能放更多的东西,垃圾回收的次数就会降低,吞吐量就会提高,一般设置 19)
    • -XX:MaxGCPauseMillis=200(毫秒,和上面的配置冲突,只能取折中,因为堆增大了,虽然 GC 次数减少,但单次的时间会增加)
    • -XX:ParallelGCThreads=n
  • 响应时间优先
    • 多线程
    • 适合堆内存较大,多核 CPU
    • 尽可能让单次 STW 的时间最短
    • -XX:+UseConcMarkSweepGC
    • -XX:+UseParNewGC SerialOld
    • -XX:ParallelGCThreads=n
    • -XX:ConcGCThreads=n
    • -XX:CMSInitiatingOccupancyFraction=percent
    • -XX:+CMSScavengeBeforeRemark

如何区分吞吐量优先和响应时间优先?

吞吐量优先:比如,每次花费 0.2 秒,一小时两次,共 0.4 秒,垃圾回收时间占比越低,吞吐量越高

响应时间优先:比如,每次只需 0.1 秒,但可能一小时发生了 5 次,共 0.5 秒

  • G1(Garbage First,JDK 9 默认,废弃 CMS)
    • 同时注重吞吐量和低延迟,默认的暂停目标是 200ms
    • 超大堆内存,将堆划分成多个大小相等的 Region
    • 整体上是标记-整理,两个区域之间是复制
    • -XX:+UseG1GZ
    • -XX:G1HeapRegionSize=size
    • -XX:MaxGCPauseMillis=ms