尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JVM核心知识全解析:内存模型、类加载机制与调优实战

JVM核心知识全解析:内存模型、类加载机制与调优实战 每个Java面试季JVM总是绕不开的那座山。很多刚入行的朋友问我要Java面试八股文资料我第一个推荐的必是JVM相关的笔记。为什么因为JVM是Java这门语言真正的“地基”也是区分“会用Java”和“懂Java”的分水岭。这篇文章总结了我从准备面试到日常工作排障中沉淀下来的JVM知识体系覆盖内存模型、类加载机制、垃圾收集器选型、调优参数与高频考题保证不堆砌术语用大白话把事情讲透让你看完能直接拿去面试也能实实在在用在排查线上问题上。这篇内容适合三类人准备校招或社招、正在刷Java面试题的朋友工作中天天跟Java程序打交道但遇到OOM、频繁GC就头疼的后端开发者想系统梳理一遍JVM知识体系却苦于资料太散太深读不下去的学习者。如果你属于其中任何一类请把这篇文章收藏起来按章节过一遍比零散刷题管用得多。1. 先搞清楚“三兄弟”JDK、JRE和JVM到底啥关系1.1 从一次环境配置说起我刚入行那会儿装Java环境照着教程配置了JAVA_HOME、PATH和CLASSPATH当时就好奇一个Java程序跑起来底下一堆文件夹到底是干什么用的bin目录里有什么jre目录又是干嘛的后来才明白这背后就是JDK、JRE和JVM三者的分工。一句话先说结论JDK是给开发人员用的完整工具包JRE是给运行环境用的运行时环境JVM是JRE内部真正执行字节码的虚拟机。很多人问“jre和jvm之间的关系”其实JRE就是“JVM加上Java核心类库和运行所需的基础组件”去掉JVMJRE就不成立但只装JVM又跑不起程序因为没有标准类库可调用。从目录结构来看JDKJava Development Kit包含binjavac编译工具、java启动命令、jstat/jmap/jstack等排查工具、include本地方法接口头文件、lib编译支持类库、jmods模块化镜像、以及整个JRE。JREJava Runtime EnvironmentJDK的一个子集包含binjava启动器、lib核心类库、时区数据、字体等。JVMJava Virtual Machine不是一个独立目录它藏在JRE/bin/server里面可能是一个动态链接库比如server\jvm.dll由java命令加载后负责承载程序运行。1.2 三者的职责边界用最贴近生活的比喻来说JDK像是一间“生产车间”。车间里既有原材料类库又有车床javac编译器还有质检员各种诊断工具如jmap、jstack。程序员用它把源代码“加工”成可交付的字节码.class文件和可执行程序.jar包。JRE像是一间“放映室”。它的职责只有一个——把已经做好的“影片”class文件播放出来。放映室里不需要车床不需要质检员只要一个播放器JVM和配套的音响灯光核心类库就够了。所以一台部署Java应用的服务器安装JRE就够了没必要装JDK。JVM则是那个“播放器”的本体。它负责读取字节码交给解释器或JIT编译器转化成机器指令再交给操作系统执行。这里有一个关键点JVM是跨平台性的根本保证。不同的操作系统上有不同的JVM实现但只要你把代码编译成class文件任何一个平台的JVM都能读、能跑。这就是“一次编写到处运行”的底层原因。注意从运行java程序的角度看操作系统不认识.class文件它只认机器码。JVM的价值就在于做了一层“翻译官”把平台无关的字节码翻译成平台相关的机器码。没有JVMJava也就不可能跨平台。1.3 面试官换着法问你该怎么答面试中关于这三者的题翻来覆去就是几种变化。第一种问法“JDK和JRE的区别是什么”标准答法JDK是Java开发工具包包含了编译器javac、JRE以及一系列开发调试工具服务于开发阶段JRE是Java运行时环境只包含JVM和核心类库服务于运行阶段。最好再补一句开发者在开发机上装JDK生产服务器上通常只需要JRE。第二种问法“JRE都包含哪些东西”这时候你如果能说出“JVM java核心类库rt.jar等 启动类加载器 配置文件”这类细节面试官会觉得你是真懂而不是背了个大框架。第三种问法“一台机器上没有JDK只有JRE可以编译Java文件吗”答案是否定的因为没有javac编译器。但如果你用工具比如Eclipse的ECJ编译器去编译那属于编译器的替代方案依然需要运行时环境才能执行。还有一些细节值得注意不管装的是JDK还是JRE环境变量里JAVA_HOME必须指向正确的安装目录。很多“no jvm could be found on your system”的报错就是因为JAVA_HOME配错了、PATH里没有java命令或者安装的只是JRE却不小心配了个JDK的路径。这类问题在Windows或者Linux服务器上都常见排查顺序永远是先看java -version是否正常再看JAVA_HOME是否指向了真实存在的目录。2. JVM内存模型这是面试八股文的“主战场”2.1 运行时数据区全景面试中只要问到“jvm内存模型”咱们讨论的其实是运行时数据区Runtime Data Area也就是JVM在启动后按规范把内存划分成的几个区域。划重点这些区域有的是线程私有的有的是线程共享的千万别记混。线程私有区程序计数器Program Counter Register、虚拟机栈JVM Stack、本地方法栈Native Method Stack。线程共享区堆Heap、方法区Method Area在JDK 8之后由元空间Metaspace实现。为什么要按线程划分很简单每个线程的执行路径都是独立的自己的调用栈、当前执行到哪一行这些信息必须自己保存而堆里的对象和方法区里的类元信息是全体线程都能访问的所以共享。2.2 堆新生代与老年代堆Heap是JVM管理的最大一块内存也是垃圾收集器工作的主战场。几乎所有对象实例都在这里分配。Java规范对堆的定义是“存放对象实例”而现代JVM还做了更细的分区典型方案是分代收集新生代Young Generation细分为Eden、Survivor From、Survivor To三个区默认比例为8:1:1。新对象首先在Eden区分配经过一次Minor GC后还存活的对象进入Survivor区在Survivor之间倒腾若干次默认15次可以通过-XX:MaxTenuringThreshold调整后仍然存活的对象晋升到老年代。老年代Old Generation存放生命周期较长的对象。当老年代空间不足时触发Major GC或Full GC。这个分代设计是经过了大量统计验证的绝大多数对象都是朝生夕死的把它放在Eden里快速回收效率最高。你想想一个简单的Spring Controller处理完一个请求内部创建的临时对象马上就没人引用了这些对象死在新生代就够了没必要推到老年代。2.3 虚拟机栈与栈帧虚拟机栈对应线程的“执行调用记录”。每个线程在执行方法时都会创建一个栈帧Stack Frame栈帧里保存了局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表里存的是基本数据类型、对象引用和returnAddress类型。特别注意局部变量表内存的单位是Slot槽64位长度的long和double占两个槽其余类型占一个槽。有些面试官喜欢问这个细节回答时要能形象说明——比如你在一个方法里声明了“long l 0L”它就会占用两个局部变量槽位。栈相关的异常有两个如果线程请求的栈深度大于虚拟机允许的深度抛StackOverflowError如果栈扩展时申请不到足够内存抛OutOfMemoryError。实际中StackOverflowError更常见通常就是无限递归导致的。写递归代码时一定要检查终止条件或者用循环代替深层递归。2.4 方法区与元空间的前世今生方法区用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。注意区分JDK 7及以前方法区在堆内被称为“永久代”PermGen且堆大小受-Xmx限制所以经常出现java.lang.OutOfMemoryError: PermGen spaceJDK 8之后方法区改为元空间Metaspace它使用本地内存不再受堆内存上限控制默认最大值是系统可用内存。这个改动的影响很实际以前如果项目里用CGLIB动态代理生成大量类很可能会打爆永久代换到元空间之后只要机器内存够大类元数据基本不会成为瓶颈。但反过来元空间也容易“无声无息”增长如果发生了内存泄漏比如每个请求都生成新的类加载器元空间也会被撑爆别忽略了它。2.5 常考面试题堆和栈到底存什么这个老生常谈的问题其实有标准答题框架基本类型的局部变量直接存栈里对象实例存在堆里对象的引用引用地址存在栈里。但更严谨一点说局部变量是基本类型时值直接存栈中的局部变量表。局部变量是引用类型时栈里存的是指向堆中对象实例的一个引用指针。成员变量实例属性跟随对象实例一起存放在堆里。静态变量属于类信息存放在方法区元空间中。网上有些说法把“栈区存引用堆区存对象”作为结论这没错但面试官如果追问“那字符串常量池在哪”“类的静态变量去哪了”很多候选人就卡壳。字符串常量池在JDK 7之前位于永久代JDK 7及以后移入堆中。静态变量JDK 8之前存在永久代里JDK 8之后跟随类元数据存在元空间。注意不同版本的JVM对内存区域的实现细节有差异面试时如果拿不准具体版本先说明“我以下说的是JDK 8的默认行为”这样既严谨又显专业。3. 类加载机制从.class字节码到可执行对象的完整链条3.1 类加载的五个阶段类加载机制在字节码层面的知识是JVM面试的重头戏。一个类从被JVM识别到最终能创建对象要经历**加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization**五个阶段平时我们常说的“类加载”其实是泛指前四个阶段初始化的完整过程。加载通过类的全限定名获取定义此类的二进制字节流将字节流所代表的静态存储结构转化为方法区的运行时数据结构并在堆中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口。简单说就是把.class文件里的内容读进JVM并建好对应的类模板。验证确保Class文件的字节流包含的信息符合《Java虚拟机规范》的全部约束要求。包括文件格式验证、元数据验证、字节码验证、符号引用验证。这一步是为了安全防止恶意字节码破坏JVM。准备为类变量static变量分配内存并设置类变量的初始零值。注意这里的“初始零值”不是代码里写的初始值而是默认零值。比如“public static int a 100”在准备阶段a先被设为0真正被设为100发生在初始化阶段。解析将常量池内的符号引用替换为直接引用。符号引用即“我们说的这个类叫什么”的一组字面量符号直接引用即“这个类/字段/方法在内存中的实际地址”。这一步对理解Spring、MyBatis等框架的动态代理尤其重要因为代理类在解析时引用的目标最终指向运行时才确定。初始化执行类构造器中的代码也就是执行静态变量的赋值动作和静态代码块的内容。这是类加载真正开始执行用户代码的阶段。3.2 双亲委派模型必须会画会说双亲委派模型是类加载机制的必考题。JVM默认有三层类加载器从上到下是启动类加载器Bootstrap ClassLoader加载JDK目录下lib中的核心类库如rt.jar里的java.*包由C实现是JVM的一部分。扩展类加载器Extension ClassLoader加载JDK目录下lib/ext中的类库JDK 9之后改为平台类加载器Platform ClassLoader。应用程序类加载器Application ClassLoader加载用户写的classpath中的类是开发中最常接触到的加载器。双亲委派的核心逻辑是当一个类加载器收到类加载请求时它不会自己先去加载而是先把这个请求委托给父类加载器每一层都如此最终传到启动类加载器只有父类加载器反馈无法完成加载时子加载器才自己尝试。用大白话说你写的类要被加载先问“你爸”能不能加载你爸再问“你爷爷”能不能加载爷爷说“我来”那就爷爷加载爷爷说“我不会”爸爸来爸爸也不会才轮到你。为什么这么设计核心就三个字安全性。以java.lang.String为例它必须由启动类加载器加载这样JVM核心类的权限都在最高层管控下。如果你自己能随便定义一个同包同类名的String去代替JDK里的类那JVM的安全防线瞬间崩塌各种恶意替换就随便玩了。其次是避免重复加载一个类不管被哪个加载器请求最终都由最顶层的同一个加载器加载一次保证类的唯一性。3.3 哪些场景要打破双亲委派面试中经常追问“双亲委派是必须遵守的吗有没有打破它的场景”答案是它不是硬性规范实际确实有合理的打破场景。最典型的场景是SPIService Provider Interface机制比如JDBC驱动加载。java.sql.DriverManager是启动类加载器加载的但它要调用各数据库厂商实现的驱动类比如com.mysql.cj.jdbc.Driver这些驱动类是放在应用classpath里的启动类加载器根本找不到。这时候就必须由应用类加载器去加载于是JDK引入了线程上下文类加载器Thread Context ClassLoader让父加载器在需要时能委托子加载器去加载用户代码。另一个典型是Tomcat等Web容器。每个Web应用都希望用到自己内部“隔离”的类库版本同时互不影响。Tomcat用一个WebAppClassLoader加载每个应用自己的类这明显打破了“先让父加载器加载”的双亲委派链。如果你在Tomcat里部署了多个应用一个应用的jar包版本冲突不能影响另一个应用那依赖类的加载就必须做到“每应用一锅”。面试回答这类问题时最好搭着例子讲不光是“我知道有打破”还能说出JDBC、Tomcat、OSGi等具体案例这比背定义高一个段位。4. 垃圾收集器选型与G1实战4.1 垃圾收集的基本判定从引用计数到可达性分析谈JVM必谈GC谈GC首先得弄明白JVM怎么知道一个对象该不该回收主流方案是可达性分析Reachability Analysis。从一组称为GC Roots的根对象出发沿着引用链向下搜索凡是到不了的对象就判定为可回收。GC Roots包括虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、本地方法栈中JNI引用的对象等。注意能作为GC Roots的必须是“根”即不会被别人持用的起点。如果面试官问“什么是GC Roots”拿上面的四点作答即可。这里有个坑引用计数算法曾被认为是判断对象存活的简单方法它实现简单、判定高效但它解决不了循环引用问题。JDK的默认实现并不用引用计数而是用可达性分析。如果你在回答里主动提及“现在主流的商业JVM都采用可达性分析而不是引用计数”会给面试官留下好印象。4.2 从CMS到G1为什么“大而全”的G1成了默认以前Java的垃圾收集器有Serial、ParNew、Parallel Scavenge、CMS、G1等一堆选择。很长一段时间里CMS是追求低延迟场景的主流选择但它有一个致命伤使用标记-清除算法会产生大量内存碎片。当大对象无法分配时CMS会触发Full GC而Full GC的STWStop The World暂停所有用户线程时间非常长最容易引发线上告警。G1Garbage First收集器从JDK 9开始成为默认垃圾收集器它的设计思路和之前的收集器完全不同。G1把堆划分为多个大小相等的Region区域不再固定区分新生代和老年代的连续内存块每个Region可以动态扮演Eden、Survivor或者老年代的角色。这种分区的做法让G1可以用“预测停顿时间模型”来控制GC停顿你可以用-XX:MaxGCPauseMillis指定停顿目标默认是200毫秒G1会根据负载自动调整每次回收的Region数量。G1最大的特点是可预测的停顿时间这对互联网高并发、低延迟业务非常重要。线上很多接口要求P99延迟在几百毫秒内如果一次Full GC停了2秒大量请求就会超时。G1能尽量把每次GC停顿控制在目标范围内这个优点让它在JDK 9之后取代CMS成为默认。4.3 G1的内存模型与关键机制G1的内存模型和传统分代模型有区别面试问“jvm g1内存模型”时可以围绕以下几点展开Region划分整个堆被划分为约2048个Region每个Region大小固定1MB~32MB取决于堆大小。每个Region可以属于Eden、Survivor、Old或Humongous区。Humongous区用于存放超过Region大小50%以上的大对象这类对象如果跨Region存放会带来很高的移动成本所以单独划分。Remembered SetRSet每个Region维护一个RSet记录哪些外部Region引用了本Region内的对象。GC时通过RSet就能快速判断本Region内的对象是否被外部引用避免对整个堆扫描。这是G1性能的重要基础。SATBSnapshot At The BeginningG1使用SATB算法处理并发标记阶段的引用变化问题。它保证在标记开始时打一个快照之后对象布局的变化不会影响标记正确性从而避免长时间STW。Mixed GCG1的落地点是Mixed GC既回收新生代也回收部分老年代Region目标是清理“垃圾最多”的区域也就是Garbage First名字的由来。一个有意思的细节是G1在大对象分配和回收上比CMS更灵活但并不是说G1在所有场景都优于CMS。对于堆很小比如几百MB或者对吞吐量要求极高如批处理任务的场景选择Parallel ScavengeSerial Old未必比G1差。提示JDK 11之后还有一个ZGC它把STW时间压缩到10毫秒以内适合超大堆几十GB乃至几百GB的低延迟场景。但如果业务用不上那么大堆G1仍然是最稳妥的默认选择。面试时如果能顺带提一嘴ZGC和G1的适用边界会显得见解比较成熟。4.4 如何排查看G1的参数与配置在应用启动参数里常见的G1配置有-XX:UseG1GC使用G1收集器JDK 9之后默认开启可以不写。-XX:MaxGCPauseMillis200设置期望的最大GC停顿时间。-XX:G1HeapRegionSize8m手动指定Region大小不建议手动改除非你很清楚影响。-XX:InitiatingHeapOccupancyPercent45设置触发并发GC周期的堆占用百分比默认45%。-XX:G1NewSizePercent5、-XX:G1MaxNewSizePercent60新生代容量在堆中的占比上下限。排查看板一般看GC日志就好。加上-verbose:gc -Xlog:gc*JDK 9之后日志风格或-XX:PrintGCDetailsJDK 8风格从日志里重点看GC停顿时间、晋升到老年代的对象量以及Mixed GC触发频率。分享一个我实际踩过的坑有一次线上服务堆设置4GB遇到高峰时段Full GC频繁。起初以为是老年代不够盲目调大-Xmx结果GC停顿反而更明显。后来用jstat一看发现新生代里Survivor区太小动态年龄判断把大量对象过早晋升到老年代老年代缓存了一些业务中间数据导致Mixed GC疯狂触发。最后调整了SurvivorRatio和MaxTenuringThreshold问题就缓解了。这个案例说明调优最忌“头痛医头、脚痛医脚”先看GC日志和内存分配情况再动参数。5. JVM调优参数与线上问题排查实录5.1 必背的调优参数清单很多朋友背参数背得头疼其实JVM参数的核心套路并不复杂。按功能分类记忆就好堆内存相关-Xms堆初始大小。生产环境建议和-Xmx设成一样避免运行期堆伸缩带来的性能抖动。-Xmx堆最大大小。-Xmn新生代大小通常设置为堆大小的1/3到1/4。-XX:SurvivorRatioEden区与Survivor区的比值默认是8也就是Eden占新生代的8/10。-XX:MaxTenuringThreshold对象在新生代的最大年龄超过后晋升老年代。非堆内存相关-XX:MetaspaceSize元空间初始大小。-XX:MaxMetaspaceSize元空间最大值不设置的话可能无限增长。-Xss每个线程的栈大小默认值因平台而异一般256KB到1MB。线程数非常多时需要调小这个值不然栈空间会把内存吃光。GC与日志相关-XX:PrintGCDetails打印GC详细日志JDK 8及以前。-Xloggc:gc.log将GC日志输出到文件。-XX:HeapDumpOnOutOfMemoryErrorOOM时自动导出堆快照。-XX:HeapDumpPath堆快照的保存路径。-XX:OnOutOfMemoryErrorOOM发生后执行一段脚本比如自动重启服务或通知值班人员。JIT编译相关-XX:CompileThreshold方法调用多少次后触发JIT编译默认是10000。这个参数可能出现在热搜词里面试中偶尔会被问到。低配机器或对启动速度要求高的场景可以把它调低让代码更早被编译为本地代码提升运行效率但要注意JIT编译本身也耗CPU调太低会导致编译线程抢资源。5.2 一次完整OOM排查过程OOM是线上最经典、也最让人头秃的问题之一。热搜词里有“java: outofmemoryerror: insufficient memory”这通常出现在容器内存受限、JVM堆超预算、或交换分区不足的场景。排查思路我总结成四步屡试不爽。第一步保留现场。在启动参数里必须加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/logs/dump。没有这两行OOM之后堆当场释放你什么都查不到。有了它们OOM发生时堆快照会自动落地事后分析就有据可依。第二步从日志定位异常类型。查看日志里的OutOfMemoryError是Java heap space、Metaspace还是GC Overhead Limit Exceeded。不同报错指向的内存区域不同排查方向完全不同。Java heap space看堆内大对象或泄漏Metaspace看动态类生成是否失控GC Overhead Limit Exceeded基本上意味着GC一直在回收但效果很差程序基本处于瘫痪状态。第三步分析堆快照。用Eclipse MAT或者VisualVM打开dump文件查看“Leak Suspects”报告。MAT里最常用的就是Dominator Tree看哪个大对象占用了最多堆内存以及它的引用链。有一次我排查一个接口OOM一抓dump发现HashMap里有上千万个缓存条目的key都是同一个业务ID的前缀拼接明显是某段循环代码误把临时变量放进了容器静态缓存。如果是无缓存、纯函数逻辑也OOM就怀疑对象创建数量爆炸一般配合jstat查看GC频率和堆使用曲线。第四步验证并回归。定位到疑似泄漏代码后修改、发布、观察堆内存曲线是否回归到正常水平。这里有一个经验技巧监控不能只看OOM前的瞬时快照要看发布后一周的内存趋势。如果堆还是缓慢上升说明泄漏没有根除。5.3 启动报错类问题速查很多朋友在环境上栽过跟头报错信息让人崩溃。我把高频启动报错整理成一个速查表遇到就能见招拆招报错特征可能原因快速处置no jvm could be found on your systemJAVA_HOME未配置或配置错误检查java -version重新配置环境变量could not get jvm parameters and dynamic configurations properly启动脚本参数格式不对或权限不足核对启动脚本确认日志目录可写源发行版17需要目标发行版17Maven和IDE的JDK版本参数不一致统一maven.compiler.source/target与当前JDK版本lombok not workingLombok版本与JDK版本不兼容升级Lombok到支持当前JDK的版本容器部署Java程序异常重启容器内存限制与JVM堆参数冲突设置-XX:MaxRAMPercentage或显式-Xmx别超过容器配额关于容器部署的问题多说一句Docker容器里跑Java应用如果不显式设置堆内存JVM会尝试读取宿主机的内存来初始化极可能导致容器被OOM Killer杀掉。规范做法是在Dockerfile或启动脚本里用-XX:MaxRAMPercentage75.0JDK 8u191支持让JVM按容器配额计算堆上限或者干脆直接指定-Xmx。5.4 常用排查工具别只会jps工具不在多会用才关键。我最常用的组合是jps查看Java进程ID。很多新手上来就ps -ef也没错但jps能直接列出Java进程和主类名更省事。jstat -gcutil 1000每秒打印一次GC情况看Eden、Survivor、Old的占用百分比以及YGC/FGC次数和耗时。定位GC频率和内存走势的第一选择。jmap -dump:formatb,filedump.hprof导出堆转储快照。注意在生产环境要用否则可能冻结服务。jstack打印线程堆栈。服务卡死、CPU飙高、死锁时用jstack看线程状态最直接。之前遇到一次CPU打满jstack一抓发现某线程卡在一个while循环里狂转配合pid找到业务代码两分钟就定位了。jcmd helpJDK自带的多功能命令可以替代jmap、jstack的一部分功能。MAT / VisualVM离线的堆分析工具分析dump文件必备。MAT里的Leak Suspects Report是入门最快的入口。排查线上问题时我习惯的流程是先用jps定位进程再用jstat看GC状态接着用jstack看线程状态如果有需要再jmap导出堆快照。顺序不要搞反不然容易在错误方向上浪费时间。6. 面试高频题速答清单6.1 一轮快速问答过一遍基础JVM面试八股文的题目翻来覆去就是那些但很多人栽在“知其然不知其所以然”。把这些高频题整理出来每个问题后面附上我建议的回答思路问JVM、JRE、JDK三者的关系答JDK是Java开发工具包包含编译器、调试工具和JREJRE是Java运行时环境由JVM和Java核心类库组成JVM是Java虚拟机负责执行字节码是跨平台的基础。问JVM内存模型是怎样的答分为线程私有的程序计数器、虚拟机栈、本地方法栈以及线程共享的堆、方法区JDK 8后元空间实现。堆再细分为新生代和老年代新生代又分Eden、Survivor From、Survivor To。问对象什么时候进入老年代答大对象直接进老年代经过多次Minor GC默认15次仍然存活的对象进入老年代动态年龄判定Survivor中同龄对象总大小超过Survivor区一半时大于等于该年龄的对象直接进老年代新生代空间不足时晋升。问什么是双亲委派模型为什么要这样设计答类加载请求先委派给父类加载器父类无法加载时才由自己加载。核心目的是保证核心类库的安全性避免类被重复加载和恶意替换。问G1和CMS有什么区别答CMS基于标记-清除算法会产生内存碎片且并发阶段会产生浮动垃圾G1基于Region分区采用标记-整理复制算法能预测GC停顿时间适合大堆和低延迟场景。G1从JDK 9起成为默认收集器。问什么是STW答Stop The WorldGC过程中为了确保对象引用关系的准确性需要暂停所有用户线程的短暂停顿。GC优化的重要目标之一就是缩短STW时间。问怎么排查线上OOM答先用启动参数确保OOM时能导出堆快照-XX:HeapDumpOnOutOfMemoryError分析报错是heap space还是metaspace然后用MAT/VisualVM分析dump定位大对象和引用链最后修复代码并观察内存曲线回归。6.2 面对追问怎么显得有深度八股文背得滚瓜烂熟还不够面试官最爱追问的就是“能不能再往深一层”。如果他问“新生代为什么要分Eden和两个Survivor一个Survivor不行吗”答两个Survivor是为了避免内存碎片化。复制算法需要两块相等的区域来放置存活对象一个Survivor充当“留营地”另一个充当“收容所”每次GC把存活对象从一个Survivor复制到另一个然后清空原来的区。如果他问“可达性分析为什么要从GC Roots出发”答只有从根出发遍历不到的对象才是真正的垃圾。根是Live对象集合的入口只要根对象可达的引用链不被切断对象就活着。如果他问“G1的RSet是怎么保证性能的”答RSet记录了引用关系让GC时不需要扫描整个堆来确认对象是否被外部引用用空间换时间保证收集效率。如果他问“有哪些情况会导致Full GC频繁”答老年代空间不足、元空间不足、大对象过多、System.gc()被显式调用、CMS的concurrent mode failure等。排查时要结合GC日志具体分析。面试官真正考察的是你有没有独立思考和排查经验所以在回答时最好穿插一个自己遇到的真实案例。哪怕很小讲清楚“现象—排查过程—结论”比背诵十条定义都有说服力。6.3 一套学习路线比刷题更重要最后再分享一点个人经验JVM的学习不要一上来就啃《深入理解Java虚拟机》整本书更不要只刷“jvm面试题合集”。我建议按“先搭框架、再补细节、最后实战”的顺序走。第一步把内存模型、类加载机制、GC这三块作为主线框架。这三块是JVM的三大支柱其他知识都是围绕它们展开的。第二步学完框架后自己用jmap、jstat、jstack把本机跑一个简单程序观察一遍。哪怕只是反复执行一个会产生临时对象的循环观察Eden区占用、Minor GC频率、老年代增长亲手看到的数据会让你对概念的理解直接“落地”。第三步尝试模拟一次内存泄漏写一段无限往静态List里add对象的代码主动触发OOM再用MAT分析dump。这个过程非常折磨人但做完一遍你对OOM的恐惧就消失了下次线上出现问题你能更快稳住阵脚。我在带新人时发现一个共性能在面试里把JVM讲得生动的人几乎都是亲手排查过GC问题、或者自己做过内存调优实验的人。学习JVM没有捷径但有清晰的路径。把今天的知识框架吃透再动手做两三次实战你就能在面试中从容应对在线上问题前不再心慌。
返回列表