news 2026/6/11 7:29:51

JVM 征服手册:从 CRUD 到性能调优的完整指南(一)- 基础入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM 征服手册:从 CRUD 到性能调优的完整指南(一)- 基础入门

你有没有遇到过这样的场景——线上服务突然变慢,CPU 飙到 100%,你却只能重启了事?面试官问你"讲讲 JVM 内存结构",你只记得堆和栈两个词,然后陷入尴尬的沉默?

别焦虑,你不是一个人!大多数 Java 开发者的日常都是在和 Spring Boot、MyBatis 打交道,JVM 就像每天呼吸的空气——看不见摸不着,但一旦出了问题,你就知道它有多重要。更微妙的是,面试官也清楚初级开发者不可能精通 JVM 调优,他们真正想考察的,是你对这门语言底层机制的好奇心和学习潜力

本文是 JVM 系列的上篇,我们会从最基础的概念讲起,不要求你有任何 JVM 前置知识。当你读完本文,你将能自信地画出 JVM 内存结构图、解释类加载过程、讲清楚垃圾回收的核心原理——这些都是面试中最基础也最高频的考点。


分层导读:不同阶段的 Java 开发者需要学什么?

在正式进入内容之前,我们先做一个"自我定位"。不同阶段对 JVM 的要求差异很大,你不必一次性掌握全部内容:

阶段核心关注点学完本文能做什么
初级(0-2 年)JVM 是什么、类加载、运行时数据区面试能画内存结构图,能解释堆、栈、方法区
中级(2-5 年)GC 原理、GC 日志、JVM 参数调优OOM 不再只会重启,能定位问题并说清原因
高级(5 年+)JIT 编译、字节码、JMM、性能诊断主导线上故障排查和架构级性能优化
面试冲刺高频考点 + 典型面试题有条理地回答 JVM 问题,不再支支吾吾

本文重点覆盖初级到中级前半段的内容。关于垃圾收集器实战、诊断工具、JIT 编译和面试题精讲,请阅读本系列下篇。


第一章:JVM 到底是什么?——先让你心里有张地图

1.1 "一次编写,到处运行"的秘密

几乎所有 Java 程序员的第一课都会听到这句话:Write Once, Run Anywhere

Windows 的 .exe 只能跑在 Windows 上,Linux 的二进制文件只能跑在 Linux 下,凭什么 Java 的 .class 文件能到处跑?答案就在 JVM。

JVM(Java Virtual Machine,Java 虚拟机)本质上是一个"翻译官"——把 .class 文件中的字节码翻译成操作系统能理解的机器指令。不同操作系统有不同的 JVM 实现,但它们面对同一份 .class 文件时,执行逻辑完全一致。

1.2 JDK、JRE、JVM 到底什么关系?

这是面试高频考点,但很多工作了两三年的人也说不清楚。三者的关系其实很简单:

┌─────────────────────────────────────┐ │ JDK │ │ ┌─────────────────────────────┐ │ │ │ JRE │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ JVM │ │ │ │ │ │ (HotSpot / OpenJ9) │ │ │ │ │ └──────────────────────┘ │ │ │ │ 核心类库 (rt.jar / lib) │ │ │ └─────────────────────────────┘ │ │ 开发工具 (javac, javap, jstack...) │ └─────────────────────────────────────┘
  • JVM:最底层,负责执行字节码。你写的 HelloWorld 能跑起来靠的就是它。
  • JRE(Java Runtime Environment)= JVM + 核心类库。光有 JVM 不够,StringHashMap这些基础类都在核心类库里。
  • JDK(Java Development Kit)= JRE + 开发工具。javac(编译器)、javap(反编译)、jstack(线程分析)都在这。

打个比方:JVM 是发动机,JRE 是能跑的车,JDK 是带全套修车工具的车。普通用户只需要车(JRE),而我们开发者需要能造车也能修车的工具(JDK)。

1.3 HotSpot 是什么?常见的 JVM 实现有哪些?

我们通常说的"JVM",绝大多数时候指的是HotSpot VM——Oracle 和 OpenJDK 默认使用的虚拟机,市场份额超过 90%。

JVM 实现特点使用场景
HotSpotOracle/OpenJDK 默认通用,99% 的 Java 应用
OpenJ9IBM 开源,启动快、内存占用低云原生、微服务容器化场景
GraalVMOracle 出品,支持多语言混编高性能计算、多语言项目
Zing/AzulC4 算法,超低延迟 GC(付费)金融交易系统等

对于绝大多数开发者来说,掌握 HotSpot 就足够了,本文所有内容也基于 HotSpot 展开。


第二章:类加载机制——你的代码是怎么"活"起来的?

搞清楚了 JVM 的定位,接下来问一个具体问题:你写的User user = new User(),这个User类到底是怎么被 JVM 认识的?

2.1 类加载三部曲:加载 → 链接 → 初始化

JVM 把类从 .class 文件变成可用的"活对象",经过三个大阶段:

关键细节——准备阶段 vs 初始化阶段,这几乎是面试必问的点:

// 假设类中有这个静态变量 public static int value = 123;
  • 准备阶段:JVM 给value分配内存,并设为默认值0(还不是123!)
  • 初始化阶段:执行<clinit>()方法,value才被真正设置为123

如果是public static final int value = 123;,则编译期就会将常量值写入字段——这就是编译时常量优化。

2.2 双亲委派模型:为什么你写的 java.lang.String 不会生效?

你有没有好奇过:如果自己写了一个java.lang.String类放到 classpath 下,能替换掉 JDK 自带的 String 吗?

答案是不能。这背后的保护机制,就是双亲委派模型

工作流程:当一个类需要被加载时,类加载器不会自己先动手,而是先把请求向上委托给父加载器。只有父加载器反馈"我找不到这个类"时,子加载器才会自己去加载。

就像审批链条:科长收到请求 → 找处长 → 处长找局长 → 局长说他不管 → 处长处理 → 处长也不管 → 科长自己处理。

这样做的好处

  1. 避免核心类被篡改:你写的java.lang.String会先被启动类加载器拦截——它发现这个全限定名属于自己管辖范围,直接加载了官方的 String,你的版本永远不会被使用。
  2. 避免重复加载:同一个类只会被加载一次。

面试官可能会追问:"如何打破双亲委派?"答案是自定义 ClassLoader 并重写loadClass()(JDK 1.2 之后更推荐重写findClass())。Tomcat 的 WebappClassLoader 就是典型案例——不同 Web 应用之间需要隔离,所以打破了双亲委派。

2.3 不得不打破的时候:SPI 机制怎么绕过去的?

实际开发中,双亲委派并不是铁板一块。JDK 自身的SPI(Service Provider Interface)机制就需要打破它。以 JDBC 为例:

// 这行代码为什么能加载到 MySQL 的驱动类? Connection conn = DriverManager.getConnection(url, user, password);

DriverManager由启动类加载器加载,但 MySQL 驱动在 classpath 下,由应用类加载器加载。按双亲委派,启动类加载器根本看不到 classpath 下的类,怎么办?

JDK 的解决方案是线程上下文类加载器(Thread Context ClassLoader)

// DriverManager 内部实际上这样获取了驱动 ClassLoader cl = Thread.currentThread().getContextClassLoader(); // cl 就是应用类加载器,可以加载 classpath 下的 MySQL 驱动

这告诉我们:没有银弹。双亲委派解决了核心类安全问题,但在需要"父加载器访问子加载器的类"时(SPI 是典型),就必须绕过了。


第三章:运行时数据区——JVM 的"五脏六腑"

类加载让代码进入了 JVM,接下来代码运行时,数据放在哪里?这就是面试中最核心的知识点之一:JVM 运行时数据区(Runtime Data Area)

3.1 先给你一张全景图

不要被术语吓到,我们先整体看一眼,然后逐个拆解:

记忆口诀:"三私两公一堆外"——程序计数器、虚拟机栈、本地方法栈是线程私有的;堆和方法区是线程共享的;直接内存是堆外面的。

3.2 程序计数器(PC Register):最简单也最不起眼

它可能是 JVM 中最低调的一块内存——只存一条信息:当前线程正在执行的字节码指令行号。

但它不可或缺。CPU 在多线程之间切换时,必须知道每个线程"执行到哪了"。程序计数器就是干这个的——记录执行位置,方便切换回来后接着执行。

两个特点:它是 JVM 中唯一不会抛出 OutOfMemoryError的区域;执行 native 方法时值为 undefined。

3.3 Java 虚拟机栈(JVM Stack):方法调用的幕后玩家

每个 Java 方法被调用时,JVM 都会同步创建一个栈帧(Stack Frame),压入虚拟机栈:

方法执行完毕,栈帧出栈。如果方法里调了另一个方法,就把新栈帧压在上面——这正是"栈"的含义(后进先出)。

你遇到过StackOverflowError吗?它就是因为方法调用层次太深(比如无限递归),虚拟机栈的容量不够了。设置栈大小的参数是-Xss,如-Xss1m

3.4 Java 堆(Heap):所有对象的故乡

User user = new User(); // 这个 new User() 创建的对象,就放在堆里

堆是 JVM 中最大的一块内存,也是 GC(垃圾回收)的主战场。几乎所有对象都在堆上分配。

堆的内存结构(分代视角):

为什么新生代要分 Eden 和 Survivor?如果只有 Eden,第一次 Minor GC 能回收的对象还好说,但存活下来的对象去哪?直接放老年代会导致老年代被快速填满、触发代价高昂的 Full GC。Survivor 区就是给那些"刚出生还死不掉"的对象一个缓冲地带。

3.5 方法区(Method Area):类的"档案馆"

方法区存放的是类信息(类名、字段、方法、接口版本)、常量、静态变量、JIT 编译后的代码缓存。

一个重要版本变更

版本方法区实现所在位置
JDK 7 及以前永久代(PermGen)堆的一部分
JDK 8 及以后元空间(Metaspace)本地内存(堆外)
# JDK 7:永久代大小固定 -XX:MaxPermSize=256m # JDK 8+:元空间默认无上限,但强烈建议设置上限 -XX:MaxMetaspaceSize=256m

这个改动非常实际——以前部署应用时经常因为加载的类太多导致java.lang.OutOfMemoryError: PermGen space,换成元空间后好多了。

3.6 运行时常量池 vs 字符串常量池

这也是个容易混淆的点:

  • 运行时常量池:每个类都有一个常量池,类加载后成为运行时常量池,属于方法区的一部分。
  • 字符串常量池:JDK 7 开始被移到了堆中。
String s1 = "hello"; // 字面量,在字符串常量池中 String s2 = new String("hello"); // 在堆中创建新对象 String s3 = s2.intern(); // 将 s2 的值放入字符串常量池并返回该引用 System.out.println(s1 == s2); // false(常量池 vs 堆中不同对象) System.out.println(s1 == s3); // true (都指向常量池中的同一对象)

这段代码是面试常客。理解它的关键是:new一定在堆中创建新对象,intern()操作的归宿是字符串常量池。


第四章:垃圾回收基础——JVM 最精彩的篇章

理解了内存是怎么划分的,下一个自然的问题就是:内存是有限的,不用的对象谁来清理?这一章我们聚焦在 **"怎么找到垃圾"**和"怎么清理垃圾"这两个核心问题上。

4.1 怎么判断一个对象"死了"?

回收垃圾的第一步是找到垃圾。

(1)引用计数法——最直观但被淘汰的方案

给每个对象加一个引用计数器,有人引用就 +1,引用失效就 -1,计数器归零就回收。听起来很合理,但有一个致命缺陷:循环引用

class Node { Node next; } Node a = new Node(); Node b = new Node(); a.next = b; b.next = a; // a 和 b 互相引用 a = null; b = null; // 外部引用都没了,但 a 和 b 的计数器都不是 0 —— 永远无法回收!
(2)可达性分析——JVM 实际采用的方法

GC Roots 包括哪些?(面试必考)

  • 虚拟机栈中引用的对象(局部变量指向的对象)

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中引用的对象

  • 被 synchronized 锁持有的对象

4.2 四种引用类型:强、软、弱、虚

Java 提供了四种引用,让开发者能更精细地控制对象的生命周期:

引用类型

回收时机

典型用途

强引用new Object()

永不回收(除非不可达)

99% 的场景

软引用SoftReference

内存不足时回收

图片缓存、网页缓存

弱引用WeakReference

下次 GC 必定回收

ThreadLocal 的 Key

虚引用PhantomReference

无法通过它获取对象

NIO DirectByteBuffer 清理

// 软引用示例——内存敏感的缓存 SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024 * 10]); byte[] data = cache.get(); if (data == null) { // 缓存已被回收,重新加载 data = loadFromDisk(); cache = new SoftReference<>(data); }

ThreadLocal 内存泄漏的根因就藏在这里:ThreadLocalMap 的 Key 是弱引用,但 Value 是强引用。Key 被 GC 后变成 null,而 Value 仍然被 Map 持有、无法回收——除非显式调用remove()

4.3 垃圾回收算法:从理论到实践

找到垃圾之后,怎么清理?有三种经典算法:

(1)标记-清除(Mark-Sweep)——最基础

缺点:效率不高,且产生内存碎片。

(2)标记-复制(Mark-Copy)——新生代主力

没有碎片,但可用内存减半。为什么适合新生代?因为新生代 98% 的对象"朝生夕死",需要复制的存活对象很少,复制成本极低。

这也解释了为什么新生代有两个 Survivor:Eden 和一个 Survivor 作为"使用中"的内存,另一个 Survivor 是"闲置空间"来接收存活对象。两个 Survivor 交替使用,永远有一块是空的。

(3)标记-整理(Mark-Compact)——老年代主力

老年代存活率高,不适合复制算法(复制成本太高)。整理算法没有碎片,但需要移动对象,耗时较长。

4.4 分代收集:为什么要"分代"?

三种算法各有优劣,JVM 的策略是因地制宜——把一个堆分成不同区域,对不同区域用不同算法:

弱分代假说:绝大多数对象是朝生夕死的,活得越久的对象越不容易死。

基于这个经验事实:

  • 新生代:"死亡率"极高 → 用标记-复制(复制成本低,回收效率高)
  • 老年代:"存活率"高 → 用标记-清除标记-整理(避免频繁复制)

这就是分代收集的核心思想。理解了它,你就理解了 JVM 垃圾回收的一大半。


下篇预告

本文带你走完了 JVM 基础知识的"主干道":JVM 是什么 → 类怎么加载进来的 → 运行时数据放在哪里 → 垃圾怎么回收。

在下篇中,我们会进入动手实战环节——认识 HotSpot 中真正干活的垃圾收集器(G1、CMS、ZGC 等),学习 JVM 核心参数和 GC 日志,掌握线上排障的诊断工具用法,并最终用十道面试精讲帮你把知识转化为面试中的得分点。

建议:在阅读下篇之前,先把本文中的内存结构图和回收算法画一到两遍——肌肉记忆会帮你在面试时更自信。


【下一篇:JVM 征服手册:从 CRUD 到性能调优的完整指南(二)- 进阶实战】

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 7:28:58

3步打造专属小米手表表盘:从零到一的完整指南

3步打造专属小米手表表盘&#xff1a;从零到一的完整指南 【免费下载链接】Mi-Create Unofficial watchface creator for Xiaomi wearables ~2021 and above 项目地址: https://gitcode.com/gh_mirrors/mi/Mi-Create 你是否曾看着手腕上的小米手表&#xff0c;觉得官方表…

作者头像 李华
网站建设 2026/6/11 7:24:53

13ft Ladder:3分钟搭建个人专属付费墙绕过阅读助手

13ft Ladder&#xff1a;3分钟搭建个人专属付费墙绕过阅读助手 【免费下载链接】13ft My own custom 12ft.io replacement 项目地址: https://gitcode.com/GitHub_Trending/13/13ft 在信息爆炸的时代&#xff0c;优质内容往往被付费墙保护&#xff0c;从《纽约时报》的深…

作者头像 李华
网站建设 2026/6/11 7:22:28

Docker 与 Kubernetes:从“集装箱”到“远洋舰队”

Docker 与 Kubernetes&#xff1a;从“集装箱”到“远洋舰队”Docker 与 Kubernetes&#xff1a;从“集装箱”到“远洋舰队”1. Docker 是什么&#xff1f;——把应用装进“集装箱”1.1 Docker 解决了什么问题&#xff1f;1.2 核心概念&#xff08;小白版&#xff09;1.3 Docke…

作者头像 李华
网站建设 2026/6/11 7:20:42

做工业控制和物联网网关的朋友最近经常问:屏幕刷新卡顿、AI算力不够、PCB面积又受限,这该怎么选型?

今天简单分享一下我们项目里刚量产的一款核心模组——ESP32-S3-WROOM-1-N8R8。这玩意儿最近挺火&#xff0c;确实是解决了不少痛点。&#x1f9e0; 核心规格与硬件参数这颗模组用的是乐鑫ESP32-S3系列芯片&#xff0c;虽然长得和经典款差不多&#xff0c;但内里完全不一样。核心…

作者头像 李华