JVM入门
Update on 2023/02/27
类加载机制
类加载时机
分为隐式加载和显示加载
隐式加载:
- 创建类的实例(
new)、访问静态变量(getstatic)、修改静态变量(putstatic)、调用静态方法(invokestatic) - 反射调用
- 加载子类时,父类会先被加载
- JVM启动时被标明为启动类的类,直接使用
java.exe命令来运行的主类
显示加载:
- 通过
Class.forName加载,默认执行static块 - 通过
ClassLoader.loaderClass加载,不执行static块 - 通过
ClassLoader.findClass加载
类加载过程
包括加载、验证、准备、解析、初始化五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载阶段
- 获取类的二进制字节流
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 运行时计算生成。如动态代理技术
- 从数据库中提取.class文件
- 将字节流转化为方法区的运行时数据结构
- 生成
Class对象
验证阶段
- 文件格式验证:是否符合Class文件规范
- 元数据验证
- 字节码验证:分析数据流、控制流,确保语义合法
- 符号引用验证(发生在解析阶段)
准备阶段
为类的静态变量在方法区分配内存,并将其初始化为默认值
- 这时候进行内存分配的仅包括静态变量
- 这里所设置的初始值通常情况下是数据类型默认的零值(如
0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。比如static int value = 3;初始化后value的值为0。而static final int value = 3;初始化后value的值为3
注意:
- 对基本数据类型来说,对于类变量和静态变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过
- 对于同时被
static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值 - 对于引用数据类型来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即
null - 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认零值
解析阶段
将常量池内的符号引用替换为直接引用
初始化阶段
为静态变量赋予正确的初始值
- 声明静态变量为指定初始值
- 使用静态代码块为类变量指定初始值
类的唯一性
一个类的唯一性由以下2点决定:
- 加载该类的类加载器
- 类的完全限定名(包+类名)
类加载器
- 启动类加载器:
BootstrapClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 - 扩展类加载器:
ExtensionClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器:
ApplicationClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
示例:
1 | ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
结果:(BootstrapLoader(引导类加载器)是用C语言实现的,所以返回为null)
1 | sun.misc.Launche r$AppClassLoader@64fef26a |
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改Class后,必须重启JVM,程序的修改才会生效
双亲委派模型
工作流程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,
双亲委派机制
- 当
AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。 - 当
ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。 - 如果
BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载; - 若
ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
自定义类加载器
- 继承
ClassLoader - 实现
findClass方法,其中调用defineClass将字节码转为Class对象
1 | public class MyClassLoader extends ClassLoader { |
JVM线程模型
Java 内存模型
- 主内存:虚拟机内存,也是物理内存
- 工作内存:线程级内存
volatile关键字
功能:
- 可见性
- 禁止指令重排优化
- 指令重排优化:线程内保证串行
原理:
- 内存屏障
Java的线程实现
线程实现的几种方式
- 1:1:内核线程,一般通过轻量级进程(LWP)实现
- 1:N:用户线程(User Thread,UT):应用自己调度
- N:M:轻量级进程作为用户线程和内核线程的桥梁,用户线程通过轻量级进程完成
Java的实现
1.2之前,Classic虚拟机使用“Green Thread”用户线程,之后都是1:1线程模型
线程调度
- 协同式线程调度
- 由线程决定执行时间
- 不稳定
- Windows 3.x
- 抢占式线程调度
- Java线程调度,
Thread::yield()主动让出线程,有10个线程优先级
- Java线程调度,
协程
- 协同式线程调度,有栈协程
- Java官方解决方案:纤程(Fiber)
JVM内存模型
堆内存 - 最大
存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区
存类信息、常量、静态变量、即时编译器编译后的代码等数据。
栈 - 线程私有
每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
- 局部创建的实例由堆分配内存,由GC回收
- 程序计数器
本地方法栈
直接内存
JVM之GC
对象存活判断方式
1. 引用计数 (Python)
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
2. 可达性分析 (Java)
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
在Java语言中,GC Roots包括:
- 虚拟机栈中引用的对象,比如方法堆栈使用的参数、局部变量等
- 方法区中类静态属性实体引用的对象,比如引用类型静态变量
- 方法区中常量引用的对象,比如字符串常量
- 本地方法栈中JNI引用的对象,比如native方法引用的对象
- 虚拟机内部的引用,比如Class对象
- 所有被同步锁(synchronized)持有的对象
- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
Java的四种引用
- 强引用(Strongly Reference):传统引用,只要存在强引用,就不会被回收
- 软引用(Soft Reference):如果下次gc后内存依然不足,则被回收
- 弱引用(Weak Reference):下次gc回收
- 虚引用(Phantom Reference):下次gc回收,引用者会收到系统通知。无法通过该引用方式获取实例。也称“幽灵引用”
finalize
- 仅执行一次,即使该对象在finalize里被“拯救”
- gc线程不会等待finalize函数执行完毕
方法区GC
- 常量池回收
- 类型回收
垃圾收集算法
分代收集理论
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短
“分代收集”(Generational Collection)算法,把Java堆分为新生代(Young Generation)和老年代(Old Generation),这样就可以根据各个年代的特点采用最适当的收集算法。
- 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用“标记-复制”算法,只需要付出少量存活对象的复制成本就可以完成收集
- 老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收
标记-清除算法
“标记-清除”(Mark-Sweep)算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
缺点:
- 效率问题,标记和清除过程的效率都不高;
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法
“标记-复制”(Mark-Coping)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
- 在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记-整理算法
“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
用户线程停顿时机、安全点与安全区域
安全点:触发GC时,程序需运行至特定的位置然后停顿,一般是指令序列的复用点,如方法调用、循环跳转、异常跳转等。
停顿的两种方案
- 抢先式中断(Preemptive Suspension)
- GC发出中断信号,用户线程判断是否在安全点,如果不在则继续运行至安全点
- 主动式中断(Voluntary Suspension)
- 在安全点设置轮询标志,如果发现中断标志则在最近的安全点主动挂起
安全区域:在一段代码片段中,引用关系不会发生变化
- 进入安全区域后,Stop the world对其无效
- 即将离开安全区域时,会检查是否存在停顿事件,如果存在则停顿
垃圾收集器
Serial收集器
Serial收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。垃圾收集的过程中会Stop The World(服务暂停)
- 新生代“标记-复制”算法、老年代“标记-整理”
- 单线程,资源少,效率高
- 多应用于客户端
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本
- “标记-复制”算法
- 新生代多线程
- 只有它能配合CMS收集器
Parallel Scavenge收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量
- 更关注可控制的吞吐量
- 多线程GC
Serial Old收集器
Serial Old是Serial收集器的老年代版本
- 配合Parallel Scavenge使用
- CMS收集器失败的后备预案
Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和算法。这个收集器是在JDK 1.6中才开始提供
- 多线程GC,“标记-整理”算法
- 配合Parallel Scavenge使用
- 注重吞吐量和处理器资源稀缺的场景
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
- 并发
- 停顿时间短
- 产生大量空间碎片、并发阶段会降低吞吐量。因为是“标记-清除”算法
- 服务器资源敏感
- 用户线程改变引用,使用增量更新(Incremental Update)算法
收集步骤:
初始标记(CMS initial mark)
- Stop The World
- 仅标记一下GC Roots能直接关联到的对象
并发标记(CMS concurrent mark)
- GC Roots Tracing
- 和用户线程并行
重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- Stop The World
- 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
并发清除(CMS concurrent sweep)
G1收集器(Garbage First)
- 将堆分为大小相等的Region(1MB~32MB且为2的N次幂),每个Region都可以扮演新生代的Eden空间、Survivor空间或者老年代空间
- 每个Region维护“记忆集”,用来保存“我指向谁”、“谁指向我”
- 用户线程改变引用,使用原始快照(snapshot-at-the-beginning,SATB)算法
- Humongous空间,专门用来存储大对象。超过Region大小的一半
- 优先处理回收价值大的Region
- 停顿预测模型
收集步骤:
- 初始标记(Initial Mark):标记GC Root关联的对象
- Stop the World
- 由普通Mintor GC伴随触发
- 并发标记(Concurrent Marking):进行可达性分析
- 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断
- 若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收
- 标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
- 最终标记(Final Marking):再标记“并发标记期间产生新的垃圾”
- Stop the World
- 处理SATB表(原始快照)
- 筛选回收(Live Data Counting and Evacuation),
- Stop the World
- 根据用户期望的停顿时间来制定回收计划
- 把决定回收的那一部分Region的存活对象复制到空的Region中,在清理掉整个旧Region的全部空间
- 并行
Shenandoah收集器
和G1收集器类似,不同点有:
- 整理时与用户线程并发
- 默认不使用分代收集
- 记忆集修改为“连接矩阵(Connection Matrix)”
- 与用户进程并行收集
- Brooks Pointer,在对象布局结构前增肌一个新的引用字段,一开始指向自己,间接性的对象访问
- 使用CAS(Compare And Swap)保证并发访问的正确性
- 第一款使用读屏障的收集器,读屏障比写屏障的代价更高
ZGC
Region分布
- 小型Region,2MB,存放小于256KB的小对象
- 中型Region,32MB,存放大于等于256KB但小于4MB的对象
- 大型Region,必须为2MB的整数倍,放置大于等于4MB的对象,只放一个对象
并发整理算法——指针颜色技术
- Linux高18位不能用来寻址,剩余46位,将46位的前4位用来染色,分别是“finalizable”、“Remapped”,“Marked1”、“Marked0”,剩余42位导致ZGC最多支持4TB内存。不能使用32位系统和指针压缩
- x86-64平台,使用虚拟内存映射技术
支持“NUMA-Aware”的内存分配,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。
- NUMA是非统一内存访问架构,简单说就是内存控制器分配给处理器内核,如果一个内核要访问被其他内核管理的内存,必须通过Inter-Connect通道,这比访问自己管理的内存慢得多,这种访问成为“跨NUMA”
步骤:
- 并发标记:标记指针中的Marked1和Marked0标志位
- 并发预备重分配:根据特定的查询条件统计得出本次收集过程中要清理哪些Region
- 扫描所有Region
- 类卸载和弱引用处理
- 并发重分配
- 每个Region维护转发表。外部引用会被内存屏障截获,然后更新该引用的值,所以只有第一次访问旧对象会慢
- 并发重映射:修正所有引用
- 合并到下一次的“合并标记”阶段
垃圾收集器组合
| 组合 | 新生代GC策略 | 老年代GC策略 | 说明 |
|---|---|---|---|
| 组合1 | Serial | Serial Old | 单线程,适合客户端 |
| 组合2 | Serial | CMS+Serial Old | 当CMS进行GC失败时,会自动使用Serial Old。 |
| 组合3 | ParNew | CMS | |
| 组合4 | ParNew | Serial Old | |
| 组合5 | Parallel Scavenge | Serial Old | 适用于后台持久运行的应用程序 |
| 组合6 | Parallel Scavenge | Parallel Old | |
| 组合7 | G1 | G1 |