这套“JVM 20卷”不是让你背字典,而是帮你把散落的知识点串成一张网。面试官问JVM,本质上不是考你记没记住某个参数,而是看你能不能讲清楚“内存怎么分、对象怎么活、垃圾怎么回收、线上怎么排查”。这篇文章我按20卷的形式拆成六大块,每卷对应一个必考主题,每个主题都从“面试官为什么问”切入,再配合原理、对比和实操经验,保证你读完不只是会背,还能讲得头头是道。
1. 这套“20卷”该怎么用:先画知识地图,再谈背诵
很多同学准备JVM面试,上来就抱着《深入理解Java虚拟机》啃,啃到第十天还在第一章,最后发现面试官问的跟书上的重点完全不是一回事。原因很简单——书是写给开发者系统学习用的,面试是抽查你核心概念的深度和横向关联能力的。这两者的节奏完全不同。
所以我把JVM面试涉及的核心考点抽成了20个必考卷,每个卷对应一个独立主题,卷与卷之间又有逻辑递进。你在复习的时候,不要按顺序死记硬背,而是先看这张地图,搞清楚自己在哪个位置。
| 卷号 | 核心主题 | 面试常见问法 |
|---|---|---|
| 卷一 | JDK、JRE、JVM关系 | JVM和JRE有什么区别?JDK包含哪些部分? |
| 卷二 | JVM生命周期与架构 | JVM启动流程是怎样的?一个Java进程怎么跑起来的? |
| 卷三 | 类加载过程 | 加载、验证、准备、解析、初始化分别做了什么? |
| 卷四 | 双亲委派模型 | 为什么需要双亲委派?如何打破? |
| 卷五 | 运行时数据区 | 堆、栈、方法区各自存什么?线程私有还是共享? |
| 卷六 | 栈帧与对象布局 | 栈帧里有哪些结构?对象头里有什么? |
| 卷七 | 对象创建与分配 | new一个对象完整过程?对象一定分配在堆上吗? |
| 卷八 | 对象存活判定 | 引用计数和可达性分析有什么区别?finalize靠谱吗? |
| 卷九 | 垃圾回收算法 | 标记清除、标记复制、标记整理各自优缺点? |
| 卷十 | 分代收集理论 | 为什么分代?新生代老年代如何配合? |
| 卷十一 | 经典收集器 | Serial、ParNew、Parallel各自场景? |
| 卷十二 | CMS收集器 | CMS有哪些阶段?什么是并发模式失败? |
| 卷十三 | G1收集器 | Region布局是核心?记住集、SATB、Mixed GC如何工作? |
| 卷十四 | ZGC与未来 | ZGC为什么能做到低延迟?染色指针是什么? |
| 卷十五 | JVM核心参数 | 堆、栈、元空间、GC日志参数怎么配? |
| 卷十六 | GC日志分析与调优思路 | 如何看懂一次Full GC日志? |
| 卷十七 | JVM故障排查工具 | jps、jstat、jmap、jstack、jcmd怎么用? |
| 卷十八 | OOM与线上事故排查 | OOM有哪些类型?完整排查链路是什么? |
| 卷十九 | JMM与并发底层 | Java内存模型与JVM内存模型的区别?happens-before是什么? |
| 卷二十 | 综合场景题 | 为什么CPU飙升?接口偶尔超时和GC有什么关系? |
看完这张表,你应该能感觉到,前四卷是“地基”,中间卷是“核心机制”,后面几卷是“实战能力”。面试官通常从任意一卷切入,然后向相邻卷发散。你准备的时候,围绕每一卷准备一个“30秒定场答案”和一个“3分钟深入答案”,这样不管面试官怎么问,你都能接得住。
2. 基础卷(卷一至卷四):JDK、JRE、JVM的三角关系与类加载体系
2.1 卷一:JDK、JRE、JVM到底谁包含谁
这道题热度常年排在前三,不是因为它难,而是因为很多人答到一半就含糊了。先把结论摆在前面:JDK包含了JRE,JRE包含了JVM,但JRE除了JVM还包括一组基础类库和启动工具,JDK除了JRE还包括编译、调试等开发工具。
用大白话理解:JVM是“发动机”,负责把字节码跑起来;JRE是“发动机加基础配件”,提供了运行Java程序所需的最小环境;JDK是“整车生产线”,不仅包含发动机和配件,还包含修车工具(javac、jdb、javap等)。
面试时能加分的是补充一句:Java是跨平台语言,靠的正是“不同平台实现各自JVM”这一设计。你在Windows上编译出的.class可以在Linux的JVM上运行,但JVM本身是不跨平台的——这就是“一次编译、到处运行”的代价与实现方式。
另一个容易被追问的细节是:现代JDK提供了jlink工具,可以裁剪出仅包含所需模块的运行时镜像。也就是说,理论上你可以构建一个不含某些基础模块的精简JRE,这种瘦身在容器化部署场景里非常实用。面试时提到这一层,会让面试官觉得你对部署形态有真实感知。
2.2 卷二:一个Java进程是怎么启动的
这个问题看起来简单,但很多人答不出层次。最低限度的回答是:执行java命令,读取.class或入口类,调用JVM启动器创建虚拟机实例,加载主类,执行main方法。但更好的回答应该按这个顺序展开:
- java命令会启动一个JVM进程,这个进程是操作系统的普通进程,有自己的内存空间。
- 启动器解析启动参数,比如-Xmx、-Xms、-classpath,然后创建JVM内存结构。
- JVM通过类加载器加载主类,完成链接(验证、准备、解析)和初始化。
- 调用主类的main方法,参数为命令行传入的String[]。
- 当main方法结束后,如果没有任何非守护线程存活,JVM进程退出。
这里面试官常延伸问“main线程结束进程就一定退出吗”。答案是否定的——只要还有非守护线程在运行,JVM就不会退出。这也是为什么某些后台任务线程没正确关闭时,应用明明“表面上结束”了却仍然占用端口的原因。
2.3 卷三:类加载的五个阶段,面试最爱追问“准备阶段”
类加载五个阶段是“加载、验证、准备、解析、初始化”。很多人能背出名字,但仔细一问就露馅。重点在两个阶段:
准备阶段是给类的静态变量分配内存并设置默认值的阶段。注意,这里设置的是零值,而不是你在代码里写的初始值。比如private static int count = 10;,在准备阶段count的值是0,真正的10是在初始化阶段赋值。
初始化阶段才真正执行类构造器<clinit>()方法,这时候静态变量才会被赋上代码里的值,静态代码块也会在这里执行。这里有个高频陷阱:如果类没有静态变量和静态代码块,就不会生成<clinit>()方法,那么初始化阶段可能没有实际操作。
另外注意“初始化”和“实例化”是两回事。初始化是类的初始化,实例化是new对象。一个类只会初始化一次,但可以被实例化无数次。面试官问你“什么时候会触发类初始化”,标准答案包括:new对象、访问静态字段、调用静态方法、反射调用、初始化子类触发父类初始化、作为JVM启动时的主类等。
2.4 卷四:双亲委派是怎么委派的,以及为什么要打破它
双亲委派模型是JVM面试的“必考常量”。一句话概括:当一个类加载器收到加载请求时,它先不自己加载,而是交给父加载器,一直向上传到启动类加载器,只有父加载器反馈无法加载,子加载器才自己尝试。
这样做的核心目的有两个:一是避免类被重复加载,同一个类在整个JVM中只需要加载一次;二是保证核心类库的完整性和安全性,比如你写一个java.lang.String,即使编译通过也无法替换JDK自带的String,因为启动类加载器会优先加载rt.jar里的String。
面试官接下来十有八九会问“怎么打破双亲委派”。三个经典场景要记住:
- SPI机制:JDBC等场景下,DriverManager由启动类加载器加载,但实际驱动实现却在classpath下,启动类加载器加载不到,于是引入
Thread.currentThread().getContextClassLoader()作为桥梁,让父加载器请求子加载器加载。这就是“逆向委派”。 - Tomcat的WebAppClassLoader:每个Web应用一个类加载器,优先加载/WEB-INF/classes下的类,实现应用隔离。这也是为什么Tomcat里不同应用可以放同一个类的不同版本。
- 自己重写loadClass方法:最简单直接的打破方式,也是很多框架热部署的基础。有些框架还会先从本地路径加载,再走双亲委派。
最容易丢分的是“为什么破坏双亲委派还能保证安全”——你要答清楚:打破的是加载顺序,但类加载流程中的验证、安全机制仍然生效,核心类库的加载依然走启动类加载器。
3. 内存与对象卷(卷五至卷十):数据区的每个角落和垃圾回收的道与术
3.1 卷五:运行时数据区,线程私有和线程共享分别有哪些
运行时数据区是JVM面试的根本盘,这个答不清楚,后面全崩。内存区域划分如下:
线程私有区:
- 程序计数器(PC寄存器):当前线程执行的字节码行号指示器。线程切换后能恢复到正确执行位置。唯一一个不会出现OOM的区域。
- 虚拟机栈:描述Java方法执行的线程内存模型,每调用一个方法就创建一个栈帧。
- 本地方法栈:为JVM使用到的Native方法服务。
线程共享区:
- 堆:存放对象实例,绝大多数对象都在这里分配,也是GC工作的主战场。
- 方法区:存储类信息、常量、静态变量、JIT编译后的代码。JDK8之后用元空间(Metaspace)实现,使用本地内存。
面试常有个引导性追问:“JDK8为什么把永久代换成元空间?”核心答案是永久代大小固定,容易触发OutOfMemoryError,而且很难调优;元空间使用本地内存,默认只受物理内存限制,大幅降低了OOM概率,并且原来永久代里的字符串常量等也做了调整。
补充一个常见误区:很多人把“JVM内存模型”当成“运行时数据区”的另一个说法,其实不对。一个是线程栈和堆的具体划分,一个是定义多线程读写共享变量时的内存可见性规则(JMM,即Java内存模型),两者在面试里会分别考察,后面卷十九再展开。
3.2 卷六:栈帧、对象头、访问定位
虚拟机栈中的每个栈帧包含四部分:局部变量表、操作数栈、动态连接、方法返回地址。
局部变量表以槽为单位,最小槽是32位,long和double占两个槽。操作数栈是计算的工作区,比如执行a + b,先把a压入栈,再把b压入栈,然后执行加法指令从栈里弹两个值,算完结果再压回去。画这个图能讲清楚“为什么虚拟机的计算这么像古老的栈式CPU”。
对象在堆中的内存布局有两块重点:普通对象头包含Mark Word(存储哈希码、GC分代年龄、锁状态标记)和类型指针(指向类的元数据);如果对象是数组,还有一个数组长度字段。这里面试官可能会问“为什么对象头里要有分代年龄”——因为新生代对象每熬过一次Minor GC年龄加1,到达阈值(默认15)就晋升老年代,这个年龄就存在Mark Word里。
对象访问方式有两种:句柄访问和直接指针访问。HotSpot默认是直接指针访问,好处是速度快、少一次间接寻址;句柄访问的好处是对象移动时引用不用修改,只需改变句柄指针。大部分时候答“HotSpot用直接指针”就够了,但如果面试官比较深,还愿意提一句GC时对象移动对这两种方式的影响,就是加分项。
3.3 卷七:对象创建从类加载到内存分配
“new一个对象到底发生什么”是综合题。完整链路是:
- 检查能否在常量池中定位到类的符号引用,并检查这个类是否已加载、链接、初始化过。
- 如果没有则触发类加载。
- 为对象分配内存。这里有两种分配方式:指针碰撞适用于堆内存规整(复制整理算法后的场景),空闲列表适用于内存不规整(标记清除算法后的场景)。分配时还要考虑并发安全,常见手段是CAS+失败重试或TLAB(Thread Local Allocation Buffer)。
- 内存空间清零,确保实例字段在不赋初值时能直接读取到默认零值。
- 设置对象头信息,比如对象属于哪个类、哈希码、GC分代年龄。
- 执行
<init>()构造方法,按程序员的意愿初始化对象。
面试官很爱追问“对象一定在堆上吗”。现代JVM有逃逸分析,如果对象不会被外部访问,可能被优化到栈上分配,甚至直接标量替换成基本类型。还有一种情况是TLAB——虽然内存是从堆上申请的,但为每个线程预分配了一块缓冲区,分配对象时无需锁,这也是提升性能的重要手段。
3.4 卷八:对象存活的判定方式
判断对象是否可回收,有引用计数和可达性分析两种思路。引用计数算法简单直观,但解决不了循环引用问题。比如A引用了B,B引用了A,但外部没有引用指向它们,这两个对象引用计数永远不为0,就泄漏了。当然现代JVM不用引用计数,但JVM之外部分语言(比如Python)还在用,所以面试官希望你能答出“引用计数的致命缺点”和“可达性分析如何避免这个问题”。
可达性分析是从GC Roots出发,向下搜索引用链,没被引用链覆盖到的对象就是可回收对象。GC Roots包括:虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、本地方法栈中JNI引用的对象、Java虚拟机内部的引用等。这里要特别记一个:不再是强引用的对象,即使被引用,也可能被回收。
顺带着引出了四种引用类型:强引用(OOM也不回收)、软引用(内存不足时回收)、弱引用(每次GC都会被回收)、虚引用(随时可被回收,主要用于跟踪对象被回收的通知)。面试常结合缓存场景问,比如“用弱引用做缓存行不行”——答案是不靠谱,因为弱引用生命周期太短,一GC就没,缓存命中率无法保证,一般还是用软引用做内存敏感缓存或配合ReferenceQueue做资源清理。
3.5 卷九到卷十:三种GC算法和分代假设
GC算法的演进是一条清晰的线。标记-清除是最基础的,先标记可回收对象,再统一回收。优点是实现简单,缺点是会产生大量内存碎片,而且标记和清除两个过程效率都不高。标记-复制把内存按容量分成两块,每次只用一块,回收时把存活对象复制到另一块,然后整块清理。优点是实现简单、运行高效、无碎片,缺点是内存浪费——可用内存只有一半。标记-整理则是在标记后让所有存活对象向一端移动,然后直接清理掉端边界以外的内存,兼顾了空间利用率和无碎片,但移动对象需要更新引用,开销更大。
现代JVM采用分代收集理论:新生代对象朝生夕灭,适合复制算法;老年代对象存活率高,适合标记-清除或标记-整理。HotSpot新生代就是这种设计:Eden区占8份,两个Survivor各占1份,每次使用Eden和一块Survivor,回收时把存活对象复制到另一块Survivor,这就确保新生代只有10%的空间是闲置的,复制算法的空间浪费被大幅压缩。
“为什么需要安全点”也在这附近被问到。GC发生时,不是所有线程都能随便停下来,必须运行到安全点(SafePoint)才可以挂起。安全点通常选在方法调用、循环跳转、异常跳转等位置。如果程序长时间执行不进入安全点,比如一个死循环,就会拖住整个GC。这类和“Stop The World”相关的知识点,建议和GC日志、线上停顿一起看,效果更好。
4. 收集器卷(卷十一至卷十四):从Serial到G1再到ZGC,面试官爱问的分水岭
4.1 卷十一:经典收集器不是基础题,是选型题
Serial是单线程收集器,GC时必须暂停所有工作线程,但简单高效,适合客户端模式或单核环境。ParNew是Serial的多线程版本,常用于服务端配合CMS。Parallel Scavenge的目标是达到可控的吞吐量,说直接点就是“在乎跑完一个任务需要多久”。
虽然这道题本身不难,但面试官真正想听的不是你罗列参数,而是根据场景选型的能力。比如你部署一个低延迟交易系统,你会选Parallel还是CMS?一个内部离线计算任务,吞吐量优先,你又怎么选?面试前如果想不出来,可以记一条经验策略:交互型应用优先低延迟,选停顿时间短的;计算型应用优先高吞吐,选总运行时间短的。
4.2 卷十二:CMS的六个阶段和“并发模式失败”
CMS全称Concurrent Mark Sweep,目标是“低停顿”。它一共六个阶段:初始标记(STW,很快)、并发标记、并发预清理、重新标记(STW,较短)、并发清理、并发重置。初始标记只找GC Roots直接关联的对象,并发标记和用户线程一起跑,重新标记修正并发期间的变动,并发清理真正开始回收。
CMS的经典缺点是“并发模式失败(Concurrent Mode Failure)”。当并发清理时老年代垃圾产生速度快于清理速度,CMS会退化为Serial Old进行Full GC,导致很长的停顿。这也是为什么设置CMS时,通常会让老年代在阈值70%左右就开始回收,留出缓冲。CMS的另一个问题是它基于标记-清除,会产生碎片,长时间运行下来碎片化严重,最终不得不Full GC。
面试官问“为什么CMS被G1取代”,答三点就够了:长时间运行碎片严重、停顿时间不可预测、在Java 9以后被标记为废弃。不过CMS的并发收集思想仍然值得学,因为G1和ZGC继承了这种思路。
4.3 卷十三:G1是面试分水岭,这几个机制必须吃透
G1是当前面试出现频率最高的收集器,因为它把“堆不再分物理连续代”变成“堆被划分为很多个Region”。每个Region既可能是Eden、Survivor、Old,也可能被回收后重新分配。这种逻辑上的分代让G1能从整体上维护优先回收列表——优先回收那些“垃圾最多”的Region,这就是G1的Garbage First名字来源。
G1的另一大创新是记忆集(Remembered Set)。因为Region之间会互相引用,GC扫描某个Region时不能只扫它自己,还要知道别的Region中有谁引用了它。RSet就是为了解决跨Region引用问题,代价是占用额外内存,所以G1的RSet大小需要结合应用的内存容量来评估。
新生代回收用STAB(Snapshot-At-The-Beginning)算法,它通过写屏障在并发标记开始时记录对象图的快照,保证并发标记过程里由于用户线程修改引用而漏标的对象也能被捕获。这个细节面试经常问到,你只要记着“它牺牲了一点精确度来换取并发标记的稳定性和吞吐”就不会答错方向。
G1的Mixed GC会分阶段回收老年代Region,真正找到“停顿时间与吞吐的平衡点”。面试时我建议你这样串讲:G1先认识Region布局,再用RSet解决跨Region引用,用SATB捕捉并发标记期间的引用变化,最后通过可预测的停顿模型选择哪些Region参与回收。
4.4 卷十四:ZGC为什么敢说“几乎不卡顿”
ZGC的目标是把暂停时间控制在10毫秒以内,核心手段是染色指针和读屏障。染色指针把少量信息,比如Mark、N44、MMAP等,直接编码在指针里,而不需要访问GC元数据。这样GC在移动对象时,可以通过读屏障感知并修正指针。
通俗解释一下:ZGC的回收过程跟用户线程并发跑,用户线程读对象时通过读屏障检查指针里的状态,如果对象被移动了,读屏障会重定向到新地址。整个过程用户线程几乎不用停。ZGC支持TB级别堆,在超大堆场景下比G1更稳。面试问到“ZGC有什么缺点”,可以答:CPU开销更高,因为读屏障在每次读引用时都有额外成本;在JDK15之前不支持分代回收,所以对于对象生命周期很短的应用效率不如G1;JDK21后ZGC才加入分代支持。
5. 调优与实战卷(卷十五至卷十八):参数、日志、工具、线上事故排查
5.1 卷十五:核心参数分类,别再背什么“8G内存配多大堆”的现成答案
调优参数题最怕的是“背答案”。面试官问你“堆内存怎么设置”,你说“-Xmx8g -Xms8g”只能拿一半分,你要说清楚为什么这么配。
堆参数分几类:初始堆大小-Xms、最大堆大小-Xmx,通常建议生产环境两者设置相等,避免堆动态伸缩带来的性能波动;新生代大小-Xmn或者通过-XX:NewRatio设置老年代和新生代比例;-XX:SurvivorRatio设置Eden和Survivor的比例;元空间-XX:MaxMetaspaceSize限制大小;线程栈-Xss设置单线程栈大小,默认在Linux x64下通常是1MB,如果线程数很多,调小栈容量可以降低内存占用,比如256KB。
理解这些参数的关键是“算一笔账”。比如你有一个4核8G的容器,跑一个Web应用,通常建议堆内存给到系统内存的50%左右,预留一些给元空间、线程栈、JIT编译器、GC相关结构和操作系统。不是无脑把-Xmx开满——开满会导致容器OOMKilled或触发系统内存交换,性能反而急剧下降。
5.2 卷十六:GC日志怎么看,一张表格说明白
很多候选人看GC日志处于“能看懂Full GC字样但不知道和线上问题怎么关联”的状态。这里给一个观察框架:先看频率,再看单次耗时,再看回收前后内存变化,最后看是Minor GC还是Full GC。
常规启动参数可以加上:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log一段典型的G1日志长这样:
[GC pause (G1 Humongous Allocation) (young) (initial-mark) 2024-01-01T00:00:01.000+0000: 1.000: [GC pause (young) (initial-mark), 0.0057310 secs] [Parallel Time: 1.2 ms, GC Workers: 8] [GC Worker Start (ms): Min: 1.0, Avg: 1.0, Max: 1.0, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.5, Diff: 0.3] [Update RS (ms): Min: 0.1, Avg: 0.2, Max: 0.4, Diff: 0.3] ... [Code Root Fixup: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.6 ms] [Choose CSet: 0.1 ms] [Ref Proc: 0.3 ms] [Redirty Cards: 0.2 ms] [Eden: 512.0M(512.0M)->0.0B(512.0M) Survivors: 64.0M->64.0M Heap: 1.2G(4.0G)->688.0M(4.0G)] [Times: user=0.02 sys=0.00 real=0.01 secs]重点看两个位置:一是Eden和Survivor的变化,说明此次回收清了哪些区域;二是Heap前后的变化,看看回收后堆占用是否真的降下来了。如果每次GC回收完内存还是迅速涨满,说明对象分配速率远高于回收速率,这时候要从代码层面排查是否有大对象泄漏,而不是一味调大堆。
5.3 卷十七:jps、jstat、jmap、jstack的实战用法
面试到工具题,光背命令名字没用,要能说出什么场景用什么命令、怎么看输出、怎么定位问题。我列一下最常用的五个工具和典型场景:
- jps:列举Java进程,类似ps,但是只针对Java进程。比如
jps -l能打印完整主类名。 - jstat:查看JVM状态,重点是
jstat -gcutil <pid> 1000每隔一秒输出一次GC统计,观察Eden、Old、Metaspace的使用百分比,以及YGC、FGC的次数和耗时。 - jmap:导出堆快照,或者查看堆信息。
jmap -dump:format=b,file=heap.hprof <pid>导出dump文件,再用MAT或VisualVM分析。 - jstack:打印线程快照,通常用于排查CPU飙高、线程死锁、长时间卡顿的调用栈。
jstack <pid>后配合top -H -p <pid>找到占用CPU高的线程ID(注意转成十六进制),再到jstack输出里查对应的栈帧。 - jcmd:一个综合工具,可以替代很多旧命令。比如
jcmd <pid> GC.heap_info。
建议你自己在测试环境压一个接口,然后用jstack抓一下线程状态,亲眼看WAITING、RUNNABLE、BLOCKED这些状态长什么样,面试聊起来会自然得多。
5.4 卷十八:OOM类型和一次完整的事故排查链路
OOM可以分为几类:java.lang.OutOfMemoryError: Java heap space(堆溢出)、GC overhead limit exceeded(GC时间过长但回收效果差)、Metaspace(元空间溢出)、unable to create new native thread(无法创建线程)。
我在生产环境遇到过最典型的一种:服务每天固定时段报“Java heap space”。排查链路建议如下:
- 先确认是不是真的堆不足。用jstat观察FGC次数和GC后占用,如果GC后占用没降下来,多半是存在对象泄漏。
- 用jmap导出堆快照,然后用MAT(Memory Analyzer Tool)或Eclipse MAT打开,看Dominator Tree,找占用最大对象。
- 找到大对象后,分析引用链,定位到具体业务代码。通常会发现是一个全局缓存无上限增长,或者一个分批处理任务把大量数据查出来塞进了集合。
- 修复后,再通过压测观察GC趋势判断是否真的解决。
注意一个反直觉的坑:有时候OOM不一定是堆小,而是代码里创建了太多大数组、大缓存、或者线程数失控。所以第一步永远是“看现场”,而不是一上来就调大-Xmx。
6. 压轴卷(卷十九、卷二十):JMM并不属于JVM内存模型,是两个体系
6.1 卷十九:JMM与并发三特性
卷十九的核心是区分两个概念:JVM运行时数据区和Java内存模型(JMM)。很多人在面试时混着讲,导致后面一聊并发就说不清。
JMM是Java规范里定义的一套抽象模型,用来解决多线程共享变量的可见性问题、有序性问题和原子性问题。它规定所有共享变量存在主内存,每个线程有自己的工作内存,线程对变量的操作必须“先拷贝到工作内存、操作完再写回主内存”。这跟CPU缓存和主内存的关系非常像——这也是为什么讲并发都会拿CPU缓存结构来类比。
面试的三连问通常是:
- 原子性:i++为什么不是原子操作?因为读取、计算、写回三个步骤可能被打断。
- 可见性:为什么一个线程修改了变量,另一个线程不一定马上看得到?因为工作内存和主内存之间存在同步时差。
- 有序性:为什么代码重排会导致诡异结果?因为编译器和CPU为了优化会调整指令顺序,而单线程内没问题、多线程下就可能出问题。
happens-before规则是JMM中比较重要的一个概念。它的核心含义是:如果操作A happened-before 操作B,那么A的结果对B可见。八大规则里重点记:程序次序规则、锁规则、volatile变量规则、线程启动/中断/终止规则、传递性。其中锁规则和volatile变量规则是面试中出现频率最高的。
6.2 卷二十:综合场景题的答题思路
到了卷二十,面试官会把前面所有卷的知识放进一个真实场景里考你。比如:
题目一:线上Java服务CPU飙高到100%,如何排查?
第一步,用top找到CPU高的Java进程;第二步,top -H -p <pid>找到具体线程;第三步,把线程ID转成十六进制(printf '%x\n' tid),用jstack输出找到对应线程栈;第四步,定位到具体业务代码,分析为什么占用CPU,常见的可能包括:死循环、频繁GC、正则回溯、锁竞争。注意,CPU飙高不一定是业务方法本身慢,也可能是GC线程疯狂工作,所以jstat也要看一眼GC状态。
题目二:老年代一直增长,FGC频繁,怎么办?
先判断是内存泄漏还是内存分配速率过高。最有效的方案是导出堆快照,查看对象直方图和Dominator Tree。如果是大量相同类型对象,基本可以定位到那个业务方法。同时配合GC日志看每次Full GC前老年代的占用趋势,是逐步爬升还是突然暴涨,两种形态对应的原因完全不同。
题目三:为什么我设置-Xmx4g,实际进程占的内存远大于4g?
这是压轴题里最让人舒服的一道,因为它考察的是“堆外内存”的理解。JVM进程占用的RSS内存不仅包含堆,还包含Metaspace、线程栈、JIT编译器代码缓存、GC结构(RSet、Card Table)、DirectByteBuffer的堆外内存、以及JNI分配的内存。所以容器里部署Java服务时,千万不要只给-Xmx加一点点余量。建议至少在-Xmx之外预留20%到30%的系统内存,否则很容易被底层监控判定为内存紧张甚至OOMKilled。
7. 我的学习路线和一点实战经验
最后分享一套我个人验证过很有效的备考方式。不要一上来就追求把所有细节背熟,而是按“三轮复习法”来:
第一轮(3-5天):搭框架。把上面20卷的所有主题浏览一遍,确保每个卷都能用两到三句话讲清楚“是什么、为什么、怎么用”。这轮不求记住参数,只求脑子里有张地图。
第二轮(3-5天):深挖高频内容。围绕双亲委派、内存模型、G1、GC日志、OOM排查这五个方向做细致复习,每个方向各准备一个“3分钟讲解”,并且用实际工具操作一遍。比如自己写个死循环的demo,然后jstack去看线程栈。这一步不是为了学会工具,而是为了让你对“命令输出的样子”有真实记忆。
第三轮(2-3天):模拟提问。找一份JVM面试题清单,对着镜子或者开着录音答一遍。重点不是“背对答案”,而是练习“从题目跳到知识地图的哪个卷”——当你能在十秒钟内定位到考点是卷四还是卷十三,你就算真正掌握了。
说句大实话,JVM面试题看着多,但绝大多数公司不会问你“哪个参数控制对象年龄阈值”这种极端冷门的问题。一般围绕五个核心问:类加载机制、内存分区、GC原理和收集器、线上调优排查、并发内存模型。把这五个核心吃透,再配合两三个真实的故障案例细节,效果远胜于背三十个问答题。你在复习过程中如果遇到“讲着讲着发现前后矛盾”,那是好事——说明你真的在理解它,而不是在假装理解它。