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

一文带你掌握JVM常见面试题

a# JVM 常见八股面试题:内存结构、类加载、垃圾回收、实战调优一篇讲透

一、内存结构

1. 介绍一下 JVM 内存模型

面试里说“JVM 内存模型”通常指 JVM 运行时数据区,不要和 JMM Java 内存模型混淆。

JVM 运行时数据区主要包括:

  • 程序计数器:记录当前线程执行到哪条字节码指令。
  • Java 虚拟机栈:每个线程私有,方法调用时创建栈帧。
  • 本地方法栈:给 native 方法使用。
  • 堆:线程共享,主要存放对象实例,是 GC 管理的重点区域。
  • 方法区:线程共享,存放类元信息、常量、静态变量、JIT 编译后的代码等。
  • 运行时常量池:方法区的一部分,存放字面量和符号引用。
  • 直接内存:不属于 JVM 运行时数据区,但经常被 NIO 使用,也可能 OOM。

一句话:线程私有的是程序计数器、虚拟机栈、本地方法栈;线程共享的是堆和方法区。

2.String s = new String("abc")执行过程中分别对应哪些内存区域?

这行代码至少涉及字符串常量池、堆、栈。

Strings=newString("abc");

执行过程可以这样理解:

  1. "abc"是字符串字面量,会放入字符串常量池。
  2. new String("abc")会在堆中创建一个新的 String 对象。
  3. 局部变量s存在当前方法的虚拟机栈栈帧里。
  4. s保存的是堆中 String 对象的引用。

常见追问:创建几个对象?

如果常量池中之前没有"abc",可能会创建两个对象:常量池里的"abc"和堆里的 new String 对象。

如果常量池中已经有"abc",只会在堆里新建一个 String 对象。

3. 堆为什么分新生代和老年代?比例默认是多少?

堆分代是因为大多数对象都有“朝生夕死”的特点。很多对象创建后很快就不用了,少数对象会存活很久。

如果把所有对象混在一起回收,每次 GC 都扫描整个堆,成本很高。分代后:

  • 新生代存放新创建、生命周期短的对象。
  • 老年代存放存活时间长、体积较大的对象。

这样新生代可以频繁、小范围回收,老年代低频、大范围回收。

常见默认比例:

  • 新生代 : 老年代 通常约为 1 : 2。
  • Eden : Survivor0 : Survivor1 通常约为 8 : 1 : 1。

不同 JDK、不同垃圾收集器可能会动态调整,面试说“常见默认比例”即可。

4. Eden、From、To 区作用?为什么要有两个 Survivor?

新生代一般分为 Eden、From Survivor、To Survivor。

大多数对象先在 Eden 区分配。Minor GC 时,Eden 和当前 From 区中存活的对象会被复制到 To 区,然后 From 和 To 角色互换。

两个 Survivor 的作用:

  • 配合复制算法使用。
  • 让存活对象在两个 Survivor 之间来回复制。
  • 记录对象年龄,达到阈值后晋升老年代。

为什么不是一个 Survivor?

复制算法需要一个空区域作为目标空间。如果只有一个 Survivor,清理和复制会混在一起,不好保证空间连续和回收效率。

为什么不是更多 Survivor?

两个已经够完成复制和年龄判断,更多会浪费空间。

5. 方法区 / 元空间作用?存放什么?

方法区是 JVM 规范中的概念,用来存放类相关信息。

它通常存放:

  • 类的元信息,比如类名、父类、接口、字段、方法。
  • 运行时常量池。
  • 静态变量。
  • JIT 编译后的代码缓存等。

JDK 8 以后,HotSpot 用元空间 Metaspace 实现方法区。元空间使用的是本地内存,不再使用永久代。

面试可以这样说:方法区存的是“类的信息”,堆里存的是“对象实例”。

6. 永久代和元空间的区别?JDK 8 为什么废弃永久代改用元空间?

永久代 PermGen 是 JDK 8 之前 HotSpot 对方法区的实现,使用 JVM 堆内存的一部分。

元空间 Metaspace 是 JDK 8 之后的方法区实现,使用本地内存。

为什么废弃永久代?

  • 永久代大小不好设置,容易出现OutOfMemoryError: PermGen space
  • 类元信息放在本地内存中更灵活,受限更少。
  • 方便 HotSpot 与其他 JVM 实现统一方法区概念。
  • 字符串常量池等内容逐步移到堆中,永久代的职责变得不合适。

注意:元空间不是无限的,也可能 OOM,可以通过-XX:MaxMetaspaceSize限制大小。

7. 直接内存是什么?会不会 OOM?

直接内存 Direct Memory 不是 JVM 堆的一部分,而是操作系统本地内存。

Java NIO 的ByteBuffer.allocateDirect()会使用直接内存。它减少了 Java 堆和 native 内存之间的数据拷贝,适合 IO 场景。

直接内存会不会 OOM?会。

常见异常:

java.lang.OutOfMemoryError: Direct buffer memory

常见原因:

  • 大量 direct buffer 没释放。
  • Netty 等框架使用直接内存过多。
  • -XX:MaxDirectMemorySize设置太小或没有规划。

直接内存不归堆管,但仍然受机器内存和 JVM 参数限制。

二、类加载机制

1. 类加载的五步

类加载通常分为五个阶段:

  1. 加载 Loading
  2. 验证 Verification
  3. 准备 Preparation
  4. 解析 Resolution
  5. 初始化 Initialization

有时也会把验证、准备、解析统称为连接 Linking。

加载:通过类全限定名获取字节码,生成 Class 对象。

验证:检查字节码是否合法,防止破坏 JVM。

准备:给类变量分配内存并设置默认值。

解析:把符号引用替换成直接引用。

初始化:执行类构造器<clinit>(),给静态变量赋真实值,执行静态代码块。

2. 什么是双亲委派机制?

双亲委派机制是类加载器加载类时的一种委派规则。

当一个类加载器收到类加载请求时,不会自己先加载,而是先把请求交给父类加载器。父类加载器还会继续向上委派。只有父类加载器加载不了,子类加载器才会尝试自己加载。

加载顺序大致是:

BootstrapClassLoader -> Extension/PlatformClassLoader -> AppClassLoader -> 自定义类加载器

它的核心思想是:先让上层加载器加载基础类,避免核心类被随意替换。

3. 双亲委派机制的缺点是什么?

双亲委派机制能保证安全和稳定,但也有缺点。

主要缺点:

  • 不够灵活,父加载器无法反过来使用子加载器加载的类。
  • 不适合某些插件化、热部署、模块隔离场景。
  • SPI 场景下,Java 核心类需要加载第三方实现类,双亲委派会受到限制。

比如 JDBC 中DriverManager是核心类库里的类,但具体数据库驱动由应用提供,所以需要线程上下文类加载器来解决。

4. 如何打破双亲委派机制?

打破双亲委派通常是自定义类加载器,重写loadClass()或调整加载顺序。

默认情况下,loadClass()实现了双亲委派。如果只重写findClass(),一般仍然遵守双亲委派。

要打破它,通常会在loadClass()中改成“自己先加载,加载不了再交给父加载器”。

典型场景:

  • Tomcat Web 应用隔离。
  • OSGi 模块化。
  • 插件系统。
  • 热部署框架。

注意:不要随便打破双亲委派,否则可能导致类冲突、安全问题和类型转换异常。

5. 如何实现自定义类加载器?

常见做法是继承ClassLoader,重写findClass()

简化示例:

publicclassMyClassLoaderextendsClassLoader{@OverrideprotectedClass<?>findClass(Stringname)throwsClassNotFoundException{byte[]bytes=loadClassBytes(name);returndefineClass(name,bytes,0,bytes.length);}privatebyte[]loadClassBytes(Stringname){// 从磁盘、网络、加密文件等位置读取 class 字节码returnnewbyte[0];}}

如果只是从特殊路径加载类,重写findClass()就够了;如果要改变双亲委派顺序,才需要谨慎重写loadClass()

三、垃圾回收机制

1. 如何判断对象是否存活?可达性分析?引用计数?

判断对象是否存活主要有两种思路:引用计数法和可达性分析法。

引用计数法:每个对象维护一个计数器,有引用指向它就 +1,引用失效就 -1,计数为 0 就可以回收。

缺点是无法解决循环引用。比如对象 A 引用 B,B 引用 A,但它们都不再被外部访问,计数仍然不为 0。

Java 主流 JVM 使用可达性分析法。

可达性分析从 GC Roots 出发,沿引用链向下搜索。能被 GC Roots 直接或间接到达的对象是存活对象,无法到达的对象可以被回收。

2. GC Root 有哪些?

常见 GC Roots 包括:

  • 虚拟机栈中引用的对象,比如局部变量。
  • 本地方法栈中 JNI 引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 被 synchronized 持有的对象。
  • JVM 内部引用,比如系统类加载器、基本数据类型 Class 对象。

面试不用背特别冷门的,只要能说出栈、本地方法栈、静态变量、常量、锁持有对象即可。

3. 三大垃圾回收算法以及各自优缺点

常见三大算法:

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

优点是实现简单,不需要移动对象。缺点是会产生内存碎片,回收效率不稳定。

复制算法:把可用内存分成两块,每次只用一块,回收时把存活对象复制到另一块,然后清空原区域。

优点是没有碎片,分配快。缺点是浪费一部分空间,存活对象多时复制成本高。

标记-整理:先标记存活对象,再把存活对象向一端移动,然后清理边界外空间。

优点是没有碎片,适合老年代。缺点是移动对象成本较高。

4. 为什么新生代使用复制算法,而老年代使用标记整理?

新生代对象大多朝生夕死,Minor GC 后存活对象很少。使用复制算法只需要复制少量存活对象,效率很高。

老年代对象存活率高,如果用复制算法,要复制大量对象,成本很高,而且还需要额外保留一大块空闲空间,不划算。

所以新生代适合复制算法,老年代更适合标记-清除或标记-整理。现代垃圾收集器会结合具体场景做更复杂的优化。

5. 新生代中的对象符合哪些条件之后会晋升到老年代?

对象晋升老年代常见情况:

  • 对象年龄达到阈值,默认最大常见值是 15,因为对象头中年龄字段通常是 4 bit。
  • 大对象直接进入老年代,比如很大的数组或字符串。
  • Survivor 空间放不下,存活对象会提前晋升。
  • 动态年龄判断:某个年龄及以上对象总大小超过 Survivor 一半时,这些对象可能直接晋升。

简单说:活得久、太大、Survivor 放不下,都可能进老年代。

6. 什么是 Minor GC、Major GC、Full GC?触发条件?

Minor GC:回收新生代。通常在 Eden 区空间不足时触发。

Major GC:通常指回收老年代,但这个术语在不同资料里不完全统一。

Full GC:回收整个 Java 堆和方法区相关区域,停顿时间通常更长。

常见 Full GC 触发条件:

  • 老年代空间不足。
  • 方法区或元空间不足。
  • 调用System.gc(),不一定立刻执行,但可能触发。
  • Minor GC 前判断老年代担保空间不足。
  • 大对象分配失败。

面试里重点说:Minor GC 比较频繁,Full GC 成本高,线上要尽量减少频繁 Full GC。

7. 什么是 STW?

STW 是 Stop The World,意思是垃圾回收时暂停用户线程。

GC 过程中,为了保证对象引用关系不再乱变,JVM 可能会暂停所有业务线程,只让 GC 线程工作。

STW 的影响是接口响应变慢、系统短暂停顿。停顿时间越长,用户感知越明显。

现代垃圾收集器如 G1、ZGC、Shenandoah 都在尽量降低 STW 时间。

8. 常用的垃圾收集器有哪些?

常见垃圾收集器:

  • Serial:单线程收集器,适合客户端或小内存场景。
  • ParNew:Serial 的多线程版本,常与 CMS 搭配。
  • Parallel Scavenge:关注吞吐量,适合后台计算任务。
  • Serial Old:老年代单线程收集器。
  • Parallel Old:Parallel Scavenge 的老年代版本。
  • CMS:低停顿老年代收集器,使用标记-清除,可能产生碎片。
  • G1:面向服务端应用,按 Region 管理堆,兼顾吞吐和低停顿。
  • ZGC:超低延迟收集器,适合大内存低停顿场景。
  • Shenandoah:低停顿收集器,目标类似 ZGC。

当前面试最常问 CMS、G1、ZGC。

9. 什么是三色标记法?

三色标记法是并发标记中常用的对象标记思想。

它把对象分成三种颜色:

  • 白色:还没被访问,最终仍是白色可能被回收。
  • 灰色:已经被访问,但它引用的对象还没扫描完。
  • 黑色:已经被访问,并且它引用的对象也扫描完了。

GC 从 GC Roots 开始,把对象从白变灰,再从灰变黑,逐步完成标记。

并发标记时,用户线程还在运行,引用关系可能变化,所以需要写屏障、读屏障等机制保证标记正确。

四、JVM 实战调优

1. 常见 OOM 有哪几种?分别什么原因?

常见 OOM 包括:

堆内存溢出:

java.lang.OutOfMemoryError: Java heap space

原因通常是对象太多、缓存无界、集合持续增长、内存泄漏。

元空间溢出:

java.lang.OutOfMemoryError: Metaspace

原因通常是动态生成类太多,比如 CGLIB、反射代理、热部署类加载器泄漏。

栈溢出:

java.lang.StackOverflowError

原因通常是递归太深、方法调用链太长、栈帧过大。

直接内存溢出:

java.lang.OutOfMemoryError: Direct buffer memory

原因通常是 NIO、Netty 直接内存使用过多或释放不及时。

2. 常见 JVM 参数:-Xms-Xmx-Xss-XX:MetaspaceSize含义?

常见参数:

-Xms:堆初始大小。

-Xmx:堆最大大小。

生产中常把-Xms-Xmx设置成一样,避免运行时频繁扩缩容。

-Xss:每个线程的栈大小。设置太大,能创建的线程数变少;设置太小,容易栈溢出。

-XX:MetaspaceSize:元空间初始触发 GC 的阈值,不是最大值。

-XX:MaxMetaspaceSize:元空间最大值。

-XX:MaxDirectMemorySize:直接内存最大值。

-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动导出堆 dump,线上排查很有用。

3. 如何排查线上 OOM?完整排查流程?

排查 OOM 可以按这个流程:

  1. 先看错误类型,是 heap、metaspace、direct memory 还是 stack。
  2. 查看应用日志,确认 OOM 发生时间和业务操作。
  3. 确认 JVM 参数,比如堆大小、元空间大小、GC 日志配置。
  4. 如果是堆 OOM,获取 heap dump。
  5. 用 MAT、VisualVM、JProfiler 等工具分析 dump。
  6. 查看大对象、对象数量、GC Roots 引用链。
  7. 判断是内存泄漏还是瞬时流量导致内存不够。
  8. 修复代码或调整参数,再压测验证。

线上建议提前加:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump

注意:生产 dump 可能很大,抓取和拷贝要注意磁盘空间和业务影响。

4. 如何排查频繁 Full GC?

频繁 Full GC 常见原因:

  • 老年代空间不足。
  • 大对象频繁进入老年代。
  • 内存泄漏,对象回收不掉。
  • 元空间不足。
  • 显式调用System.gc()
  • 新生代太小,导致对象过早晋升。

排查流程:

  1. 查看 GC 日志,确认 Full GC 频率、耗时、回收前后内存变化。
  2. 如果 Full GC 后老年代下降明显,可能是内存压力大或参数不合理。
  3. 如果 Full GC 后老年代几乎不下降,重点怀疑内存泄漏。
  4. jstat观察各区域变化。
  5. jmap导出 dump 分析大对象和引用链。
  6. 检查是否有大缓存、无界队列、大集合、ThreadLocal 未清理。

调优方向通常是先定位对象来源,再考虑调整堆、新生代比例或更换 GC。

5. 什么是内存泄漏?和内存溢出区别?

内存泄漏是指对象已经不再被业务需要,但仍然被引用,导致 GC 无法回收。

内存溢出是指程序申请内存时,JVM 没有足够内存可以分配,于是抛出 OOM。

关系是:内存泄漏持续发生,最终可能导致内存溢出。

常见内存泄漏场景:

  • 静态集合一直添加不清理。
  • ThreadLocal 用完不 remove。
  • 监听器、回调未注销。
  • 连接、文件句柄没关闭。
  • 本地缓存没有容量和过期限制。

6. 线上 CPU 飙高、线程死锁怎么排查?

CPU 飙高排查流程:

  1. top找到 CPU 高的 Java 进程 pid。
  2. top -Hp pid找到 CPU 高的线程 tid。
  3. 把 tid 转成 16 进制。
  4. jstack pid导出线程栈。
  5. 在栈里搜索 16 进制线程 id,定位具体代码。

线程死锁排查:

  • 使用jstack pid,它通常会直接显示 deadlock 信息。
  • 查看哪些线程持有哪些锁、等待哪些锁。
  • 根据堆栈定位代码中的锁顺序问题。

常见修复方式:

  • 统一加锁顺序。
  • 减少锁粒度。
  • 避免嵌套锁。
  • 使用tryLock超时退出。

7.jpsjstackjmapjhatjstat常用命令作用?

jps:查看当前机器上的 Java 进程。

jps-l

jstack:查看线程栈,排查死锁、线程阻塞、CPU 飙高。

jstack<pid>>thread.txt

jmap:查看堆信息、导出 heap dump。

jmap-dump:format=b,file=heap.hprof<pid>

jhat:分析 heap dump 的老工具,现在不太推荐,更多用 MAT、VisualVM。

jstat:查看 GC、类加载、JIT 等运行时统计。

jstat-gcutil<pid>100010

面试重点:CPU 高看jstack,堆问题看jmap,GC 趋势看jstat

8. 堆 dump 怎么抓取,怎么分析?

抓取 heap dump 常见方式:

OOM 自动抓:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump

手动抓:

jmap-dump:format=b,file=heap.hprof<pid>

也可以用:

jcmd<pid>GC.heap_dump heap.hprof

分析思路:

  1. 用 MAT 或 VisualVM 打开 dump。
  2. 看占用内存最大的对象。
  3. 看 Dominator Tree。
  4. 找 GC Roots 引用链。
  5. 判断对象为什么没有被回收。
  6. 回到代码里定位集合、缓存、ThreadLocal、类加载器等来源。

注意:线上抓 dump 可能导致短暂停顿,文件也可能很大,操作前要确认磁盘空间和业务影响。

五、面试回答小抄

  • JVM 运行时数据区包括程序计数器、虚拟机栈、本地方法栈、堆、方法区,直接内存也常被问到。
  • 堆分代是因为大多数对象生命周期短,新生代用复制算法,老年代更适合标记整理。
  • 两个 Survivor 是为了配合复制算法,一个作为来源,一个作为目标。
  • JDK 8 后永久代被元空间替代,元空间使用本地内存。
  • 类加载五步是加载、验证、准备、解析、初始化。
  • 双亲委派是先交给父加载器加载,父加载不了子加载器再加载。
  • Java 判断对象存活主要靠可达性分析,而不是引用计数。
  • GC Roots 常见有栈引用、静态变量、常量、JNI 引用、锁持有对象。
  • STW 是暂停用户线程,现代 GC 都在尽量减少 STW 时间。
  • OOM 排查先看类型,再抓日志、GC、dump,最后分析引用链。

总结

JVM 面试题可以按四条主线理解:

  • 内存结构:知道每块区域存什么、是否线程共享、会不会 OOM。
  • 类加载:掌握五步流程、双亲委派、自定义类加载器。
  • 垃圾回收:理解对象存活判断、GC Roots、回收算法、分代收集、STW。
  • 实战调优:会看 OOM、Full GC、CPU 飙高、死锁和常用工具。

面试回答 JVM 题时,不需要一上来背源码。先讲清楚“这块区域/机制解决什么问题”,再讲“怎么工作”,最后补一句“线上怎么排查或有什么坑”,回答就会更像真正做过项目。

http://www.cnnetsun.cn/news/2695032.html

相关文章:

  • 从零构建高效无变压器并网逆变器:前馈反馈控制与硬件设计详解
  • 走同一条航线的两条船,为什么效率天差地别?
  • 基于Google Charts与树莓派的物联网数据可视化实战
  • 基于Arduino与FFT算法的DIY吉他调音器:从信号采集到频谱分析
  • 无源UHF RFID温度传感标签设计:电网热监测的低功耗系统级优化
  • 人工智能时代:小白程序员如何提升技能,避免被大模型淘汰?收藏必备!
  • 树莓派Pico外挂EEPROM存储方案:从硬件连接到MicroPython驱动实战
  • Gin 框架响应格式与 HTML 模板渲染完整实战教程
  • YoloMouse:让游戏光标不再消失的智能解决方案
  • HTML到Figma转换工具:网页设计逆向工程的终极解决方案
  • 魔兽争霸3在Windows 11完美运行:WarcraftHelper三步快速配置指南
  • 基于树莓派与ESP32的智能书籍保存箱DIY全栈物联网项目实践
  • 【独家首发】Sora 2体育视频生成性能白皮书(内部测试版V2.3.1):17项关键指标对比Runway/PIKA/Pika Labs,仅限前500名开发者下载
  • 别再手动提特征了!用Python+PyTorch搭建你的第一个智能故障诊断模型(以轴承振动数据为例)
  • 告别重复劳动:用CodeFuse插件5分钟搞定Java/Python单元测试生成(附避坑指南)
  • 现在不看就晚了:Sora 2.4即将废弃的录制协议v1.7——30天倒计时内必须迁移的5个接口、2个事件钩子与1套兼容性验证清单
  • Windows上安装APK的终极方案:告别模拟器,体验原生安卓应用
  • 编写个人家庭应急物资管理系统,分类统计保质期,储备量,适配家庭突发应急场景。
  • 开发小区垃圾分类智能指引程序,识别垃圾品类,精准引导分类投放,贴合社区治理。
  • 超越振动信号:用IMS轴承数据集玩转5种故障预测模型(附PyTorch/Sklearn代码)
  • 自制2.4GHz全波偶极天线:原理、制作与WiFi信号增强实战
  • Unity Addressables热更实战:从本地模拟到远程服务器部署的保姆级流程(含Hosting服务)
  • 戴尔新款 XPS 13 7 月上市,低价对标 MacBook Neo,轻薄优势下能否突围?
  • Sora 2背景音乐自动裁剪失效?揭秘底层时间码映射机制:如何用Python脚本动态生成合规.wav头文件
  • 测试文章123
  • PyMobileDevice3终极指南:Python控制iOS设备的完整实战教程
  • 如何在Windows上快速安装安卓应用:APK-Installer完整实战指南
  • 霞鹜文楷:终极免费开源中文字体解决方案,轻松解决你的中文排版难题
  • Fibronectin CS-1 Fragment (1978-1985) ;EILDVPST
  • 告别混乱开发:用平头哥CDK的组件池功能管理你的多芯片项目