JVM 被叫做Java虚拟机; 对于虚拟机的概念, 我们了解的是: 物理电脑(有CPU、内存、硬盘)就像一栋大楼。虚拟机技术则允许你在这栋大楼里,用软件隔出多个独立的、功能齐全的“公寓”。常见的说法, 我们一般在使用Windows电脑同时想学习 Linux的一些操作时, 会在 Windows上安装一个 "虚拟机"; 它就像一台仿真的 Linux机器供我们操作学习。
官方定义:虚拟机(VM)是通过软件模拟的、具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。
那么Java 虚拟机,就是我们编写 Java 代码,编译 Java 代码,目的不是让它在 Linux、Windows 或者 MacOS 上跑,而是在 JVM 上跑。
虚拟机的组织架构
JVM 大致可以分为三个部分:
- 类加载器 Class Loader
- 运行时数据区 Runtime Data Areas
- 执行引擎 Excution Engine
其中每个部分又包含很多结构, 可以参考下面这张图片

JVM的作用
- 跨平台执行
JVM 充当了字节码和硬件/操作系统之间的翻译官,我们无需针对不同平台重写代码。也就是“一次编写,到处运行”。 - 内存管理与垃圾回收
JVM 管理堆(Heap)、栈(Stack)、方法区等内存区域,自动为对象分配内存。 也会通过垃圾回收(GC)自动回收不再使用的对象内存,避免内存泄漏,减少手动内存管理的负担。 - 即时编译
早期 JVM 逐行解释字节码,启动快但执行慢。现代 JVM(如 HotSpot)通过即时编译(JIT)将热点代码(频繁执行的代码)编译成本地机器码,大幅提升执行效率。 自适应优化:JVM 会监控程序运行状态,动态调整编译策略(如内联、逃逸分析等)。 - ...
下面单独说下每个部分的划分以及作用
类加载器
作用:负责将 .class 字节码文件加载到 JVM 内存中,并生成对应的 Class 对象。
类加载器负责将字节码文件加载到内存中, 主要会经历加载->连接->实例化这三个阶段
- 加载 (Loading):通过类的全限定名获取其二进制字节流,将类的静态存储结构转化为方法区的运行时数据结构,并在堆中生成一个代表这个类的 java.lang.Class 对象,作为访问方法区这些数据的入口。
- 链接 (Linking):
- 验证 (Verification):确保被加载的类符合 JVM 规范,是安全的,不会损害 JVM 的完整性(如文件格式、元数据、字节码、符号引用的验证)。
- 准备 (Preparation):为类的静态变量分配内存并设置默认初始值(零值),如
static int会被初始化为0。 - 解析 (Resolution):将常量池内的符号引用替换为直接引用(内存地址偏移量)。
- 初始化 (Initialization):执行类的构造器
<clinit>()方法的过程,该方法由编译器自动收集类中所有静态变量的赋值动作和静态代码块中的语句合并产生。这时才会真正将静态变量赋值为代码中定义的初始值。
对象创建过程
Java中创建一个对象, 主要包含5个步骤:
- 类加载检查
- 分配内存
- 初始化零值
- 设置对象头
- 执行
<init>方法
1.类加载检查: 当 JVM 遇到一条 new 指令时,首先并不是立即去分配内存,而是先检查这个指令的参数是否能在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果未加载, 那就先执行相应的类加载过程。
这样做的目的是能确保类元信息在方法区/元空间中 是存在的,因为后续的所有操作都依赖于这些信息。
2.分配内存: 在通过类加载检查后,JVM 将为新生对象在 Java 堆 中分配内存。所需内存大小在类加载完成后就已经完全确定(通过元数据信息计算出来)。
这又引出问题: 如何分配内存? 主要使用两种策略:指针碰撞和空闲列表。
- 指针碰撞, 适用于较规整的堆(即所有使用的内存在一边, 空闲的在另一边)。分配内存时, Java虚拟机会维护一个指针作为分界点,分配内存就是将指针向空闲区域移动一段距离,没有发生指针碰撞的话就会把这段内存分配给对象实例。
- 空闲列表, 适用于较不规整的堆,CMS 这种基于 标记整理 算法的收集器通常采用此方式。JVM 会维护一个列表,记录哪些内存块是可用的。在分配时,从列表中找出一块足够大的空间分配给对象实例,并更新列表记录。分配后,如果选择的内存块没有被完全利用,剩余部分会作为一个新的内存块加入到空闲列表中。
3.初始化零值: 内存分配完成后,JVM 会将分配到的内存空间(不包括对象头)都初始化为零值(0, null, false 等)。保证了对象的实例变量在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头: 初始化零值之后,JVM 要对对象进行必要的设置,这些信息存放在 对象头 中。对象头主要包括两类信息:(包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息)
- Mark Word:用于存储对象自身的运行时数据。
- 哈希码
- GC 分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程 ID、偏向时间戳等
- (这部分数据在 32 位和 64 位的虚拟机中分别为 32bit 和 64bit,被称为“Mark Word”)
- 类型指针:即对象指向它的类元数据的指针。
- JVM 通过这个指针来确定这个对象是哪个类的实例。
- (并不是所有的虚拟机实现都必须在对象数据上保留类型指针,这取决于对象的访问定位方式)。
此外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。
5.执行 <init> 方法: 从 Java 程序的视角看,对象创建才刚刚开始。JVM 会执行构造方法 <init> 完成赋值操作,将成员变量赋值为预期的值,比如 int age = 18,这样一个对象就创建完成了。
双亲委派模型
Parent Delegation Model
这是类加载器的重要工作机制。
- 工作原理:当一个类加载器收到加载请求时,它首先不会自己去尝试加载,而是将这个请求委派给父类加载器去完成。每一层都是如此。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 层次结构:
- 启动类加载器 (Bootstrap Class Loader):加载 JRE/lib/rt.jar 等核心库,由 C++ 实现,是最高层。
- 扩展类加载器 (Extension Class Loader):加载 JRE/lib/ext 目录下的扩展包。
- 应用程序类加载器 (Application Class Loader):加载用户类路径(ClassPath)上指定的类库。是程序中默认的类加载器。
- 好处:
- 避免重复加载:确保一个类在全局唯一。
- 安全:防止核心 API 被随意篡改。比如用户自定义了一个 java.lang.String 类,如果没有双亲委派,它会被加载,从而破坏 Java 核心代码的安全。但根据模型,这个请求会最终委派给启动类加载器,而启动类加载器会加载核心的
String类,用户自定义的String类不会被加载。
运行时数据区
这是 JVM 管理的内存区域,用于存储程序运行时的数据。
方法区、堆、虚拟机栈、本地方法栈以及程序计数器五个部分。不过,运行时数据区的划分也随着JDK的发展不断变迁,JDK 1.6、JDK 1.7、JDK 1.8 的内存划分都会有所不同。
- 方法区 (Method Area):
- 线程共享。存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 在 HotSpot JVM 中,它常被称为 “永久代” (PermGen)(Java 8 之前),但在 Java 8 及之后版本中被 元空间 (Metaspace) 取代,元空间使用本地内存(Native Memory),不再受 JVM 堆大小限制。
- 堆 (Heap):
- 线程共享。这是 JVM 中最大的一块内存,几乎所有对象实例和数组都在这里分配内存。
- 是垃圾回收器 (Garbage Collector, GC) 管理的主要区域,因此也被称为 “GC 堆”。
- 从内存回收角度,Java 堆可分为:
- 新生代 (Young Generation):又分为 Eden 区和两个 Survivor 区 (S0, S1)。新创建的对象首先放在 Eden 区。
- 老年代 (Old Generation/Tenured):在新生代中经历多次 GC 后仍然存活的对象会被移到老年代。
- 从内存分配角度,Java 堆可划分出线程私有的分配缓冲区 (TLAB),以提升对象分配时的效率。
- 虚拟机栈 (JVM Stack):
- 线程私有,生命周期与线程相同。
- 描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧 (Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 我们常说的“堆内存”和“栈内存”中的“栈”,指的就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分。局部变量表存放了编译期可知的各种基本数据类型和对象引用。
- 本地方法栈 (Native Method Stack):
- 线程私有。作用与虚拟机栈非常相似,区别在于虚拟机栈为执行 Java 方法服务,而本地方法栈为 JVM 使用的 Native 方法(通常用 C/C++ 编写)服务。
- 程序计数器 (Program Counter Register):
- 线程私有。它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 此区域是唯一一个在《Java 虚拟机规范》中没有规定任何
OutOfMemoryError情况的区域。
堆和栈的区别
区别主要在以下几个方面:
| 对比维度 | 堆 | 栈 |
|---|---|---|
| 功能 | 存储对象实例和数组。几乎所有的对象实例都在这里分配内存。 | 存储栈帧。每个栈帧对应一个被调用的方法,包含局部变量表、操作数栈等。 |
| 线程可共享 | 线程共享。堆中的所有对象对所有线程都是可见的。 | 线程私有。每个线程都有自己独立的虚拟机栈。栈中的数据(如局部变量)是线程私有的。 |
| 内存分配 | 在JVM启动时创建,是运行时数据区中最大的一块。 | 在线程创建时创建,生命周期与线程相同。 |
| 存储内容 | 主要存放对象本身。 | 存放基本数据类型的变量,以及对象引用(reference变量,它指向对象在堆中的地址)。 |
| 生命周期 | 随着JVM的启动而创建,随着JVM的退出而销毁。 | 随着线程的创建而创建,随着线程的结束而销毁。 |
| GC | 是垃圾回收器管理的主要区域。 | 基本不涉及垃圾回收。方法执行完毕后,栈帧出栈,内存自动释放。 |
堆内存的划分
堆内存的划分是面试中的高频考点, 也是JVM中很重要的部分。
现代垃圾收集器大部分都基于分代收集理论设计,所以堆内存也相应地被划分为了不同的“代”。
这种划分的核心思想源于两个普遍的观察经验,即弱分代假说和强分代假说:
- 弱分代假说:绝大多数对象都是“朝生夕死”的,在创建后很快变得不可达。
- 强分代假说:熬过越多次垃圾收集过程的对象,就越难以消亡。
基于这些假说,将堆划分为不同区域,并对不同区域采用不同的垃圾收集策略,可以显著提升GC效率,实现时间和空间上的高效平衡。
Java堆在逻辑上主要分为两大区域:
1. 新生代 (Young Generation)
用于存放新创建的对象。由于大部分对象生命周期很短,所以新生代会发生非常频繁的垃圾回收(称为 Minor GC 或 Young GC)。
为了更好地管理这些“朝生夕死”的对象,新生代内部又被细分为三个区域:
- Eden(伊甸)区 :对象“诞生”的地方。几乎所有新对象都首先在Eden区分配。当Eden区内存不足时,就会触发一次Minor GC。
- Survivor区 (幸存者区):包含两个通常大小相等的区域——Survivor 0 (S0/From) 和 Survivor 1 (S1/To)。在Minor GC后,Eden区中仍然存活的对象会被移动到其中一个Survivor区。另一个Survivor区则用于在GC过程中复制存活对象。这两个Survivor区角色是互换的(每次GC后,From和To标签会交换)。
- 晋升过程:对象在Survivor区之间每熬过一次Minor GC,其年龄计数器就会增加1。当对象的年龄增加到一定程度(默认15,可通过
-XX:MaxTenuringThreshold设置)后,它就会被晋升(Promote) 到老年代。
2. 老年代 (Old Generation/Tenured Generation)
这里存放生命周期较长的对象,以及一些大对象(取决于具体JVM实现和参数)。经过多次Minor GC后依然存活的对象,最终会晋升到这里。
老年代的对象比较稳定,因此发生垃圾回收的频率远低于新生代;发生在老年代的GC称为 Major GC。
通常将同时收集整个堆(新生代+老年代)的GC称为 Full GC。Full GC的速度通常比Minor GC慢10倍以上,是导致应用长时间停顿(Stop-The-World)的主要元凶之一。

为什么分代?
核心是针对不同生命周期的对象采用最合适的回收算法(如新生代用高效的复制算法,老年代用标记-清除或标记-整理算法),从而降低整体GC开销。
STW
即Stop-The-World,它指的是在垃圾收集过程中,会涉及到对象的移动,为了保证对象引用在移动过程中不被修改,JVM会暂停所有应用程序线程(除了垃圾收集器线程本身)。
在 STW 期间,应用程序的响应时间(RT)和吞吐量(QPS)都会受到影响,这可能导致性能表现的不确定性,特别是在负载较高的情况下。
为了减少STW的影响,应选择合适的低延迟收集器(如 G1/ZGC,这些并发回收器主要关注的是减少 STW 的时长。它允许垃圾收集线程在应用程序线程运行的同时执行部分垃圾收集工作,从而减少了 STW 的时间),合理配置堆和分代大小,优化代码减少对象分配与提升,并持续监控调优以平衡吞吐与停顿。
执行引擎
负责执行字节码。它读取运行时数据区中的字节码并执行。
核心组件包括:
- 解释器 (Interpreter):逐条地读取、解释并执行字节码指令。优点是启动快,立即执行;缺点是执行速度相对较慢。
- 即时编译器 (Just-In-Time Compiler, JIT):为了提升性能,JVM 会识别热点代码(如被频繁调用的方法、循环体等),并将这些字节码编译成本地机器码。当下次执行时,直接运行机器码,大大提高了效率。
- HotSpot JVM 内置了两个主要的 JIT 编译器:
C1(客户端编译器)和C2(服务端编译器)。Java 7 引入了分层编译,结合了两者的优点。
- HotSpot JVM 内置了两个主要的 JIT 编译器:
- 垃圾回收器 (Garbage Collector, GC):自动管理堆内存的机制,负责回收不再被使用的对象,释放内存。它是 Java 内存自动管理的核心,其算法和实现非常复杂(如标记-清除、复制、标记-整理等),有多种不同的 GC 实现(如 Serial, Parallel, CMS, G1, ZGC等),适用于不同的场景。
GC触发场景
触发GC的条件主要取决于垃圾收集器的具体实现和内存分配情况,但通常可以分为自动触发和手动触发两大类。下面主要从自动触发的角度,按内存区域讲解GC触发的条件。
新生代GC (YoungGC / MinorGC)
尝试为新对象分配内存, 但是Eden区空间不足时。这是最常见、最频繁的GC。几乎所有新对象都在Eden区创建。当Eden区满,JVM会启动一次Minor GC,清除死亡对象,并将存活对象复制到Survivor区或晋升到老年代。
老年代GC (MajorGC)
MajorGC触发场景较多, 这里例举几个例子说明:
- 空间分配担保失败。
发生Minor GC前,JVM会检查一个规则:“老年代最大可用连续空间 > 新生代所有对象总大小”(或者历次晋升的平均大小)。 - 对于CMS这类并发收集器,有一个参数
-XX:CMSInitiatingOccupancyFraction(默认68%)。当老年代空间使用率达到这个阈值时,CMS会在后台并发地启动一次Major GC,以避免空间被完全填满(Concurrent Mode Failure)。 - 大对象直接进入老年代。当一个对象的大小超过
-XX:PretenureSizeThreshold参数设定值(比如2MB),它将直接在老年代分配。如果此时老年代没有足够的连续空间来容纳这个大对象,即使年轻代还有空间,也会直接触发Major GC(或Full GC)。
整堆GC (FullGC)
停顿时间最长,需要重点说明。
1.方法区(元空间)空间不足. 当元空间内存耗尽,无法加载新的类或为类分配元数据时,会触发Full GC。如果Full GC后仍不足,则会抛出OutOfMemoryError: Metaspace。
2.晋升失败. Minor GC时,Survivor区放不下存活对象,需要向老年代晋升,但老年代也没有足够空间。
3.担保失败后的真正失败. 上述“空间分配担保”后,即使进行了Major GC,老年代空间依然无法容纳从年轻代晋升上来的对象, 此时会触发一次Full GC。
4.Concurrent Mode Failure (针对CMS). 当CMS正在进行并发清理时,老年代空间正好被快速填满。此时JVM会暂停所有应用线程(STW),启动一次Serial Old收集器进行的Full GC,这是非常耗时的。
5.调用System.gc(). 显式调用System.gc()或Runtime.getRuntime().gc()。
除了这些明确的触发条件,现代垃圾收集器(如G1、ZGC)的触发逻辑更加智能和复杂。它们不再严格遵守分代物理边界,而是通过成本模型、预测停顿时间、区域回收价值等来动态决定何时、回收哪些区域(G1的Mixed GC就是一个例子)。其核心目标是在吞吐量和停顿时间之间取得最佳平衡,避免应用出现长时间的‘卡顿’。
Mixed GC 是 G1 垃圾收集器特有的一种 GC 类型,它在一次 GC 中同时清理年轻代和部分老年代。

Comments NOTHING