深入理解Java虚拟机(第3版)笔记
第一部分 走进Java
第1章 走进Java
1.1 概述
一次编写,到处运行
相对安全的内存管理跟访问机制
热点代码检测和运行时编译及优化
1.2 Java技术体系
- 按功能划分
- 按业务划分
Java Card(小内存设备) Java ME(移动终端)Java SE(桌面级应用)Java EE(企业应用)
1.3 Java发展史
1.4 Java虚拟机家族
1.5 展望Java技术的未来
各种语言上都能跑(Graal VM)
即时编译–>提前编译(Substrate VM:Graal VM极小型的运行时环境)
1.6 实战:自己编译JDK
- 获取源码
第二部分 自动内存管理
第2章 Java内存区域与内存溢出异常
2.1 概述
2.2 运行时数据区域
- 程序计数器:较小的内存空间,相当于一个“书签”,分支、循环、跳转、异常处理、多线程切换时从这个“书签”继续干活 –> 各线程不能互相干扰,线程私有内存,如果正在执行的是本地(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存放对象的句柄地址
- 直接指针:reference中存储的直接就是对象地址(更快,节省一次开销)
2.4 实战:OutOfMemoryError异常
2.4.1 Java堆溢出
1 | //-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError |
怎么解决:对象是否必要(内存泄漏\内存溢出)
内存泄漏:检查GCRoots,定义对象产生的源代码
内存溢出:检查JVM参数,内存是否要扩大,对象是否合理
2.4.2 虚拟机栈和本地方法栈溢出
HotSpot不区分虚拟机栈和本地方法栈,不支持扩展,只有在线程创建时未获取到足够内存才会oom
1 | //-Xss128k ·使用-Xss参数减少栈内存容量 |
2.4.3 方法区和运行时常量池溢出
永久代实现方法区:通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,间接限制常量池容量
1 | // -XX:PermSize=6M -XX:MaxPermSize=6M 在JDK6中运行 |
2.4.4 本机直接内存溢出
-XX:MaxDirectMemorySize指定,默认与-Xmx 一致
1 | // -Xmx20M -XX:MaxDirectMemorySize=10M |
在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 | /** |
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 经典垃圾收集器(实践者)
连线表示可搭配使用,并非一成不变
3.5.1 Serial收集器
最基础,历史最悠久,单线程(需要暂停所有用户线程),内存开销最小(适合客户端模式)
新生代:复制算法 老年代:标记-整理算法
3.5.2 ParNew收集器
Serial的多线程并行版本,其余行为与与Serial收集器完全一致,只有这两能与CMS一起工作
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的老年代版本,多线程,标记-整理
3.5.6 CMS收集器
目标:最短停顿时间
标记-清除算法,步骤:
初始标记:Stop the world,GC Roots能关联到的对象
并发标记:并发,遍历整个对象图
重新标记:Stop the world,修正并发时更改的对象
并发清除:并发,直接清除
成功但不完美:
- 对处理器资源非常敏感
- 无法处理“浮动垃圾”,需要预留空间
- 标记-清除,内存碎片化
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统计数据,回收价值和成本排序,根据期望停顿时间来进行回收
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
蓝色:用户线程用来分配对象的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位并未使用,日后开发出来不得了
潜在问题:随便重新定义指针的几位,操作系统是否支持?
多重映射来解决:多对一,多个虚拟对应一个真
ZGC运作过程:
- 并发标记:遍历对象图可达性分析,短暂停顿,更新染色指针中的Marked0、Marked1标志位
- 并发预备重分配:统计要清理哪些Region,组成重分配集;扫描所有,针对全堆,省去G1中记忆集的维护成本
- 并发重分配:核心阶段,重分配集存活对象复制到新Region,维护一个转发表(从旧对象到新对象),用户并发访问,直接截获,转向新对象,修改引用的值(只需要截获一次),存活对象复制完毕,直接释放掉(转发表还得留着)
- 并发重映射:不急迫,合并到下一次并发标记(减少一次遍历开销),所有指针修正后,可释放转发表
与PGC、C4一脉相承,最前沿成果
缺点:不分代,能承受的对象分配速率不会太高,大量新建对象不会进入标记阶段,产生大量浮动垃圾
优点:支持“NUMA-Aware”的内存分配,处理器本地内存优先分配