曹耘豪的博客

JVM入门

  1. 类加载机制
    1. 类加载时机
    2. 类加载过程
      1. 加载阶段
      2. 验证阶段
      3. 准备阶段
      4. 解析阶段
      5. 初始化阶段
    3. 类的唯一性
    4. 类加载器
    5. JVM类加载机制
    6. 双亲委派模型
      1. 工作流程
      2. 双亲委派机制
    7. 自定义类加载器
  2. JVM线程模型
    1. Java 内存模型
      1. volatile关键字
    2. Java的线程实现
      1. 线程实现的几种方式
      2. Java的实现
    3. 线程调度
    4. 协程
  3. JVM内存模型
    1. 堆内存 - 最大
    2. 方法区
    3. 栈 - 线程私有
    4. 本地方法栈
    5. 直接内存
  4. JVM之GC
    1. 对象存活判断方式
      1. 1. 引用计数 (Python)
      2. 2. 可达性分析 (Java)
    2. Java的四种引用
    3. finalize
    4. 方法区GC
    5. 垃圾收集算法
      1. 分代收集理论
      2. 标记-清除算法
      3. 标记-复制算法
      4. 标记-整理算法
    6. 用户线程停顿时机、安全点与安全区域
      1. 停顿的两种方案
    7. 垃圾收集器
      1. Serial收集器
      2. ParNew收集器
      3. Parallel Scavenge收集器
      4. Serial Old收集器
      5. Parallel Old 收集器
      6. CMS收集器
      7. G1收集器(Garbage First)
      8. Shenandoah收集器
      9. ZGC
    8. 垃圾收集器组合

Update on 2023/02/27

类加载机制

类加载时机

分为隐式加载显示加载

隐式加载:

显示加载:

类加载过程

包括加载验证准备解析初始化五个阶段。

在这五个阶段中,加载验证准备初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

加载阶段

验证阶段

准备阶段

为类的静态变量在方法区分配内存,并将其初始化为默认值

注意:

解析阶段

将常量池内的符号引用替换为直接引用

初始化阶段

为静态变量赋予正确的初始值

类的唯一性

一个类的唯一性由以下2点决定:

类加载器

示例:

1
2
3
4
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());

结果:(BootstrapLoader(引导类加载器)是用C语言实现的,所以返回为null

1
2
3
sun.misc.Launche r$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null

JVM类加载机制

双亲委派模型

工作流程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,

双亲委派机制

  1. AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;
  4. ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 ClassNotFoundException

自定义类加载器

  1. 继承ClassLoader
  2. 实现findClass方法,其中调用defineClass将字节码转为Class对象
1
2
3
4
5
6
7
8
9
10
11
public class MyClassLoader extends ClassLoader {

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

byte[] bytes; // Loaded class binary;

return defineClass(name, bytes, 0, bytes.length);
}

}

JVM线程模型

Java 内存模型

volatile关键字

功能:

原理:

Java的线程实现

线程实现的几种方式

Java的实现

1.2之前,Classic虚拟机使用“Green Thread”用户线程,之后都是1:1线程模型

线程调度

协程

JVM内存模型

堆内存 - 最大

存放对象实例,几乎所有的对象实例都在这里分配内存。

方法区

存类信息、常量、静态变量、即时编译器编译后的代码等数据。

栈 - 线程私有

每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表操作栈动态链接方法出口等信息。

本地方法栈

直接内存

JVM之GC

对象存活判断方式

1. 引用计数 (Python)

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

2. 可达性分析 (Java)

从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

Java的四种引用

finalize

方法区GC

垃圾收集算法

分代收集理论

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短

“分代收集”(Generational Collection)算法,把Java堆分为新生代(Young Generation)和老年代(Old Generation),这样就可以根据各个年代的特点采用最适当的收集算法。

标记-清除算法

“标记-清除”(Mark-Sweep)算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

缺点:

标记-复制算法

“标记-复制”(Mark-Coping)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记-整理算法

“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

用户线程停顿时机、安全点与安全区域

安全点:触发GC时,程序需运行至特定的位置然后停顿,一般是指令序列的复用点,如方法调用循环跳转异常跳转等。

停顿的两种方案

  1. 抢先式中断(Preemptive Suspension)
    • GC发出中断信号,用户线程判断是否在安全点,如果不在则继续运行至安全点
  2. 主动式中断(Voluntary Suspension)
    • 在安全点设置轮询标志,如果发现中断标志则在最近的安全点主动挂起

安全区域:在一段代码片段中,引用关系不会发生变化

垃圾收集器

Serial收集器

Serial收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。垃圾收集的过程中会Stop The World(服务暂停)

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本

Parallel Scavenge收集器

Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量

Serial Old收集器

Serial Old是Serial收集器的老年代版本

Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和算法。这个收集器是在JDK 1.6中才开始提供

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

收集步骤:

  1. 初始标记(CMS initial mark)

    • Stop The World
    • 仅标记一下GC Roots能直接关联到的对象
  2. 并发标记(CMS concurrent mark)

    • GC Roots Tracing
    • 和用户线程并行
  3. 重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

    • Stop The World
    • 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
  4. 并发清除(CMS concurrent sweep)

G1收集器(Garbage First)

收集步骤:

  1. 初始标记(Initial Mark):标记GC Root关联的对象
    • Stop the World
    • 由普通Mintor GC伴随触发
  2. 并发标记(Concurrent Marking):进行可达性分析
    • 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断
    • 若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收
    • 标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  3. 最终标记(Final Marking):再标记“并发标记期间产生新的垃圾”
    • Stop the World
    • 处理SATB表(原始快照)
  4. 筛选回收(Live Data Counting and Evacuation),
    • Stop the World
    • 根据用户期望的停顿时间来制定回收计划
    • 把决定回收的那一部分Region的存活对象复制到空的Region中,在清理掉整个旧Region的全部空间
    • 并行

Shenandoah收集器

和G1收集器类似,不同点有:

ZGC

Region分布

并发整理算法——指针颜色技术

支持“NUMA-Aware”的内存分配,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。

步骤:

  1. 并发标记:标记指针中的Marked1和Marked0标志位
  2. 并发预备重分配:根据特定的查询条件统计得出本次收集过程中要清理哪些Region
    • 扫描所有Region
    • 类卸载和弱引用处理
  3. 并发重分配
    • 每个Region维护转发表。外部引用会被内存屏障截获,然后更新该引用的值,所以只有第一次访问旧对象会慢
  4. 并发重映射:修正所有引用
    • 合并到下一次的“合并标记”阶段

垃圾收集器组合

组合新生代GC策略老年代GC策略说明
组合1SerialSerial Old单线程,适合客户端
组合2SerialCMS+Serial Old当CMS进行GC失败时,会自动使用Serial Old。
组合3ParNewCMS
组合4ParNewSerial Old
组合5Parallel ScavengeSerial Old适用于后台持久运行的应用程序
组合6Parallel ScavengeParallel Old
组合7G1G1
   /