
准备JVM面试时最大的问题往往不是单个知识点记不住而是资料太碎。类加载、运行时数据区、GC原理、垃圾回收器、性能调优和MySQL延展题看起来是六个独立板块面试官却经常把它们串成一条完整的链路来问。比如“一个对象从 new 出来到被回收JVM 到底做了哪些事”这个问题就会同时涉及类加载、对象分配、GC Roots、三色标记和收集器选型。与其零散背题不如先建立一条主线类加载机制 - 运行时数据区 - 对象分配 - GC算法与三色标记 - 调优参数与工具 - 高频MySQL面试题。这篇文章按这条线整理每个部分都尽量给出面试答题要点、验证命令和实际项目中的排查思路适合作为面试复盘清单也适合在本地JDK环境里对照练习。1. 先建立JVM面试知识地图避免死记硬背1.1 JVM面试真正在考察什么很多JVM题目表面问的是理论实际考察的是候选人能不能从“一段Java代码”推导出“JVM内部行为”。比如问“类加载过程有哪些阶段”不是要你背出五个阶段名称而是想确认你理解验证、准备、解析、初始化之间的先后关系以及静态变量在准备阶段和初始化阶段分别是什么值。再比如问“CMS和G1的区别”背后考察的是你知道不知道并发标记阶段存在漏标问题以及不同收集器用什么手段解决漏标。所以JVM面试复习不应该以“背下45问”为目标而应该以“能回答一条完整的问题链”为目标。这条链通常长这样JVM和JDK、JRE的关系是什么。一个 class 文件是怎样被加载到JVM里的。加载后的类放在JVM的哪个区域。运行时数据区怎么划分哪些线程共享哪些线程私有。new 出来的对象放在哪里对象头里有什么。对象什么时候变成垃圾GC怎么判断对象是否存活。并发标记时怎么避免误回收存活对象三色标记有什么用。不同收集器怎么配合分代模型工作。线上GC频繁或OOM时用什么命令排查。如果接口慢了如何在JVM线程栈和MySQL慢查询之间快速定位。顺着这条链去准备会比单独背“双亲委派”“可达性分析”“G1的RSet”更有体系。1.2 一条主线串起所有高频问题推荐用“一次对象生命周期”来串知识点。从下面这张对应关系可以看出JVM知识并不是孤立的生命周期阶段涉及JVM知识点常见面试题编译并生成class文件JDK、JRE、JVM区别javacJRE和JVM之间是什么关系类加载加载、验证、准备、解析、初始化类加载有哪些阶段静态变量何时赋值类加载器启动类加载器、平台/扩展类加载器、应用类加载器双亲委派模型是怎样的对象分配运行时数据区、TLAB、对象头new出来的对象放在哪里对象使用虚拟机栈、栈帧、局部变量表栈溢出和堆溢出分别什么表现对象回收可达性分析、引用类型、分代收集怎么判断对象可以被回收并发标记三色标记、增量更新、SATB什么是三色标记G1怎么解决漏标回收执行Serial、Parallel、CMS、G1、ZGC垃圾收集器怎么选线上排障jps、jstat、jmap、jstack、jcmd线上Full GC频繁怎么排查数据访问MySQL索引、事务、MVCC、锁SQL慢、死锁、索引失效怎么处理面试时如果能把某个问题放回这条链路里回答比直接背结论更容易让面试官认可。1.3 面试准备清单和自测问题开始正式复习前可以先做一次自测。下面这份清单不需要一次写满但每个问题至少要在脑中说出一版答案类加载的五个阶段分别做了什么有没有哪个阶段可以被延迟。双亲委派模型是什么为什么JVM要这样设计。NoClassDefFoundError 和 ClassNotFoundException 有什么区别。运行时数据区哪些区域是线程私有的哪些是线程共享的。一个对象从new出来到进入老年代经历了什么。对象可以被回收的话为什么还要分代。三色标记中的白色、灰色、黑色分别代表什么。CMS和G1分别是如何解决并发标记漏标问题的。线上老年代持续增长应该先看jstat还是先看jmap。MySQL索引为什么用B树什么情况下索引会失效。如果这些问题现在都能答出具体层次后面的内容可以快速浏览如果答不出来建议按下面章节继续往下看。2. 类加载机制从字节码到Class对象2.1 类加载的五个阶段各阶段做了什么类从字节码变成可以被JVM使用的Class对象要经历加载、验证、准备、解析、初始化五个阶段。这里要先记住一个原则初始化阶段才是真正执行类中Java代码的阶段前面的验证和准备阶段主要是校验和分配内存。加载阶段要做三件事通过类的全限定名获取定义此类的二进制字节流把字节流中的静态存储结构转化为方法区的运行时数据结构在堆中生成一个代表该类的Class对象作为访问方法区入口。加载阶段不一定从本地class文件读取也可以从JAR包、网络、动态代理生成、JSP编译结果中读取。验证阶段主要保证class文件的字节流符合JVM规范并且不会危害JVM自身安全。包括文件格式验证、元数据验证、字节码验证、符号引用验证。这个阶段在面试中不需要展开太深但要能说出“验证是安全的第一道防线”。准备阶段是很多面试题的隐蔽考点。这个阶段会为类变量分配内存并设置零值。例如private static int count 10;在这步count的值是0而不是10真正的10需要在初始化阶段执行clinit方法后才会赋值。如果是private static final int COUNT 10;在准备阶段就可能直接赋值为10因为编译期已经确定了常量值。解析阶段是把常量池内的符号引用替换为直接引用。符号引用就是“org.example.User.sayHello”这种字面描述直接引用则是指向目标对象的指针、偏移量或句柄。解析可能在初始化之后才开始因为JVM规范允许在第一次使用某个符号引用时才触发解析。初始化阶段是执行类构造器clinit方法的过程。JVM会保证一个类的clinit在多线程环境下被正确加锁同步。类初始化的触发条件包括遇到new、getstatic、putstatic、invokestatic字节码指令或反射调用类时如果类没有初始化就先初始化初始化子类时如果父类还没初始化就先初始化父类。用一张表可以快速记忆阶段核心工作最容易考的点加载读取字节流生成Class对象可以从哪些来源加载类验证校验字节码是否符合规范防止恶意字节码准备为静态变量分配内存并设零值count此时是0不是10解析符号引用换成直接引用可延迟到使用阶段初始化执行clinit父类先初始化线程安全2.2 类加载器与双亲委派模型类加载阶段的工作是由类加载器完成的。先厘清三者的关系JVM是执行Java字节码的虚拟机JRE是JVM加上Java标准类库JDK是JRE加上编译器和其他开发工具。面试中问“JRE和JVM之间是什么关系”答案就是JVM只是JRE的一部分JRE还包含运行Java程序所需的类库和其他资源文件。JVM内置了三个重要的类加载器。启动类加载器负责加载JAVA_HOME/lib下的核心类库例如rt.jar或JDK9模块化后的java.base模块它不是一个普通的Java类在Java代码中通常表现为null。平台类加载器在JDK9之前叫扩展类加载器负责加载扩展目录下的类JDK模块化后叫平台类加载器。应用类加载器负责加载classpath或模块路径上的类也是默认的应用程序类加载器。双亲委派模型的流程是当一个类加载器收到加载请求时先不自己加载而是把请求委派给父加载器每一层都往上抛直到启动类加载器。只有父加载器加载不到时子加载器才尝试自己加载。这样做最直接的好处是避免核心类被重复加载和篡改。比如我们也可以写一个java.lang.String类放到classpath里但由于双亲委派机制加载String的工作最终由启动类加载器完成JVM运行时使用的是核心类库里的String而不是自定义的那个。下面这段代码可以打印出各类加载器之间的关系public class ClassLoaderDemo { public static void main(String[] args) { ClassLoader cl ClassLoaderDemo.class.getClassLoader(); System.out.println(应用类加载器: cl); System.out.println(平台/扩展类加载器: cl.getParent()); System.out.println(启动类加载器: cl.getParent().getParent()); ClassLoader strCl String.class.getClassLoader(); System.out.println(String的类加载器: strCl); } }运行后会看到String的类加载器输出为null说明它由启动类加载器加载。应用类加载器的父是平台类加载器再往上的parent为null。这个输出能直接用于面试现场演示。2.3 双亲委派不是唯一答案什么时候要打破常见的打破双亲委派场景有三个SPI机制、Tomcat等Web容器、OSGi或模块化系统。SPI是典型的例子。JDBC驱动接口定义在java.sql中由启动类加载器加载但具体驱动实现比如MySQL驱动却在classpath中启动类加载器加载不到。为了解决这个问题JDK引入了线程上下文类加载器让启动类加载器“反向”使用应用类加载器来加载实际实现类。这就是所谓“父加载器请求子加载器加载”的场景。Tomcat打破双亲委派的原因是不同的Web应用可能包含同名但不同版本的类库如果统一由应用类加载器加载会互相冲突。所以Tomcat为每个Web应用提供独立的WebAppClassLoader优先加载当前Web应用下的类加载不到再交给父加载器。如果需要自己写一个类加载器通常只需要继承ClassLoader并重写findClass方法。下面是简化版实现public class SimpleClassLoader extends ClassLoader { Override protected Class? findClass(String name) throws ClassNotFoundException { String path name.replace(., /) .class; try (InputStream in getResourceAsStream(path)) { if (in null) { throw new ClassNotFoundException(name); } byte[] bytes in.readAllBytes(); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } }这个示例说明自定义类加载器的核心是把字节数组交给defineClass由JVM完成后续验证和Class对象生成。实际项目中还需要考虑缓存已加载类、关闭资源、处理父子加载器可见性等问题。2.4 类加载相关的高频报错与常见误区面试和实战中都容易遇到类加载相关异常。两个高频异常经常被混淆异常含义典型场景ClassNotFoundException运行时找不到指定类抛出在classpath加载阶段依赖缺失、JAR未引入、全限定名写错NoClassDefFoundError类在编译期存在运行时初始化失败或链接失败静态初始化块抛异常、依赖版本冲突、类加载器不一致例如Eclipse中启动Tomcat报找不到或无法加载主类 org.apache.catalina.startup.Bootstrap多数情况下不是Tomcat本身坏了而是classpath没有包含Tomcat的bootstrap.jar和tomcat-juli.jar或者启动配置里的MainClass被改错。排查时先检查项目的Run Configuration再看依赖JAR是否完整最后看是否多个Tomcat版本同时存在。另一个常见误区是认为“父加载器加载过的类子加载器一定可见”。实际上可见性方向是单向的子加载器加载的类对父加载器不可见。双亲委派保证的是“向上委派”不是“向下可见”。3. JVM运行时数据区与对象创建过程3.1 运行时数据区怎么划分面试题里经常出现“JVM的三大区域”这个说法并不统一。更规范的回答是运行时数据区可以分成线程共享区域和线程私有区域。线程共享的是堆和方法区线程私有的是虚拟机栈、本地方法栈、程序计数器。程序计数器是当前线程所执行字节码的行号指示器是唯一不会出现OutOfMemoryError的区域。虚拟机栈保存栈帧每个方法从调用到结束对应一次入栈出栈。栈帧里包含局部变量表、操作数栈、动态连接、方法返回地址等。本地方法栈为JVM使用到的Native方法服务HotSpot直接把本地方法栈和虚拟机栈合并实现。堆是JVM管理最大的一块内存区域也是垃圾回收的重点区域。几乎所有对象实例都在这里分配。JDK8开始方法区由元空间实现直接使用本地内存不再是JVM堆的一部分。运行时常量池是方法区的一部分用于保存class文件中的常量池数据。字符串常量池在JDK7以后移到了堆中这导致字符串相关的OOM表现可能出现在堆里而不是永久代。可以用表格快速整理区域线程共享是否可能OOM典型参数程序计数器否不会无虚拟机栈否StackOverflowError / OOM-Xss本地方法栈否StackOverflowError / OOM-Xss堆是Java heap space-Xms、-Xmx、-Xmn方法区/元空间是Metaspace-XX:MetaspaceSize、-XX:MaxMetaspaceSize堆可以再细分为新生代和老年代。新生代默认包含Eden区和两个Survivor区。在HotSpot里Eden与Survivor的默认比例是8:1可用-XX:SurvivorRatio调整。大多数对象在Eden区创建后很快变成垃圾Minor GC会把这些存活对象复制到Survivor区经过多次回收后仍有存活的才有机会晋升到老年代。3.2 一个对象new出来JVM做了什么很多面试题会问“new一个对象的过程是怎样的”。完整回答要覆盖五步。第一步是类加载检查。虚拟机遇到new指令时先检查常量池能否定位到这个类的符号引用并检查这个类是否已经完成加载、解析和初始化。如果没有先触发类加载流程。第二步是分配内存。对象所需内存大小在类加载完成后就可以确定。如果堆内存是规整的使用指针碰撞分配如果不规整使用空闲列表分配。并发环境下为了保证分配安全在新生代通常会使用TLAB即每个线程在Eden区预先分配一块本地缓冲减少线程竞争。第三步是把分配到的内存空间初始化为零值。这一步保证了实例字段不赋值也能直接读取默认值比如int是0、boolean是false、引用是null。第四步是设置对象头。对象头包含Mark Word、指向类元数据的指针如果数组对象还包括数组长度。Mark Word中存储了对象的哈希码、GC分代年龄、锁状态等信息。第五步是执行init方法。也就是执行类中定义的构造函数按程序员的意图初始化实例字段。到这一步一个可用的对象才算真正创建完成。这个流程回答了“对象还在栈上还是堆上”的问题绝大多数情况下对象在堆中分配栈上分配只是JIT在逃逸分析后做的优化。3.3 从内存异常反推配置问题线上应用最常见的几类内存异常适合用表格对照排查异常信息问题区域排查方向java.lang.StackOverflowError虚拟机栈递归过深检查递归方法、深层次JSON序列化java.lang.OutOfMemoryError: Java heap space堆内存不足检查大对象、对象泄漏、堆参数java.lang.OutOfMemoryError: Metaspace元空间不足检查动态生成类、CGLib/反射场景java.lang.OutOfMemoryError: GC overhead limit exceeded堆回收效率过低说明GC后几乎回收不到空间优先dump堆java.lang.OutOfMemoryError: unable to create new native thread线程或系统资源不足检查线程数、ulimit、操作系统限制如果需要在本地复现堆OOM可以用下面的参数启动一个Java程序并不断向集合中添加对象java -Xms128m -Xmx128m -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/logs/app.hprof -jar oom-demo.jarHeapDumpOnOutOfMemoryError是生产环境强烈建议打开的参数这样OOM发生时能自动留下堆快照。要注意HeapDumpPath要指向持久化目录如果应用运行在Docker容器里必须挂载到宿主机目录否则容器一重建堆快照就丢了。学习环境里可以临时把堆调大生产环境调整堆大小前必须评估机器内存、容器限制和GC停顿。容器场景下还要注意JVM是否能正确识别容器内存限制JDK8u191之后默认支持容器感知老版本可能需要显式配置-XX:MaxRAMPercentage等参数。4. GC原理与三色标记垃圾回收不能只靠背4.1 怎么判断一个对象是否存活引用计数法的思路是给对象维护一个计数器被引用一次加1引用失效减1计数器为0时认为可回收。它实现简单但解决不了循环引用问题。比如A对象持有B对象引用B对象持有A对象引用外部已经没有引用指向它们时两个对象计数器仍然不为0永远不会被回收。HotSpot主流的做法是可达性分析。从一组GC Roots出发沿着引用链向下搜索能到达的对象就是存活对象不能到达的对象会被标记为可回收。GC Roots包括虚拟栈帧中的局部变量表引用的对象、静态属性引用的对象、JNI中引用的对象、活跃线程等。这里要理解“可达”不等于“一定存活”因为对象可能重写finalize方法实现一次自我拯救但在实际项目中不建议依赖finalize它执行时机不确定而且会影响GC效率。面试时能说出“可达性分析 GC Roots”就足够了。4.2 四种引用类型和使用场景Java提供了四种引用类型它们在GC中的处理方式完全不同引用类型回收时机典型场景强引用只要强引用存在永远不会回收普通对象引用软引用内存不足时回收本地缓存、图片缓存弱引用下一次GC时回收ThreadLocalMap中的Entry、缓存框架虚引用随时可能回收必须配合引用队列堆外内存回收通知、对象销毁跟踪软引用适合用来做内存敏感的缓存。比如一个图片加载组件内存充足时保留图片对象内存紧张时可以先回收图片而不是直接OOM。弱引用常见于ThreadLocal里的Entry这也是ThreadLocal内存泄漏问题讨论的起点。一个问题值得单独记忆ThreadLocal中Entry的key是弱引用value是强引用。如果ThreadLocal外部不再持有key但线程仍然存活那么key会被回收而value不会最终导致内存泄漏。规范做法是在使用完ThreadLocal后调用remove清理。4.3 分代收集理论与GC算法分代收集说法是当前JVM的主流设计思路。新生代对象死亡率高适合用标记-复制算法每次GC只保留少量存活对象复制到空闲Survivor区成本低。老年代对象存活率高不适合频繁复制一般用标记-清除或标记-整理算法。标记-清除算法先标记出所有需要回收的对象然后统一回收。优点是实现简单缺点是会产生大量内存碎片后续分配大对象时可能找不到连续空间。标记-整理算法在标记之后让所有存活对象向一端移动解决碎片问题但移动对象需要更新所有引用成本更高。标记-复制算法把可用内存分成两块每次只使用一块GC时把存活对象复制到另外一块然后清空原来那半块实现简单但空间利用率偏低。HotSpot的年轻代采用Eden和两个Survivor的设计本质就是标记-复制算法的优化版本。Survivor区空间比较小所以默认Eden与Survivor比例是8:1也说明复制算法牺牲了一部分空间来换效率。对象晋升到老年代的常见条件包括在Survivor区熬过一定次数的Minor GC默认15次可用-XX:MaxTenuringThreshold调整大对象直接进入老年代可用-XX:PretenureSizeThreshold设置。晋升条件不是单一固定的如果Survivor区中相同年龄对象总大小超过Survivor空间一半则年龄大于等于该年龄的对象也会直接晋升。4.4 三色标记并发标记阶段的底层原理CMS和G1这类收集器在并发标记阶段不会暂停所有业务线程这就带来一个问题一边标记一边业务线程还在修改对象引用怎么保证不把存活对象漏掉。三色标记就是解决这个问题的抽象模型。三色标记把对象分成三种状态。白色对象表示尚未被垃圾回收器访问过的对象在标记结束后仍然为白色的对象会被视为不可达。灰色对象表示当前已被访问但它的引用字段还没有全部扫描完。黑色对象表示自身和它的引用字段都已经被扫描完毕不会再被重复扫描。标记开始前所有对象都是白色。从GC Roots直接可达的对象变为灰色然后逐个扫描灰色对象的引用把引用到的白色对象标成灰色当灰色对象的所有引用都扫描完后这个对象变成黑色。重复这个过程直到没有灰色对象剩下的白色对象就是不可达对象。并发标记最大的隐患是漏标也就是把一个本应存活的白色对象误判成垃圾。漏标必须同时满足两个条件插入了一条或多个黑色对象到白色对象的引用删除了一条或多个灰色对象到该白色对象的引用。CMS使用增量更新解决漏标做法是在写屏障中记录黑色对象新增的引用然后在重新标记阶段再次扫描这些引用把黑色对象变成灰色重新处理。G1使用SATB全称Snapshot At The Beginning它在并发标记开始时记录一个对象快照通过写前屏障记录引用被覆盖前的对象保证在标记开始时存活的旧对象不会被漏标。增量更新侧重“记录新增引用”SATB侧重“保留标记开始时点”。这个知识点是JVM面试中区分度最高的问题之一。回答时不要只说“G1用SATB”要能画逻辑过程白色-灰色-黑色说明漏标两个条件再说明两种收集器的处理差异。4.5 常用垃圾收集器对比与选型不同收集器适合不同场景面试时不要只背名称还要能解释为什么选它。收集器线程模式特点适用场景Serial单线程简单Client模式默认单核小内存、学习环境Parallel多线程吞吐量优先后台计算、批处理任务CMS多线程并发低延迟标记-清除会产生碎片重视响应时间的Web应用G1多线程并发分区堆可预测停顿JDK9后默认多核、大堆、服务端通用ZGC多线程并发停顿极短吞吐可能略低超大堆、要求毫秒级停顿CMS在JDK9开始标记废弃JDK14中移除所以新项目不建议继续选CMS。G1把堆划分成多个Region逻辑上仍然区分年轻代和老年代通过跟踪每个Region的回收价值来优先回收收益最大的区域。因此G1能以-XX:MaxGCPauseMillis为参考目标控制GC停顿。使用G1时可以显式指定java -XX:UseG1GC -XX:MaxGCPauseMillis200 -jar app.jarMaxGCPauseMillis不是硬性指标G1会尝试通过调整Region回收数量、晋升阈值等方式接近目标。设置太小会导致G1频繁调整反而影响吞吐量。ZGC的延迟能力更强但在JDK版本之间的成熟度不同选择前要结合自己使用的JDK版本确认特性支持。5. JVM性能调优从定位问题到生产参数5.1 调优目标到底是什么JVM调优不是把堆调大这么简单。调优目标通常有三个方向低延迟、高吞吐、低内存占用。这三个目标经常互相冲突比如为了降低延迟可能需要更多内存缓存为了提升吞吐量GC停顿时间可能变长。所以调优前必须先定义指标例如“TP99小于200ms”“每小时Full GC不超过1次”“堆使用率峰值不超过70%”。没有指标的调优很容易变成参数赌博。正确顺序是先收集现状数据再定位瓶颈最后用最小参数变更做验证。每次只改一个参数改完观察一段时间不要同时把-Xms、-Xmx、GC日志、收集器全部换一遍否则出问题后无法判断是哪个改动引起的。5.2 调优前置先采集数据再改参数JDK自带了很多命令行工具下面是高频使用的五个命令作用典型用法jps查看Java进程jps -ljstat查看GC和类加载统计jstat -gcutil 1000 10jmap查看堆信息和导出堆快照jmap -histojstack导出线程快照jstackjcmd综合诊断命令jcmd VM.flags实际排查时推荐先用jstat看GC情况再决定是否导出堆快照。下面这组命令适合先用起来# 查看Java进程 jps -l # 每1秒打印一次GC统计打印10次 jstat -gcutil pid 1000 10 # 查看堆中对象数量排行取前30行 jmap -histo pid | head -30 # 导出线程栈排查死锁、线程阻塞 jstack pid thread_dump.txt # 查看JVM启动参数 jcmd pid VM.flagsjstat输出中的E、O、M分别代表Eden、老年代和元空间的使用率FGC表示Full GC次数FGCT表示Full GC耗时。如果Old区持续增长且Full GC越来越频繁说明可能存在内存泄漏或老年代空间不足。这里要特别提醒生产环境执行jmap前要先确认业务可以接受短暂停顿。jmap -histo:live和jmap -dump:live会触发Full GC风险更高。如果应用有多个节点先摘除一个节点再dump。5.3 常用JVM参数的语义和坑很多面试题会直接问“你常用哪些JVM参数”。重点不是背参数而是说出参数对内存布局和GC行为的实际影响。参数含义调大影响调小影响-Xms堆初始大小启动时占用更多内存启动后频繁扩容-Xmx堆最大大小降低OOM概率可能出现Heap OOM-Xmn新生代大小降低Minor GC频率年轻代对象过早晋升-XX:MetaspaceSize元空间初始大小启动期更稳触发多次类加载扩容-XX:MaxMetaspaceSize元空间最大大小支持更多动态生成类动态类场景易OOM-XX:SurvivorRatioEden与Survivor比例Survivor变大晋升可能更快-XX:MaxTenuringThreshold对象晋升年龄阈值对象更久留在新生代对象更快晋升老年代-XX:MaxGCPauseMillisG1停顿目标更易满足停顿回收更激进-XX:HeapDumpOnOutOfMemoryErrorOOM时导出堆快照推荐开启不开启无法定位GC日志参数需要区分JDK版本。JDK8及以前常用-XX:PrintGCDetailsJDK9以后建议使用-Xlog:gc*。下面两种写法是不同版本对应的生产方式# JDK8示例 java -Xloggc:/data/logs/gc-%t.log -XX:PrintGCDetails -XX:PrintGCDateStamps -jar app.jar# JDK9及以上示例 java -Xlog:gc*:file/data/logs/gc-%t.log:time,uptime,level,tags -jar app.jar保留GC日志是排查GC问题的前提。生产环境应确保日志路径在持久化磁盘上并且配置日志轮转否则长时间运行会占用大量磁盘。没有GC日志很多线上GC问题只能靠猜。另一个容易被问到的参数是-XX:CompileThreshold。它控制方法触发JIT编译的调用次数阈值默认值在不同编译策略下并不相同。这个参数在实际生产中很少需要调整盲目调小会让JIT过早介入增加编译开销盲目调大又会让热点方法长时间解释执行。面试时能说出“它是JIT编译阈值受编译层级影响不要随便改”就足够了。5.4 一个OOM排查思路从现象到代码假设线上接口突然变慢JVM的Old区使用率接近100%Full GC频繁。推荐按下面顺序排查。第一步用jstat确认是否真的频繁Full GCjstat -gcutil pid 1000 10如果看到O列持续接近100FGC数量持续增长且每次FGCT时间较长可以确定老年代回收压力大。第二步用jmap查看对象占用量判断是否存在明显的大对象或泄漏对象jmap -histo pid | head -30这个命令会输出类名、实例数量和字节数。如果某个业务对象实例数量异常高比如订单对象、日志对象、缓存对象数量远超预期就要去代码里查这个对象的引用来源。第三步导出堆快照jmap -dump:formatb,file/data/logs/app.hprof pid然后使用内存分析工具打开hprof文件查看Dominator Tree或Leak Suspects找到GC Roots到占用对象的引用链。常见原因包括静态集合只增不减、ThreadLocal未清理、连接池泄漏、大批量数据一次性加载到内存。第四步修复后观察指标。不要在高峰期直接对线上执行dump优先在压测环境或备用节点上操作。5.5 一个生产环境参数基线示例实际项目中的参数会因业务不同而调整但可以给出一版常见基线作为起点。假设应用运行在4核8G的容器中堆大小约4Gjava -Xms4g -Xmx4g -Xmn1g \ -XX:MetaspaceSize256m -XX:MaxMetaspaceSize256m \ -XX:UseG1GC \ -XX:MaxGCPauseMillis200 \ -XX:HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath/data/logs/app.hprof \ -Xlog:gc*:file/data/logs/gc-%t.log:time,uptime,level,tags \ -jar app.jar学习环境可以追求简单直接设置-Xms256m -Xmx256m并打开GC日志即可。生产环境要额外考虑容器内存限制、最大线程数、文件句柄数和监控告警。如果使用JDK8gc日志参数要换成上一节提到的写法。参数调整遵循“小步快跑”原则。每次发布只改一个关键参数并同时变更监控大盘和告警阈值。回滚计划也要提前准备否则参数上线后出现异常会很难分辨是代码问题还是JVM配置问题。5.6 调优中的常见误区第一个误区是只调堆大小。堆调大只是延缓OOM出现的时间如果代码存在泄漏调大堆反而会让问题更难暴露。正确做法是先分析对象占用再决定是否需要调堆。第二个误区是动辄使用jmap。线上应用执行jmap可能会导致Full GC或长时间卡顿尤其是大堆应用。更安全的路径是先看jstat和GC日志再在低峰期或备用节点执行dump。第三个误区是生产环境不保留GC日志。很多项目直到出现Full GC才发现没开GC日志只能靠事后猜。GC日志是垃圾回收问题最重要的现场证据必须默认开启。第四个误区是盲目把Parallel换G1或ZGC。收集器切换不是只看一两个衡量指标G1在超大堆和低延迟场景有优势但吞吐量场景下Parallel可能更合适。没有数据支撑的切换等于把线上稳定性交给运气。6. 与JVM问题经常同台的MySQL高频面试题6.1 MySQL索引为什么常用B树很多Java岗位面试中MySQL题目会紧跟着JVM出现因为两者都是后端性能问题的共同来源。常见的第一问是为什么InnoDB索引选B树而不是B树、红黑树或哈希表。B树的非叶子节点不保存数据记录只保存索引键这样每个节点可以容纳更多键值整棵树更矮更宽减少磁盘IO次数。叶子节点之间通过双向链表连接对于范围查询非常友好比如where id between 100 and 200只需要从定位到的叶子节点顺序扫描。B树的非叶子节点也会保存数据树更高范围查询需要反复回溯效率不如B树。红黑树虽然能保持树平衡但高度通常远高于B树在磁盘场景下多次IO是不合适的。哈希索引能做到O(1)等值查找但不支持范围查询和排序。而InnoDB聚簇索引的叶子节点直接保存主键和整行数据二级索引的叶子节点保存主键值回表时根据主键到聚簇索引中再查一次。回答时如果能补充“覆盖索引可以避免回表”会更好。比如select id, name from user where name tom如果存在(name, id)联合索引二级索引里已经有id和name查询就不需要回表。6.2 事务隔离级别与MVCC事务的四个特性ACID不需要多解释重点要能说出InnoDB如何保证隔离性。SQL标准定义了四种隔离级别读未提交、读已提交、可重复读、可串行化。MySQL默认是可重复读。隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交不会可能可能可重复读不会不会可能InnoDB通过间隙锁解决大部分可串行化不会不会不会MVCC是多版本并发控制InnoDB每行隐藏两个关键字段事务ID和回滚指针。undo log里保存历史版本事务执行快照读时根据ReadView判断当前事务能看到哪个版本。ReadView在事务第一次执行快照读时生成包含活跃事务列表等信息。读已提交级别每次快照读都生成新的ReadView所以能看到其他事务已提交的数据。可重复读级别同一个事务内使用同一个ReadView所以多次读取结果一致。这解释了为什么MySQL默认隔离级别下普通select不会出现幻读但当前读比如select ... for update或update语句会加锁需要靠间隙锁和临键锁解决幻读。这里有一个容易混淆的点MVCC本身不解决当前读下的幻读它只保证快照读的一致性。面试时不要只说“MVCC解决了幻读”要补充“快照读由MVCC保证一致性当前读需要依赖行锁和间隙锁”。6.3 MySQL索引失效和Explain排查接口慢时除了查JVM线程还要看SQL有没有走索引。下面这些场景经常导致索引失效错误写法原因推荐写法where name like %tom前导通配符导致无法使用普通索引改为where name like tom%where id 1 5索引列参与计算改为where id 5 - 1where date(create_time) 2025-01-01索引列使用函数改为区间查询where mobile 13812345678字符列隐式类型转换改为字符串比较13812345678where name tom or age 18OR可能使索引合并优化失效优先用UNION或分别查询后合并联合索引不满足最左前缀无法使用联合索引按联合索引字段顺序设计SQL排查SQL是否走索引时使用EXPLAINEXPLAIN SELECT id, order_no FROM order_info WHERE user_id 123 AND status 1;EXPLAIN输出中主要看几列。type列从好到坏常见有const、eq_ref、ref、range、index、ALL。ALL表示全表扫描需要优先优化。key列表示实际使用的索引rows表示预估扫描行数Extra中如果出现Using filesort或Using temporary说明在排序或去重时使用了临时文件常见于order by和group by没有利用索引。判断标准很简单rows越小越好key不为空type尽量达到range或ref以上。6.4 一个容易答错的MySQL题OR能不能去重网上经常出现“MySQL的OR能去重吗”这类题。这个问题的正确理解是OR只是条件连接逻辑不是去重逻辑。select * from table where a x or b y返回的是满足任意一个条件的行集合并集不会因为某一行同时满足两个条件就只返回一次。如果预期“同一行的重复出现”是问题需要先搞清楚重复来自哪里。如果是多表JOIN产生的重复应该优化JOIN条件如果是数据本身有重复应该使用DISTINCT或GROUP BY。但DISTINCT影响多列时去重范围是“所有查询列组合”不是只对某个字段去重。例如SELECT DISTINCT user_id FROM order_info WHERE status 1 OR pay_type 2;这里DISTINCT去重的是user_id相同的结果。OR条件是不是能走索引合并取决于MySQL优化器和表上的索引。不能假设OR一定会走全表扫描也不能假设OR具备去重能力。6.5 JVM线程与MySQL慢查询联合排查后端接口变慢时只查JVM或只查MySQL都容易漏掉根因。实际项目中推荐两边同时取证。JVM侧查看当前线程在干什么jstack pid thread_dump.txt重点看线程状态是RUNNABLE还是BLOCKED是否大量线程卡在数据库驱动的JDBC调用上。如果线程栈里能看到大量at com.mysql.cj.jdbc.ConnectionImpl方法说明瓶颈大概率在数据库侧。MySQL侧查看当前连接状态SHOW FULL PROCESSLIST;如果大量连接处于Waiting for table metadata lock、Lock wait timeout exceeded或在执行全表扫描的大SQL说明问题在SQL或表锁。再用EXPLAIN看执行计划结合慢查询日志确认执行时长。还有一种典型情况JVM参数没问题MySQL索引也正常但接口仍然慢。这时要检查数据库连接池配置是否过小比如默认连接池只有10个连接而高峰期并发请求超过100线程会排队等待连接。排查时要同时看线程栈里的连接获取等待以及连接池监控指标。JVM和MySQL的联合排查并没有多高深核心是保留现场JVM保留线程栈、GC日志和堆快照MySQL保留慢查询日志、processlist和explain结果。数据齐全后再定位根因不建议凭经验直接改参数或改SQL。