目录
一、JVM 简介
JVM(Java Virtual Machine)是 Java 程序的运行核心,它是 Java 实现跨平台特性的关键所在。Java 源代码经过编译后生成字节码文件(.class 文件),这些字节码文件可以在任何安装了兼容 JVM 的操作系统上运行,无需重新编译。这种特性使得 Java 应用能够在不同的操作系统环境(如 Windows、Linux、macOS 等)中无缝部署和执行。
二、JVM 的组成部分
-
程序计数器(PC Register)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在多线程环境下,每个线程都有自己独立的程序计数器。它的作用是记录当前线程执行的位置,以便在线程切换后能够准确地恢复执行。例如,当线程 A 正在执行一个方法的字节码,执行到第 10 行,此时程序计数器的值就是 10;当线程 A 被切换出去,再次切换回来时,根据程序计数器的值就能继续从第 10 行开始执行。 -
Java 堆(Heap)
Java 堆是 JVM 中内存最大的一块,用于存储对象实例。所有的对象实例以及数组都在堆上分配内存。它是垃圾回收器管理的主要区域。根据对象的存活周期和垃圾回收算法,堆可以进一步划分为不同的区域,比如在分代回收算法中,堆通常被分为新生代和老生代。新生代又可细分为 Eden 区、Survivor0 区和 Survivor1 区。新创建的对象一般首先在 Eden 区分配内存,经过垃圾回收后,存活的对象可能会在 Survivor 区之间移动或者晋升到老生代。例如,一个简单的 Java 类实例化对象:
public class MyObject {
private int value;
public MyObject(int value) {
this.value = value;
}
public static void main(String[] args) {
MyObject object = new MyObject(42);
// 这个 object 对象就存储在 Java 堆中
}
}
- 方法区(Method Area)
方法区用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在 Java 8 之前,方法区也被称为永久代(PermGen),但在 Java 8 及以后,使用元空间(Metaspace)代替了永久代。这里存储着类的结构信息,比如类的方法、字段、接口信息等。例如,当我们定义一个类:
public class MyClass {
public static final int CONSTANT_VALUE = 10;
public void myMethod() {
// 方法实现
}
}
MyClass
类的相关信息(包括myMethod
方法的字节码、CONSTANT_VALUE
这个静态常量等)就存储在方法区。
- 虚拟机栈(VM Stack)
每个线程在创建时都会创建一个自己的虚拟机栈。虚拟机栈用于存储栈帧。栈帧中包含了局部变量表、操作数栈、动态连接、方法返回地址等信息。每当一个方法被调用时,就会创建一个新的栈帧并压入虚拟机栈,方法执行完成后,栈帧弹出。例如,当在一个方法中定义局部变量:
public void anotherMethod() {
int localVariable = 5;
// 这里的 localVariable 就存储在当前栈帧的局部变量表中
}
-
本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈类似,不过它是为本地方法(使用 native 关键字修饰的方法)服务的。本地方法通常是用 C 或 C++ 编写的代码,通过本地方法接口(JNI)与 Java 代码交互。当执行本地方法时,本地方法栈会为其分配相应的栈帧。 -
直接内存(Direct Memory)
直接内存并不是 JVM 运行时数据区的一部分,但它也与 JVM 的内存管理相关。直接内存可以直接访问操作系统的内存,避免了在 Java 堆和本地堆之间的数据复制,提高了某些操作的效率。例如,在一些高性能的网络通信或文件读写场景中,可能会使用直接内存。
三、类加载器
-
类加载器的概念与类型
类加载器负责将字节码文件加载到 JVM 中。JVM 中有不同类型的类加载器,包括启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)等。启动类加载器负责加载 Java 核心类库,扩展类加载器负责加载 Java 的扩展类库,应用程序类加载器负责加载用户自定义的类路径下的类。 -
双亲委派模型
双亲委派模型是类加载器的一种重要机制。当一个类加载器收到类加载请求时,它首先会把请求委派给父类加载器去尝试加载,如果父类加载器无法加载,才会由当前类加载器进行加载。这种机制保证了类的加载顺序和安全性,避免了核心类被恶意篡改等问题。例如,当应用程序类加载器收到加载java.lang.String
类的请求时,它会先将请求委派给扩展类加载器,扩展类加载器又会委派给启动类加载器,因为java.lang.String
是核心类,由启动类加载器加载。
四、垃圾回收
-
垃圾回收的基本概念
垃圾回收是 JVM 自动管理内存的一种机制。它负责回收那些不再被程序使用的对象所占用的内存空间。对象何时可以被垃圾回收器回收呢?当一个对象没有任何引用指向它时,这个对象就可以被视为垃圾。例如,在一个方法中创建了一个局部对象,当方法执行结束,这个对象没有被其他地方引用,就可能被垃圾回收。 -
垃圾回收算法
JVM 中有多种垃圾回收算法,常见的有标记 - 清除算法、复制算法、标记 - 整理算法、分代回收算法等。- 标记 - 清除算法:首先标记出所有需要回收的对象,然后统一回收这些被标记的对象。这种算法的缺点是容易产生内存碎片。
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将存活的对象复制到另一块内存中,然后把原来那块内存全部清理掉。这种算法的优点是简单高效,不会产生内存碎片,但缺点是内存利用率不高。
- 标记 - 整理算法:首先标记出所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉端边界以外的内存。这种算法解决了标记 - 清除算法的内存碎片问题。
- 分代回收算法:根据对象的存活周期将内存划分为不同的代,比如新生代和老生代。不同代采用不同的垃圾回收算法,一般新生代采用复制算法,老生代采用标记 - 整理算法或标记 - 清除算法。
-
垃圾回收器
JVM 有多种垃圾回收器,不同的垃圾回收器适用于不同的场景。例如,Serial 垃圾回收器是一个单线程的垃圾回收器,适用于小型应用程序;Parallel 垃圾回收器是多线程的,可以提高垃圾回收的效率;CMS(Concurrent Mark Sweep)垃圾回收器是一种以获取最短回收停顿时间为目标的垃圾回收器;G1(Garbage - First)垃圾回收器是一款面向服务器端应用的垃圾回收器,它具有可预测的停顿时间和高效的内存回收能力。详细介绍 G1 垃圾回收器,它将堆内存划分为多个大小相等的 Region,在回收时会优先回收垃圾最多的 Region。它的回收过程包括初始标记、并发标记、最终标记、筛选回收等阶段,通过这些阶段的配合,可以在不影响应用程序响应时间的情况下高效地回收垃圾。 -
引用类型与垃圾回收的关系
JVM 中有强引用、软引用、弱引用、虚引用等不同类型的引用。- 强引用:是最常见的引用类型,如
Object obj = new Object();
,只要强引用存在,对象就不会被垃圾回收。 - 软引用:当内存空间不足时,软引用指向的对象会被垃圾回收。软引用通常用于实现缓存。
- 弱引用:其强度比软引用更弱,无论内存是否充足,只要垃圾回收器运行,弱引用指向的对象就会被回收。
- 虚引用:虚引用不会决定对象的生命周期,它主要用于在对象被回收时收到一个系统通知。
- 强引用:是最常见的引用类型,如
五、JVM 调优
-
JVM 调优参数设置位置
JVM 调优参数可以在启动 Java 应用程序时设置。可以在命令行中使用java
命令,并通过-XX
等参数来指定。例如,可以在启动一个 Java Web 应用服务器时设置堆内存大小、垃圾回收器类型等参数。此外,在一些开发工具(如 Eclipse、IntelliJ IDEA 等)中,也可以在项目的运行配置中设置 JVM 参数。 -
常用的 JVM 调优参数
常用的调优参数包括设置堆内存大小(-Xms
和-Xmx
分别设置初始堆大小和最大堆大小)、新生代和老生代的比例(-XX:NewRatio
)、垃圾回收器相关参数(如-XX:+UseG1GC
指定使用 G1 垃圾回收器)等。通过合理设置这些参数,可以优化 JVM 的性能,提高应用程序的运行效率和稳定性。 -
JVM 调优工具
JVM 调优有多种工具可用。例如,jconsole
可以用于监控 JVM 的内存使用情况、线程状态等;jvisualvm
是一个功能更强大的可视化工具,它可以分析内存泄漏、查看 CPU 使用率等;jmap
可以生成堆内存的快照,用于分析对象的分布情况;jstack
可以查看线程的堆栈信息,用于排查死锁等线程相关的问题。 -
内存泄漏排查思路与 CPU 飙高排查方案
对于内存泄漏排查,可以通过查看内存使用趋势、分析堆内存快照等方式。使用工具生成堆内存快照后,可以查看对象的数量和大小,判断是否存在某些对象没有被正确回收。对于 CPU 飙高问题,可以使用top
命令等查看是哪个 Java 进程占用 CPU 过高,然后使用jstack
查看线程堆栈信息,确定是哪个线程导致的问题,进一步分析是业务逻辑问题还是 JVM 内部问题(如垃圾回收过度消耗 CPU 资源等)。
总之,JVM 虚拟机是一个复杂而强大的系统,深入理解它的各个组成部分、类加载机制、垃圾回收以及调优方法对于开发高性能、稳定的 Java 应用程序至关重要。无论是解决性能问题还是优化系统架构,JVM 的知识都是开发人员不可或缺的工具。