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++ 保存的)
  • 数组长度(数组才有)

如何计算对象内存大小

对象指针压缩

减少内存消耗

对象栈上分配、逃逸分析和标量替换

image-20230206114550763

先从栈上分配,通过 逃逸分析 判断是否在栈内分配

逃逸分析,判断外部是否有引用,没有则直接在栈内分配

标量替换,如果确定对象不会被引用,则 JVM 不会创建对象,而是将成员变量分解成该方法上若干个成员变量,聚合量
即可分解量,标量 即不可分解量

判断对象是否是垃圾的引用计数法有什么问题

即有引用则计数器 +1,失效一个减 1,为 0 时为垃圾对象

但如果相互引用,则永远不会回收

GC 可达性分析

GC Roots 根节点:线程栈的本地变量、静态变量、本地方法栈变量

将 GC Roots 对象作为起点,从节点开始往下搜索引用对象,找到的标记为非垃圾对象,未标记的为垃圾对象

大对象直接进入老年代

长期存活对象进入老年代

​ 会有一个 age 判断是否进入老年代,默认 15,CMS6

对象动态年龄判断

​ 即一批对象准备放入 survive 区,当这批对象总大小大于 survive 区的 50% 时,会直接将这批对象中所有最大值的对象放入老年代中

老年代空间分配担保机制

​ 每次 young GC 都会计算老年代剩余可用空间,如果可用空间小于年轻代现有所有对象大小之和,就会 Full GC

image-20230207101357271

什么类能够回收

  • 该类所有类对象实例都已回收
  • 该类类加载器已回收
  • 该类 Class 对象没有任何引用,无法在任何地方通过反射访问该类

JVM 内部各种垃圾收集算法

分代收集理论:

​ 分为新生代和老年代

  • 新生代因为对象更迭快,使用的是 复制 算法
  • 老年代存活几率高,必须要用 标记清除 或者 标记整理
  • 标记算法 复制算法 慢 10 倍以上

复制算法:

  • 空出一半的内存,每次复制已标记对象到另一半,把原来那一半给清理完
  • 空间利用率只有 50%

标记清理:

  • 删除清除未标记对象
  • 碎片化十分严重
  • 标记对象太多,效率不高

标记整理:

  • 将标记对象依次赋值给可回收或者未使用空间,保证内存连续性

CMS 收集器垃圾收集过程

并发收集器,使用的是 标记清除 算法

image-20230206121924000

  1. 初始标记 (STW):暂停其他所有线程 (STW),找到所有 GCRoots 对象
  2. 并发标记:从 GCRoots 对象往下找所有引用对象,标记为非垃圾对象
  3. 重新标记 (STW):并发标记是和线程同步运行,所以对象状态会更改,重新标记会查找当时所有少标(未标记) 对象。
  • 多标 = 即引用后又不再使用,算是多标,可在下次 GC 回收处理
  • 三色标记,即标记,未标记,新增作为三种颜色
  1. 并发清理:此时新增的对象直接标记为黑色,把所有未标记区域做清理
  2. 并发重置:重置此次 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 中修改

image-20230313201222543

G1 收集过程

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收:不会回收所有垃圾,通过用户设定的 STW 时间制定垃圾回收计划,回收部分垃圾

G1 最大停顿时间如何实现

G1 有个 region,标记也是标记 region,它会维护一个 region 列表,每次在允许的收集时间内,优先收集回收价值最大的
region

image-20230206124953523

GC 是什么时候都能做的嘛?CG 安全点与安全区域

需要代码运行到安全点或者安全区域才能做的

安全点条件有:

  • 方法返回之前

  • 调用每个方法之后

  • 抛出循环位置

  • 循环末尾

    需要中断时会在这些安全点设置一个标记位,为 true 时线程主动在安全点中断挂起

安全区域:

​ 指一段代码内不会有引用关系的变化,这块区域都叫安全区域

字符串常量池

位置:

  • 1.6 及之前运行时常量池在永久代,运行时常量池包含字符串常量池
  • 1.7,有永久代,字符串常量池分离到堆里
  • 1.8,无永久代,运行时常量池在元空间,字符串常量池在堆里