引言

什么是JVM

  • 定义:
    • Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)
  • 好处:
    • 一次编写,到处运行
    • 自动内存管理,垃圾回收功能
    • 数组下标越界检查 多态 比较: jvm jre jdk
    • 多态
  • 比较: jvm jre jdk
    • 基础类库: IO List等
    • 编译工具:javac 内存监测工具

学jvm有什么用

  • 面试
  • 理解底层的实现原理
  • 中高级程序员的必备技能

常见的JVM

  • jVM是一种规范
  • 实现很多
  • 本课程讲hotspot

学习路线

一、内存结构

1. 程序计数器

|600

1.1 定义

Program Counter Register 程序计数器(寄存器)

  • 作用
    • 是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的 (每个线程有属于自己的程序计数器)
    • 不会存在内存溢出 唯一一个不会内存溢出的地方

1.2 作用

  • 通过寄存器(CPU组件)实现java程序计数器
    • 屏蔽物理硬件

2.虚拟机栈

2.1 定义

  • Java Virtual Machine Stacks (Java 虚拟机栈)
  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 问题辨析:
    1. 垃圾回收是否涉及栈内存? 不涉及,栈内存自动释放
    2. 栈内存分配越大越好吗? -Xss指定大小 默认1024kb 越大线程越少
    3. 方法内的局部变量是否线程安全?
      1. 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
      2. 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全 如static全局变量

demo:

2.2 栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出
  • 演示
    • 执行了 42171
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_2 {
private static int count;

public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}

private static void method1() {
count++;
method1();
}
}

循环依赖问题

  • 原因:
    • 部门下有员工,员工属性有部门,部门下有员工
    • 无限套娃
  • 如何正确使用:
    • 在员工下的部门加上 @JsonIgnore
    • |450
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
/**  
* json 数据转换
* 循环依赖问题
*/
public class Demo1_19 {

public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");

Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);

Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);

d.setEmps(Arrays.asList(e1, e2));

// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}

// 员工
class Emp {
private String name;
// !!! @JsonIgnore
private Dept dept;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Dept getDept() {
return dept;
}

public void setDept(Dept dept) {
this.dept = dept;
}
}
// 部门
class Dept {
private String name;
private List<Emp> emps;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Emp> getEmps() {
return emps;
}

public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}

2.3 线程运行诊断

  • 案例1:cpu 占用过多 nohup java Dem1_16 &

  • 定位

    • 用top定位哪个进程对cpu的占用过高
    • ps H -eo pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪个线程引起的cpu占用过高)
    • jstack 进程id
      • 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
  • 案例2:程序运行很长时间没有结果 (死锁)

3. 本地方法栈

  • 用C语言写的native方法,直接和底层交互
  • 举例 Object类下的方法
    • protected native Object clone() throws CloneNotSupportedException;

4. 堆

  • 上面三个线程私有 堆和方法区线程共享

4.1 定义

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

4.2 堆内存溢出

  • 控制堆空间参数:-Xmx8m
  • |600
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 演示堆内存溢出 java.lang.OutOfMemoryError: Java heap space
* -Xmx8m */
public class Demo1_5 {

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

4.3 堆内存诊断

  1. jps 工具
    • 查看当前系统中有哪些 java 进程
  2. jmap 工具
    • 查看堆内存占用情况 jmap - heap 进程id
    • 只能查看某一时刻
    • |625
  3. jconsole 工具
    • 图形界面的,多功能的监测工具,可以连续监测
    • 功能全 。终端输入 jconsole 弹出选择框选择java类
    • |550
  • 案例 垃圾回收后,内存占用仍然很高
  • jvisualvm 终端输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**  
* 演示查看对象个数 堆转储 dump
*/public class Demo1_13 {

public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];
}

5. 方法区

5.1 定义

  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html

  • The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.

  • Java 虚拟机具有在所有 Java 虚拟机线程之间共享的方法_区域_ 。方法区域类似于常规语言的编译代码的存储区域,或类似于作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化中使用的特殊方法 (§2.9) 和接口初始化。

  • The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

  • 方法区域在虚拟机启动时创建。尽管方法区域在逻辑上是堆的一部分,但简单的实现可以选择不进行垃圾回收或压缩。此规范不强制要求方法区域的位置或用于管理编译代码的策略。方法区域可以是固定大小的,也可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。

5.2 组成

  • 1.6用 永久代
  • 1.8用 元空间

5.3 方法区内存溢出

  • 1.8 以前会导致永久代内存溢出
    • `* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space *
    • -XX:MaxPermSize=8m
  • 1.8 之后会导致元空间内存溢出
    • * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
    • * -XX:MaxMetaspaceSize=8m
      案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**  
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
// 加载一万个类
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[] byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}

场景

  • 使用cglib
    • spring
    • mybatis

5.4 运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息
  • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量 池,并把里面的符号地址变为真实地址
  • 执行 javap -v 如下
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
❯ javap -v out.production.jvm.cn.itcast.jvm.t5.HelloWorld
警告: 文件 ./out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class 不包含类 out.production.jvm.cn.itcast.jvm.t5.HelloWorld
Classfile /Users/cyt/workspace/java/jvm-std/jvm/out/production/jvm/cn/itcast/jvm/t5/HelloWorld.class
Last modified 2025年10月22日; size 567 bytes
SHA-256 checksum 37204bf6e654f64ae56660a1e8becfaa98b3ae7592b81b4b6e331de92a460b96
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t5.HelloWorld
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // cn/itcast/jvm/t5/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool: # 常量池
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // cn/itcast/jvm/t5/HelloWorld
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 HelloWorld.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 cn/itcast/jvm/t5/HelloWorld
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
# 无参构造方法
public cn.itcast.jvm.t5.HelloWorld();
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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;

# mian 方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
# 虚拟机指令
stack=2, locals=1, args_size=1
# #2上面常量池地址
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
# 执行方法调用
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

5.5 StringTable

先看几道面试题:
|725

案例解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 串池(类似哈希表) StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容  
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象

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() new String("ab") 创建一个新对象
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s4); // false s3在串池中 s4在堆中
System.out.println(s3 == s5); // true
}
}
  • javac -g Demo1_22.java
    • 编译期优化
    • 可以发现s5在javac编译时已经赋值“ab”
    • 此时“ab”已在串池中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package cn.itcast.jvm.t1.stringtable;

public class Demo1_22 {
public Demo1_22() {
}

public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
(new StringBuilder()).append(s1).append(s2).toString();
String s5 = "ab";
}
}
  • javap -v Demo1_22.class > output.txt
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
❯ javap -v my1
Classfile /Users/cyt/workspace/java/jvm-std/jvm/src/cn/itcast/jvm/t1/stringtable/Demo1_22.class
Last modified 2025-10-23; size 741 bytes
MD5 checksum d4b9eed9cffe233ec5fa007c264eb2d2
Compiled from "Demo1_22.java"
public class cn.itcast.jvm.t1.stringtable.Demo1_22
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 // cn/itcast/jvm/t1/stringtable/Demo1_22
#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 Lcn/itcast/jvm/t1/stringtable/Demo1_22;
#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 Demo1_22.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 cn/itcast/jvm/t1/stringtable/Demo1_22
#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 cn.itcast.jvm.t1.stringtable.Demo1_22();
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 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/itcast/jvm/t1/stringtable/Demo1_22;

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 第一次ldc #4
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 第二次ldc #4
31: astore 5
33: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
line 14: 9
line 15: 29
line 18: 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: "Demo1_22.java"

5.5 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
public class Demo1_23 {  

public static void main(String[] args) {

String x = "ab"; // 串池中有 "ab"
// 此时堆中有: new String("a") new String("b") new String("ab")
// 字符串池中有: ["a", "b"] 还没有 "ab"
String s = new String("a") + new String("b");

// 串池有“ab”, 此时s没有被放进串池中,s仍在堆中
String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

System.out.println(s2 == x); // true
System.out.println(s == "ab"); // false
System.out.println(s == x); // false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo1_23 {  

public static void main(String[] args) {

// 此时堆中有: new String("a") new String("b") new String("ab")
// 字符串池中有: ["a", "b"] 还没有 "ab"
String s = new String("a") + new String("b");

// 串池无“ab” s被放进串池,返回: s2也是串池
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
/**  
* 演示字符串相关面试题
*/
public class Demo1_21 {

public static void main(String[] args) {
String s1 = "a"; // 池中
String s2 = "b"; // 池中
String s3 = "a" + "b"; // 池中 ab String s4 = s1 + s2; // 堆中 调用StringBuiler new String("ab")
String s5 = "ab"; // 池中
String s6 = s4.intern(); // s4没有放进池中(仍在堆中) s6池中

// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
System.out.println(s6 == "ab"); // true

String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern(); // 放进池中
String x1 = "cd"; // 池中

// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2); // true
}
}

5.6 StringTable位置

演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/public class Demo1_6 {

public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
  • jdk1.6报错
    |600
  • jdk1.8报错
    |550

5.7 StringTable 垃圾回收

  • 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**  
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}

}
}
  • 日志
    • 循环十万次,放入十万个数到 StringTable
    • 触发GC回收 GC (Allocation Failure) [PSYoungGen: 4096K->496K(4608K)] 4096K->536K(15872K),
    • 并没有真正放入十万个数(53774个): Number of entries : 53774 = 1290576 bytes, avg 24.000
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
[GC (Allocation Failure) [PSYoungGen: 4096K->496K(4608K)] 4096K->536K(15872K), 0.0007230 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
100000
Heap
PSYoungGen total 4608K, used 3435K [0x00000007bfb00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 4096K, 71% used [0x00000007bfb00000,0x00000007bfddee40,0x00000007bff00000)
from space 512K, 96% used [0x00000007bff00000,0x00000007bff7c010,0x00000007bff80000)
to space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
ParOldGen total 11264K, used 40K [0x00000007bf000000, 0x00000007bfb00000, 0x00000007bfb00000)
object space 11264K, 0% used [0x00000007bf000000,0x00000007bf00a000,0x00000007bfb00000)
Metaspace used 3413K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 368K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13264 = 318336 bytes, avg 24.000
Number of literals : 13264 = 521312 bytes, avg 39.303
Total footprint : = 999736 bytes
Average bucket size : 0.663
Variance of bucket size : 0.666
Std. dev. of bucket size: 0.816
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 53774 = 1290576 bytes, avg 24.000
Number of literals : 53774 = 3021376 bytes, avg 56.187
Total footprint : = 4792056 bytes
Average bucket size : 0.896
Variance of bucket size : 0.704
Std. dev. of bucket size: 0.839
Maximum bucket size : 5

5.8 StringTable 性能调优

调整 -XX:StringTableSize=桶个数

  • 案例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/public class Demo1_24 {

public static void main(String[] args) throws IOException {
// 读入48万个单词
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); // 124ms
}
}
}
  • 默认 60013
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cost:129 (时间 ms)
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13266 = 318384 bytes, avg 24.000
Number of literals : 13266 = 521352 bytes, avg 39.300
Total footprint : = 999824 bytes
Average bucket size : 0.663
Variance of bucket size : 0.666
Std. dev. of bucket size: 0.816
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 480634 = 11535216 bytes, avg 24.000
Number of literals : 480634 = 29632912 bytes, avg 61.654
Total footprint : = 41648232 bytes
Average bucket size : 8.009
Variance of bucket size : 8.067
Std. dev. of bucket size: 2.840
Maximum bucket size : 23
  • 修改为 200000个后
    • 时间为98ms
    • -XX:StringTableSize=200000
    • 原因:空间换时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cost:98
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13266 = 318384 bytes, avg 24.000
Number of literals : 13266 = 521352 bytes, avg 39.300
Total footprint : = 999824 bytes
Average bucket size : 0.663
Variance of bucket size : 0.666
Std. dev. of bucket size: 0.816
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 200000 = 1600000 bytes, avg 8.000
Number of entries : 480634 = 11535216 bytes, avg 24.000
Number of literals : 480634 = 29632912 bytes, avg 61.654
Total footprint : = 42768128 bytes
Average bucket size : 2.403
Variance of bucket size : 2.416
Std. dev. of bucket size: 1.554
Maximum bucket size : 12
  • 调小,修改为 1009
    • -XX:StringTableSize=1009
    • 时间明显变慢
    • 原因:哈希冲突太多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cost:1497
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13266 = 318384 bytes, avg 24.000
Number of literals : 13266 = 521352 bytes, avg 39.300
Total footprint : = 999824 bytes
Average bucket size : 0.663
Variance of bucket size : 0.666
Std. dev. of bucket size: 0.816
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 1009 = 8072 bytes, avg 8.000
Number of entries : 480634 = 11535216 bytes, avg 24.000
Number of literals : 480634 = 29632912 bytes, avg 61.654
Total footprint : = 41176200 bytes
Average bucket size : 476.347
Variance of bucket size : 434.024
Std. dev. of bucket size: 20.833
Maximum bucket size : 544

考虑将字符串对象是否入池

  • 入池和不入池对内存占比
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
/**  
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/

public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
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;
}
// address.add(line);
address.add(line.intern());
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
System.in.read();
}
}
  • 使用 visualvm 工具
  • 读取文件之前
    |1000
  • 开始读文件 address.add(line);
    • 加起来占用 80%
  • 使用放入串池方法 address.add(line.intern());
    • 加起来占用 60% String和char[]

6. 直接内存

6.1 定义

Direct Memory

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

案例

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
/**  
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "/Users/cyt/Movies/test/1.mp4";
static final String TO = "/Users/cyt/Movies/test/2.mp4";
static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}

private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}

private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}


普通io

  • 切换为内核态
  • 在内存申请java缓冲区
  • 系统缓冲区内容复制到java缓冲区
    |625

directBuffer

  • 调用 ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
  • direct memory java和系统都可以直接访问
    |700

内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 演示直接内存溢出
*/
public class Demo1_10 {
static 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); // 直接内存分配 100MB list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}

6.2 分配和回收原理

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleanerclean 方法调 用 freeMemory 来释放直接内存

演示垃圾回收

  • 使用 bytebuffer
  • -XX:+DisableExplicitGC 禁用显式的垃圾回收 System.gc();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**  
* 禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;

/*
* -XX:+DisableExplicitGC 禁用显式的垃圾回收 System.gc();
*/
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
System.gc(); // 显式的垃圾回收,Full GC
byteBuffer = null;
System.in.read();
}
}
  • 按下第一次回车出现java进程 占用1GB
  • 再次按下回车释放内存
    |800

unsafe

  • 通过反射获取unsafe
  • java不推荐程序员使用
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
/**  
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb); // 内存地址
System.out.println("base地址: " + base);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();

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

public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

byteBuffer源码剖析

  • ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
  • 多开一个线程
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
DirectByteBuffer(int cap) {                   // package-private  

super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

cleaner对象

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
public class Cleaner  
extends PhantomReference<Object>
{

// Dummy reference queue, needed because the PhantomReference constructor
// insists that we pass a queue. Nothing will ever be placed on this queue
// since the reference handler invokes cleaners explicitly.
// private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

// Doubly-linked list of live cleaners, which prevents the cleaners
// themselves from being GC'd before their referents
// static private Cleaner first = null;

private Cleaner
next = null,
prev = null;

private static synchronized Cleaner add(Cleaner cl) {
if (first != null) {
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}

private static synchronized boolean remove(Cleaner cl) {

// If already removed, do nothing
if (cl.next == cl)
return false;

// Update list
if (first == cl) {
if (cl.next != null)
first = cl.next;
else
first = cl.prev;
}
if (cl.next != null)
cl.next.prev = cl.prev;
if (cl.prev != null)
cl.prev.next = cl.next;

// Indicate removal by pointing the cleaner to itself
cl.next = cl;
cl.prev = cl;
return true;

}

private final Runnable thunk;

private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}

/**
* Creates a new cleaner. * * @param ob the referent object to be cleaned
* @param thunk
* The cleanup code to be run when the cleaner is invoked. The
* cleanup code is run directly from the reference-handler thread, *
so it should be as simple and straightforward as possible. * *
@return The new cleaner
*/
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}

/**
* Runs this cleaner, if it has not been run before. */ public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}

}