当前位置: 首页 > news >正文

初步理解 JVM:类加载机制、内存结构与核心运行原理

Java 之所以能够“一次编写,到处运行”,核心原因之一就是 JVM。Java 源代码经过编译后生成 .class 字节码文件,JVM 负责加载字节码、解释或编译执行、管理内存、执行垃圾回收,并提供线程、安全、异常等运行时能力。

理解 JVM,是为了在实际开发中解决诸如:

  • 类冲突、类找不到、版本冲突
  • 内存溢出、内存泄漏
  • GC 频繁、接口抖动
  • 线程安全、可见性问题
  • 应用启动慢、运行慢、吞吐低

本文将从以下几个方面系统介绍 JVM:

  1. JVM 类加载机制
  2. 双亲委派模型
  3. JVM 运行时内存结构
  4. Java 对象创建过程
  5. 垃圾回收机制
  6. Java 内存模型 JMM
  7. JVM 常见参数与问题排查
  8. JVM 重要知识点总结

一、JVM 整体工作流程

其中,类加载器负责把 .class 文件加载进 JVM;运行时数据区负责存储程序运行时需要的数据;执行引擎负责执行字节码;垃圾回收器负责自动管理内存。

二、JVM 类加载机制

1. 什么是类加载?

类加载指的是 JVM 把 .class 字节码文件加载到内存中,并对其进行校验、转换、初始化,使其最终成为 JVM 可以直接使用的 Java 类型。

类加载的完整生命周期如下:

加载 -> 验证 -> 准备 -> 解析 -> 初始化

其中,验证、准备、解析统称为连接 Linking

2. 加载阶段 Loading

加载阶段主要完成三件事:

  1. 通过类的全限定名获取该类的二进制字节流。
  2. 将字节流中的静态存储结构转换为方法区中的运行时数据结构。
  3. 在堆中生成一个 java.lang.Class 对象,作为访问这个类的入口。

例如:

Class<?> clazz = User.class;

这里的 clazz 就是 JVM 为 User 类生成的 Class 对象。

需要注意的是,.class 字节流不一定来自本地文件,也可以来自:

  • jar 包
  • 网络
  • 动态代理生成
  • 运行时动态编译
  • 自定义类加载器生成

这也是 Java 能够支持热部署、插件化、动态代理等能力的重要基础。

3. 验证阶段 Verification

验证阶段的目标是:确保字节码文件是安全、合法、符合 JVM 规范的

主要包括:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

例如,JVM 会检查:

  • .class 文件魔数是否正确
  • 版本号是否被当前 JVM 支持
  • 是否继承了不允许继承的类
  • 方法调用是否合法
  • 操作数栈使用是否正确

验证阶段非常重要,因为 JVM 不能信任外部传入的字节码。即使不是由 Java 编译器生成的字节码,只要符合 JVM 规范,也可以被执行。

4. 准备阶段 Preparation

准备阶段会为类变量,也就是 static 变量分配内存,并设置默认初始值。

例如:

public class User { public static int age = 18; }

在准备阶段,age 的值不是 18,而是默认值 0。

真正赋值为 18,发生在初始化阶段。

再看一个例子:

public class User { public static int age = 18; public static final int count = 10; }

对于 static final 修饰的编译期常量,count 可能在准备阶段就被赋值为 10。

常见默认值如下:

5. 解析阶段 Resolution

解析阶段的作用是:将常量池中的符号引用转换为直接引用

什么是符号引用?

可以简单理解为“用名字描述目标”。

6. 初始化阶段 Initialization

初始化阶段才是真正执行 Java 代码的阶段。

JVM 会执行类构造器方法 <clinit>(),它由以下内容合并生成:

  • 静态变量显式赋值
  • 静态代码块
public class User { static int age = 18; static { age = 20; } }

执行初始化后,age 的最终值是 20。

<clinit>() 方法由编译器自动生成,程序员不能直接编写。

三、类加载器与双亲委派模型

1. 类加载器分类

JVM 中常见类加载器如下:

  • Bootstrap ClassLoader
  • Extension ClassLoader
  • Application ClassLoader

在 Java 9 之后,模块化机制引入后,Extension ClassLoader 被 Platform ClassLoader 替代。

2. Bootstrap ClassLoader

启动类加载器,负责加载 Java 核心类库,例如:

它通常由 C/C++ 实现,不是普通 Java 类。

所以执行:

System.out.println(String.class.getClassLoader());

可能输出:null

这个 null 并不是没有类加载器,而是表示它由 Bootstrap ClassLoader 加载

3. Platform ClassLoader / Extension ClassLoader

Java 8 中叫 Extension ClassLoader,负责加载扩展类库。

Java 9 之后叫 Platform ClassLoader,负责加载平台相关模块。

4. Application ClassLoader

应用类加载器,负责加载 classpath 下的业务类。

我们自己写的大多数 Java 类,默认都是由 Application ClassLoader 加载的。

System.out.println(User.class.getClassLoader());

通常会输出类似:jdk.internal.loader.ClassLoaders$AppClassLoader

5. 双亲委派模型

双亲委派模型的核心思想是:

一个类加载器收到类加载请求后,不会立即自己加载,而是先委托给父类加载器。只有父类加载器无法加载时,子类加载器才会尝试自己加载。

流程如下:

6. 为什么需要双亲委派?

双亲委派主要有两个好处:

第一,保证核心类库安全

假如我们自己写一个类:

package java.lang; public class String { }

如果没有双亲委派,那么这个伪造的 String 类可能会替代 JDK 自带的 String 类,造成严重安全问题。

有了双亲委派后,java.lang.String 会优先由 Bootstrap ClassLoader 加载,应用程序无法随意替换核心类库。

第二,避免类重复加载

同一个类如果被多个类加载器重复加载,就会在 JVM 中形成多个不同的类型。

JVM 判断两个类是否相同,不仅看类的全限定名,还看加载它的类加载器。

也就是说:

类唯一性 = 类全限定名 + 类加载器

四、JVM 运行时内存结构

JVM 运行时数据区是 JVM 管理内存的核心区域。

按照线程是否共享,可以分为两类:

线程共享:

- 堆

- 方法区

线程私有:

- 程序计数器

- Java 虚拟机栈

- 本地方法栈

1. 程序计数器

程序计数器是一块很小的内存区域,用来记录当前线程正在执行的字节码指令地址。

它有两个特点:

  1. 线程私有。
  2. 是 JVM 规范中唯一一个不会出现 OutOfMemoryError 的区域。

为什么需要程序计数器?

因为 Java 是多线程的,线程之间会频繁切换。线程切换回来后,JVM 需要知道这个线程上次执行到哪里了。

2. Java 虚拟机栈

Java 虚拟机栈也是线程私有的,它描述的是 Java 方法执行的内存模型。

每个方法执行时,JVM 会创建一个栈帧。

一个栈帧主要包含:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址

方法调用过程可以理解为栈帧入栈和出栈:

常见异常:

StackOverflowError

OutOfMemoryError

3. 本地方法栈

本地方法栈服务于 Native 方法,也就是使用 native 关键字修饰的方法。

例如:

public native int hashCode();

本地方法通常由 C/C++ 实现。

4. 堆 Heap

堆是 JVM 中最大的一块内存区域,用于存放对象实例和数组。

几乎所有对象都在堆上分配。

堆是线程共享的,也是垃圾回收器重点管理的区域。

常见异常:java.lang.OutOfMemoryError: Java heap space

常见异常: java.lang.OutOfMemoryError: Java heap space

堆通常可以进一步分为:

对象通常先分配在 Eden 区,经过多次 Minor GC 后仍然存活的对象,会进入老年代。

5. 方法区 Method Area

方法区用于存储类相关信息,例如:

  • 类元信息
  • 常量
  • 静态变量
  • 即时编译后的代码缓存

在不同 JDK 版本中,方法区的具体实现不同

元空间使用的是本地内存,而不是 JVM 堆内存。

常见异常:java.lang.OutOfMemoryError: Metaspace

五、Java 对象创建过程

当我们执行:

User user = new User();

JVM 并不是简单地“创建一个对象”,而是经历了多个步骤。

1. 类加载检查

JVM 首先检查 User 类是否已经被加载、解析、初始化。

如果没有,就先执行类加载过程。

2. 分配内存

类加载完成后,JVM 会根据类的信息确定对象需要的内存大小,然后在堆中分配内存。

3. 初始化零值

内存分配完成后,JVM 会把对象的字段设置为默认零值。

例如:

int -> 0 boolean -> false 引用类型 -> null

4. 设置对象头

对象头中会存储一些运行时信息,例如:

  • 哈希码
  • GC 分代年龄
  • 锁状态标志
  • 类型指针

对象头是 synchronized 锁升级、GC 判断对象年龄等机制的基础。

5. 执行构造方法

最后执行对象的构造方法,也就是 <init>() 方法。

此时对象才真正按照我们代码中的逻辑完成初始化。

六、垃圾回收机制 GC

Java 程序员不需要手动释放对象内存,因为 JVM 提供了自动垃圾回收机制。

但自动并不等于不用理解。线上很多性能问题,本质都和 GC 有关。

1. 如何判断对象是否可以回收?

常见有两种思路:

引用计数法

给对象维护一个引用计数器。

有引用指向它,计数加一;引用失效,计数减一。计数为零,说明可以回收。

缺点是无法解决循环引用问题。

a.ref = b; b.ref = a;

即使 a 和 b 已经不再被外部访问,它们的引用计数仍然不为零。

因此,主流 JVM 不使用单纯的引用计数法判断对象是否存活。

可达性分析算法

JVM 主要使用可达性分析算法。

它从一组称为 GC Roots 的对象出发,向下搜索引用链。

如果一个对象到 GC Roots 没有任何引用链相连,就说明它不可达,可以被回收。

常见 GC Roots 包括:

  • 虚拟机栈中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象
  • 正在运行的线程对象
  • 被 synchronized 锁持有的对象

2. Java 引用类型

强引用

最常见的引用。

User user = new User();

只要强引用存在,对象就不会被回收。

软引用

内存不足时才会被回收。

适合做缓存。

SoftReference<User> ref = new SoftReference<>(new User());

弱引用

只要发生 GC,就可能被回收。

WeakReference<User> ref = new WeakReference<>(new User());

ThreadLocalMap 中的 key 就是弱引用。

3. 常见垃圾回收算法

标记-清除

先标记存活对象,再清除未标记对象。

缺点:

  • 会产生内存碎片
  • 清理效率不稳定

标记-复制

把内存分为两块,每次只使用一块。GC 时把存活对象复制到另一块,然后清空原区域。

优点:

  • 简单高效
  • 没有内存碎片

缺点:

  • 浪费一部分内存

新生代的 Survivor 区常用类似思路。

标记-整理

先标记存活对象,然后把存活对象向一端移动,再清理边界外的内存。

优点:

  • 没有内存碎片

适合老年代。

分代收集

JVM 根据对象存活时间,将堆分为新生代和老年代。

大多数对象朝生夕死,因此新生代 GC 比较频繁;长期存活的对象进入老年代,老年代 GC 频率较低。

4. 常见垃圾回收器

不同 JDK 版本默认 GC 有差异。

常见垃圾回收器包括:

5. Minor GC、Major GC、Full GC

Minor GC

发生在新生代,频率较高,速度通常较快。

Major GC

一般指老年代 GC,不同资料中定义可能略有差异。

Full GC

回收整个 Java 堆和方法区,成本较高,可能造成较长时间停顿。

线上系统应尽量避免频繁 Full GC。

七、JVM 常见问题与排查思路

1. StackOverflowError

常见原因:

  • 无限递归
  • 方法调用链过深
  • 单个线程栈空间太小

示例:

public void recursion() {
recursion();
}

可以通过 -Xss 调整线程栈大小:-Xss1m

2. Java heap space

表示堆内存不足。

常见原因:

  • 创建大量对象
  • 集合无限增长
  • 缓存没有淘汰策略
  • 内存泄漏

相关参数:

-Xms 初始堆大小

-Xmx 最大堆大小

示例:

-Xms512m

-Xmx512m

3. Metaspace OOM

常见原因:

  • 动态生成大量类
  • 频繁部署导致类加载器无法卸载
  • CGLIB、动态代理滥用

相关参数:

-XX:MetaspaceSize=128m

-XX:MaxMetaspaceSize=256m

4. GC 频繁

可能原因:

  • 堆设置过小
  • 对象创建速度太快
  • 老年代空间不足
  • 内存泄漏
  • 大对象频繁分配

排查工具:
VisualVM
Arthas

http://www.jsqmd.com/news/890219/

相关文章:

  • EABJLM:基于增强注意力与多视图嵌入的意图槽位联合解析模型
  • Pyfa完全指南:如何在EVE Online中打造完美船舰装配
  • 2026新榜单:湘西母婴除甲醛CMA甲醛检测治理公司多少钱怎么收费 - 金诚回收
  • 生长因子——皮肤修复的“神奇工程师”
  • D3keyHelper暗黑3终极宏工具:从零开始的完整免费指南
  • 小米手表表盘设计终极指南:5分钟掌握Mi-Create免费工具
  • 华硕笔记本性能优化新选择:G-Helper轻量级控制工具完全指南
  • 智能解锁B站缓存:m4s-converter完整恢复指南
  • 基于多模态边聚类的LBSN重叠社区发现与用户画像构建
  • 2026年10款精选论文降AI工具亲测:降AI率实战对比实用指南 - 降AI实验室
  • 2026巴州库尔勒纽恩泰空气能维修售卖全攻略:选型、落地、避坑一站式指南 - GrowthUME
  • 算力飞速增长下,国内数据中心液冷厂家该怎么选? - GrowthUME
  • 生物网络链接预测:从图论到GNN的算法解析与应用实战
  • 如何在PC上免费畅玩Switch游戏?Ryujinx模拟器完整指南
  • 单例模式在C++中的使用:原子操作
  • 明日方舟游戏美术资源完整指南:8000+高清素材免费获取与创意应用
  • 浙江成考别等报名才复习!提前多久准备才不慌? - 奔跑123
  • 从Matlab到Vivado:高效生成.coe文件并配置ROM IP核的完整工作流
  • 2026新榜单:三门峡母婴除甲醛CMA甲醛检测治理公司推荐品牌排行榜 - 金诚回收
  • JiYuTrainer终极指南:如何在极域电子教室中找回你的电脑控制权
  • 2026新榜单:南平CMA甲醛检测治理及公共卫生检测报告地址联系方式集合(2026版) - 金诚回收
  • Node js 服务中如何集成 Taotoken 实现统一的多模型 API 调用
  • 基于深度信念网络的软件缺陷预测:从原理到工程实践
  • 2026年长沙宁乡汽车贴膜行业趋势与选型指南白皮书 - GrowthUME
  • 企业级微信SDK深度解析:高性能Java集成的最佳实践
  • 匠心筑家,质胜千言——涿州老王匠全屋定制 - GrowthUME
  • Mi-Create 终极指南:免费制作个性化小米手表表盘的完整教程
  • 常州黄金上门回收怕被坑?福运来手把手教你卖高价 - 黄金回收
  • 2026新榜单:三明CMA甲醛检测治理及公共卫生检测报告地址联系方式集合(2026版) - 金诚回收
  • Google搜索高级语法实战:三类问题精准检索方法论