面试问到JVM,最怕的不是你不会,而是你明明背了一堆八股文,却答不到面试官的心趴上。这些年我面过不少候选人,也带过不少新人,发现一个规律:凡是能把JVM讲出“为什么”的人,基本都能扛住后面的压力面;凡是只背定义、说不清底层逻辑的,几个追问就露馅了。
这次的《面试八股文》系列第20期,我直接把JVM这条线给你捋干净。不堆概念,不灌水,我会把每个高频考点背后的原理、面试官真正想看的东西,以及我自己在线上实战里踩过的坑一起讲清楚。内容比较多,建议你收藏起来,面试前一两天照着过一遍,比盲目刷题有效得多。
1. JVM基础三件套:JDK、JRE、JVM到底是什么关系
1.1 一个类比讲清三者关系
很多人一上来就被这个基础问题问懵了。其实这三者的关系特别像“厨房、餐厅、灶台”:
JDK(Java Development Kit)是整套厨房,厨具、食材、菜谱全在里面,它是给开发者用的,包含了编译器、调试工具、类库等一整套开发环境。JRE(Java Runtime Environment)是餐厅,消费者只需要进来吃饭,不需要自己做饭,它给运行Java程序提供了标准环境。JVM(Java Virtual Machine)是灶台,真正把菜炒熟的地方,它是Java程序运行的核心引擎。
从包含关系上看,JDK包含JRE,JRE包含JVM。更准确地说,JDK = JRE + 开发工具(javac、jdb、javap等),JRE = JVM + 核心类库(rt.jar等)+ Java运行所需的基础文件。
1.2 面试官为什么喜欢问这个问题
这道题表面是在考你对Java生态的基本认知,实际上是在判断你有没有真正理解“跨平台”这几个字。
Java号称“一次编写,到处运行”,靠的是什么?靠的就是JVM这层虚拟化抽象。不同的操作系统上有不同的JVM实现,比如Windows版、Linux版、macOS版,但你的源码只要编译成字节码(.class文件),在任何平台的JVM上都能跑。也就是说,真正和操作系统打交道的是JVM,而不是你的代码。
如果你在面试时能补充一句“Java的跨平台是JVM层面的跨平台,不是字节码层面的跨平台”,面试官会对你高看一眼。因为这说明你能区分“编译产物”和“运行环境”两个完全不同的概念。
1.3 常见追问:JRE和JVM的运行差异
面试官还可能追问:既然JRE已经包含了JVM,为什么还要单独提JVM?这个问题要这样答:JRE是Java程序的运行环境,它提供了类库和启动工具;JVM是JRE内部最核心的子系统,负责字节码的解析和执行。JVM不感知它跑在JDK还是JRE里,它只认字节码。
我还遇到过面试官这样考:如果你的生产环境只是跑Java程序,装JRE就够了,但为什么很多公司还是选择装JDK?因为线上排查问题的时候,你需要用到JDK里自带的工具,比如jmap、jstack、jstat,这些工具都在JDK的bin目录下。我自己就吃过亏,之前在一台只装了JRE的机器上部署服务,出了问题想用jstack看线程状态,结果发现命令不存在,只能先装一个完整JDK再排查,白白耽误了半小时。
2. JVM内存模型:这道题答得好不好,决定了你的offer薪资
2.1 运行时数据区到底分几块
JVM内存模型,严格来说应该叫“运行时数据区”,这是JVM面试绝对绕不开的核心考点。面试官一开口问你“JVM内存模型是怎样的”,你不是要背出JMM里的主内存和工作内存那套,他问的其实是运行时数据区的划分。
一共六块:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区、运行时常量池(从方法区里分出来的)。我建议你记这样一张表,按“线程是否私有”和“会抛什么异常”两个维度去记:
| 区域 | 线程私有/共享 | 存储内容 | 异常 |
|---|---|---|---|
| 程序计数器 | 私有 | 当前线程执行的字节码行号 | 无 |
| 虚拟机栈 | 私有 | 栈帧(局部变量表、操作数栈、动态链接、返回地址) | StackOverflowError、OOM |
| 本地方法栈 | 私有 | native方法调用 | StackOverflowError、OOM |
| Java堆 | 共享 | 几乎所有对象实例和数组 | OOM |
| 方法区 | 共享 | 类型信息、常量、静态变量、JIT缓存 | OOM |
| 运行时常量池 | 共享 | 编译期常量与运行期产生的符号引用 | OOM |
这个表背下来不难,难的是理解每个区域背后为什么要这样设计。比如虚拟机栈为什么是线程私有?因为每个线程都有自己的执行流程和局部变量,如果栈是共享的,多线程并发下局部变量的隔离性就彻底没了。
2.2 堆内存为什么是OOM重灾区
堆是所有线程共享的一块内存区域,几乎所有对象和数组都在这里分配。堆也是面试中最高频的考点,因为它直接关联到对象创建、GC回收和线上OOM排查。
从上到下,堆又被划分为新生代(Young Generation)和老年代(Old Generation),其中新生代又细分为Eden区和两个Survivor区(S0、S1),默认比例是8:1:1。这个比例来自一个经典的实验结论:新生代中大约90%的对象生命周期很短,活不过第一轮GC。所以Eden区划大一点,Survivor区划小一点,两个Survivor区轮流存放幸存对象,用复制算法避免内存碎片。
你如果只是背出这个比例,面试官不会满意。你还要能说出为什么两个Survivor区必须是一样大的,因为复制算法要求“从一个Survivor复制到另一个为空Survivor”,如果两个区大小不一样,那么较大的那个区永远不会被完全使用,空间就浪费了。之前我调优过一个服务,线上把SurvivorRatio设成了2,结果Survivor区过小,对象频繁晋升老年代,老年代一下就被塞满了,频频Full GC。调成默认的8之后,GC频率直线下降。
2.3 Direct Memory和元空间为什么单独存在
在JDK 8之后,方法区的实现从永久代(PermGen)换成了元空间(Metaspace),最大的区别是:永久代在堆内,大小受JVM堆限制;元空间在本地内存,默认不设上限,只受操作系统的物理内存限制。这个改变的意义在于:以前PermGen容易被类加载过多撑爆,报OOM: PermGen space,现在用元空间,出现这个报错的概率小了很多。
另一块容易被忽略的是直接内存(Direct Memory),它不属于JVM运行时数据区,但被频繁使用。NIO和Netty底层会通过DirectByteBuffer在堆外分配内存,好处是省去了一次从堆内拷贝到堆外的操作,适合大块数据的IO读写。它的坑在于:堆外内存不受-Xmx限制,如果你用了Netty但没有合理设置MaxDirectMemorySize,很容易出现“明明堆内存很充足,机器却频繁Full GC或直接崩溃”的诡异情况。
我之前处理过一个服务,堆内存才用了40%,GC也很正常,但容器时不时被OOM Killer杀掉。排查到最后发现是Netty在堆外分配了大量内存,加上容器cgroup限制没对上,直接内存把整台机器的物理内存打满了。这种问题在容器环境里特别常见,后面我在第5节会专门展开讲。
3. 类加载机制与双亲委派:面试里最容易翻车的一站
3.1 类的生命周期不仅仅是“加载-初始化”
面试总考“类加载过程”,很多人能背出七个阶段:加载、验证、准备、解析、初始化、使用、卸载,但具体每步要背的更细:
加载阶段,通过类的全限定名获取二进制字节流,并在堆中生成一个Class对象作为方法区该类的访问入口。注意,这里的“获取”不限于从文件系统读.class文件,还可以从JAR包、网络、甚至是运行时动态生成的代理类里获取。
验证阶段,做的是字节码级别的安全检查,包括文件格式验证、元数据验证、字节码验证、符号引用验证,目的是防止恶意或错误的字节码破坏JVM。
准备阶段,为静态变量分配内存并设置初始零值。很多人在这里犯错:public static int count = 100,准备阶段count是多少?答案是0,而不是100。赋值为100的动作要到初始化阶段才执行,因为准备阶段只是“分配内存+给默认值”,真正的赋值指令要等编译器生成的构造器方法执行。
解析阶段,把常量池里的符号引用替换为直接引用。这里要理解一个概念:常量池里的引用只是文字描述,比如“java/lang/String”,而直接引用就是真实内存地址或句柄。
初始化阶段,执行类构造器方法,这里才真正执行静态变量的赋值和静态代码块。面试官会接着问你:父类和子类的初始化顺序是什么?答案永远是父类先初始化,子类后初始化,如果类有接口,接口的初始化不触发父接口的初始化,除非接口里定义了默认方法。
3.2 双亲委派模型:为什么这么设计,什么时候会被破坏
双亲委派模型的含义是:当一个类加载器收到类加载请求时,它不会自己先去加载,而是先把请求委托给父类加载器,每一层都往上抛,直到最顶层的启动类加载器(Bootstrap ClassLoader)。只有父加载器反馈自己无法完成加载时,子加载器才尝试自己加载。
为什么要这么设计?两个核心原因:第一,避免同一个类被重复加载,同一个Class对象在不同加载器下会被视为不同的类,这个容易导致类型转换异常;第二,保证核心类库的安全性,比如java.lang.String永远由启动类加载器加载,你就算自己写一个java.lang.String也加载不进去,这就防止了恶意代码覆盖JDK核心类。
但双亲委派模型不是永远正确的答案。面试官经常会追问:什么情况下需要打破它?你至少要知道以下几个场景:
SPI(Service Provider Interface)机制的JDBC驱动加载,比如DriverManager是启动类加载器加载的,但JDBC的具体驱动实现(如MySQL的Driver)在classpath下,按双亲委派模型,启动类加载器根本加载不到第三方jar里的类。所以就用了线程上下文类加载器从下往上加载,这打破了双亲委派。Tomcat的WebAppClassLoader也打破了双亲委派,每个Web应用有独立的类加载器,优先加载自己WEB-INF/lib下的类,是为了实现不同应用之间的类隔离。你如果能把这两个场景举出来,这道题基本满分。
3.3 一个小实验验证双亲委派
我自己面试候选人时,喜欢让他手写一个类加载器或者问一个问题:下面的代码会不会加载成功?
我故意写了一个全限定名为java.lang.MyObject的类,然后放到classpath里。结果是不会加载成功,因为JVM对java.*开头的包有保护机制,这类类会直接交给启动类加载器处理。如果在自定义类加载器里强行加载java.lang开头的类,会抛SecurityException。
你可以自己试一下,这样印象会很深刻。另外,自定义类加载器的核心就是继承ClassLoader并重写findClass方法,在里面调用defineClass来生成Class对象,这个代码很简单,网上很多,但想真正理解还得动手跑一跑。
4. 垃圾回收与G1收集器:从原理到参数,一条龙讲透
4.1 判断对象是否可回收:引用计数法的致命缺陷
GC要回收垃圾,第一步是找到垃圾。怎么找?两种主流算法:引用计数法和可达性分析算法。
引用计数法听起来很简单:每个对象维护一个引用计数器,被引用一次就加一,引用失效就减一,计数器归零就代表可回收。但有个致命缺陷,就是循环引用。A引用B、B引用A,哪怕这两个对象已经没有任何外部引用了,计数器依然不为零,永远无法回收。
所以JVM主流的商用虚拟机用的都是可达性分析算法。它的思想是从一组叫做“GC Roots”的根对象出发,沿着引用链向下遍历,能被遍历到的对象标记为存活,遍历不到的对象就是可回收的。
面试高频追问就会出现在这里:哪些对象可以被当作GC Roots?至少记住四类:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI引用的对象。你可以把GC Roots理解成“不会回收的初始对象”,所有存活对象都是从这里能走到的。
我之前遇到过一道很有意思的面试题:一个对象被局部变量引用着,此时发生GC,这个对象会被回收吗?答案是不会,因为局部变量在栈帧的局部变量表里,局部变量表里的引用就是GC Roots,只要这条引用链还存在,对象就活着。但如果你在这个局部变量作用域之外再去强制GC,这个对象就失去了可达性,会被回收。
4.2 四种引用类型,别只背名字
强引用、软引用、弱引用、虚引用,这四种引用在面试里属于必考。关键是你要清楚它们的GC回收时机:
强引用就是普通的Object obj = new Object(),只要强引用还在,JVM宁可抛OOM也不会回收它。软引用用来描述“还有用但非必需”的对象,在内存不足时会被回收,适合做缓存,比如图片缓存。弱引用比软引用更脆弱,只要发生GC就会被回收,典型应用是ThreadLocal的ThreadLocalMap里的Entry,它的key就是弱引用。虚引用最特殊,它不会影响对象的生命周期,也无法通过它取得对象实例,唯一作用是在对象被回收时收到一个系统通知,用于对象回收跟踪。
我建议你顺带把ThreadLocal的内存泄漏场景准备一下:ThreadLocalMap的key是弱引用,value是强引用。如果线程池里的线程长期存活,ThreadLocal的key被回收了但value还指向那个大对象,就造成了value的内存泄漏。解决办法就是每次用完ThreadLocal都调用remove方法。这个点既能体现你看过源码,又能展示你对内存泄漏的理解深度。
4.3 三种基本回收算法与分代理论
标记-清除、标记-复制、标记-整理,这三种算法的优缺点要能脱口而出:
标记-清除:先标记可回收对象,再统一清除。缺点是会产生大量不连续的内存碎片,后面分配大对象可能找不到连续空间而提前触发Full GC。
标记-复制:将内存分成两块,每次只使用一块,GC时把存活对象复制到另一块,然后清空当前块。缺点是空间利用率低(只有一半),优点是实现简单、无碎片。新生代就用了优化的复制算法(Eden+S0+S1),不是一半开一半,而是只浪费一个Survivor空间。
标记-整理:和标记-清除一样先标记,但清除时会把存活对象向一端移动,然后清理掉边界之外的内存。适合老年代,因为老年代垃圾少、存活对象多,大量移动对象的成本可以接受。
分代收集理论简单说就是:新生代对象死亡率高,用复制算法,成本低;老年代对象存活率高,用标记-清除或标记-整理,避免频繁复制。面试时你把“为什么新生代用复制、老年代用整理”讲清楚,面试官就不会再追了。
4.4 G1收集器:region模型和可预测停顿时间
G1(Garbage First)是HotSpot JVM在JDK 9之后默认的垃圾收集器。它和之前的CMS最大的区别是:G1不再严格区分年轻代和老年代,而是把整个堆划分为多个大小相等的Region(默认2048个),每个Region都可以被单独回收。G1还有两个关键概念:
RSet(Remembered Set):每个Region都有一个RSet,记录了哪些Region有引用指向当前Region。回收某个Region时,只需要扫描它的RSet就知道谁引用了它,不用全堆扫描。这就是G1能做到部分回收的理论基础。SATB(Snapshot-At-The-Beginning):G1的并发标记采用TAMS(Top At Mark Start)配合SATB,保证了并发标记期间新分配的对象不会被误标,同时用“原始快照”的方式解决并发标记时漏标的问题。
在面试中你被问到G1时,可以这样组织回答:G1以Region为单位,维护了一个优先级列表,优先回收垃圾最多的Region,这样能保证“有限时间内完成尽可能多的回收”,这也是G1名字里“Garbage First”的由来。它通过 -XX:MaxGCPauseMillis参数来设定软性的停顿目标,比如100ms,G1会根据历史数据自动调整新生代大小来达到这个目标。
但要注意,G1不是万能的。我之前在一个大内存、低延迟的服务上试过把G1的停顿目标设成50ms,结果发现GC频繁触发,吞吐量反而下降了。后来把目标放宽到100ms,情况才好起来。这个经验说明:停顿时间调的越短,GC就越频繁,你要在延迟和吞吐量之间找平衡,不能一味追求低延迟。
4.5 三色标记法:为什么CMS会有漏标问题
如果你想把GC聊得更深,可以主动引出三色标记法。在并发标记阶段,对象被分为三种颜色:黑色表示自身已标记且它的引用都已被处理过,灰色表示自身已标记但引用还没处理完,白色表示还没被扫描到。回收时白色对象就是垃圾。
CMS的并发标记用的是“增量更新”来解决漏标问题:当一个黑色对象新增了一个到白色对象的引用时,会把这个黑色对象记录下来,标记结束后重新扫描它。G1用的是“原始快照(SATB)”:当一个灰色对象删除了指向白色对象的引用时,记录下这个引用,标记结束后重新扫描这个引用,这样即使引用关系变了,也能保证那个白色对象被标记为存活。
你不需要把每个细节都背得死死的,但“增量更新 vs 原始快照”的区别最好能说出来,因为这是区分“背过八股”和“真正理解”的分水岭。
5. JVM调参与线上排查:实战里的硬功夫
5.1 高频JVM参数速查表
JVM参数在面试中虽然不一定单独出题,但线上调优和排查一定会用到。我整理了一些高频参数,建议你直接存下来:
| 参数 | 作用 | 默认值/建议 |
|---|---|---|
| -Xms | 初始堆大小 | 建议和服务器的物理内存匹配,别设1M等不合理值 |
| -Xmx | 最大堆大小 | 生产建议和-Xms设为相同值,避免堆动态伸缩 |
| -Xmn | 新生代大小 | 一般占堆的1/3到1/4 |
| -XX:SurvivorRatio | Eden区和Survivor区的比例 | 默认8,即8:1:1 |
| -XX:MaxTenuringThreshold | 对象晋升老年代的年龄阈值 | 默认15 |
| -XX:MaxMetaspaceSize | 元空间上限 | 根据类数量设定,防止无限占用本地内存 |
| -XX:CompileThreshold | 方法触发JIT编译的调用次数阈值 | C1默认1500,C2默认10000 |
| -XX:MaxGCPauseMillis | G1停顿时间目标 | 默认200ms |
| -XX:+HeapDumpOnOutOfMemoryError | OOM时自动生成堆转储文件 | 生产必开 |
| -XX:HeapDumpPath | 堆转储文件路径 | 建议设到独立磁盘,避免占满系统盘 |
这里单独把-XX:CompileThreshold拎出来说,因为它是热词里出现过的点。这个参数的含义是:一个方法被调用多少次之后,JVM会认为它是“热点方法”,从而触发JIT(Just-In-Time)编译。C1(Client Compiler)的默认阈值是1500次,C2(Server Compiler)的默认阈值是10000次。通过-XX:CompileThreshold可以手动调整这个次数,让热点方法更快被编译成本地机器码,提高执行速度。但要注意,阈值设置太小会让JIT编译启动得太早,容易误判热点,浪费编译资源;设置太大又会让真正的方法在解释执行模式下运行太久,性能上不去。一般我不建议乱调这个参数,除非你很清楚代码里的热路径。
5.2 线上Java进程异常重启或崩溃,去哪找日志
很多人线上遇到问题就慌,尤其是容器部署的Java程序重启了,问“日志在哪儿”,其实是没搞清楚日志类型。JVM相关的日志主要有四类:
应用日志:程序自己打的日志(Logback、Log4j),配置的log路径,通常能看到业务异常和报错堆栈。
GC日志:通过JVM参数打印的回收日志,比如-XX:+PrintGCDetails -Xloggc:/path/to/gc.log,分析GC频率、停顿时间、晋升情况。
JVM Crash日志:JVM进程崩溃(比如JIT编译bug、OS资源耗尽)时,JVM会在工作目录下生成hs_err_pid*.log,记录崩溃时的线程、寄存器、内存映射等信息。这个文件非常重要,但很多人不知道它存在,找了半天才发现就在启动目录下。
系统日志:对于容器环境,需要查看docker logs或者k8s的事件,观察进程是被kill(OOMKilled)还是正常退出。
我举一个真实案例。之前在K8s环境里部署Java服务,容器频繁重启,看应用日志没有任何报错,看docker events发现是OOMKilled。我先用dmesg查了系统日志,确认是容器进程被cgroup OOM Killer杀掉,然后检查了Pod的memory limit,发现只有512Mi,而应用设置的-Xmx是512m,加上元空间、直接内存、线程栈,内存早就超过limit了。这就是典型的“容器内存limit比JVM实际占用还小”的问题。解决办法是把-Xmx调小到384m,并且加上-XX:MaxDirectMemorySize=128m限制堆外内存,重启后再没崩过。
5.3 OOM与频繁Full GC的排查思路
如果线上出现OutOfMemoryError,第一步是确认堆转储文件有没有生成。所以生产环境一定要开-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath。拿到堆文件后用MAT(Memory Analyzer)打开,重点看Dominator Tree和Leak Suspect,找出占内存最大的对象以及它的引用链。
如果只是频繁Full GC但没报OOM,排查流程是这样:先用jstat -gcutil pid 1000连续观察GC情况,确认Full GC的频率和耗时长不长。如果Full GC很频繁,基本可以考虑两种情况:堆设置太小,或者存在内存泄漏。接着用jmap -histo:live pid看存活对象的分布,找出数量异常的对象。如果想进一步看引用链,可以用jmap -dump导出堆转储,再用MAT分析。
我举一个之前处理过的案例:一个订单处理服务,运行几天后TP99从50ms涨到3秒,jstat一看Full GC每分钟一次。追查堆转储发现,有一个自定义的缓存Map一直在增长,key是订单号,value是订单详情对象。业务代码把订单号放进了map,但从来没有移除逻辑,导致订单越来越多,最终把老年代撑爆。定位到问题后,把缓存改成Caffeine并设置了过期时间,Full GC降到了几小时一次。
5.4 CPU飙高与线程问题定位
CPU飙高的第一反应是找线程,而不是重启服务。我常用的命令是这样的:
先jps找到Java进程PID,再用top -Hp pid查看该进程内CPU占用最高的线程ID。拿到线程ID后转成十六进制(printf '%x\n'),然后执行jstack pid | grep -A 20 'nid=0x十六进制',就能看到这个线程在干什么。大多数情况下,要么是业务死循环,要么是GC线程疯狂回收,要么是锁竞争导致自旋等待。
这里说一个容易踩的坑:很多人会用jstack -F pid,但-F是强制模式,会暂停JVM,生产环境慎用。正常情况下直接jstack pid就行,如果输出的是“Unable to open socket file”,大概率是进程不是Java启动的,或者权限不够,用和进程相同用户执行即可。
6. 面试答题技巧与避坑经验:八股文的正确打开方式
6.1 不要把内存模型和运行时数据区搞混
这是我在面试中最常遇到的误区。很多候选人开口就说“JVM内存模型分主内存和工作内存,基于缓存一致性协议”,这明显是被JMM(Java Memory Model)的并发语义带跑了。
面试官如果问“JVM内存模型”,你要先分辨他想问什么。如果聊的是并发,比如volatile、synchronized,那就答JMM的主内存和工作内存模型;如果聊的是运行时区域,那就答运行时数据区的六个分区。最好是主动问一句:“您说的是运行时数据区的划分,还是并发相关的JMM?”这样既显得你专业,也给自己的回答选对了方向。
6.2 答八股文的核心技巧:定义+原理+场景
背八股文的通病是只背“是什么”,不准备“为什么”和“怎么用”。我教你的方法是按“定义-原理-场景”三层来组织每个知识点。
比如问到“双亲委派模型”,定义讲清楚“请求逐层上抛,父加载器加载不了才自己加载”;原理讲清楚“避免重复加载和核心类被篡改”;场景举出“JDBC的SPI打破双亲委派”、“Tomcat的类隔离”。这样回答下来,时长至少能撑两分钟,而且信息密度高,面试官很难打断你。
再问“G1收集器”,定义是“一种以Region为单位的并发垃圾收集器,目标是可预测的停顿时间”;原理是“维护优先列表,回收垃圾最多的Region,配合RSet和SATB”;场景讲清楚“适合大堆、多核、需要控制停顿时间的服务,不适合小堆应用”。这套回答结构,除了能应对记忆题,还能让你在面对“你项目里怎么优化GC”这种开放性问题时也有话可说。
6.3 面试时尽量避开这些坑点
有几个坑是我面过很多人后总结出来的,提醒大家注意:
第一,不要死记硬背参数,要理解参数的含义和影响。比如你背了-XX:SurvivorRatio=8,但面试官问“为什么是这个比例”,如果你说不出来,印象分会大打折扣。
第二,别把“并发”和“并行”混谈。G1的并发标记是“GC线程和应用线程同时执行”,并行是“多条GC线程同时回收”,这两个概念在JVM面试里是严格区分的。
第三,别拿Spring Boot的超时设置往JVM参数上靠。有人问我“jvm或者spring boot会设置一个sql执行10秒自动关闭吗”,这个问题本身和JVM关系不大,SQL超时是数据库连接池或驱动层控制的,比如Druid的maxWait啊、HikariCP的connectionTimeout啊,不是JVM参数能管的。面试的时候不要把这个和JVM混在一起讲,一旦混了,面试官会认为你概念不清晰。
第四,如果你主动提了“调优”,一定要准备好被追问“你怎么发现需要调优的”。这个问题最好的回答路径是:先观察GC日志(jstat、gcviewer)发现Full GC频率高,再定位大对象或内存泄漏,最后调参验证并对比指标。千万别一上来就说“我调了-Xmx”,没有前后对比的调优等于没调。
6.4 后续怎么继续深挖
JVM这个主题其实很深,一次面试卷肯定覆盖不完。我建议你照着这条学习路径往后延伸:先从JMM(Java内存模型)和并发编程的关系入手,搞懂volatile和synchronized的底层语义;再去看JIT编译相关的逃逸分析和锁消除,理解JVM到底做了哪些优化;最后有条件的话,拿一个压测工具跑几轮GC日志分析,自己动手配一下不同的垃圾收集器,观察它们在不同堆大小下的表现。
我自己带人的时候,经常要求新人做一件事:用JDK自带工具打包解决一个模拟的OOM问题,从jmap导出堆到MAT找出泄漏点,写一份排查报告。能把这件事独立做完,JVM这块的实战能力基本就够了。面试的时候能讲出一个这样的实际排查案例,比背一百道八股文都管用。
我在实际面试和带人过程中最大的体会是:JVM的知识点并不难,难的是把零散的概念串成体系。你不需要记住每一处细节,但你要能从一个点延伸到另一个点,比如从“垃圾回收”自然聊到“收集器”,从“双亲委派”自然聊到“SPI”,从“参数配置”自然聊到“线上排查”。这种串联能力,才是面试官真正在意的。这些内容你花几天、甚至几周去啃都是值得的,毕竟JVM这块在Java面试里的权重,一直都没有降过。