深入理解Java虚拟机(第3版)笔记

第一部分 走进Java

第1章 走进Java

1.1 概述

  • 一次编写,到处运行

  • 相对安全的内存管理跟访问机制

  • 热点代码检测和运行时编译及优化

1.2 Java技术体系

  • 按功能划分

image-1-2

  • 按业务划分

Java Card(小内存设备) Java ME(移动终端)Java SE(桌面级应用)Java EE(企业应用)

1.3 Java发展史

image-20240814194645425

1.4 Java虚拟机家族

1.5 展望Java技术的未来

各种语言上都能跑(Graal VM)

即时编译–>提前编译(Substrate VM:Graal VM极小型的运行时环境)

1.6 实战:自己编译JDK

  • 获取源码

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

2.1 概述

2.2 运行时数据区域

image-20240816212918902

  • 程序计数器:较小的内存空间,相当于一个“书签”,分支、循环、跳转、异常处理、多线程切换时从这个“书签”继续干活 –> 各线程不能互相干扰,线程私有内存,如果正在执行的是本地(Native)方法,值为空(Undefined)
  • Java虚拟机栈:线程私有

​ 每个方法被调用时,创建栈帧,存储局部变量表(存放基本数据类型、对象引用【reference类型】;Double 跟Long占两个局部变量槽)、操作数表、动态连接、方法出口等,执行完毕出栈

​ 可能会有StackOverflowError和OutOfMemoryError异常

  • 本地方法栈:虚拟机使用到的本地(Native)方法,同样可能有出StackOverflowError和OutOfMemoryError异常
  • Java堆:内存最大,启动时创建,所有线程共享,存放对象实例,内存不足时会产生OutOfMemoryError异常;内存空间物理不连续,逻辑上应该连续;已经出现不采用分代设计的垃圾收集器了

​ 大小可固定也可扩展,通过参数-Xmx和-Xms设定

  • 方法区:与Java堆类似,所有线程共享,但存放的是已被虚拟机加载的类型信息、常量、静态变量等,内存不足时会产生OutOfMemoryError异常
  • 运行时常量池:方法区的一部分,除了保存Class文件中描述的各种字面量、符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中;String.intern()可动态放入;OutOfMemoryError
  • 直接内存:并不是虚拟机运行时数据区的一部分,为了方便NIO操作直接分配的堆外内存

2.3 HotSpot虚拟机对象探秘

深入探讨一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程

2.3.1 对象创建:

  • new指令:检查常量池是否有该类符号引用,是否已被加载、解析和初始化
  • 内存分配:(固定分布、指针偏移)指针碰撞与(交错分布、分配足额)空闲列表
  • 线程安全:CAS与失败重试;本地线程分配缓存(各线程先分一点)
  • 必要设置:初始化零值,(实例、元数据信息、哈希码等)对象头设置,是否启用偏向锁等
  • 初始化执行:执行()方法

2.3.2 对象的内存布局

  • 对象头:一部分为存储对象自身的运行时数据(哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等);另一部分为类型指针;如果为Java数组,还需记录长度
  • 实例数据:对象真正存储的有效信息
  • 对齐填充:占位符作用,非必然

2.3.3 对象的访问定位

  • 句柄:Java堆划出一块句柄池,reference存放对象的句柄地址

image-20240820215458194

  • 直接指针:reference中存储的直接就是对象地址(更快,节省一次开销)

image-20240820215624223

2.4 实战:OutOfMemoryError异常

2.4.1 Java堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
//结果 会进一步提示Java heap space
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10924.hprof ...
Heap dump file created [28166528 bytes in 0.087 secs]

image-20240821215327142

怎么解决:对象是否必要(内存泄漏\内存溢出)

内存泄漏:检查GCRoots,定义对象产生的源代码

内存溢出:检查JVM参数,内存是否要扩大,对象是否合理

2.4.2 虚拟机栈和本地方法栈溢出

HotSpot不区分虚拟机栈和本地方法栈,不支持扩展,只有在线程创建时未获取到足够内存才会oom

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
//-Xss128k ·使用-Xss参数减少栈内存容量
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
//只会出现StackOverflowError
stack length:999
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:5)

//定义大量本地变量
public class JavaVMStackSOF {
private static int stackLength = 0;
public static void test() {
long unused1, ..., unused100;
stackLength ++;
test();
unused1 = ... = unused100 = 0;
}
public static void main(String[] args) {
try {
test();
}catch (Error e){
System.out.println("stack length:" + stackLength);
throw e;
}
}
}

//也是只会出现StackOverflowError
stack length:7243
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.test(JavaVMStackSOF.java:43)

//通过无限创建线程来达到oom
//-Xss2M
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

2.4.3 方法区和运行时常量池溢出

永久代实现方法区:通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,间接限制常量池容量

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
// -XX:PermSize=6M -XX:MaxPermSize=6M 在JDK6中运行
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
// 在short范围内足以让6MB的PermSize产生OOM了
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}

//结果 oom PermGen space
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java: 18)

//在JDK7及以上无效果,因为字符串常量池被移至Java堆之中
//使用-Xmx参数限制最大堆到6MB就能够看到以下两种运行结果之一,具体取决于哪里的对象分配时产生了溢出
// OOM异常一
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.lang.Integer.toString(Integer.java:440)
at java.base/java.lang.String.valueOf(String.java:3058)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)
// OOM异常二
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.HashMap.resize(HashMap.java:699)
at java.base/java.util.HashMap.putVal(HashMap.java:658)
at java.base/java.util.HashMap.put(HashMap.java:607)
at java.base/java.util.HashSet.add(HashSet.java:220)
at RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java from InputFile-Object:14)

//JDk6: false false JDK7:true false 只需要在常量池里记录一下首次出现的实例引用即可
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
//java在这个类sun.misc.Version进入常量池
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}

//借助CGLib使得方法区出现内存溢出异常
// -XX:PermSize=10M -XX:MaxPermSize=10M
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}

//JDK7中结果
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8 more

2.4.4 本机直接内存溢出

-XX:MaxDirectMemorySize指定,默认与-Xmx 一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// -Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

//结果
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at DirectMemoryOOM.main(DirectMemoryOOM.java:13)

在Heap Dump看不见明显的异常,但dump文件很小,考虑是否直接或间接使用(间接:NIO)

第3章 垃圾收集器与内存分配策略

3.1 概述

只关注Java堆和方法区(共有)

3.2 对象已死?

3.2.1 引用计数算法

引用+1,失效-1,0为不可能再使用,Java不使用,如对象之间互相循环引用

3.2.2 可达性分析算法(主流使用)

GC Roots出发,不可达判定可回收

GC Roots对象:

  • 虚拟机栈中引用的对象
  • 方法区类静态属性、常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部引用,基本数据类型对应的Class对象,常驻异常对象(nullpoint,oom),系统加载器
  • 被同步(synchronized)锁持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
3.2.3 再谈引用

强(传统)–>软(有用,非必须,将要溢出时才回收)–>弱(非必须,直接回收)–>虚(对象回收通知)

3.2.4 生存还是死亡?

两次标记过程:标记–>筛选(是否有必要执行finalize()方法)–>进入F-Queue(执行,不承诺等待结束)–> 存活对象再次标记(被调用则引出队列)

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
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
//不推荐使用
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}


//结果
finalize method executed!
yes, i am still alive :)
no, i am dead :(
3.2.5 回收方法区

回收内容:废弃常量和不再使用的类型

回收类需要满足的条件比较苛刻:

  • 该类所有实例已回收
  • 该类 类加载器已被回收
  • 该类 对应的java.lang.Class对象没有在任何地方被引用,无法在任何方法通过反射访问该类的方法
  • 参数控制:-Xnoclassgc

3.3 垃圾收集算法(推荐:垃圾回收算法手册)

分类:追踪式垃圾收集和引用计数式垃圾收集(主流Java虚拟机不涉及)

3.3.1 分代收集理论

三条假说:弱分代(朝生夕灭)–>新生代,强分代(难以消亡)–>老年代,跨代引用(极少数)–>划分老年代,只扫描一小块区域

部分收集(Partial GC):

  • 新生代收集(Minor/Young GC);

  • 老年代收集(Major GC:也可能指整堆收集/Old GC):只有CMS;

  • 混合收集(Mixed GC):整个新生代和部分老年代,只有G1;

  • 整堆收集(Full GC)

3.3.2 标记-清除算法

字面意思,标记(存活/非存活),清除

缺点:

  • 执行效率不稳定;内存空间碎片化
3.3.3 标记-复制算法

新生代大部分使用这种

分两半1,2 –> 只使用1,标记存活 –> 复制到2,清除1

Appel式回收:两块survivor1,2,一块大Eden 3,默认1:8 –> 标记1,3,转移到survivor2,清除1,3 –> 2不够用?分配担保,先转移多余到老年代

3.3.4 标记-整理算法

是否移动对象都存在弊端?内存回收更复杂,吞吐量更多:内存分配更复杂,停顿时间更短

3.4 HotSpot算法细节实现

3.4.1 根节点枚举

必须暂停用户线程,保证引用关系不变

准确式垃圾收集:直接知道哪些位置是引用;OopMap

3.4.2 安全点

指令太多,不可能生成太多OopMap –> 安全点(方法调用、循环跳转、异常跳转),停顿时必须到达

怎么快速到达:

  • 抢先式中断:全部中断 –> 不在安全点怎么办?继续跑,到安全点中断
  • 主动式中断:设置标志位(安全点;需要分配内存的地方:创建对象时) –> 各线程不断轮询,为真,到安全点主动中断
3.4.3 安全区域

一段代码片段中,引用关系不发生改变

3.4.4 记忆集与卡表

为了解决跨代引用而生,非收集区域指向收集区域的指针

记录粒度:

  • 字长精度:精确到每一个机器字长
  • 对象精度:精确到每一个对象
  • 卡精度(卡表):一块内存区域,最常用,标记1(变脏)/0
3.4.5 写屏障

维护卡表状态:何时变脏,谁来操作

引用对象赋值 –> AOP切面,Around通知 –> 赋值前为前屏障(G1用了),赋值后为写后屏障(大部分,更新卡表)

伪共享:多线程同时操作缓存行 –> 检查标记,不为0才更新,多一次开销

3.4.6 并发的可达性分析

三色标记:

  • 白色:尚未被垃圾收集器访问过
  • 黑色:已访问,包括所有引用
  • 灰色:已访问,但至少一个引用未扫描

产生问题:存活对象错误标记已消亡

两个条件:

  • 插入了一条或多条从黑色对象到白色对象的新引用
  • 器删除了全部从灰色对象到该白色对象的直接或间接引用

怎么解决:增量更新(破坏第一个,插入新的,记录,扫描记录的黑的,CMS)&&原始快照(破坏第二个,删除,记录,重新扫描灰的,G1、Shenandoah)

3.5 经典垃圾收集器(实践者)

image-20240830201212011

连线表示可搭配使用,并非一成不变

3.5.1 Serial收集器

最基础,历史最悠久,单线程(需要暂停所有用户线程),内存开销最小(适合客户端模式)

新生代:复制算法 老年代:标记-整理算法

image-20240830203012148

3.5.2 ParNew收集器

Serial的多线程并行版本,其余行为与与Serial收集器完全一致,只有这两能与CMS一起工作

image-20240830203034491

3.5.3 Parallel Scavenge收集器

诸多特性表面上与ParNew相似,但关注点不同,其他收集器只想缩短时间,PS想达到一个可控制的吞吐量

停顿时间短(-XX:MaxGCPauseMillis):用户交互好,响应快

高吞吐量(-XX:GCTimeRatio):高效利用处理器资源

自适应调节策略(-XX:PretenureSizeThreshold):动态调节新生代、老年代内存参数等

3.5.4 Serial Old收集器

Serial的老年代版本,单线程,标记-整理

3.5.5 Parallel Old收集器

Parallel Scavenge的老年代版本,多线程,标记-整理

image-20240830205357227

3.5.6 CMS收集器

目标:最短停顿时间

标记-清除算法,步骤:

  • 初始标记:Stop the world,GC Roots能关联到的对象

  • 并发标记:并发,遍历整个对象图

  • 重新标记:Stop the world,修正并发时更改的对象

  • 并发清除:并发,直接清除

image-20240830210359051

成功但不完美:

  • 对处理器资源非常敏感
  • 无法处理“浮动垃圾”,需要预留空间
  • 标记-清除,内存碎片化
3.5.7 Garbage First收集器 G1

面向堆内存任何区域,划分相同大小Region,Humongous Region存放大对象(一般超过Region一半)

值得思考的问题:

  • 跨Region引用对象如何解决?每个Region有自己的记忆集,哈希表(key:别的Region的地址,Value:卡表的索引号),多占10%-20%内存

  • 在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?:原始快照,两个TAMS用来存放并发时创建的对象

  • 怎样建立起可靠的停顿预测模型?可以指定最大停顿时间,在回收时根据耗时等数据更新最新的平均值等

大致运行步骤(忽略写屏障维护记忆集):

  • 初始标记:停顿,标记一下GC Roots直接关联到的对象,修改TAMS指针的值,与Minor GC同步
  • 并发标记:并发,从GC Roots开始可达性分析,找出要回收对象
  • 最终标记:停顿,处理并发结束后仍遗留的少量SATB记录
  • 筛选回收:停顿,更新Region统计数据,回收价值和成本排序,根据期望停顿时间来进行回收

image-20240830214250941

CMS/G1 CMS G1
内存占用 少,卡表只关注老年代–>新生代
执行负载 写后屏障维护卡表,增量更新:最终标记阶段停顿时间过长 额外使用写前屏障来跟踪并发时指针变化情况(原始快照:减少并发标记和重新标记阶段的消耗),队列异步处理

3.6 低延迟垃圾收集器

指标:内存占用、吞吐量、延迟(最重视)

3.6.1 Shenandoah收集器

与G1高度相似,但有不同

管理堆内存:支持并发的整理算法;默认不使用分代收集;丢弃记忆集,改用连接矩阵(类似二维表格 N有对象指向M N行M列打x)

九大阶段:

  • 2.0:Initial Partial、Concurrent Partial和Final Partial阶段(粗略理解为Minor GC)

  • 初始标记:停顿,标记GC Roots直接关联的对象

  • 并发标记:并发,遍历对象图,标出所有可达

  • 最终标记:停顿,处理剩余的SATB扫描,统计回收价值最高的Region,构成一组回收集

  • 并发清理:清除连一个存活对象都没的Region

  • 并发回收核心差异,并发,将存活对象复制到未使用的Region,难点:移动中用户线程仍不停访问,引用仍指向旧地址(通过写屏障和Brooks Pointers转发指针解决)

  • 初始引用更新:停顿时间很短,线程集合点,确保每个线程完成分配的移动任务

  • 并发引用更新:按照物理内存的地址,线性地搜索出引用类型,旧值改新

  • 最终引用更新:停顿,修改存在于GC Roots中的引用

  • 并发清理:回收整个回收集中的Region

image-20240901213012519

蓝色:用户线程用来分配对象的Region 绿色:存活对象 黄色:被选入回收集合的Region

Brooks Points:

  • 出现之前,用户访问旧的–>内存保护陷阱,返回异常–>访问新的引用(用户态与核心态频繁切换)

  • 对象布局结构前添加一个新引用,不移动时指向自己,移动时修改引用指向新对象

  • 并发写入问题:并发更改对象值时通过CAS来保证访问正确性

  • 执行效率问题:设置读、写屏障(读屏障太可怕,计划改为“引用访问屏障“)

3.6.2 ZGC收集器

基于Region内存分局,(暂时)不设分代,使用读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理

Region动态性:动态创建和销毁,动态容量

  • 小型Region:2MB,存放<256KB对象
  • 中型:32MB,256<=对象<4MB
  • 大型:动态,2MB整数倍,对象>=4MB,只放一个

染色指针技术:把标记放在引用对象的指针上,将高4位提取出来存储四个标志信息,只剩4TB(42位)

  • 某个Region的存活对象被移走后,立即能被释放和重用掉
  • 大幅减少内存屏障的使用数量,只使用了读屏障
  • 作为一种可扩展的存储结构,64位的前18位并未使用,日后开发出来不得了

潜在问题:随便重新定义指针的几位,操作系统是否支持?

多重映射来解决:多对一,多个虚拟对应一个真

image-20240902212204871

ZGC运作过程:

  • 并发标记:遍历对象图可达性分析,短暂停顿,更新染色指针中的Marked0、Marked1标志位
  • 并发预备重分配:统计要清理哪些Region,组成重分配集;扫描所有,针对全堆,省去G1中记忆集的维护成本
  • 并发重分配:核心阶段,重分配集存活对象复制到新Region,维护一个转发表(从旧对象到新对象),用户并发访问,直接截获,转向新对象,修改引用的值(只需要截获一次),存活对象复制完毕,直接释放掉(转发表还得留着)
  • 并发重映射:不急迫,合并到下一次并发标记(减少一次遍历开销),所有指针修正后,可释放转发表

与PGC、C4一脉相承,最前沿成果

缺点:不分代,能承受的对象分配速率不会太高,大量新建对象不会进入标记阶段,产生大量浮动垃圾

优点:支持“NUMA-Aware”的内存分配,处理器本地内存优先分配

第4章 虚拟机性能监控、故障处理工具

第5章 调优案例分析与实战


本站由[Lito] 使用 [Stellar 1.29.1] 主题创建。 本博客所有文章除特别声明外,均采用[CC BY-NC-SA 4.0] 许可协议,转载请注明出处。

本"页面"访问 次 | 👀总访问 次 | 🥷总访客