JVM体系
本文深入剖析JVM体系架构,从类加载机制到垃圾回收算法,从内存模型到性能优化,全面解读Java虚拟机的核心原理。
# 一、引言
Java虚拟机(Java Virtual Machine,JVM)是运行Java程序的虚拟计算机,它提供了一个与平台无关的运行环境。JVM
是Java实现"一次编写,到处运行"(Write Once, Run Anywhere)的核心技术基础。
理解JVM对于Java开发者来说至关重要,它不仅能帮助我们:
- 编写出更高效的代码
- 快速定位和解决生产环境的性能问题
- 深入理解Java程序的运行机制
- 进行有效的JVM调优和故障诊断
本文将从架构设计、内存管理、垃圾回收、性能优化等多个维度,全面剖析JVM的核心技术。
# 二、JVM体系结构概述
JVM是一个复杂而精密的系统,其架构设计体现了软件工程的诸多最佳实践。JVM主要由以下核心组件构成:
# 1、核心组件
- 类加载器子系统(Class Loader Subsystem):负责加载、链接和初始化类文件
- 运行时数据区(Runtime Data Areas):管理程序运行时的内存结构
- 执行引擎(Execution Engine):解释或编译字节码为机器指令
- 垃圾回收器(Garbage Collector):自动管理内存的分配与回收
- 本地方法接口(JNI):实现Java与本地代码的交互
# 2、执行流程
.java源文件 → javac编译 → .class字节码文件 → 类加载器 → 运行时数据区 → 执行引擎 → 操作系统
接下来,我们将深入探讨每个组件的工作原理和实现细节。
# 三、类加载器子系统
类加载器子系统负责将.class
文件加载到JVM中,这个过程包括加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段。
# 1、类加载器层次结构
JVM采用双亲委派模型(Parent Delegation Model)来组织类加载器,形成一个层次化的结构:
# 1.1、引导类加载器(Bootstrap Class Loader)
- 由C++实现,是JVM的一部分
- 负责加载
$JAVA_HOME/jre/lib
下的核心类库 - 加载
java.lang.*
、java.util.*
等核心包 - 无法被Java程序直接引用
# 1.2、扩展类加载器(Extension Class Loader)
- 由
sun.misc.Launcher$ExtClassLoader
实现 - 负责加载
$JAVA_HOME/jre/lib/ext
目录下的扩展类库 - 加载
javax.*
等扩展包 - 父加载器为Bootstrap ClassLoader
# 1.3、应用类加载器(Application Class Loader)
- 由
sun.misc.Launcher$AppClassLoader
实现 - 负责加载用户类路径(
classpath
)上的类库 - 是程序中默认的类加载器
- 父加载器为Extension ClassLoader
# 2、双亲委派机制
双亲委派模型的工作流程:
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 委派给父加载器加载
if (parent != null) {
c = parent.loadClass(name);
} else {
// 如果没有父加载器,使用Bootstrap加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载,尝试自己加载
c = findClass(name);
}
}
return c;
}
# 3、打破双亲委派
在某些场景下需要打破双亲委派模型:
- SPI机制:如JDBC驱动加载,使用线程上下文类加载器
- 热部署:如Tomcat的WebAppClassLoader
- 模块化系统:如OSGi、Java 9的模块系统
详细示例请参考:Java运行期动态能力
# 四、运行时数据区
运行时数据区负责存储Java程序运行过程中产生的数据。它主要包括以下几个部分:
# 1、方法区(Method Area)
方法区是JVM规范中定义的逻辑概念,用于存储类的元数据信息。不同JVM实现对方法区有不同的实现方式。
# 1.1、存储内容
方法区主要存储以下信息:
- 类信息:类的完整名称、父类名称、接口列表、访问修饰符等
- 字段信息:字段名称、类型、修饰符、属性表等
- 方法信息:方法名称、返回类型、参数列表、字节码、异常表等
- 运行时常量池:字面量和符号引用
- 静态变量:类级别的变量
- JIT编译后的代码缓存
# 1.2、实现演进
永久代(PermGen)- Java 7及之前
- 使用JVM堆的一部分来实现方法区
- 固定大小,容易发生
OutOfMemoryError: PermGen space
- 通过
-XX:PermSize
和-XX:MaxPermSize
设置大小
元空间(Metaspace)- Java 8及之后
- 使用本地内存(Native Memory)实现
- 动态增长,默认只受系统可用内存限制
- 通过
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
控制 - 解决了永久代的内存溢出问题
// 查看元空间使用情况的示例
public class MetaspaceDemo {
public static void main(String[] args) {
List<Class<?>> classes = new ArrayList<>();
// 动态生成类,观察元空间增长
for (int i = 0; i < 100000; i++) {
ClassLoader classLoader = new ClassLoader() {};
// 动态生成类的逻辑
}
}
}
# 2、堆(Heap)
堆是JVM管理的最大内存区域,也是垃圾回收的主要区域,因此也被称为"GC堆"。所有线程共享堆内存,几乎所有的对象实例都在这里分配。
# 2.1、堆内存结构
┌──────────────────────────────────────────────────────────┐
│ 堆内存 │
├──────────────────────────┬──────────────────────────────┤
│ 年轻代(1/3) │ 老年代(2/3) │
├────────┬────────┬────────┤ │
│ Eden │ S0 │ S1 │ Old Generation │
│ (8) │ (1) │ (1) │ │
└────────┴────────┴────────┴──────────────────────────────┘
# 2.2、年轻代(Young Generation)
Eden区
- 新对象的诞生地
- 占年轻代的8/10(默认)
- 当Eden区满时触发
Minor GC
Survivor区(S0和S1)
- 两个大小相等的区域,互为From和To
- 存放
Minor GC
后的幸存对象 - 采用复制算法,保证其中一个始终为空
对象晋升机制
// 对象年龄计算示例
public class ObjectAgeDemo {
public static void main(String[] args) {
// 1. 新对象在Eden区分配
Object obj = new Object();
// 2. 经过Minor GC后,存活对象年龄+1,移到Survivor区
// 3. 年龄达到阈值(默认15)后,晋升到老年代
// 可通过 -XX:MaxTenuringThreshold 设置
}
}
# 2.3、老年代(Old Generation)
- 存放长期存活的对象
- 大对象直接进入老年代(通过
-XX:PretenureSizeThreshold
设置) - 空间不足时触发
Major GC
或Full GC
- 采用标记-清除或标记-整理算法
# 2.4、内存分配策略
- 对象优先在Eden分配
- 大对象直接进入老年代
- 长期存活对象进入老年代
- 动态对象年龄判定:Survivor区中相同年龄对象大小总和大于Survivor空间一半时,该年龄及以上对象直接进入老年代
- 空间分配担保:老年代为新生代提供分配担保
# 3、虚拟机栈(JVM Stack)
虚拟机栈是线程私有的内存区域,生命周期与线程相同。每个方法执行时都会创建一个栈帧(Stack Frame)。
# 3.1、栈帧结构
public class StackFrameDemo {
public int calculate(int a, int b) {
int c = a + b; // 局部变量表
return c * 2; // 操作数栈
}
}
栈帧包含以下组件:
- 局部变量表:存放方法参数和局部变量
- 操作数栈:方法执行时的工作区
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:方法正常退出或异常退出的返回地址
# 3.2、异常情况
StackOverflowError
:线程请求的栈深度超过虚拟机允许的深度OutOfMemoryError
:虚拟机栈动态扩展时无法申请到足够内存
# 4、本地方法栈(Native Method Stack)
本地方法栈为虚拟机使用到的Native
方法服务,与虚拟机栈类似,但服务对象不同:
- 虚拟机栈:执行Java方法(字节码)
- 本地方法栈:执行
Native
方法(如C/C++)
HotSpot虚拟机将本地方法栈和虚拟机栈合二为一。
# 5、程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,它是线程私有的,可以看作当前线程所执行的字节码的行号指示器。
# 5.1、特点
- JVM中唯一不会发生
OutOfMemoryError
的内存区域 - 执行Java方法时,记录正在执行的虚拟机字节码指令地址
- 执行
Native
方法时,计数器值为空(Undefined)
// 程序计数器记录的是字节码指令的地址
public void test() {
int a = 10; // PC: 0
int b = 20; // PC: 2
int c = a + b; // PC: 4
}
# 6、直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但这部分内存被频繁使用。
# 6.1、特点
- 使用
Native
函数库直接分配堆外内存 - 通过
DirectByteBuffer
对象作为这块内存的引用进行操作 - 避免了Java堆和
Native
堆之间的数据复制,提高性能
// 直接内存的使用示例
public class DirectMemoryDemo {
public static void main(String[] args) {
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 写入数据
buffer.put("Hello Direct Memory".getBytes());
// 读取数据
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
}
}
# 6.2、使用场景
- NIO:频繁的I/O操作
- 网络传输:减少数据拷贝
- 大文件处理:避免堆内存溢出
# 6.3、注意事项
- 不受JVM GC管理,需要手动释放
- 受本机总内存限制,可能导致
OutOfMemoryError
- 通过
-XX:MaxDirectMemorySize
指定大小
# 五、垃圾回收
垃圾回收(Garbage Collection,GC)是JVM自动内存管理的核心机制。它负责自动识别和回收不再使用的对象,从而避免内存泄漏和手动内存管理的复杂性。
# 1、垃圾回收的基本概念
垃圾回收主要关注堆和方法区的内存管理。GC的核心任务包括:
- 识别垃圾:判断哪些对象是"存活"的,哪些是"死亡"的
- 回收内存:清理死亡对象占用的内存空间
- 整理内存:消除内存碎片,提高内存利用率
# 2、对象存活判定算法
# 2.1、引用计数算法(Reference Counting)
为每个对象维护一个引用计数器:
- 对象被引用时,计数器+1
- 引用失效时,计数器-1
- 计数器为0时,对象可被回收
缺点:无法解决循环引用问题
// 循环引用示例
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB; // objA引用objB
objB.instance = objA; // objB引用objA
objA = null;
objB = null;
// 此时两个对象互相引用,引用计数都不为0,但实际已不可达
}
}
# 2.2、可达性分析算法(Reachability Analysis)
主流JVM采用的算法,通过一系列GC Roots
对象作为起点进行搜索:
GC Roots对象包括:
- 虚拟机栈中引用的对象(局部变量)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 同步锁(synchronized)持有的对象
- JVM内部的引用(如基本数据类型对应的Class对象)
# 3、Java引用类型
Java 1.2后扩展了引用类型,不同引用类型在垃圾回收时有不同的处理策略:
# 3.1、强引用(Strong Reference)
// 默认的引用类型
Object obj = new Object(); // 强引用
// 只要强引用存在,对象永远不会被回收
# 3.2、软引用(Soft Reference)
// 内存不足时才会被回收,适合缓存
SoftReference<User> softRef = new SoftReference<>(new User());
User user = softRef.get(); // 可能返回null
# 3.3、弱引用(Weak Reference)
// 下次GC时就会被回收
WeakReference<User> weakRef = new WeakReference<>(new User());
User user = weakRef.get(); // GC后返回null
// 典型应用:WeakHashMap
WeakHashMap<Key, Value> cache = new WeakHashMap<>();
# 3.4、虚引用(Phantom Reference)
// 用于跟踪对象被回收的状态
ReferenceQueue<User> queue = new ReferenceQueue<>();
PhantomReference<User> phantomRef = new PhantomReference<>(new User(), queue);
// phantomRef.get() 永远返回null
引用强度对比:强引用 > 软引用 > 弱引用 > 虚引用
# 3.5、不可达对象
不可达的对象,也并非“非死不可”。宣告对象的死亡,至少经历两次标记过程:
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
- 如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(这种情况下,finalize()方法将永远无法结束),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
# 3.6、方法区的回收
堆中,新生代,一次垃圾收集可以回收70%-90%的空间,而永久带的垃圾收集效率远低于此。
永久带的垃圾收集主要两部分:废弃常量和无用的类。
回收废弃常量与回收Java堆中的对象非常相似。
判断无用的类则相对苛刻许多:
- 该类所有的实例都已被回收
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
是否对类进行回收,HotSpot提供了-Xnoclassgc
参数,还可以使用-verbose:class
以及-XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看类加载和卸载信息。
在旧版的Java虚拟机(JVM)中,方法区使用的是传统的垃圾回收器,例如串行回收器(Serial Collector)或并行回收器(Parallel Collector)。
然而,从Java 8开始,随着永久代的移除,方法区被替换为元数据区(Metaspace),并且不再使用传统的垃圾回收器。元数据区的内存管理是基于垃圾回收器的动态分配和释放机制,而不是通过特定的垃圾回收算法来回收内存。
具体来说,元数据区使用了一种称为"条目列表"(Chunk List)的数据结构来管理元数据的分配和释放。当元数据不再被使用时,它们会被自动释放,无需进行垃圾回收。
# 4、垃圾回收算法
# 4.1、标记-清除算法(Mark-Sweep)
最基础的收集算法,分为"标记"和"清除"两个阶段:
优点:实现简单
缺点:
- 效率问题:标记和清除效率都不高
- 空间问题:产生大量内存碎片
标记前:[对象A][对象B][对象C][对象D]
标记后:[对象A*][对象B][对象C*][对象D] (* 表示标记为垃圾)
清除后:[ ][对象B][ ][对象D] (产生碎片)
# 4.2、复制算法(Copying)
将内存分为两块,每次只使用一块,GC时将存活对象复制到另一块:
优点:没有内存碎片,实现简单
缺点:内存利用率只有50%
复制前:[对象A][垃圾][对象B][垃圾] | [空闲区域]
复制后:[已清空区域] | [对象A][对象B]
# 4.3、标记-整理算法(Mark-Compact)
标记后不直接清理,而是将存活对象向一端移动:
优点:没有内存碎片,内存利用率高
缺点:移动对象成本较高
整理前:[对象A][垃圾][对象B][垃圾][对象C]
整理后:[对象A][对象B][对象C][空闲区域]
# 4.4、分代收集算法(Generational Collection)
根据对象存活周期的不同将内存划分为几块:
- 新生代:大量对象死亡,少量存活 → 使用复制算法
- 老年代:对象存活率高 → 使用标记-清除或标记-整理算法
# 5、垃圾回收器
HotSpot虚拟机的垃圾回收器发展历程及特点:
# 5.1、经典垃圾回收器
# a、Serial收集器
- 特点:单线程,
Stop-The-World
- 算法:新生代复制算法,老年代标记-整理
- 适用场景:单核CPU,小内存应用
- 启用:
-XX:+UseSerialGC
# b、ParNew收集器
- 特点:Serial的多线程版本
- 算法:新生代复制算法
- 适用场景:多核CPU,配合CMS使用
- 启用:
-XX:+UseParNewGC
# c、Scavenge收集器
- 特点:吞吐量优先,自适应调节
- 算法:新生代复制算法,老年代标记-整理
- 适用场景:后台计算任务
- 启用:
-XX:+UseParallelGC
# d、CMS收集器(Concurrent Mark Sweep)
- 特点:低延迟,并发标记清除
- 过程:初始标记→并发标记→重新标记→并发清除
- 缺点:产生内存碎片,CPU敏感
- 启用:
-XX:+UseConcMarkSweepGC
(Java 14已废弃)
# 5.2、新一代垃圾回收器
# a、G1收集器(Garbage First)
- 特点:面向服务端,可预测停顿
- 创新:Region内存布局,优先回收价值最大的Region
- 适用场景:大内存,低延迟要求
- 启用:
-XX:+UseG1GC
(Java 9后默认)
// G1相关参数配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 // 最大停顿时间目标
-XX:G1HeapRegionSize=32m // Region大小
# b、ZGC(Z Garbage Collector)
- 特点:超低延迟,停顿时间不超过10ms
- 支持:TB级堆内存
- 技术:着色指针,读屏障
- 版本:Java 11引入,Java 15正式发布
- 启用:
-XX:+UseZGC
# ZGC配置示例
java -XX:+UseZGC -Xmx16g -Xms16g MyApp
# c、Shenandoah
- 特点:低延迟,与ZGC目标相似
- 技术:Brooks Pointers,并发整理
- 版本:Java 12引入
- 启用:
-XX:+UseShenandoahGC
# 5.3、最新发展(Java 21+)
# a、ZGC
- Java 21引入分代ZGC
- 进一步提升性能和吞吐量
- 启用:
-XX:+UseZGC -XX:+ZGenerational
# 5.4、垃圾回收器选择建议
// 小型应用(< 100MB)
-XX:+UseSerialGC
// 吞吐量优先(批处理)
-XX:+UseParallelGC
// 低延迟(< 1秒)
-XX:+UseG1GC
// 超低延迟(< 10ms)
-XX:+UseZGC // 或 -XX:+UseShenandoahGC
// 默认选择(Java 9+)
// JVM会根据硬件自动选择
# 6、调优和性能监控
在实际应用中,垃圾回收器的选择和配置对程序性能有很大影响。因此,了解如何调优垃圾回收器以满足特定应用需求是非常重要的。以下是一些建议:
- 选择合适的垃圾回收器:根据应用的特点和需求,选择最合适的垃圾回收器。例如,对于对延迟敏感的应用,可以选择CMS或G1;对于吞吐量优先的应用,可以选择Parallel回收器。
- 调整堆大小和内存分代:根据应用的内存使用情况,合理调整堆的大小、新生代和老年代的比例。这可以降低垃圾回收的频率和暂停时间,提高程序性能。
- 监控和诊断:使用JVM提供的工具(如JConsole、VisualVM等)监控程序的内存使用和垃圾回收情况,根据监控结果调整垃圾回收器的配置。
# 7、诊断垃圾回收问题
在某些情况下,应用程序可能会遇到由垃圾回收引起的性能问题。以下是一些建议和技巧,以帮助您诊断和解决垃圾回收相关的问题:
- 分析垃圾回收日志:通过启用垃圾回收日志记录,您可以收集有关垃圾回收事件的详细信息。这些日志通常包含每次垃圾回收的时间、持续时间、回收的对象数量等信息。通过分析这些日志,您可以确定垃圾回收的频率、暂停时间以及可能的性能瓶颈。
- 使用性能分析工具:利用性能分析工具(例如:VisualVM、Java Flight Recorder等)可以帮助您分析应用程序的内存使用情况、垃圾回收统计信息以及其他性能指标。这些工具通常提供可视化界面,使您更容易发现和诊断性能问题。
- 识别内存泄漏:内存泄漏是指应用程序无法释放不再需要的对象,从而导致内存耗尽。内存泄漏可能会导致频繁的垃圾回收和最终导致应用程序崩溃。使用堆分析工具(如:Eclipse Memory Analyzer等)可以帮助您识别和修复内存泄漏问题。
- 优化对象分配和内存使用:通过优化应用程序的内存使用和对象分配策略,可以减少垃圾回收的负担。例如,使用对象池来重用对象、避免创建大量短暂的临时对象、使用更小的数据结构等。
# 六、JVM的执行引擎
执行引擎(Execution Engine)是 Java 虚拟机(JVM)的一个核心组件,负责执行 Java 字节码。执行引擎将 Java 字节码转换为本地机器指令,从而实现 Java 程序的运行。
# 1、解释器
当 JVM 加载字节码后,解释器将逐条解释并执行字节码指令。解释器是执行引擎的基本组成部分,但解释执行的效率较低,因为每条字节码都需要在运行时解释为机器码。
# 2、Just-In-Time编译器(JIT)
Just-In-Time编译器是Java虚拟机中用于实现即时编译的组件,主要负责将字节码指令编译成本地机器码,并优化代码执行过程中的性能瓶颈。
JIT编译器的优点是可以针对具体的硬件平台和程序运行环境进行优化,提升程序的执行速度和性能,缺点是在编译过程中会消耗一定的系统资源和时间。
# 3、HotSpot虚拟机的即时编译
HotSpot虚拟机是Java平台上应用最广泛的虚拟机之一,是Java的默认虚拟机实现,也是一款高性能、可扩展的虚拟机。在HotSpot虚拟机中,即时编译器采用了C1和C2两个阶段的编译器流水线,分别负责对冷代码和热代码进行编译和优化。
C1 编译器(Client 编译器):C1 编译器主要负责编译冷代码,即执行次数较少的代码。C1 编译器会在编译时进行一些简单的优化,但不会花费太多时间。这样做的目的是在启动速度和执行性能之间找到一个平衡点。编译后的代码会被执行,同时 HotSpot 虚拟机会收集运行时信息,以便进一步优化。
C2 编译器(Server 编译器):C2 编译器主要负责编译热代码,即经常执行的代码。它会使用更复杂、更耗时的优化方法来提高代码的执行效率。这些优化方法可能包括内联、循环展开、常量折叠等。C2 编译器会根据 HotSpot 虚拟机收集到的运行时信息对代码进行优化,以达到最佳的性能。
# 4、优化技术
Java虚拟机提供了一些优化技术,用于提高代码执行的效率和性能。以下是一些常见的优化技术:
# 4.1、方法内联
方法内联是指在编译时将方法调用直接替换为方法体的代码。这样可以减少方法调用的开销和栈帧的创建,提高代码的执行效率。
# 4.2、逃逸分析
逃逸分析是指在编译时分析对象的作用域和生命周期,确定对象是否会被外部引用,以便进行优化。如果对象不会逃逸出方法的作用域,可以将其分配在栈上,而不是在堆上,避免了垃圾回收的开销。
# 4.3、循环展开
循环展开是指在编译时将循环体中的语句复制多次,以减少循环的迭代次数和分支判断,提高代码的执行效率。但循环展开也会增加代码的体积,可能会影响缓存的命中率和分支预测的准确性。
# 4.4、其他优化技术
Java虚拟机还提供了其他一些优化技术,如栈上分配、标量替换、去除冗余操作、类加载优化等。这些技术都是为了提高代码执行的效率和性能,减少垃圾回收的开销和内存的占用。
# 4.5、补充:JIT、AOT与JSR 269和Java编译器
JIT和AOT都是属于虚拟机的一部分,而JSR 269属于Java编译器(javac)的一部分。
Java编译器(Java Compiler)不是JVM的一部分,它是独立于JVM的一个工具,用于将Java源代码编译成Java字节码文件,以供JVM执行。
在Java开发中,Java编译器是将Java源代码转化为Java字节码文件的必备工具,它将Java源代码编译成平台无关的字节码指令,以便在任何支持Java虚拟机的平台上运行。Java编译器的主要作用是检查Java源代码的语法和语义,并将其转化为字节码指令,同时还可以对Java源代码进行优化和压缩,以提高程序的执行效率和性能。
Java编译器通常包括两个主要的工具:javac和javadoc。其中,javac用于编译Java源代码,生成对应的字节码文件;javadoc用于生成Java源代码的文档注释。
在JVM中,编译后的Java字节码文件是由JVM解释执行或者即时编译执行的。因此,Java编译器和JVM是紧密相关的两个组件,但并不是JVM的一部分。
# 七、JMM内存模型
JMM定义了一套规范,描述了在Java程序中线程如何访问内存。JMM规范中涉及到的概念有:
- 主内存(Main Memory):Java虚拟机所管理的内存,是所有线程共享的内存区域。
- 工作内存(Working Memory):每个线程独立的内存区域,存储线程运行过程中需要读写的变量拷贝。
- 内存间交互操作:Java程序中使用volatile、synchronized、final、Lock等关键字进行内存间的交互操作。
# 1、原子性、可见性和有序性
JMM规范中定义了原子性、可见性和有序性三个概念。它们分别对应了在Java程序中线程对内存访问时可能出现的问题。
- 原子性:指一个操作是不可中断的整体,要么全部执行成功,要么全部执行失败。
- 可见性:指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。
- 有序性:指程序执行的顺序,JMM规定了一些约束条件,保证程序执行的顺序是正确的。
# 2、volatile关键字
volatile关键字可以保证共享变量的可见性和一定程度上的有序性,但并不能保证原子性。当一个变量被volatile修饰时,所有线程都会从主内存中读取这个变量的值,而不是从自己的工作内存中读取。当一个线程修改了共享变量的值后,这个值会立即写回到主内存,而不是在某个时刻才写回。
# 3、Happens-Before规则
Happens-Before规则是JMM中一个重要的概念,用于描述在多线程并发访问内存时的时序关系。它规定了一些原则,确保程序执行的结果是正确的。Happens-Before规则可以帮助我们理解Java程序中内存访问的行为和规则,从而更好地编写高效、正确和线程安全的Java程序。
Happens-Before 规则包括以下几个方面:
- 程序顺序规则:在一个线程内,一个操作(如变量赋值、方法调用等)按程序顺序发生在另一个操作之前,那么这两个操作满足 Happens-Before 关系。换句话说,在同一个线程中,代码的执行顺序遵循书写顺序。
- 监视器锁规则:对一个锁的解锁操作 Happens-Before 该锁的后续加锁操作。当线程 A 释放一个锁,然后线程 B 获取该锁时,线程 A 在释放锁之前所进行的所有操作对线程 B 都是可见的。
- volatile 变量规则:对一个 volatile 变量的写操作 Happens-Before 该变量的后续读操作。当线程 A 写入一个 volatile 变量,然后线程 B 读取该变量时,线程 A 在写入该变量之前所进行的所有操作对线程 B 都是可见的。
- 线程启动规则:线程 A 启动线程 B 时,线程 A 对线程 B 的启动操作 Happens-Before 线程 B 中的任何操作。换句话说,当线程 A 启动线程 B 时,线程 A 在启动线程 B 之前所进行的所有操作对线程 B 都是可见的。
- 线程终止规则:线程 A 的终止操作 Happens-Before 任何线程 B 检测到线程 A 已经终止(通过 Thread.join() 方法或其他方式)。当线程 A 结束时,线程 A 在结束之前所进行的所有操作对线程 B 都是可见的,前提是线程 B 检测到了线程 A 的终止。
- 线程中断规则:线程 A 对线程 B 的中断操作 Happens-Before 线程 B 发现中断事件(通过调用 Thread.interrupted() 方法或其他方式)。
- 对象构造函数规则:对象构造函数的执行 Happens-Before 该对象的 finalizer 方法。在构造对象时,构造函数中所进行的所有操作对 finalizer 方法都是可见的。
# 4、Java中的锁和同步原语
Java中提供了多种锁和同步原语,用于保证线程安全。其中最常用的有synchronized关键字和ReentrantLock类。synchronized关键字是Java中最基本的同步机制,它采用的是互斥锁机制,确保同一时刻只有一个线程可以进入同步代码块。而ReentrantLock是一个可重入的互斥锁,相比synchronized具有更高的灵活性和可定制性。
除了基本的锁和同步机制外,Java中还提供了一些高级的同步工具,如Semaphore、CountDownLatch、CyclicBarrier等,它们可以帮助我们更好地管理线程和协调线程之间的交互行为。
# 八、Java Native Interface (JNI)
Java Native Interface (JNI) 是Java平台提供的一种机制,用于在Java代码和本地代码(如C/C++代码)之间进行交互。通过JNI,Java应用程序可以调用本地代码提供的功能,也可以将Java对象传递给本地代码进行处理。
# 1、JNI的基本概念
JNI提供了一些标准的API和约定,用于描述Java和本地代码之间的交互。JNI的基本概念如下:
Native Method:在Java类中声明的本地方法,使用native关键字修饰。
JNIEnv:JNI环境接口,用于调用JNI提供的各种函数。
jclass、jobject、jarray等:JNI定义的一系列类型,用于表示Java中的类、对象和数组等数据结构。
jfieldID、jmethodID等:用于标识Java中的字段和方法等成员的唯一ID。
# 2、使用JNI调用本地代码
在Java中调用本地代码,需要经过以下步骤:
编写本地代码,并生成本地库文件(如动态链接库.so文件)。
在Java类中声明native方法,以便在Java中调用本地代码。
在Java代码中使用System.loadLibrary()函数加载本地库文件。
在Java代码中调用native方法,触发JNI机制将调用传递到本地代码中。
# 3、在本地代码中调用Java方法
在本地代码中调用Java方法,需要经过以下步骤:
获取JNIEnv接口指针,以便在本地代码中调用JNI提供的函数。
使用FindClass()函数查找Java类,使用GetMethodID()函数获取Java方法的ID。
使用CallXXXMethod()函数调用Java方法,其中XXX可以是Void、Boolean、Byte、Char、Short、Int、Long、Float或Double等基本类型。
# 4、Java和本地代码之间的数据传递
在Java和本地代码之间传递数据,需要经过以下步骤:
在Java中创建jobject或jarray对象,用于表示数据结构。
在本地代码中使用GetXXXArrayElements()函数获取数组元素或GetXXXField()函数获取对象字段的值。
在本地代码中使用SetXXXArrayElements()函数设置数组元素或SetXXXField()函数设置对象字段的值。
在本地代码中使用NewXXXArray()函数创建数组对象或NewObject()函数创建Java对象。
综上所述,JNI提供了Java和本地代码之间进行交互的一种机制,可以帮助Java应用程序获得更高的灵活性和性能。但是,由于涉及到跨语言交互,JNI的使用也需要非常小心谨慎,避免出现内存泄漏、数据类型不匹配等问题。
# 九、字节码
Java 之所以如此受欢迎,一方面是因为它具有跨平台性,另一方面则是因为它使用了一种称为 Java 字节码的中间语言。
# 1、什么是 Java 字节码?
Java 字节码是一种中间语言,是 Java 源代码编译后得到的二进制代码,也被称为 JVM 代码。Java 虚拟机可以解释并执行这些字节码。由于字节码是中间语言,所以它具有跨平台性,可以在不同的操作系统和硬件平台上运行。
Java 字节码可以通过使用 javac 命令将 Java 源代码编译成 .class 文件得到。.class 文件包含了 Java 字节码以及一些其他的元数据信息,例如类名、方法名等。Java 虚拟机可以读取 .class 文件,并将其中的字节码解释成机器码来执行程序。
# 2、Java 字节码的结构
Java 字节码是由一系列指令组成的,每个指令都对应着一些操作。
指令是 Java 虚拟机(JVM)执行 Java 字节码时所遵循的指令集。这些指令控制 JVM 如何处理数据、执行操作以及调用方法等。按用途分类,字节码操作指令大致分为9类。以下是这9类指令的详细介绍:
- 加载指令(Load):这类指令用于将数据从内存(如局部变量表或类字段)加载到操作数栈中。例如,
iload
指令从局部变量表加载 int 类型数据到操作数栈,aload
指令从局部变量表加载引用类型数据到操作数栈。 - 存储指令(Store):这类指令用于将数据从操作数栈存储到内存(如局部变量表或类字段)。例如,
istore
指令将 int 类型数据从操作数栈存储到局部变量表,astore
指令将引用类型数据从操作数栈存储到局部变量表。 - 栈操作指令(Stack):这类指令用于操作数栈的管理,如压栈、弹栈、交换栈顶元素等。例如,
pop
指令用于弹出操作数栈顶的元素,dup
指令用于复制操作数栈顶的元素。 - 算术指令(Arithmetic):这类指令用于执行基本的算术运算,如加法、减法、乘法、除法等。例如,
iadd
指令用于执行 int 类型的加法运算,ddiv
指令用于执行 double 类型的除法运算。 - 类型转换指令(Type Conversion):这类指令用于在不同数据类型之间进行转换。例如,
i2f
指令用于将 int 类型数据转换为 float 类型数据,l2d
指令用于将 long 类型数据转换为 double 类型数据。 - 对象操作指令(Object Manipulation):这类指令用于操作对象,如创建对象、访问字段、调用方法等。例如,
new
指令用于创建一个新对象,getfield
指令用于访问对象的实例字段,invokevirtual
指令用于调用对象的虚方法。 - 控制转移指令(Control Transfer):这类指令用于控制程序的执行流程,如条件跳转、无条件跳转、循环等。例如,
ifeq
指令用于在条件为真时跳转到指定位置,goto
指令用于无条件跳转到指定位置。 - 异常处理指令(Exception Handling):这类指令用于异常的处理和抛出。例如,athrow 指令用于抛出异常,jsr 和 ret 指令用于跳转到异常处理程序并返回。
- 同步指令(Synchronization):这类指令用于实现线程同步,如进入或退出监视器(monitor)。例如,monitorenter 指令用于获取对象的监视器,monitorexit 指令用于释放对象的监视器。
# 3、Java 字节码的使用
Java 字节码可以通过使用反编译工具来查看,例如 javap 命令。使用 javap 命令可以查看 .class 文件中包含的字节码信息,例如方法名、方法参数、返回类型以及字节码指令等。
Java 字节码也可以通过 ASM(Java 字节码操作框架)这样的字节码操作框架来生成和修改。ASM 提供了一组 API,用于操作字节码,并提供了一些工具类,例如 ClassWriter、MethodVisitor 等,用于生成和修改字节码。通过使用 ASM,开发人员可以在不改变 Java 语言语法的情况下,生成高效的字节码。
Java 字节码还可以用于进行代码分析和优化。在进行代码分析时,可以使用字节码工具来分析代码中的指令、变量和方法等信息,以便进行代码调试和性能优化。在进行代码优化时,可以通过改进字节码的生成方式来提高程序的性能。
# 4、Java 字节码的优缺点
Java 字节码的主要优点是跨平台性。由于字节码是一种中间语言,可以在不同的操作系统和硬件平台上运行。这样可以使开发人员只需要编写一次代码,就可以在不同的平台上运行。
Java 字节码的另一个优点是安全性。Java 字节码可以通过 Java 虚拟机的安全机制来执行,这样可以有效地防止一些安全漏洞,例如缓冲区溢出等。
Java 字节码的主要缺点是性能。由于 Java 字节码需要被解释成机器码来执行,所以它的执行速度相对较慢。这个问题可以通过 JIT(Just-In-Time)编译器来解决,JIT 编译器可以将字节码编译成本地机器码来执行,这样可以大大提高程序的执行速度。
另外,由于 Java 字节码是一种中间语言,所以它的可读性相对较差。对于开发人员来说,需要掌握一定的字节码知识才能够有效地分析和优化字节码。
# 十、JVM参数
JVM参数分为标准参数、非标准参数(-X)和高级参数(-XX):
# 1、内存相关参数
# 1.1、堆内存设置
-Xms<size> # 初始堆大小,如 -Xms2g
-Xmx<size> # 最大堆大小,如 -Xmx4g
-Xmn<size> # 年轻代大小
-XX:NewRatio=<n> # 老年代/年轻代比例,默认2
-XX:SurvivorRatio=<n> # Eden/Survivor比例,默认8
# 1.2、元空间设置(Java 8+)
-XX:MetaspaceSize=<size> # 元空间初始大小
-XX:MaxMetaspaceSize=<size> # 元空间最大大小
# 1.3、栈内存设置
-Xss<size> # 线程栈大小,如 -Xss1m
# 1.4、直接内存设置
-XX:MaxDirectMemorySize=<size> # 最大直接内存
# 2、垃圾回收器参数
# 选择垃圾回收器
-XX:+UseSerialGC # Serial收集器
-XX:+UseParallelGC # Parallel收集器
-XX:+UseG1GC # G1收集器
-XX:+UseZGC # ZGC收集器(Java 11+)
-XX:+UseShenandoahGC # Shenandoah收集器
# G1特定参数
-XX:MaxGCPauseMillis=<n> # 最大停顿时间目标
-XX:G1HeapRegionSize=<size> # Region大小
# ZGC特定参数(Java 15+)
-XX:ZCollectionInterval=<seconds> # GC间隔
-XX:+ZGenerational # 启用分代ZGC(Java 21+)
# 3、GC日志参数
# Java 9之前
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
# Java 9+统一日志
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+heap=debug:file=heap.log
# 4、诊断参数
# OOM时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=<path>
# 打印JVM参数
-XX:+PrintFlagsFinal
-XX:+PrintCommandLineFlags
# JIT编译
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
# 5、性能优化参数
# 字符串去重(G1)
-XX:+UseStringDeduplication
# TLAB(线程本地分配缓冲)
-XX:+UseTLAB
-XX:TLABSize=<size>
# 大页内存
-XX:+UseLargePages
-XX:LargePageSizeInBytes=<size>
# 6、实战配置示例
# 高并发Web应用(G1)
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+ParallelRefProcEnabled \
-Xlog:gc*:file=gc.log \
MyApp
# 大内存低延迟应用(ZGC)
java -Xms16g -Xmx16g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:ConcGCThreads=4 \
MyApp
# 批处理应用(Parallel)
java -Xms8g -Xmx8g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=8 \
MyApp
# 7、常见问题与参数调整
问题 | 可能原因 | 建议参数调整 |
---|---|---|
OutOfMemoryError: Java heap space | 堆内存不足 | 增大-Xmx |
OutOfMemoryError: Metaspace | 元空间不足 | 增大-XX:MaxMetaspaceSize |
StackOverflowError | 栈深度过大 | 增大-Xss |
GC频繁 | 堆内存过小 | 增大-Xms 和-Xmx |
GC停顿时间长 | 堆内存过大或GC不合适 | 使用G1或ZGC |
# 十一、JVM相关工具
# 1、命令行工具
# 1.1、基础工具
# jps - 查看Java进程
jps -l # 显示完整类名
jps -v # 显示JVM参数
# jinfo - 查看/修改JVM参数
jinfo -flags <pid> # 查看所有JVM参数
jinfo -flag MaxHeapSize <pid> # 查看具体参数
# jstat - 监控JVM统计信息
jstat -gc <pid> 1000 # 每秒输出GC信息
jstat -gcutil <pid> # 查看GC使用率
jstat -class <pid> # 查看类加载信息
# jstack - 线程堆栈分析
jstack <pid> # 打印线程堆栈
jstack -l <pid> # 包含锁信息
# jmap - 内存映像工具
jmap -heap <pid> # 查看堆配置和使用
jmap -histo <pid> # 查看对象统计
jmap -dump:format=b,file=heap.bin <pid> # 生成堆转储
# 1.2、高级工具(Java 9+)
# jhsdb - 调试工具
jhsdb jmap --heap --pid <pid>
jhsdb jstack --locks --pid <pid>
# jcmd - 多功能诊断命令
jcmd <pid> VM.version
jcmd <pid> GC.run
jcmd <pid> Thread.print
jcmd <pid> VM.native_memory
# 2、可视化工具
# 2.1、VisualVM
- 免费的多合一故障处理工具
- 支持性能分析、内存分析、线程分析
- 可以安装各种插件扩展功能
# 2.2、Flight Recorder (JFR)
# 启动JFR记录
jcmd <pid> JFR.start duration=60s filename=recording.jfr
# 分析JFR文件
jfr print recording.jfr
jfr summary recording.jfr
# 2.3、JConsole
- JDK自带的监控工具
- 实时监控内存、线程、类加载、MBean
# 3、第三方工具
# 3.1、Arthas(阿里巴巴)
# 安装启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 常用命令
dashboard # 实时数据面板
thread # 查看线程信息
trace # 方法内部调用路径
watch # 观察方法返回值
profiler # 性能分析
特点:
- 无需重启应用即可诊断
- 支持动态追踪方法调用
- 可以热更新代码
- Web Console界面
# 3.2、MAT (Memory Analyzer)
- 专业的Java堆内存分析工具
- 可以分析大型堆转储文件
- 自动检测内存泄漏
# 3.3、GC日志分析工具
- GCEasy:https://gceasy.io/ (opens new window)
- GCViewer:开源的GC日志可视化工具
- FastThread:线程分析工具
# 4、性能分析实战
# 1. 发现高CPU占用进程
top -H -p <pid>
# 2. 将线程ID转换为16进制
printf "%x\n" <thread_id>
# 3. 查找对应线程堆栈
jstack <pid> | grep -A 20 <hex_thread_id>
# 4. 分析内存泄漏
jmap -dump:live,format=b,file=heap.bin <pid>
# 使用MAT或VisualVM分析heap.bin
# 5. 实时监控GC
jstat -gcutil <pid> 1000 10
# 5、工具选择建议
场景 | 推荐工具 |
---|---|
快速诊断 | Arthas |
内存泄漏分析 | Eclipse MAT |
性能剖析 | JFR + JMC |
实时监控 | VisualVM |
GC优化 | GCEasy |
线程问题 | jstack + FastThread |
# 十二、总结
# 1、核心知识回顾
通过本文的深入剖析,我们系统地了解了JVM的核心技术体系:
- 类加载机制:双亲委派模型保证了Java核心类库的安全性
- 内存模型:分代收集思想优化了垃圾回收效率
- 垃圾回收:从Serial到ZGC,GC技术不断演进以适应不同场景
- 执行引擎:JIT编译技术让Java性能接近原生代码
- 诊断工具:丰富的工具链支持线上问题快速定位
# 2、JVM发展趋势
# 2.1、低延迟GC成为主流
- ZGC和Shenandoah将停顿时间控制在10ms以内
- 分代ZGC(Java 21)进一步提升吞吐量
- 未来GC将向亚毫秒级停顿发展
# 2.2、云原生优化
- 启动速度优化:
CDS
(Class Data Sharing)、AOT
编译 - 内存占用优化:更智能的内存管理策略
- 容器感知:自动适配容器资源限制
# 2.3、Valhalla
- 值类型(Value Types):减少对象头开销
- 泛型特化(Generic Specialization):消除装箱开销
# 2.4、Loom
- 虚拟线程(Virtual Threads):轻量级并发模型
- 结构化并发:简化并发编程模型
JVM是一个持续演进的技术体系,深入理解JVM不仅能帮助我们写出更好的代码,更能让我们在遇到问题时快速定位和解决。希望本文能为你的JVM学习之旅提供帮助。
祝你变得更强!