JVM 概述与历史演进
Java 虚拟机(Java Virtual Machine,JVM)是 Java 平台的核心组件,是实现 Java"一次编写,到处运行"(Write Once, Run Anywhere)理念的关键技术。JVM 本质上是一个抽象的计算模型,它模拟了实际计算机的各种功能,包括处理器、寄存器、堆栈、内存等。
JVM 的历史里程碑
- 1995 年:Java 语言诞生,JVM 作为其运行环境首次出现
- 1996 年:JDK 1.0 发布,包含第一个正式版本的 JVM
- 1998 年:JDK 1.2 发布,引入 JIT 编译器,大大提升执行效率
- 2000 年:JDK 1.3 发布,HotSpot JVM 成为默认虚拟机
- 2002 年:JDK 1.4 发布,引入 NIO 和新的垃圾回收算法
- 2004 年:JDK 5 发布,引入泛型、注解等新特性
- 2006 年:JDK 6 发布,改进垃圾回收器,引入 G1 收集器早期版本
- 2011 年:JDK 7 发布,引入 invokedynamic 指令
- 2014 年:JDK 8 发布,引入 Lambda 表达式,元空间取代永久代
- 2017 年:JDK 9 发布,引入模块化系统,G1 成为默认 GC
- 2018 年:JDK 11 发布,长期支持版本,引入 ZGC
- 2021 年:JDK 17 发布,最新长期支持版本
JVM 的核心价值
JVM 的核心价值体现在以下几个方面:
- 平台无关性:实现 "一次编写,到处运行"
- 内存自动管理:垃圾回收机制自动管理内存
- 安全性:提供安全的执行环境
- 动态性:支持动态加载类和动态扩展
- 高性能:通过 JIT 编译等技术提供高性能执行
JVM 整体架构
JVM 的整体架构可以分为四个主要子系统:
- 类加载器子系统(Class Loader Subsystem)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地方法接口(Native Method Interface)
这些子系统协同工作,完成 Java 程序的加载、验证、执行和退出等整个生命周期。
JVM 架构图解析
plaintext
┌─────────────────────────────────────────────────────────────┐
│ JVM整体架构 │
├───────────────┬─────────────────┬───────────────┬───────────┤
│ 类加载器子系统 │ 运行时数据区 │ 执行引擎 │ 本地方法接口 │
│ Class Loader │ Runtime Data │ Execution │ Native │
│ Subsystem │ Area │ Engine │ Method │
│ │ │ │ Interface │
└───────────────┴─────────────────┴───────────────┴───────────┘
每个子系统又包含多个组件,这些组件之间通过复杂的交互完成 Java 程序的执行。
类加载系统深度剖析
类加载器子系统负责将.class 文件加载到 JVM 中,并对其进行验证、准备和解析。
类加载的生命周期
一个类从被加载到 JVM 中开始,到卸载出内存为止,整个生命周期包括七个阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始。
加载阶段详解
加载阶段主要完成以下三件事情:
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
类加载器的类型
JVM 提供了三种主要的类加载器:
-
引导类加载器(Bootstrap ClassLoader)
- 负责加载 Java 核心类库,如
rt.jar、charsets.jar等 - 由 C++ 实现,不是 Java 类
- 没有父类加载器
- 加载的类位于
JAVA_HOME/jre/lib目录下
- 负责加载 Java 核心类库,如
-
扩展类加载器(Extension ClassLoader)
- 负责加载 Java 扩展类库
- 由 Java 实现,是
sun.misc.Launcher$ExtClassLoader的实例 - 父类加载器是引导类加载器
- 加载的类位于
JAVA_HOME/jre/lib/ext目录下
-
应用程序类加载器(Application ClassLoader)
- 负责加载应用程序类路径下的类
- 由 Java 实现,是
sun.misc.Launcher$AppClassLoader的实例 - 父类加载器是扩展类加载器
- 加载的类位于
CLASSPATH环境变量指定的路径下
类加载器的双亲委派模型
JVM 的类加载器采用双亲委派模型(Parents Delegation Model),其工作过程如下:
- 当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类
- 而是把这个请求委派给父类加载器去完成
- 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的引导类加载器中
- 如果父类加载器在自己的搜索范围内找不到所需的类,就会将加载请求退回给子类加载器,由子类加载器尝试自己去加载
双亲委派模型的优点:
- 避免类的重复加载:当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次
- 保护程序安全:防止核心 API 被篡改
验证阶段详解
验证阶段是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求,并且不会危害 JVM 自身的安全。
验证阶段主要包括四个验证过程:
-
文件格式验证
- 验证字节流是否符合 Class 文件格式的规范
- 检查魔数(0xCAFEBABE)是否正确
- 检查主次版本号是否在当前 JVM 支持范围内
- 检查常量池中的常量是否有不被支持的常量类型
-
元数据验证
- 对字节码描述的信息进行语义分析
- 确保其描述的信息符合 Java 语言规范的要求
- 检查这个类是否有父类(除了 java.lang.Object)
- 检查这个类的父类是否继承了不允许被继承的类
-
字节码验证
- 最复杂的一个阶段
- 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
- 保证被校验类的方法在运行时不会做出危害 JVM 安全的事件
-
符号引用验证
- 对类自身以外的信息进行匹配性校验
- 确保解析动作能正常执行
- 检查符号引用中通过字符串描述的全限定名是否能找到对应的类
准备阶段详解
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
注意:
- 这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量
- 实例变量将会在对象实例化时随着对象一起分配在堆中
- 这里所说的初始值 "通常情况" 下是数据类型的零值
解析阶段详解
解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):
- 以一组符号来描述所引用的目标
- 符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可
- 符号引用与虚拟机实现的内存布局无关
直接引用(Direct References):
- 可以是直接指向目标的指针
- 相对偏移量
- 一个能间接定位到目标的句柄
- 直接引用是和虚拟机实现的内存布局相关的
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化阶段详解
初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法的特点:
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的- 编译器收集的顺序是由语句在源文件中出现的顺序所决定的
- 静态语句块中只能访问到定义在静态语句块之前的变量
<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器- 父类的
<clinit>()方法会优先于子类的<clinit>()方法执行 <clinit>()方法对于类或接口来说并不是必需的- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作
- 接口的
<clinit>()方法不需要先执行父接口的<clinit>()方法 - JVM 会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁、同步
运行时数据区详解
运行时数据区是 JVM 在执行 Java 程序的过程中管理的内存区域,根据 JVM 规范,运行时数据区包括以下几个部分:
- 程序计数器(Program Counter Register)
- Java 虚拟机栈(Java Virtual Machine Stacks)
- 本地方法栈(Native Method Stacks)
- Java 堆(Java Heap)
- 方法区(Method Area)
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
程序计数器的特点
- 线程私有:每个线程都有一个独立的程序计数器
- 生命周期:与线程的生命周期保持一致
- 内存区域:唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域
程序计数器的作用
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
- 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
栈帧的组成
每个栈帧都包含以下几个部分:
-
局部变量表(Local Variable Table)
- 一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
- 局部变量表的容量以变量槽(Variable Slot)为最小单位
- 一个 Slot 可以存放一个 32 位以内的数据类型
-
操作数栈(Operand Stack)
- 一个后入先出(LIFO)的栈
- 操作数栈的每一个元素可以是任意的 Java 数据类型
- 32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2
-
动态链接(Dynamic Linking)
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用
- 持有这个引用是为了支持方法调用过程中的动态链接
-
方法返回地址(Method Return Address)
- 当一个方法开始执行后,只有两种方式可以退出这个方法
- 正常完成出口:方法正常完成执行
- 异常完成出口:方法执行过程中遇到了异常
虚拟机栈的异常情况
Java 虚拟机规范允许 Java 虚拟机栈的大小是动态的或者是固定不变的:
- 如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定
- 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,JVM 将会抛出一个 StackOverflowError 异常
- 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,JVM 将会抛出一个 OutOfMemoryError 异常
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java 堆
Java 堆(Java Heap)是 JVM 所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java 堆的特点
- 线程共享:被所有线程共享
- GC 管理:Java 堆是垃圾收集器管理的主要区域
- 内存分配:几乎所有的对象实例都在这里分配内存
Java 堆的分代划分
为了更好地进行垃圾回收,Java 堆通常被划分为几个不同的区域:
-
新生代(Young Generation)
- Eden 区:新创建的对象首先分配在这里
- Survivor 区:分为 From Survivor 和 To Survivor 两个区域
- 新生代的垃圾回收称为 Minor GC
-
老年代(Old Generation/Tenured Generation)
- 存放经过多次 Minor GC 仍然存活的对象
- 老年代的垃圾回收称为 Major GC 或 Full GC
-
永久代(Permanent Generation)
- JDK 8 之前用于存放类元数据
- JDK 8 及以后被元空间(Metaspace)取代
Java 堆的异常情况
如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,JVM 将会抛出 OutOfMemoryError 异常。
方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区的特点
- 线程共享:被所有线程共享
- 存储内容:存储类信息、常量、静态变量等
- 内存回收:主要回收目标是常量池的回收和对类型的卸载
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
执行引擎工作原理
执行引擎(Execution Engine)是 JVM 的核心组件之一,它负责执行字节码指令。执行引擎的主要任务是将字节码指令解释或编译为本地机器码并执行。
执行引擎的类型
JVM 的执行引擎主要有两种类型:
-
解释器(Interpreter)
- 逐行解释执行字节码
- 启动速度快,但执行速度相对较慢
- 适合执行频率较低的代码
-
即时编译器(Just-In-Time Compiler,JIT)
- 将字节码编译成本地机器码
- 启动速度慢,但执行速度快
- 适合执行频率较高的热点代码
现代 JVM 通常采用混合模式,结合了解释器和即时编译器的优点。
解释器工作原理
解释器的工作原理相对简单,它逐行读取字节码指令,并将其翻译成对应的机器码指令执行。
解释器的优点:
- 启动速度快:不需要等待编译完成就可以开始执行
- 内存占用小:不需要存储编译后的机器码
- 调试方便:可以逐行执行代码,方便调试
解释器的缺点:
- 执行速度慢:每次执行都需要重新解释
- 效率低:无法进行全局优化
即时编译器工作原理
即时编译器(JIT)将字节码编译成本地机器码,以提高执行效率。JIT 编译器通常在程序运行过程中,将执行频率较高的热点代码编译成本地机器码。
热点代码的识别
JVM 通过热点探测(Hot Spot Detection)技术来识别热点代码:
- 基于采样的热点探测:周期性地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,就认为这个方法是热点方法
- 基于计数器的热点探测:为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法
JIT 编译的层次
现代 JVM 的 JIT 编译器通常分为多个层次:
-
C1 编译器(Client Compiler)
- 简单快速的编译器
- 编译时间短,生成的代码优化程度较低
- 适合客户端应用
-
C2 编译器(Server Compiler)
- 复杂的优化编译器
- 编译时间长,生成的代码优化程度高
- 适合服务端应用
-
Graal 编译器
- JDK 9 引入的新编译器
- 基于 Java 实现,模块化设计
- 支持多种编译策略
JIT 编译的优化技术
JIT 编译器采用多种优化技术来提高代码执行效率:
- 方法内联(Method Inlining):将被调用方法的代码直接嵌入到调用方法中
- 逃逸分析(Escape Analysis):分析对象的作用域,确定对象是否逃逸到方法外部
- 锁消除(Lock Elimination):消除不可能存在竞争的锁
- 锁粗化(Lock Coarsening):将多个连续的锁合并为一个更大的锁
- 循环优化:包括循环展开、循环不变量外提等
- 公共子表达式消除:消除重复计算的表达式
- 常量传播:将常量值传播到使用它们的地方
本地方法接口与本地方法库
本地方法接口(Native Method Interface,JNI)是 JVM 提供的一个框架,允许 Java 代码与其他语言(如 C、C++)编写的代码进行交互。
JNI 的作用
JNI 的主要作用包括:
- 调用本地方法:允许 Java 代码调用本地方法
- 注册本地方法:允许本地方法注册到 JVM 中
- 访问 Java 对象:允许本地方法访问和操作 Java 对象
- 异常处理:提供异常处理机制
JNI 的工作原理
JNI 的工作过程如下:
- Java 代码通过
native关键字声明本地方法 - JVM 在加载类时发现
native方法,会在本地方法库中查找对应的实现 - 如果找到对应的实现,JVM 会调用该本地方法
- 本地方法可以通过 JNI 提供的接口访问 Java 对象和调用 Java 方法
本地方法库
本地方法库(Native Method Libraries)是一系列的本地方法集合,这些方法通常用 C、C++ 等语言实现。
Java 核心类库中包含了大量的本地方法,如:
java.lang.Object类中的hashCode()、wait()、notify()等方法java.lang.Thread类中的start()、sleep()等方法java.io包中的 I/O 操作方法java.net包中的网络操作方法
垃圾回收机制深度解析
垃圾回收(Garbage Collection,GC)是 JVM 自动管理内存的机制,它负责回收不再使用的对象所占用的内存空间。
垃圾回收的基本概念
垃圾的定义
在 Java 中,垃圾是指不再被任何存活对象引用的对象。
垃圾回收的目标
- 自动内存管理:自动分配和回收内存
- 避免内存泄漏:防止内存泄漏导致的 OutOfMemoryError
- 提高内存利用率:合理利用内存资源
垃圾判定算法
JVM 采用以下两种主要算法来判定对象是否为垃圾:
引用计数算法
引用计数算法(Reference Counting)是一种简单的垃圾判定算法:
- 工作原理:给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器就加 1;当引用失效时,计数器就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的
- 优点:实现简单,判定效率高
- 缺点:无法解决循环引用的问题
可达性分析算法
可达性分析算法(Reachability Analysis)是 JVM 采用的主要垃圾判定算法:
- 工作原理:通过一系列的称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的
- GC Roots 的类型 :
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
垃圾回收算法
JVM 采用多种垃圾回收算法来回收垃圾对象:
标记 - 清除算法
标记 - 清除算法(Mark-Sweep)是最基础的垃圾回收算法:
- 标记阶段:标记出所有需要回收的对象
- 清除阶段:回收被标记的对象所占用的内存空间
- 优点:实现简单
- 缺点 :
- 效率问题:标记和清除两个过程的效率都不高
- 空间问题:会产生大量不连续的内存碎片
复制算法
复制算法(Copying)是为了解决标记 - 清除算法的效率问题而提出的:
- 工作原理:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 优点:实现简单,运行高效,不会产生内存碎片
- 缺点 :
- 内存利用率低:只能使用一半的内存空间
- 复制成本高:如果对象存活率较高,复制操作的成本会很高
标记 - 整理算法
标记 - 整理算法(Mark-Compact)是针对老年代对象存活率高的特点而提出的:
- 标记阶段:标记出所有需要回收的对象
- 整理阶段:让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
- 优点:不会产生内存碎片,内存利用率高
- 缺点:整理过程需要移动对象,效率较低
分代收集算法
分代收集算法(Generational Collection)是目前大部分 JVM 采用的垃圾回收算法:
- 工作原理:根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代
- 新生代:每次垃圾回收都有大量对象死去,只有少量存活,采用复制算法
- 老年代:对象存活率高、没有额外空间对它进行分配担保,采用标记 - 清除或标记 - 整理算法
垃圾回收器
JVM 提供了多种垃圾回收器,每种回收器都有其特点和适用场景:
Serial 收集器
Serial 收集器是最基本、发展历史最悠久的收集器:
- 特点:单线程收集器,在进行垃圾收集时,必须暂停其他所有的工作线程
- 优点:简单高效,对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率
- 缺点:垃圾收集时需要暂停所有工作线程
- 适用场景:Client 模式下的默认新生代收集器
ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本:
- 特点:多线程收集器,除了使用多线程进行垃圾收集外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样
- 优点:在多 CPU 环境下,垃圾收集效率比 Serial 收集器高
- 缺点:垃圾收集时仍然需要暂停所有工作线程
- 适用场景:Server 模式下的新生代收集器,是许多运行在 Server 模式下的 JVM 中首选的新生代收集器
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器:
- 特点:关注点是达到一个可控制的吞吐量(Throughput)
- 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
- 优点:可以精确控制吞吐量
- 适用场景:注重吞吐量的应用
Serial Old 收集器
Serial Old 收集器是 Serial 收集器的老年代版本:
- 特点:单线程收集器,使用标记 - 整理算法
- 适用场景:Client 模式下的老年代收集器,作为 CMS 收集器的后备预案
Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本:
- 特点:多线程收集器,使用标记 - 整理算法
- 优点:可以与 Parallel Scavenge 收集器搭配使用,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器的组合
- 适用场景:注重吞吐量的应用
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器:
- 特点:基于标记 - 清除算法实现,并发收集、低停顿
- 工作过程 :
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 优点:并发收集、低停顿
- 缺点 :
- 对 CPU 资源非常敏感
- 无法处理浮动垃圾
- 会产生大量内存碎片
- 适用场景:注重响应时间的应用
G1 收集器
G1(Garbage-First)收集器是 JDK 9 及以后的默认垃圾回收器:
- 特点:面向服务端应用的垃圾回收器,基于 Region 的内存布局,可预测的停顿时间模型
- 工作过程 :
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
- 优点 :
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
- 适用场景:大内存应用,注重响应时间的应用
ZGC 收集器
ZGC(Z Garbage Collector)是 JDK 11 引入的低延迟垃圾回收器:
- 特点:低延迟、可扩展、支持 TB 级内存
- 优点 :
- 停顿时间极短(通常在 10ms 以内)
- 支持大内存(从几百 MB 到几 TB)
- 停顿时间不会随着堆的大小或活跃对象的大小而增加
- 适用场景:大内存、低延迟要求的应用
Shenandoah 收集器
Shenandoah 收集器是一种低延迟垃圾回收器:
- 特点:低延迟、并发整理
- 优点 :
- 停顿时间短
- 支持大内存
- 并发整理,不会产生内存碎片
- 适用场景:大内存、低延迟要求的应用
JVM 内存模型与线程同步
JVM 内存模型(Java Memory Model,JMM)是 Java 虚拟机规范中定义的一种内存模型,它规定了线程如何访问共享内存以及在并发环境下如何同步。
JVM 内存模型的核心概念
主内存与工作内存
JMM 规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
内存间交互操作
JMM 定义了以下八种操作来完成主内存和工作内存之间的交互:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中
同步规则
JMM 规定了以下同步规则:
- 不允许 read 和 load、store 和 write 操作之一单独出现
- 不允许一个线程丢弃它的最近的 assign 操作
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中
- 一个新的变量只能在主内存中 "诞生"
- 一个变量在同一时刻只允许一条线程对其进行 lock 操作
- 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值
- 如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作
- 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中
线程同步机制
JVM 提供了多种线程同步机制:
synchronized 关键字
synchronized 关键字是 Java 中最基本的同步机制:
- 作用:确保同一时刻只有一个线程可以执行某个方法或某个代码块
- 实现原理:基于对象头中的 Mark Word 和 Monitor 实现
- 使用方式 :
- 修饰实例方法
- 修饰静态方法
- 修饰代码块
volatile 关键字
volatile 关键字用于确保变量的可见性和禁止指令重排序:
- 可见性:当一个线程修改了 volatile 变量的值,新值会立即同步到主内存,其他线程读取时会立即从主内存中读取新值
- 禁止指令重排序:volatile 变量的读写操作不会被重排序
- 注意事项:volatile 不能保证原子性
final 关键字
final 关键字用于声明常量:
- 不可变性:final 变量一旦被初始化,就不能再被修改
- 可见性:final 变量的初始化完成后,其他线程可以立即看到它的值
原子类
Java 提供了一系列原子类来支持原子操作:
- AtomicBoolean:原子布尔类型
- AtomicInteger:原子整数类型
- AtomicLong:原子长整数类型
- AtomicReference:原子引用类型
- AtomicStampedReference:带版本号的原子引用类型,解决 ABA 问题
锁接口
Java 提供了 Lock 接口及其实现类来支持更灵活的锁机制:
- ReentrantLock:可重入锁
- ReadWriteLock:读写锁
- StampedLock:带版本号的锁
并发工具类
Java 提供了一系列并发工具类来支持并发编程:
- CountDownLatch:倒计时门闩
- CyclicBarrier:循环屏障
- Semaphore:信号量
- Exchanger:交换器
- Phaser:阶段器
JVM 指令集架构
JVM 指令集是 JVM 执行的字节码指令的集合,它定义了 JVM 可以执行的操作。
JVM 指令集的特点
- 面向栈的架构:JVM 指令集是面向栈的,大多数指令都要从操作数栈中弹出操作数,执行运算后再将结果压回操作数栈
- 指令长度固定:大多数 JVM 指令都是一个字节的操作码,后面跟着零至多个操作数
- 操作码丰富:JVM 指令集包含了 200 多个操作码,涵盖了各种基本操作
JVM 指令的分类
JVM 指令可以分为以下几类:
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间传输:
- 将局部变量加载到操作数栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
- 将操作数栈中的值存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
- 将常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
运算指令
运算指令用于对操作数栈中的值进行运算:
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 取模指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
- 局部变量自增指令:iinc
类型转换指令
类型转换指令用于将一种数值类型转换为另一种数值类型:
- 宽化类型转换:i2l、i2f、i2d、l2f、l2d、f2d
- 窄化类型转换:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f
对象创建与访问指令
对象创建与访问指令用于创建对象和访问对象的字段和方法:
- 创建类实例:new
- 创建数组:newarray、anewarray、multianewarray
- 访问类字段和实例字段:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值存储到数组元素中:bastore、castore、sastore、iastore、lastore、fastore、dastore、aastore
- 获取数组长度:arraylength
- 检查类实例类型:instanceof、checkcast
操作数栈管理指令
操作数栈管理指令用于直接操作操作数栈:
- 将一个或两个元素从栈顶弹出:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 将栈最顶端的两个数值互换:swap
控制转移指令
控制转移指令用于改变程序的执行流程:
- 条件分支:ifeq、ifne、iflt、ifge、ifgt、ifle、if_icmpeq、if_icmpne、if_icmplt、if_icmpge、if_icmpgt、if_icmple、if_acmpeq、if_acmpne、ifnull、ifnonnull
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
方法调用和返回指令
方法调用和返回指令用于调用方法和从方法返回:
- invokevirtual:调用对象的实例方法
- invokeinterface:调用接口方法
- invokespecial:调用需要特殊处理的实例方法
- invokestatic:调用类方法
- invokedynamic:调用动态方法
- ireturn:从当前方法返回 int
- lreturn:从当前方法返回 long
- freturn:从当前方法返回 float
- dreturn:从当前方法返回 double
- areturn:从当前方法返回对象引用
- return:从当前方法返回 void
异常处理指令
异常处理指令用于处理异常:
- athrow:抛出异常或错误
- 异常表:每个方法的异常处理是由异常表来描述的
同步指令
同步指令用于支持同步操作:
- monitorenter:获取对象的监视器锁
- monitorexit:释放对象的监视器锁
即时编译技术
即时编译(Just-In-Time Compilation,JIT)是 JVM 提高执行效率的关键技术,它将字节码编译成本地机器码,以提高执行速度。
JIT 编译的基本概念
解释执行与编译执行
- 解释执行:逐行解释字节码并执行,启动速度快,但执行速度慢
- 编译执行:将字节码编译成本地机器码后执行,启动速度慢,但执行速度快
现代 JVM 通常采用混合模式,结合了解释执行和编译执行的优点。
热点代码
热点代码是指执行频率较高的代码,JVM 通过热点探测技术来识别热点代码:
- 方法调用计数器:统计方法的调用次数
- 回边计数器:统计循环的执行次数
当代码的执行次数超过一定阈值时,就会被认为是热点代码,JVM 会将其编译成本地机器码。
JIT 编译器的类型
C1 编译器
C1 编译器(Client Compiler)是一种简单快速的编译器:
- 编译速度快:编译时间短,适合客户端应用
- 优化程度低:生成的代码优化程度较低
- 编译策略:采用方法内联、常量传播等简单优化
C2 编译器
C2 编译器(Server Compiler)是一种复杂的优化编译器:
- 编译速度慢:编译时间长,适合服务端应用
- 优化程度高:生成的代码优化程度高
- 编译策略:采用逃逸分析、循环优化等复杂优化
Graal 编译器
Graal 编译器是 JDK 9 引入的新编译器:
- 基于 Java 实现:用 Java 语言实现,模块化设计
- 可扩展性强:支持多种编译策略
- 优化程度高:生成的代码优化程度高
- 支持 AOT 编译:支持提前编译(Ahead-Of-Time Compilation)
JIT 编译的优化技术
JIT 编译器采用多种优化技术来提高代码执行效率:
方法内联
方法内联(Method Inlining)是将被调用方法的代码直接嵌入到调用方法中:
- 优点:消除方法调用的开销,为其他优化创造条件
- 挑战:需要平衡代码膨胀和性能提升
逃逸分析
逃逸分析(Escape Analysis)是分析对象的作用域,确定对象是否逃逸到方法外部:
- 栈上分配:如果对象没有逃逸,可以将其分配在栈上,避免垃圾回收
- 同步消除:如果对象没有逃逸,可以消除对其的同步操作
- 标量替换:如果对象没有逃逸,可以将其分解为标量,避免对象创建的开销
循环优化
循环优化是对循环代码的优化:
- 循环展开:将循环体展开,减少循环控制的开销
- 循环不变量外提:将循环中不变的表达式提到循环外面
- 循环条件判断外移:将循环条件判断提到循环外面
- 循环向量化:利用 SIMD 指令并行执行循环
公共子表达式消除
公共子表达式消除(Common Subexpression Elimination)是消除重复计算的表达式:
- 工作原理:如果一个表达式在之前已经计算过,并且表达式中的变量没有发生变化,就可以直接使用之前的计算结果
- 优点:减少重复计算,提高执行效率
常量传播
常量传播(Constant Propagation)是将常量值传播到使用它们的地方:
- 工作原理:如果一个变量被赋值为常量,就可以将变量替换为常量
- 优点:减少变量访问的开销,为其他优化创造条件
死代码消除
死代码消除(Dead Code Elimination)是消除不会被执行的代码:
- 工作原理:识别并删除不会被执行的代码
- 优点:减少代码体积,提高执行效率
指令重排序
指令重排序(Instruction Reordering)是重新排列指令的执行顺序:
- 工作原理:在不改变程序语义的前提下,重新排列指令的执行顺序
- 优点:提高 CPU 的执行效率,充分利用 CPU 的流水线
JVM 启动与初始化过程
JVM 的启动与初始化过程是一个复杂的过程,涉及多个组件的协同工作。
JVM 启动的基本流程
JVM 的启动过程可以分为以下几个阶段:
- 加载 JVM 库:操作系统加载 JVM 库(如 libjvm.so、jvm.dll 等)
- 初始化 JVM:调用 JVM 的初始化函数,初始化 JVM 的各个组件
- 加载主类:加载应用程序的主类
- 调用 main 方法:调用主类的 main 方法,开始执行应用程序
JVM 初始化的详细过程
JVM 的初始化过程涉及多个组件的初始化:
类加载器初始化
- 初始化引导类加载器:初始化引导类加载器,加载 Java 核心类库
- 初始化扩展类加载器:初始化扩展类加载器,加载 Java 扩展类库
- 初始化应用程序类加载器:初始化应用程序类加载器,加载应用程序类路径下的类
运行时数据区初始化
- 初始化程序计数器:初始化程序计数器,设置为 0
- 初始化虚拟机栈:初始化虚拟机栈,设置栈大小
- 初始化本地方法栈:初始化本地方法栈,设置栈大小
- 初始化 Java 堆:初始化 Java 堆,设置堆大小和分代划分
- 初始化方法区:初始化方法区,设置方法区大小
执行引擎初始化
- 初始化解释器:初始化解释器,准备解释执行字节码
- 初始化 JIT 编译器:初始化 JIT 编译器,准备编译热点代码
- 初始化垃圾回收器:初始化垃圾回收器,准备回收垃圾对象
JVM 的关闭过程
JVM 的关闭过程可以分为以下几种情况:
- 正常关闭:应用程序正常退出,调用 System.exit () 方法
- 异常关闭:应用程序抛出未捕获的异常
- 强制关闭:操作系统强制关闭 JVM 进程
在 JVM 关闭之前,会执行以下操作:
- 执行关闭钩子:执行通过 Runtime.addShutdownHook () 注册的关闭钩子
- 执行终结方法:执行对象的 finalize () 方法
- 释放资源:释放 JVM 占用的资源
JVM 性能监控与调优
JVM 性能监控与调优是保证 Java 应用程序高性能运行的关键。
JVM 性能监控工具
JVM 提供了多种性能监控工具:
jps
jps(JVM Process Status Tool)是用于查看 Java 进程的工具:
- 功能:列出正在运行的 Java 进程
- 常用命令 :
jps:列出 Java 进程jps -l:列出 Java 进程的完整类名jps -v:列出 Java 进程的 JVM 参数
jstat
jstat(JVM Statistics Monitoring Tool)是用于监控 JVM 统计信息的工具:
- 功能:监控 JVM 的内存使用、垃圾回收、类加载等统计信息
- 常用命令 :
jstat -gc <pid>:查看垃圾回收统计信息jstat -class <pid>:查看类加载统计信息jstat -compiler <pid>:查看 JIT 编译统计信息
jinfo
jinfo(Configuration Info for Java)是用于查看和修改 JVM 配置的工具:
- 功能:查看和修改 JVM 的配置参数
- 常用命令 :
jinfo <pid>:查看 JVM 的配置参数jinfo -flag <name> <pid>:查看指定 JVM 参数的值jinfo -flag <+|-name> <pid>:启用或禁用指定 JVM 参数
jmap
jmap(Memory Map for Java)是用于生成堆转储快照的工具:
- 功能:生成堆转储快照,查看堆内存使用情况
- 常用命令 :
jmap -dump:format=b,file=<filename> <pid>:生成堆转储快照jmap -heap <pid>:查看堆内存配置和使用情况jmap -histo <pid>:查看堆内存中对象的统计信息
jhat
jhat(JVM Heap Analysis Tool)是用于分析堆转储快照的工具:
- 功能:分析堆转储快照,查看对象的引用关系
- 常用命令 :
jhat <filename>:分析堆转储快照
jstack
jstack(Stack Trace for Java)是用于生成线程转储的工具:
- 功能:生成线程转储,查看线程的执行状态
- 常用命令 :
jstack <pid>:生成线程转储jstack -l <pid>:生成线程转储,包含锁信息
VisualVM
VisualVM 是一个可视化的 JVM 监控工具:
- 功能:可视化监控 JVM 的内存使用、垃圾回收、线程状态等
- 特点:图形化界面,操作简单,功能强大
JConsole
JConsole 是一个基于 JMX 的 JVM 监控工具:
- 功能:监控 JVM 的内存使用、垃圾回收、线程状态等
- 特点:基于 JMX,支持远程监控
JVM 调优参数
JVM 提供了多种调优参数,用于优化 JVM 的性能:
内存参数
-
堆内存参数:
-Xms:初始堆大小-Xmx:最大堆大小-Xmn:新生代大小-XX:SurvivorRatio:Eden 区与 Survivor 区的比例
-
方法区参数:
-XX:PermSize:永久代初始大小(JDK 8 之前)-XX:MaxPermSize:永久代最大大小(JDK 8 之前)-XX:MetaspaceSize:元空间初始大小(JDK 8 及以后)-XX:MaxMetaspaceSize:元空间最大大小(JDK 8 及以后)
-
栈内存参数:
-Xss:线程栈大小
垃圾回收参数
-
垃圾回收器选择:
-XX:+UseSerialGC:使用 Serial 收集器-XX:+UseParallelGC:使用 Parallel 收集器-XX:+UseConcMarkSweepGC:使用 CMS 收集器-XX:+UseG1GC:使用 G1 收集器-XX:+UseZGC:使用 ZGC 收集器
-
垃圾回收日志:
-verbose:gc:输出 GC 详细信息-XX:+PrintGCDetails:打印 GC 详细信息-XX:+PrintGCTimeStamps:打印 GC 时间戳-XX:+PrintHeapAtGC:打印 GC 前后的堆信息-Xloggc:<filename>:将 GC 日志输出到文件
JIT 编译参数
-
JIT 编译器选择:
-XX:+TieredCompilation:启用分层编译-XX:+UseGraalJIT:使用 Graal 编译器
-
JIT 编译日志:
-XX:+PrintCompilation:打印 JIT 编译信息-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining:打印方法内联信息
性能监控参数
-
JMX 参数:
-Dcom.sun.management.jmxremote:启用 JMX 远程监控-Dcom.sun.management.jmxremote.port=<port>:设置 JMX 端口-Dcom.sun.management.jmxremote.authenticate=false:禁用 JMX 认证-Dcom.sun.management.jmxremote.ssl=false:禁用 JMX SSL
-
性能监控参数:
-XX:+PrintFlagsFinal:打印 JVM 参数的最终值-XX:+PrintFlagsInitial:打印 JVM 参数的初始值
JVM 调优策略
JVM 调优是一个复杂的过程,需要根据应用程序的特点和运行环境进行调整:
内存调优
-
堆内存调优:
- 设置合理的初始堆大小和最大堆大小
- 调整新生代和老年代的比例
- 调整 Eden 区和 Survivor 区的比例
-
方法区调优:
- 设置合理的永久代或元空间大小
- 避免永久代或元空间溢出
垃圾回收调优
-
垃圾回收器选择:
- 根据应用程序的特点选择合适的垃圾回收器
- 客户端应用适合使用 Serial 或 ParNew 收集器
- 服务端应用适合使用 Parallel 或 G1 收集器
- 低延迟应用适合使用 CMS 或 ZGC 收集器
-
垃圾回收参数调优:
- 调整垃圾回收的阈值
- 调整垃圾回收的并行线程数
- 调整垃圾回收的停顿时间
JIT 编译调优
-
JIT 编译器选择:
- 根据应用程序的特点选择合适的 JIT 编译器
- 客户端应用适合使用 C1 编译器
- 服务端应用适合使用 C2 编译器
-
JIT 编译参数调优:
- 调整 JIT 编译的阈值
- 调整 JIT 编译的优化级别
- 调整 JIT 编译的线程数
性能监控与分析
-
定期监控:
- 定期监控 JVM 的内存使用情况
- 定期监控 JVM 的垃圾回收情况
- 定期监控 JVM 的线程状态
-
性能分析:
- 分析 GC 日志,找出性能瓶颈
- 分析堆转储快照,找出内存泄漏
- 分析线程转储,找出线程死锁
主流 JVM 实现比较
除了 Oracle 的 HotSpot JVM 外,还有其他几种主流的 JVM 实现:
HotSpot JVM
HotSpot JVM 是 Oracle 的 JVM 实现,是目前使用最广泛的 JVM:
-
特点:
- 采用解释执行和 JIT 编译混合模式
- 支持多种垃圾回收器
- 性能优秀,稳定性好
-
版本:
- JDK 8 及以前:HotSpot JVM
- JDK 9 及以后:HotSpot JVM,G1 成为默认垃圾回收器
- JDK 11 及以后:引入 ZGC 垃圾回收器
OpenJ9
OpenJ9 是 Eclipse 基金会的 JVM 实现,前身是 IBM 的 J9 JVM:
-
特点:
- 内存占用小
- 启动速度快
- 垃圾回收效率高
-
优势:
- 适合云原生应用
- 适合微服务架构
- 适合内存受限的环境
GraalVM
GraalVM 是 Oracle 的新一代 JVM 实现:
-
特点:
- 支持多种语言
- 支持 AOT 编译
- 支持即时编译和提前编译
-
优势:
- 适合多语言应用
- 适合高性能计算
- 适合云原生应用
Zing JVM
Zing JVM 是 Azul Systems 的 JVM 实现:
-
特点:
- 低延迟垃圾回收
- 支持大内存
- 性能稳定
-
优势:
- 适合金融应用
- 适合实时应用
- 适合大内存应用
各 JVM 实现的比较
| 特性 | HotSpot JVM | OpenJ9 | GraalVM | Zing JVM |
|---|---|---|---|---|
| 内存占用 | 中等 | 小 | 中等 | 大 |
| 启动速度 | 中等 | 快 | 中等 | 慢 |
| 执行速度 | 快 | 中等 | 快 | 快 |
| 垃圾回收 | 多种选择 | 高效 | 高效 | 低延迟 |
| 多语言支持 | 一般 | 一般 | 优秀 | 一般 |
| 云原生支持 | 一般 | 优秀 | 优秀 | 一般 |
| 大内存支持 | 一般 | 一般 | 一般 | 优秀 |
| 低延迟支持 | 一般 | 一般 | 一般 | 优秀 |
JVM 未来发展趋势
随着硬件技术的不断发展和应用需求的变化,JVM 也在不断演进:
垃圾回收技术的发展
- 低延迟垃圾回收:ZGC、Shenandoah 等低延迟垃圾回收器的发展,使得 JVM 能够更好地支持大内存应用
- 并发垃圾回收:垃圾回收与应用程序并发执行,减少停顿时间
- 自适应垃圾回收:根据应用程序的特点自动调整垃圾回收策略
JIT 编译技术的发展
- 分层编译:结合 C1 和 C2 编译器的优点,提供更好的性能
- Graal 编译器:基于 Java 实现的模块化编译器,提供更好的可扩展性
- AOT 编译:提前编译技术,进一步提高启动速度
云原生支持
- 容器化支持:更好地支持容器化部署
- 微服务支持:更好地支持微服务架构
- 无服务器支持:更好地支持无服务器架构
多语言支持
- 多语言虚拟机:支持更多的编程语言
- 语言互操作:更好地支持不同语言之间的互操作
- 统一运行时:为不同语言提供统一的运行时环境
安全性增强
- 内存安全:进一步提高内存安全性
- 代码安全:进一步提高代码安全性
- 数据安全:进一步提高数据安全性
总结
JVM 作为 Java 平台的核心组件,其底层结构非常复杂,涉及类加载、内存管理、垃圾回收、即时编译等多个方面。深入理解 JVM 的底层结构,对于开发高性能的 Java 应用程序至关重要。
随着硬件技术的不断发展和应用需求的变化,JVM 也在不断演进,垃圾回收技术、JIT 编译技术、云原生支持等方面都取得了很大的进步。未来,JVM 将继续发展,为开发者提供更好的性能和更丰富的功能。
掌握 JVM 的底层结构,不仅有助于解决实际开发中遇到的性能问题,还能帮助开发者写出更高效、更健壮的 Java 代码。无论是 Java 初学者还是有经验的开发者,都应该不断学习和了解 JVM 的最新发展,以适应不断变化的技术环境。