第一部分内存模型与区域详解1. 面试官请聊聊你对JVM运行时数据区的理解JDK8之后有什么比较大的变化吗候选人好的面试官。JVM的运行时数据区是Java程序运行时的内存规划根据JDK8及以后的规范主要分为线程私有和线程共享两大类。线程私有的区域包括程序计数器、Java虚拟机栈和本地方法栈。它们的生命周期与线程相同随线程而生随线程而灭。线程共享的区域主要是堆Heap和方法区Method Area。我分别解释一下程序计数器可以看作是当前线程所执行的字节码的行号指示器。它的作用在于线程切换后能恢复到正确的执行位置是JVM中唯一一个没有规定任何OutOfMemoryError情况的区域。Java虚拟机栈描述的是Java方法执行的线程内存模型。每个方法执行时JVM都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用到完成对应着一个栈帧在虚拟机栈中入栈到出栈的过程。本地方法栈与虚拟机栈类似区别在于它为虚拟机使用到的本地方法Native Method服务。在HotSpot虚拟机中这两者是合二为一的。堆Java Heap这是JVM内存管理中最大的一块被所有线程共享。它的唯一目的就是存放对象实例几乎所有对象和数组都在这里分配内存。从垃圾回收的角度看堆被细分为新生代Eden区、Survivor区和老年代。方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。JDK8之后最重要的变化就是用元空间Metaspace彻底取代了永久代PermGen。以前的方法区在JDK7及以前被称为永久代它使用的是JVM堆内的内存。到了JDK8元空间被移到了本地内存Native Memory中。这个改变解决了永久代容易出现的java.lang.OutOfMemoryError: PermGen问题因为本地内存的大小只受物理内存的限制默认情况下理论上是无限的避免了方法区因为固定大小而导致的内存溢出。2. 面试官栈和堆有什么区别栈上到底存的是什么候选人这个问题问得很细。栈和堆是JVM内存中两个核心且功能迥异的区域它们的区别主要体现在以下几个方面存储内容不同栈存储的是方法的调用信息具体来说是栈帧。每个栈帧里包含了局部变量表、操作数栈等。在局部变量表中存储的是基本数据类型如int, boolean和对象的引用reference。对象本身并不在栈上而是在堆上。堆存储的是对象实例本身。无论是通过new关键字创建的普通对象还是数组都在堆上分配内存。线程共享性不同栈是线程私有的每个线程都有自己的栈互不干扰这保证了局部变量的线程安全性。堆是线程共享的所有线程都可以访问堆上的对象因此需要关注并发访问的同步问题。生命周期不同栈的生命周期与方法调用绑定。方法调用开始栈帧入栈方法结束栈帧出栈并销毁局部变量也随之清除这种回收是确定且高效的。堆的生命周期与对象本身绑定。一个对象被创建后只要还有引用指向它它就会一直存活。它的回收依赖于垃圾回收器GC的不定时回收具有不确定性。异常情况不同栈主要抛出StackOverflowError比如方法递归调用过深导致栈帧过多超出了栈的深度。堆主要抛出OutOfMemoryError比如创建了过多的对象堆内存无法容纳。所以回到您的问题栈中存的是对象的引用也就是指针而不是对象本身。例如Person p new Person();这里的p这个变量在栈上它存的是堆中那个Person实例的内存地址。3. 面试官聊聊Java中不同引用类型强、软、弱、虚的区别和应用场景候选人好的这四种引用强度依次递减它们的设计主要是为了让我们能更灵活地控制对象的生命周期尤其是在内存紧张时能够通过GC来回收一些非必须的对象从而避免OOM。强引用Strong Reference特点这是我们最常见的比如Object obj new Object()。只要强引用还存在GC永远不会回收被引用的对象哪怕JVM抛出OOM。场景日常编码中的普遍赋值。软引用Soft Reference特点通过SoftReference类实现。它表示一些有用但非必须的对象。在系统将要发生内存溢出异常之前GC会把只被软引用关联的对象列进回收范围并进行二次回收。如果这次回收后内存依然不足才会抛出OOM。场景非常适合做缓存。比如一个图片缓存器内存够的时候就留着内存不够时就清掉保证了程序的健壮性。弱引用Weak Reference特点通过WeakReference类实现。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。无论当前内存是否足够只要GC发生被弱引用指向的对象都会被回收。场景最典型的应用就是ThreadLocal和WeakHashMap。ThreadLocal的Entry继承了WeakReferencekey就是弱引用这样当ThreadLocal对象本身被回收后其对应的key也会在下一次GC时被清理有助于防止内存泄漏。虚引用Phantom Reference特点通过PhantomReference类实现是最弱的一种。它无法通过get()方法获取到对象的实例。它唯一的作用就是当对象被GC回收时能收到一个系统通知。虚引用必须与ReferenceQueue联合使用。场景主要用于堆外内存的回收。比如在NIO中使用直接内存DirectByteBuffer时JVM通过虚引用来管理和确保堆外内存被正确释放。第二部分垃圾回收GC核心4. 面试官JVM是如何判定一个对象是“垃圾”的能讲讲“三色标记”算法吗候选人判断对象是否存活主流的虚拟机如HotSpot采用的都是可达性分析算法。它的核心思想是从一组称为GC Roots的根对象出发通过引用链向下搜索整个搜索过程走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连即从GC Roots到这个对象不可达则证明此对象是不可用的可以被回收。常见的GC Roots包括虚拟机栈栈帧中的本地变量表中引用的对象。方法区中类的静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNI即一般说的Native方法引用的对象。Java虚拟机内部的引用如基本数据类型对应的Class对象常驻的异常对象等。关于三色标记算法 这是现代垃圾收集器如CMS、G1在并发标记阶段常用的一种算法用来解决在GC线程和应用线程并发执行时准确标记存活对象的问题。它将遍历对象图过程中遇到的对象按“是否访问过”标记成三种颜色白色尚未被垃圾收集器访问过的对象。在分析阶段结束所有白色对象都会被判定为可回收。黑色已经被垃圾收集器访问过的对象且该对象的所有引用都已经扫描过。它是安全存活的如果有其他对象指向了黑色对象无需重新扫描。黑色对象不可能直接不经过灰色对象指向白色对象。灰色已经被垃圾收集器访问过的对象但至少还有一个引用没有被扫描过。它是介于黑色和白色之间的中间状态。并发标记过程中如果用户线程修改了引用关系可能导致对象“消失”问题——即原本应该被标记为存活的对象黑色对象突然切断了与某个白色对象的引用导致该白色对象被错误回收。为了解决这个问题JVM引入了两种解决方案增量更新CMS采用和原始快照G1采用。5. 面试官现在G1收集器很流行你能对比一下G1和CMS的主要区别吗为什么G1会取代CMS候选人好的G1Garbage-First和CMSConcurrent Mark Sweep都是追求低停顿的并发收集器但它们的设计理念和实现方式有很大不同。CMS可以说是G1的前辈但G1在很多方面做了优化现在已成为JDK9的默认垃圾收集器。主要区别体现在以下几个方面维度CMS 收集器G1 收集器回收范围专门回收老年代需配合ParNew等新生代收集器。面向整个堆无需配合其他收集器。内存布局传统分代布局连续的新生代、老年代。分区Region布局将堆划分为多个大小相等的Region每个Region可以动态扮演Eden、Survivor或Old区逻辑上分代。回收算法标记-清除Mark-Sweep算法。整体上基于标记-整理Mark-Compact算法局部两个Region之间基于复制算法。核心目标获取最短回收停顿时间。建立可预测的停顿时间模型用户可以指定期望的GC停顿时间如 -XX:MaxGCPauseMillis200。垃圾碎片会产生大量内存碎片最终可能导致并发失败退化为Serial Old进行Full GC。通过复制算法进行整理不会产生内存碎片。G1取代CMS的原因可控的停顿时间CMS无法设定具体的停顿时间目标而G1通过将堆划分为Region并有选择地优先回收垃圾最多的RegionGarbage-First可以很好地控制回收成本实现接近实时的停顿。解决内存碎片问题CMS使用标记-清除长时间运行后会产生大量碎片导致分配大对象失败最终触发长时间的Full GC。而G1使用标记-整理通过复制对象进行压缩彻底解决了碎片化问题。更高的吞吐量和更智能的并发G1的设计能更好地利用多核CPU其并发标记阶段的“原始快照”算法也比CMS的“增量更新”算法在特定场景下能更有效地避免浮动垃圾问题。第三部分类加载机制6. 面试官能详细讲讲类的加载过程吗以及什么是双亲委派模型为什么要这么设计候选人当然。类从被加载到虚拟机内存开始到卸载出内存为止整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析统称为连接。加载通过类的全限定名获取定义此类的二进制字节流可以从JAR、网络、动态代理等获取。将字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。验证确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的要求保证被加载类的正确性不会危害虚拟机自身的安全。包括文件格式验证、元数据验证、字节码验证、符号引用验证。准备为类变量static修饰的变量分配内存并设置初始零值如int为0对象引用为null。这里需要注意的是如果是static final的常量在准备阶段就会直接赋值为用户指定的值。解析将常量池内的符号引用替换为直接引用的过程。比如一个类的方法com/xxx/A.method这个符号在解析阶段会被替换成一个指向方法内存地址的指针或偏移量。初始化执行类构造器clinit()方法的过程。这一步才会真正执行我们编写的静态代码块中的代码和对静态变量的赋值操作。这是类加载的最后一步也是真正开始执行Java代码的阶段。关于双亲委派模型 当一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到最顶层的启动类加载器Bootstrap ClassLoader中。只有当父类加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载。为什么这么设计避免类的重复加载父加载器已经加载过的类子加载器就不会再加载一遍。保证核心API的安全这是最重要的原因。它确保了Java核心类库如java.lang.Object的统一性。比如如果有人想自己写一个java.lang.String类并试图在JVM中加载由于双亲委派模型这个请求会一直委托给Bootstrap ClassLoader而Bootstrap ClassLoader会去加载rt.jar里的标准String类从而防止恶意代码冒充核心API。第四部分内存泄漏与调优实战大厂必问7. 面试官你在项目中有没有遇到过内存泄漏或OOM的情况是怎么排查的候选人注意这里要根据自己准备的案例来讲建议准备一个真实的或模拟的场景。下面是一个用ThreadLocal不当导致泄漏的常见案例候选人有的。之前在项目中使用ThreadLocal来存储一些用户上下文信息由于项目部署在Tomcat这种使用了线程池的容器中且在线程处理完请求后忘记调用ThreadLocal的remove()方法清理数据导致内存泄漏。当时现象是服务器运行一段时间后内存占用持续升高Full GC越来越频繁接口响应速度明显变慢最后出现了java.lang.OutOfMemoryError: Java heap space异常。排查过程我分了几个步骤获取现场首先我使用了JDK自带的jps命令找到出问题的Java进程ID。初步监控然后使用jstat -gcutil pid 1000 10每隔1秒打印一次GC情况。发现老年代Old的占用率OU一直在增长且Full GC次数FGC不断增加说明内存确实无法正常回收。Dump内存快照接着我使用jmap -dump:formatb,fileheap.hprof pid命令生成了一个堆内存快照文件。在生产环境操作时需要谨慎dump过程会暂停应用所以通常会配置JVM参数-XX:HeapDumpOnOutOfMemoryError让JVM在OOM发生时自动dump。分析Dump文件我把heap.hprof文件下载到本地用Eclipse MATMemory Analyzer Tool打开。MAT的Leak Suspects Report功能直接帮我定位到了问题java.lang.ThreadLocal$ThreadLocalMap占用了大量内存。定位代码顺着ThreadLocalMap的引用链我发现是某个自定义的UserContext对象一直被Thread对象引用着无法释放。检查代码后发现是在拦截器里设置了ThreadLocal值但在完成后没有在finally块中执行remove()。解决方案最终在拦截器的afterCompletion()方法中对ThreadLocal调用了remove()方法解决了这个内存泄漏问题。这次经历让我深刻理解了ThreadLocal必须与remove()成对使用特别是在线程池环境下否则线程复用会导致上一个请求的数据被下一个请求拿到造成业务逻辑错乱和内存泄漏。8. 面试官如果线上服务器CPU飙升到100%你怎么排查这可能和JVM有什么关系候选人CPU飙升通常有两种情况业务流量突然激增或者是代码进入了死循环或频繁GC。排查步骤一般是这样的定位进程使用top命令找到CPU使用率最高的那个Java进程记录其PID。定位线程使用top -H -p PID查看该进程内各个线程的CPU使用情况找到CPU占用异常的线程记录其TID线程ID。转换线程ID将线程的TID十进制转换为十六进制printf %x\n TID因为Java线程栈里的nid是用十六进制表示的。查看线程栈使用jstack PID | grep 十六进制nid -A 30打印出该线程的堆栈信息。通过堆栈我们可以看到这个线程具体在执行哪一段代码。可能和JVM的关系如果堆栈信息显示线程在GC相关的方法如ConcurrentMarkSweepThreadG1ParTask里那么CPU飙升很可能是因为频繁的垃圾回收特别是Full GC导致的。为什么会频繁GC通常是内存分配不合理或者有内存泄漏。频繁的GC会占用大量CPU资源导致业务线程执行缓慢。怎么验证可以同时执行jstat -gcutil PID 2000 10观察GC频率和耗时。如果FGC列的数字在快速增加那就印证了猜想。如果是GC问题就需要进一步分析堆内存如前面讲的Dump分析。如果不是GC问题而是业务代码如某个正则匹配、大循环那就根据栈信息去优化具体代码逻辑。
【八股必备】JVM常见面试题2
发布时间:2026/6/15 0:21:41
第一部分内存模型与区域详解1. 面试官请聊聊你对JVM运行时数据区的理解JDK8之后有什么比较大的变化吗候选人好的面试官。JVM的运行时数据区是Java程序运行时的内存规划根据JDK8及以后的规范主要分为线程私有和线程共享两大类。线程私有的区域包括程序计数器、Java虚拟机栈和本地方法栈。它们的生命周期与线程相同随线程而生随线程而灭。线程共享的区域主要是堆Heap和方法区Method Area。我分别解释一下程序计数器可以看作是当前线程所执行的字节码的行号指示器。它的作用在于线程切换后能恢复到正确的执行位置是JVM中唯一一个没有规定任何OutOfMemoryError情况的区域。Java虚拟机栈描述的是Java方法执行的线程内存模型。每个方法执行时JVM都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用到完成对应着一个栈帧在虚拟机栈中入栈到出栈的过程。本地方法栈与虚拟机栈类似区别在于它为虚拟机使用到的本地方法Native Method服务。在HotSpot虚拟机中这两者是合二为一的。堆Java Heap这是JVM内存管理中最大的一块被所有线程共享。它的唯一目的就是存放对象实例几乎所有对象和数组都在这里分配内存。从垃圾回收的角度看堆被细分为新生代Eden区、Survivor区和老年代。方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。JDK8之后最重要的变化就是用元空间Metaspace彻底取代了永久代PermGen。以前的方法区在JDK7及以前被称为永久代它使用的是JVM堆内的内存。到了JDK8元空间被移到了本地内存Native Memory中。这个改变解决了永久代容易出现的java.lang.OutOfMemoryError: PermGen问题因为本地内存的大小只受物理内存的限制默认情况下理论上是无限的避免了方法区因为固定大小而导致的内存溢出。2. 面试官栈和堆有什么区别栈上到底存的是什么候选人这个问题问得很细。栈和堆是JVM内存中两个核心且功能迥异的区域它们的区别主要体现在以下几个方面存储内容不同栈存储的是方法的调用信息具体来说是栈帧。每个栈帧里包含了局部变量表、操作数栈等。在局部变量表中存储的是基本数据类型如int, boolean和对象的引用reference。对象本身并不在栈上而是在堆上。堆存储的是对象实例本身。无论是通过new关键字创建的普通对象还是数组都在堆上分配内存。线程共享性不同栈是线程私有的每个线程都有自己的栈互不干扰这保证了局部变量的线程安全性。堆是线程共享的所有线程都可以访问堆上的对象因此需要关注并发访问的同步问题。生命周期不同栈的生命周期与方法调用绑定。方法调用开始栈帧入栈方法结束栈帧出栈并销毁局部变量也随之清除这种回收是确定且高效的。堆的生命周期与对象本身绑定。一个对象被创建后只要还有引用指向它它就会一直存活。它的回收依赖于垃圾回收器GC的不定时回收具有不确定性。异常情况不同栈主要抛出StackOverflowError比如方法递归调用过深导致栈帧过多超出了栈的深度。堆主要抛出OutOfMemoryError比如创建了过多的对象堆内存无法容纳。所以回到您的问题栈中存的是对象的引用也就是指针而不是对象本身。例如Person p new Person();这里的p这个变量在栈上它存的是堆中那个Person实例的内存地址。3. 面试官聊聊Java中不同引用类型强、软、弱、虚的区别和应用场景候选人好的这四种引用强度依次递减它们的设计主要是为了让我们能更灵活地控制对象的生命周期尤其是在内存紧张时能够通过GC来回收一些非必须的对象从而避免OOM。强引用Strong Reference特点这是我们最常见的比如Object obj new Object()。只要强引用还存在GC永远不会回收被引用的对象哪怕JVM抛出OOM。场景日常编码中的普遍赋值。软引用Soft Reference特点通过SoftReference类实现。它表示一些有用但非必须的对象。在系统将要发生内存溢出异常之前GC会把只被软引用关联的对象列进回收范围并进行二次回收。如果这次回收后内存依然不足才会抛出OOM。场景非常适合做缓存。比如一个图片缓存器内存够的时候就留着内存不够时就清掉保证了程序的健壮性。弱引用Weak Reference特点通过WeakReference类实现。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。无论当前内存是否足够只要GC发生被弱引用指向的对象都会被回收。场景最典型的应用就是ThreadLocal和WeakHashMap。ThreadLocal的Entry继承了WeakReferencekey就是弱引用这样当ThreadLocal对象本身被回收后其对应的key也会在下一次GC时被清理有助于防止内存泄漏。虚引用Phantom Reference特点通过PhantomReference类实现是最弱的一种。它无法通过get()方法获取到对象的实例。它唯一的作用就是当对象被GC回收时能收到一个系统通知。虚引用必须与ReferenceQueue联合使用。场景主要用于堆外内存的回收。比如在NIO中使用直接内存DirectByteBuffer时JVM通过虚引用来管理和确保堆外内存被正确释放。第二部分垃圾回收GC核心4. 面试官JVM是如何判定一个对象是“垃圾”的能讲讲“三色标记”算法吗候选人判断对象是否存活主流的虚拟机如HotSpot采用的都是可达性分析算法。它的核心思想是从一组称为GC Roots的根对象出发通过引用链向下搜索整个搜索过程走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连即从GC Roots到这个对象不可达则证明此对象是不可用的可以被回收。常见的GC Roots包括虚拟机栈栈帧中的本地变量表中引用的对象。方法区中类的静态属性引用的对象。方法区中常量引用的对象。本地方法栈中JNI即一般说的Native方法引用的对象。Java虚拟机内部的引用如基本数据类型对应的Class对象常驻的异常对象等。关于三色标记算法 这是现代垃圾收集器如CMS、G1在并发标记阶段常用的一种算法用来解决在GC线程和应用线程并发执行时准确标记存活对象的问题。它将遍历对象图过程中遇到的对象按“是否访问过”标记成三种颜色白色尚未被垃圾收集器访问过的对象。在分析阶段结束所有白色对象都会被判定为可回收。黑色已经被垃圾收集器访问过的对象且该对象的所有引用都已经扫描过。它是安全存活的如果有其他对象指向了黑色对象无需重新扫描。黑色对象不可能直接不经过灰色对象指向白色对象。灰色已经被垃圾收集器访问过的对象但至少还有一个引用没有被扫描过。它是介于黑色和白色之间的中间状态。并发标记过程中如果用户线程修改了引用关系可能导致对象“消失”问题——即原本应该被标记为存活的对象黑色对象突然切断了与某个白色对象的引用导致该白色对象被错误回收。为了解决这个问题JVM引入了两种解决方案增量更新CMS采用和原始快照G1采用。5. 面试官现在G1收集器很流行你能对比一下G1和CMS的主要区别吗为什么G1会取代CMS候选人好的G1Garbage-First和CMSConcurrent Mark Sweep都是追求低停顿的并发收集器但它们的设计理念和实现方式有很大不同。CMS可以说是G1的前辈但G1在很多方面做了优化现在已成为JDK9的默认垃圾收集器。主要区别体现在以下几个方面维度CMS 收集器G1 收集器回收范围专门回收老年代需配合ParNew等新生代收集器。面向整个堆无需配合其他收集器。内存布局传统分代布局连续的新生代、老年代。分区Region布局将堆划分为多个大小相等的Region每个Region可以动态扮演Eden、Survivor或Old区逻辑上分代。回收算法标记-清除Mark-Sweep算法。整体上基于标记-整理Mark-Compact算法局部两个Region之间基于复制算法。核心目标获取最短回收停顿时间。建立可预测的停顿时间模型用户可以指定期望的GC停顿时间如 -XX:MaxGCPauseMillis200。垃圾碎片会产生大量内存碎片最终可能导致并发失败退化为Serial Old进行Full GC。通过复制算法进行整理不会产生内存碎片。G1取代CMS的原因可控的停顿时间CMS无法设定具体的停顿时间目标而G1通过将堆划分为Region并有选择地优先回收垃圾最多的RegionGarbage-First可以很好地控制回收成本实现接近实时的停顿。解决内存碎片问题CMS使用标记-清除长时间运行后会产生大量碎片导致分配大对象失败最终触发长时间的Full GC。而G1使用标记-整理通过复制对象进行压缩彻底解决了碎片化问题。更高的吞吐量和更智能的并发G1的设计能更好地利用多核CPU其并发标记阶段的“原始快照”算法也比CMS的“增量更新”算法在特定场景下能更有效地避免浮动垃圾问题。第三部分类加载机制6. 面试官能详细讲讲类的加载过程吗以及什么是双亲委派模型为什么要这么设计候选人当然。类从被加载到虚拟机内存开始到卸载出内存为止整个生命周期包括加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备、解析统称为连接。加载通过类的全限定名获取定义此类的二进制字节流可以从JAR、网络、动态代理等获取。将字节流所代表的静态存储结构转化为方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。验证确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的要求保证被加载类的正确性不会危害虚拟机自身的安全。包括文件格式验证、元数据验证、字节码验证、符号引用验证。准备为类变量static修饰的变量分配内存并设置初始零值如int为0对象引用为null。这里需要注意的是如果是static final的常量在准备阶段就会直接赋值为用户指定的值。解析将常量池内的符号引用替换为直接引用的过程。比如一个类的方法com/xxx/A.method这个符号在解析阶段会被替换成一个指向方法内存地址的指针或偏移量。初始化执行类构造器clinit()方法的过程。这一步才会真正执行我们编写的静态代码块中的代码和对静态变量的赋值操作。这是类加载的最后一步也是真正开始执行Java代码的阶段。关于双亲委派模型 当一个类加载器收到了类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到最顶层的启动类加载器Bootstrap ClassLoader中。只有当父类加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载。为什么这么设计避免类的重复加载父加载器已经加载过的类子加载器就不会再加载一遍。保证核心API的安全这是最重要的原因。它确保了Java核心类库如java.lang.Object的统一性。比如如果有人想自己写一个java.lang.String类并试图在JVM中加载由于双亲委派模型这个请求会一直委托给Bootstrap ClassLoader而Bootstrap ClassLoader会去加载rt.jar里的标准String类从而防止恶意代码冒充核心API。第四部分内存泄漏与调优实战大厂必问7. 面试官你在项目中有没有遇到过内存泄漏或OOM的情况是怎么排查的候选人注意这里要根据自己准备的案例来讲建议准备一个真实的或模拟的场景。下面是一个用ThreadLocal不当导致泄漏的常见案例候选人有的。之前在项目中使用ThreadLocal来存储一些用户上下文信息由于项目部署在Tomcat这种使用了线程池的容器中且在线程处理完请求后忘记调用ThreadLocal的remove()方法清理数据导致内存泄漏。当时现象是服务器运行一段时间后内存占用持续升高Full GC越来越频繁接口响应速度明显变慢最后出现了java.lang.OutOfMemoryError: Java heap space异常。排查过程我分了几个步骤获取现场首先我使用了JDK自带的jps命令找到出问题的Java进程ID。初步监控然后使用jstat -gcutil pid 1000 10每隔1秒打印一次GC情况。发现老年代Old的占用率OU一直在增长且Full GC次数FGC不断增加说明内存确实无法正常回收。Dump内存快照接着我使用jmap -dump:formatb,fileheap.hprof pid命令生成了一个堆内存快照文件。在生产环境操作时需要谨慎dump过程会暂停应用所以通常会配置JVM参数-XX:HeapDumpOnOutOfMemoryError让JVM在OOM发生时自动dump。分析Dump文件我把heap.hprof文件下载到本地用Eclipse MATMemory Analyzer Tool打开。MAT的Leak Suspects Report功能直接帮我定位到了问题java.lang.ThreadLocal$ThreadLocalMap占用了大量内存。定位代码顺着ThreadLocalMap的引用链我发现是某个自定义的UserContext对象一直被Thread对象引用着无法释放。检查代码后发现是在拦截器里设置了ThreadLocal值但在完成后没有在finally块中执行remove()。解决方案最终在拦截器的afterCompletion()方法中对ThreadLocal调用了remove()方法解决了这个内存泄漏问题。这次经历让我深刻理解了ThreadLocal必须与remove()成对使用特别是在线程池环境下否则线程复用会导致上一个请求的数据被下一个请求拿到造成业务逻辑错乱和内存泄漏。8. 面试官如果线上服务器CPU飙升到100%你怎么排查这可能和JVM有什么关系候选人CPU飙升通常有两种情况业务流量突然激增或者是代码进入了死循环或频繁GC。排查步骤一般是这样的定位进程使用top命令找到CPU使用率最高的那个Java进程记录其PID。定位线程使用top -H -p PID查看该进程内各个线程的CPU使用情况找到CPU占用异常的线程记录其TID线程ID。转换线程ID将线程的TID十进制转换为十六进制printf %x\n TID因为Java线程栈里的nid是用十六进制表示的。查看线程栈使用jstack PID | grep 十六进制nid -A 30打印出该线程的堆栈信息。通过堆栈我们可以看到这个线程具体在执行哪一段代码。可能和JVM的关系如果堆栈信息显示线程在GC相关的方法如ConcurrentMarkSweepThreadG1ParTask里那么CPU飙升很可能是因为频繁的垃圾回收特别是Full GC导致的。为什么会频繁GC通常是内存分配不合理或者有内存泄漏。频繁的GC会占用大量CPU资源导致业务线程执行缓慢。怎么验证可以同时执行jstat -gcutil PID 2000 10观察GC频率和耗时。如果FGC列的数字在快速增加那就印证了猜想。如果是GC问题就需要进一步分析堆内存如前面讲的Dump分析。如果不是GC问题而是业务代码如某个正则匹配、大循环那就根据栈信息去优化具体代码逻辑。