JVM 知识点集合
JVM 类加载器分类和核心功能
引导类加载器,加载 jre 中 lib 目录的核心类库
扩展类加载器,加载 jre 中 lib 目录下 ext 扩展目录下的 jar 包
应用程序类加载器,加载 ClassPath 路径下的类包,加载我们自己写的类
自定义加载器,加载自定义路径下的类包
分为引导类加载器,扩展类加载,应用加载器,自定义加载器
引导加载器用于加载 java 自带的一些类
扩展类用于加载一些应用的类
应用加载器加载我们自己写得程序类
自定义加载是我们自己手写判断的类
自定义加载器在 Tomcat 中有过使用,如 JSP 的热加载,通过更改类加载器来改变加载的类
类加载双亲委派机制
先有父亲加载器加载,失败再由儿子加载器加载
即每次使用类加载器加载时,先查询自己是否已经加载,若没有,则调用 preant 属性,获取更高一级的类加载器加载此类,若最后 preant 为 null 时,则用引导类加载器加载此类,若无法加载此类,再依次传递给下一级进行加载
为什么有双亲委派机制
沙箱安全机制:防止用户写的 java.lang 下的类不会被加载,防止核心 API 库被随意篡改
避免类的重复加载:保证每个类只会被加载一次,类的唯一性
对象创建的完整流程
类加载检查 => 是否已加载 => 分配内存 => 初始化零值 => 设置对象头 => 执行 init 方法
对象头:有锁标志,分代年龄,对象 HashCode
init 方法:属性赋值和执行构造方法
对象分配内存时的指针碰撞和空闲列表机制 (对象分配内存时解决并发的 CAS 和 TLAB
指针碰撞:内存规整,中间放指针指示器,分配内存即将指针后移
空闲列表:内存不规整,则维护列表记录那些内存块是可用的
CAS:比较交换
TLAB:本地线程分配缓冲,每个线程预先分配一块内存,通过 UseTLAB 参数来设定,UseTLABSize 指定大小
对象头
对象分为对象头、实例数据、对齐填充
对象头有
- Mark Word 标记字段 (hashCode、分代年龄、锁标志、线程 id)
- Klass Pointer 类型指针(类的元数据指针,类元数据为 C++ 保存的)
- 数组长度(数组才有)
如何计算对象内存大小
对象指针压缩
减少内存消耗
对象栈上分配、逃逸分析和标量替换
先从栈上分配,通过 逃逸分析 判断是否在栈内分配
逃逸分析,判断外部是否有引用,没有则直接在栈内分配
标量替换,如果确定对象不会被引用,则 JVM 不会创建对象,而是将成员变量分解成该方法上若干个成员变量,聚合量
即可分解量,标量 即不可分解量
判断对象是否是垃圾的引用计数法有什么问题
即有引用则计数器 +1,失效一个减 1,为 0 时为垃圾对象
但如果相互引用,则永远不会回收
GC 可达性分析
GC Roots 根节点:线程栈的本地变量、静态变量、本地方法栈变量
将 GC Roots 对象作为起点,从节点开始往下搜索引用对象,找到的标记为非垃圾对象,未标记的为垃圾对象
大对象直接进入老年代
长期存活对象进入老年代
会有一个 age 判断是否进入老年代,默认 15,CMS6
对象动态年龄判断
即一批对象准备放入 survive 区,当这批对象总大小大于 survive 区的 50% 时,会直接将这批对象中所有最大值的对象放入老年代中
老年代空间分配担保机制
每次 young GC 都会计算老年代剩余可用空间,如果可用空间小于年轻代现有所有对象大小之和,就会 Full GC
什么类能够回收
- 该类所有类对象实例都已回收
- 该类类加载器已回收
- 该类 Class 对象没有任何引用,无法在任何地方通过反射访问该类
JVM 内部各种垃圾收集算法
分代收集理论:
分为新生代和老年代
- 新生代因为对象更迭快,使用的是 复制 算法
- 老年代存活几率高,必须要用 标记清除 或者 标记整理
- 标记算法 比 复制算法 慢 10 倍以上
复制算法:
- 空出一半的内存,每次复制已标记对象到另一半,把原来那一半给清理完
- 空间利用率只有 50%
标记清理:
- 删除清除未标记对象
- 碎片化十分严重
- 标记对象太多,效率不高
标记整理:
- 将标记对象依次赋值给可回收或者未使用空间,保证内存连续性
CMS 收集器垃圾收集过程
并发收集器,使用的是 标记清除 算法
- 初始标记 (STW):暂停其他所有线程 (STW),找到所有 GCRoots 对象
- 并发标记:从 GCRoots 对象往下找所有引用对象,标记为非垃圾对象
- 重新标记 (STW):并发标记是和线程同步运行,所以对象状态会更改,重新标记会查找当时所有少标(未标记) 对象。
- 多标 = 即引用后又不再使用,算是多标,可在下次 GC 回收处理
- 三色标记,即标记,未标记,新增作为三种颜色
- 并发清理:此时新增的对象直接标记为黑色,把所有未标记区域做清理
- 并发重置:重置此次 GC 标记数据
CMS 并发阶段再次触发 Full GC 怎么处理
会将所有线程 STW,使用 serial old 垃圾收集器回收
CMS 缺点
CMS 收集器在老年代使用了 68% 的空间时就会被激活,需要预留空间处理
三色标记算法
黑色:垃圾收集器访问过,有方法指向黑色对象则不用再次扫描,不可能有直接指向白色对象
灰色:访问过,但至少还有一个引用未扫描过
白色:未扫描过,最后未扫描过则为不可达(无引用)
增量更新:
每次新增时直接将新增对象设为黑色
原始快照(SATB):
每次删除引用时,记录被删除引用对象,重新标记时扫描被保存对象再做标记为黑
CMS:写屏障 + 增量更新
G1、Shenandoah:写屏障 + SATB
ZGC:读屏障
G1 里的 region
分为 eden、s 区、o 区,特殊区域 H 区(存入大对象)
H 区
如果大于 region 大小一半以上,放入 H 区,如果 H 区放不下,则放入连续 H 区,如果连续 H 区放不下,则 FullGC
Rset
只记录老年代和新生代之间的引用,因为每次 GC 都会扫描所有新生代
反向指针,记录其他 Region 对当前 region 的引用,GC 时,只用扫描外部引用,可知当前对象是否存活
Card Table
将 Region 继续细分,从而查询分区对象时,通过 Card 来查询,同时 region 在多线程修改时,可以满足在各自 table 中修改
G1 收集过程
- 初始标记
- 并发标记
- 最终标记
- 筛选回收:不会回收所有垃圾,通过用户设定的 STW 时间制定垃圾回收计划,回收部分垃圾
G1 最大停顿时间如何实现
G1 有个 region,标记也是标记 region,它会维护一个 region 列表,每次在允许的收集时间内,优先收集回收价值最大的
region
GC 是什么时候都能做的嘛?CG 安全点与安全区域
需要代码运行到安全点或者安全区域才能做的
安全点条件有:
方法返回之前
调用每个方法之后
抛出循环位置
循环末尾
需要中断时会在这些安全点设置一个标记位,为 true 时线程主动在安全点中断挂起
安全区域:
指一段代码内不会有引用关系的变化,这块区域都叫安全区域
字符串常量池
位置:
- 1.6 及之前运行时常量池在永久代,运行时常量池包含字符串常量池
- 1.7,有永久代,字符串常量池分离到堆里
- 1.8,无永久代,运行时常量池在元空间,字符串常量池在堆里