十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JVM 篇 · Java 架构师面试备考文档

JVM 篇 · Java 架构师面试备考文档 对应 2 周计划:D1-D2 | 优先级: 必考 | 大厂一面必问,二面常追问调优实战〇、设计哲学:JVM 为什么这么设计?(先读这节)一句话主线:JVM 的一切设计都是「权衡」——每个机制背后都是 问题 → 权衡 → 选择。JVM 设计哲学:一切皆为「权衡」① 用「便宜方案」处理大概率事件对象大多早死 → 新生代用复制算法锁大多无竞争 → 先偏向/轻量热点代码少 → 只 JIT 编译热区(延迟付出成本,按需加码)② 牺牲「局部」换「整体」STW 暂停所有线程 → 换来正确 GC方法区挪到元空间 → 换来不易 OOM对象头塞锁信息 → 换来内存复用(为正确性让渡部分性能)③ 用「空间」换「时间」/时间换空间TLAB 划线程私有区 → 减少分配竞争RSet 记录跨区引用 → 避免全堆扫描GC Roots 标记 → 换来不扫全堆(牺牲内存换速度,或反之)④ 正确的算法依赖正确的观测弱分代假说 → 分代收集竞争统计 → 锁升级热点计数 → JIT 编译(JVM 边跑边「观察」,动态调优)面试答「为什么」的万能句式「因为 X 场景占比高/代价大,所以 JVM 选择 Y 方案换取 Z —— 一次 A vs B 的权衡。」图 0 · JVM 设计哲学四原则 —— 下面的每个 Q 都是权衡的产物四个核心思想:1.用便宜方案处理大概率事件:对象大多早死 → 新生代用复制算法;锁大多无竞争 → 先偏向/轻量;热点代码少 → 只 JIT 编译热区(延迟付出成本,按需加码) 2.牺牲局部换整体:STW 暂停所有线程 → 换来一次正确的 GC;方法区挪元空间 → 换来不易 OOM(为正确性让渡部分性能) 3.空间换时间/时间换空间:TLAB 划线程私有区 → 减少分配竞争;RSet 记录跨区引用 → 避免全堆扫描 4.正确的算法依赖正确的观测:弱分代假说 → 分代收集;竞争统计 → 锁升级;热点计数 → JIT 编译各机制的设计思路:为什么要有 JVM:在「你的代码」和「操作系统」之间加一层你写的 Java 代码(.java)只写一次,不关心跑在什么系统上编译器 javac → 字节码(.class)与操作系统无关的中间表示,这是跨平台的关键JVM(Java 虚拟机)解释/编译执行字节码 · 自动内存管理(GC) · 线程调度屏蔽了操作系统差异 —— 你不需要自己 free 内存、不需要管系统调用操作系统(Windows / Linux / macOS)JVM 是运行在 OS 上的一个「程序」,把差异都挡在它下面三个设计动机(面试答题开头)① 跨平台:字节码 虚拟机,一次编写到处运行② 自动内存管理:GC 接管内存,代价是停顿与开销(权衡!)图 0-1 · 分层的代价:多一层就多一层开销,但换来跨平台 自动内存为什么有 JVM:跨平台(字节码中间层) 自动内存管理(GC);代价是性能损失(用 JIT 补偿)和 GC 停顿为什么分线程私有/共享:私有区域无锁无竞争所以快;共享区域让对象跨线程可见但需要 GC 兜底为什么用栈:方法调用天然 LIFO,栈帧大小编译期可确定,分配只需移动指针;方法返回自动弹出,生命周期确定 → 不需要 GC为什么分代:弱分代假说——90% 对象朝生夕灭,分代后用不同算法低成本回收最多垃圾为什么锁升级:大多数锁无竞争,先用偏向锁/轻量锁(便宜),竞争加剧才升级重量级(贵)为什么 GC 要 STW:可达性分析要求引用关系静止,短暂停顿换一次正确回收;CMS/G1 把部分步骤挪到并发减少停顿为什么不用引用计数:解决不了循环引用;可达性分析从 Roots 出发天然免疫面试答题串联:「JVM 的设计核心是一连串权衡。它存在的意义是跨平台 自动内存管理,为此引入字节码和 GC。内存分线程私有/共享:私有区域无锁无竞争所以快,共享区域让对象跨线程可见但需要 GC 兜底。堆分代基于弱分代假说——绝大多数对象朝生夕灭,所以新生代用复制算法低成本回收,老年代用标记整理稳妥处理长寿对象。GC 需要 STW 是因为可达性分析要求引用关系静止,这是用短暂停顿换正确性。锁升级体现了延迟付出成本的思想。JVM 还会边运行边观测,用运行时信息动态优化。」一、JVM 内存区域Q1: JVM 内存分为哪些区域?哪些线程私有、哪些共享?答案要点:-线程私有:程序计数器(PC)、虚拟机栈(Java 栈)、本地方法栈 -线程共享:堆(Heap)、方法区(Method Area,JDK8 为元空间 Metaspace) -额外:直接内存(Direct Memory,堆外,不受 GC 管理)逐区域说明:| 区域 | 作用 | 异常 | |---|---|---| | 程序计数器 | 当前线程执行字节码的行号指示器 | 无 OOM(唯一不会 OOM 的区域) | | 虚拟机栈 | 存储栈帧(局部变量表、操作数栈、动态链接、返回地址) | StackOverflowError / OOM | | 本地方法栈 | 为 native 方法服务 | StackOverflowError / OOM | | 堆 | 对象实例与数组,GC 主战场 | OOM: Java heap space | | 方法区/元空间 | 类信息、常量、静态变量、JIT 产物 | OOM: Metaspace(元空间默认无上限,受物理内存限制) | | 直接内存 | NIO 使用堆外内存,避免拷贝 | OOM: Direct buffer memory |深追问:- 为什么 JDK8 用元空间替代永久代?→ 永久代大小固定易 OOM;字符串常量池移入堆;元空间使用本地内存,受物理内存限制更灵活 - JDK7 中字符串常量池在永久代 → JDK8 移到堆中 - 栈帧里有什么?→ 局部变量表(基本类型引用returnAddress)、操作数栈、动态链接、方法返回地址Q2: 对象创建流程?答案要点(7 步):1.类加载检查:new 时检查类是否已加载、解析、初始化(符号引用→直接引用) 2.分配内存:堆中划分一块内存(指针碰撞 or 空闲列表,取决于堆是否规整,规整由 GC 算法决定) 3.处理并发安全:CAS 失败重试(乐观)或 TLAB 本地线程分配缓冲(优先) 4.内存空间初始化零值:保证字段不赋值也可用 5.设置对象头:Mark Word(锁状态、hashcode、GC 分代年龄)、类型指针 6.执行 init 方法:即构造函数,完成字段初始化深追问:- 指针碰撞 vs 空闲列表?→ 内存规整用指针碰撞(复制/标记整理),不规整用空闲列表(标记清除) - TLAB 是什么?→ Thread Local Allocation Buffer,线程私有缓冲区,减少并发分配竞争,默认开启,-XX:-UseTLAB 可关闭Q3: 对象在内存中的布局?答案要点:-对象头(Mark Word 类型指针):Mark Word 存锁信息/hashcode/GC 年龄;数组还有数组长度 -实例数据:字段内容 -对齐填充:HotSpot 要求 8 字节对齐深追问:- Mark Word 里锁状态怎么变的?→ 无锁 → 偏向锁 → 轻量级锁 → 重量级锁(与 synchronized 锁升级对应)二、类加载机制Q4: 类加载过程?答案要点(加载 → 验证 → 准备 → 解析 → 初始化):1.加载:通过全限定名获取二进制字节流 → 转为方法区运行时数据结构 → 生成 Class 对象 2.验证:文件格式、元数据、字节码、符号引用验证(安全性) 3.准备:为静态变量分配内存并设默认零值(注意:final static 在此阶段直接赋初值) 4.解析:符号引用 → 直接引用(可延迟到初始化后,动态绑定) 5.初始化:执行 clinit 方法,赋静态变量真实值、执行静态代码块(父类先初始化)深追问:- 什么情况会触发初始化(主动引用)?→ new/getstatic/putstatic/invokestatic、反射、初始化子类先初始化父类、main 类、JDK7 动态语言 - 什么情况不会触发?→ 引用父类静态字段(只初始化父类)、定义数组、引用常量(编译期进常量池)、Class.forName(name, false, loader)Q5: 双亲委派模型?答案要点:- 层级:Bootstrap ClassLoader(启动,lib/rt.jar)→ Extension(扩展,JDK9 改为 Platform)→ Application(应用,classpath) - 流程:类加载请求自底向上委派,父加载器能加载则父加载,不能则子加载 - 目的:避免类重复加载 安全性(防止自定义 java.lang.String 替换核心类)深追问(必问):- 怎么打破双亲委派?→ 重写 loadClass(而不是 findClass),如 Tomcat(隔离 Web 应用)、JDBC SPI(ServiceLoader)、OSGi - 为什么 JDBC 要打破?→ DriverManager 是 Bootstrap 加载,但驱动实现是应用类,需线程上下文类加载器(Thread Context ClassLoader)反向下钻 - 为什么打破后要破坏?→ 实现父加载器请求子加载器加载的 SPI 场景三、GC 与垃圾回收Q6: 判断对象可回收?答案要点:-可达性分析算法(主流):以 GC Roots 为起点向下搜索,不可达即回收 - GC Roots 包括:虚拟机栈引用、静态属性引用、常量引用、本地方法栈 JNI 引用、活跃线程、锁持有的对象 - 补充:引用计数法(无法解决循环引用,已弃用) - 四种引用:强引用(不回收)、软引用 SoftReference(内存不足回收)、弱引用 WeakReference(下次 GC 即回收)、虚引用 PhantomReference(跟踪回收)深追问:- finalize() 方法?→ JDK9 已废弃,对象自救机会,不推荐使用 - 一个对象可被回收要经历几次标记?→ 两次:第一次不可达,第二次(在 finalize 中未自救)才回收Q7: 三大 GC 算法?算法原理优点缺点标记-清除标记后直接清除简单碎片化、效率低标记-复制内存分两块,存活对象复制到另一块无碎片、高效空间浪费(浪费一半)标记-整理标记后存活对象向一端移动无碎片、空间利用率高移动成本高,STW 长深追问:- 为什么新生代用复制?→ 新生代对象 90% 存活率低,朝生夕灭,复制成本小;Eden:Survivor0:Survivor1 8:1:1(浪费 10%) - 为什么老年代不用复制?→ 存活率高,复制成本大Q8: 分代收集与回收器?答案要点:- 新生代:Serial、ParNew、Parallel Scavenge - 老年代:CMS、Serial Old、Parallel Old - 跨代:G1、ZGC核心回收器对比(大厂高频):| 回收器 | 特点 | 停顿 | |---|---|---| | CMS | 老年代,标记-清除,并发收集 | 低停顿,有碎片 | | G1 | 全堆分区,Region 化,可预测停顿 | 可指定 MaxGCPauseMillis | | ZGC | 染色指针读屏障,停顿 10ms | 极低停顿,大堆 |CMS 详细流程(必背):1. 初始标记(STW,快) 2. 并发标记(与业务并发) 3. 重新标记(STW,修正并发期间变化) 4. 并发清除CMS 缺点:CPU 敏感、浮动垃圾、Concurrent Mode Failure(触发 Serial Old 兜底)、内存碎片G1 详细流程:1. 初始标记(STW) 2. 并发标记 3. 最终标记(STW) 4. 筛选回收(可配置停顿时间)G1 特点:Region 分块(每块 1-32MB)、Humongous 大对象区、Remembered Set 记录跨区引用、可达性分析不扫描全堆Q9: 什么时候触发 GC?怎么调优?触发时机:- Minor GC:新生代 Eden 满(复制到 Survivor,存活次数 1,15 次进老年代) - Major/Old GC:CMS 老年代空间使用率达到阈值(默认 92%) - Full GC:老年代满、Metaspace 满、System.gc()、大对象直接进老年代(空间不足) - 动态年龄判定:Survivor 同年龄对象总和 Survivor 一半,直接进老年代调优排查(线上实操,大厂必问):# 查看进程 PID jps -l # 查看堆使用、GC 情况 jstat -gcutil pid 1000 # 每秒刷新,看 FGC 次数、Old 区占比 # 打印堆转储 jmap -dump:formatb,fileheap.bin pid # 线程快照(排查死锁/线程阻塞) jstack pid thread.log # 查看 JVM 参数 jinfo -flags pid # 常用参数 -Xms4g -Xmx4g # 堆大小(建议相等避免扩容) -Xmn2g # 新生代 -XX:UseG1GC # 或 -XX:UseConcMarkSweepGC -XX:MaxGCPauseMillis200 # G1 停顿目标 -XX:HeapDumpOnOutOfMemoryError -XX:PrintGCDetails调优思路:1. 先看指标:GC 频率、停顿时间、吞吐量 2. Full GC 频繁 → 老年代容量不足 / 大对象过多 / 内存泄漏 3. 排查泄漏:jmap dump → MAT 分析大对象、支配树 4. 调整:加大堆、调整新生代比例、换 G1、优化代码(减少大对象、释放引用)四、考前速记(10 条)线程私有:PC/栈/本地方法栈;共享:堆/元空间对象头 Mark Word 存锁状态;8 字节对齐类加载 5 阶段:加载→验证→准备→解析→初始化双亲委派:自底向上委派;破坏方式:重写 loadClassJDBC 打破双亲委派用线程上下文类加载器(SPI)可达性分析 GC Roots;引用计数法已废弃新生代复制算法 8:1:1;老年代标记整理/清除CMS 四步:初始标记→并发标记→重新标记→并发清除(两次 STW)G1:Region 化、RSet、可预测停顿、MaxGCPauseMillis排查命令:jps / jstat -gcutil / jmap dump / jstack / jinfo五、易错点提醒「方法区 永久代 元空间」是演进关系,不是同一概念:JDK8 前永久代实现方法区,JDK8 元空间实现Full GC 时新生代和老年代都会被回收,不只是老年代双亲委派破坏不是坏事,Tomcat 破坏是为了类隔离静态变量准备阶段赋默认值,初始化阶段才赋真实值(final static 例外,准备阶段赋初值)
返回列表