想象你是一家快速扩张公司的运维主管,公司租下了一整栋写字楼。这栋楼就是 JVM 进程:一层是中央仓库(堆),全公司共享,所有货物(对象)都码在这里;每层工位上都贴着一本任务便签(虚拟机栈),员工每接手一个任务就翻开新的一页,做完就撕掉;楼道尽头锁着岗位说明书和规章制度手册(方法区/元空间),写清每个岗位有哪些职责、流程怎么走;而每位员工的手指(程序计数器)始终点在自己正在读的那一行上,提醒自己做到哪一步。有一天仓库爆满、货堆到天花板,员工加班搬货(GC)也搬不干净——这就是传说中的 OOM。搞懂这栋楼里谁管什么、东西怎么流动,你才能准确定位“哪里满了、为什么满、怎么腾”。本文就用这家公司的故事,把堆、栈、方法区、程序计数器的分工讲透,再带你走一遍对象从 new 到 GC 的一生,最后给你一份 OOM 排查与调优的实战清单。
对 AI 从业者而言,AI 服务部署离不开 JVM 系基础设施(Java 网关、Flink/Spark、Elasticsearch)。看懂内存模型,OOM 不再靠猜、调优不再靠蒙,这是模型服务稳定性的基本功。
可视化演示
展示JVM内存布局:堆分代结构(Eden/Survivor/Old)、虚拟机栈帧组成、以及OOM类型诊断,交互式探索各内存区域职责与对象分配过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
堆内存分代结构 新生代=Eden+S0+S1,对象优先在Eden分配 Survivor区经历GC后晋升老年代 heap = eden + s0 + s1 + old 分代设计基于弱代假说
内存模型总览
这栋写字楼一进门就能看到一张平面图:哪些是公共区域、哪些是私人区域。公共区域有两处——中央仓库(堆)和岗位手册库(方法区),所有人都能进;私人区域有三处——每位员工工位上的任务便签本(虚拟机栈)、外包专家的专用工作台(本地方法栈),还有员工那根总是指着当前阅读位置的手指(程序计数器)。JVM 运行时数据区,就是这五块。
从线程的视角看,这种“公共 vs 私人”的划分是理解内存的钥匙。堆和方法区被所有线程共享,任何线程 new 出来的对象、加载进来的类都要放进去;而虚拟机栈、本地方法栈、程序计数器都是线程私有的——线程创建时各配一份,线程销毁时随之清零。换句话说,你开的线程越多,私有区占用的内存就越多,这也是高并发服务内存暴涨的一个重要来源。
Java 8 之后,方法区的落点从“永久代”换成了“元空间(Metaspace)”。永久代在堆内、有固定上限,容易踩线;元空间改用本地内存,默认只受物理内存约束。这一改变让类加载导致的“手册库爆满”问题不再那么容易触发,但也带来了新的排查点。内存模型总览,记住“两共享、三私有、元空间取代永久代”即可。
拿到任何一份内存监控数据,先对着这张“大楼平面图”看数字落在哪块区域——是仓库满了、还是某层工位太多。这就是内存模型总览的作用:先定位,再深挖。
- 识别共享区堆和元空间是全公司共用的“仓库与手册库”,所有线程都能访问。
- 识别私有区每个线程各持有一份栈、本地方法栈和程序计数器,线程结束即销毁。
- 累加总内存进程总内存约等于共享区加上 N 个线程各自的私有区,线程数暴涨时私有区开销不可忽视。
堆区详解
中央仓库(堆)是写字楼里占地最大的区域,内部又分为三个货区。收货区(Eden)负责接收所有新到的货;中转货架(S0/S1 两块 Survivor)供临时周转;长期仓储区(老年代)存放待了很久的货。仓库定期盘点,把过期货(垃圾对象)清出去,这个过程就是垃圾回收(GC)。
用 new 创建的对象,就像一辆辆送货卡车先开到收货区,全部停在 Eden。当 Eden 满到放不下时,触发一次“年轻代清理”(Minor GC),它只打扫收货区和中转架:Eden 里存活下来的对象年龄加 1,然后被复制到另一块空闲的 Survivor 上;默认 -XX:SurvivorRatio=8,所以 Eden : S0 : S1 = 8 : 1 : 1,两块 Survivor 轮流当“中转架”和“空货架”。
对象在中转架之间每扛过一次 Minor GC,年龄就加 1,直到达到阈值 MaxTenuringThreshold(默认 15)就晋升到老年代。大对象(比如很长的数组)默认不经过中转架,直接进老年代,省得反复复制。老年代满时触发 Major GC / Full GC,往往伴随较长的停顿。堆的初始和最大容量分别由 -Xms 和 -Xmx 控制,JDK 8 以后默认最大堆约为物理内存的四分之一。
用 jstat 观察堆时,如果看到 Eden 频繁波动、老年代缓慢爬升,就像仓库里“新货来了又走、老货越积越多”——八成是老年代积压了长寿垃圾,甚至存在泄漏。堆区详解的核心,就是看懂这套“新货 → 中转 → 长期存放”的流转节奏。
- Eden 是入口new 出来的对象先到 Eden 落脚,Eden 是对象分配的起点。
- Survivor 是中转站Minor GC 后存活对象被复制到另一块空闲 Survivor,用复制算法避免碎片。
- 晋升老年代对象年龄达到阈值,或对象本身过大,直接进入老年代。
- 汇总堆结构新生代按 8:1:1 划分,堆整体由新生代与老年代组成。
虚拟机栈
每位员工工位上都有一叠任务便签,每接手一个任务就抽出新的一页,记下这个任务的参数、临时变量、算到哪一步;任务结束就把这页撕掉。这叠便签就是虚拟机栈(简称栈),一张纸就是一个栈帧,对应一次方法调用。栈是后进先出(LIFO)的:最先调用的方法在最底下,最晚调用的在最上面,返回时从上往下逐层弹出。
一张栈帧里写着四样东西。局部变量表是“便签上的清单”,存放方法的参数和局部变量——基本类型直接记值,对象类型记一个“指向仓库的地址牌”(引用);操作数栈是“草稿区”,做 a + b 时先把两个数压进去,弹出来算出结果再压回去;动态链接是“手册索引”,指向运行时常量池里的目标方法,实现多态调用;方法返回地址则让方法结束能回到调用处。
每个线程默认大约有 1MB 的栈空间(-Xss 可调)。如果递归没有出口,或一个方法套一个方法无休止压栈,栈帧越叠越高,最终把栈压爆,抛出 StackOverflowError(SOF)。栈溢出跟堆 OOM 完全不同:一个是便签纸叠到天花板,一个是仓库货堆到门口。
排查递归死循环或超深调用时,看异常堆栈里抛出的 SOF,就知道某条执行链路太深了——好比有人把便签纸叠到了天花板。虚拟机栈的职责,就是为每一次“正在进行的任务”提供一张可靠的纸。
- 局部变量表按槽位保存方法的参数与局部变量,基本类型记值、引用类型记地址。
- 操作数栈运算时的临时草稿区,压入操作数、弹出结果。
- 动态链接与返回地址找到目标方法入口并记录返回位置。
- 溢出条件栈深度与栈帧大小的乘积超过栈容量即抛出 SOF。
方法区与元空间
写字楼楼道尽头锁着一间档案室,里面放着岗位说明书和规章制度手册:每个岗位(类)有哪些职责(方法)、字段怎么定义、公司有哪些公共字面量公约(运行时常量池)。新员工入职(类加载)时过来查手册,但手册本身不放货物——货物都在中央仓库里。这间档案室对应的就是方法区,Java 8 之后它的实现叫元空间。
方法区在规范层面存放四类东西:类型信息(类名、修饰符、实现的接口)、运行时常量池(字面量与符号引用)、方法字节码、字段描述。HotSpot 7 用“永久代”实现方法区,它被划在堆内、大小固定;Java 8 起改为元空间,使用本地内存,默认不设上限,可用 -XX:MaxMetaspaceSize 限制。
有两个细节面试高频:第一,字符串常量池在 JDK 7 被移出方法区、放进堆里,所以字符串字面量目前是“仓库里的货物”;第二,规范层面静态变量属于方法区,但 HotSpot 实现里静态变量的实体跟随 Class 对象放在堆上,元空间存的只是类元信息。如果热部署、反射、CGLIB 代理频繁创建新类加载器,元空间会像档案室被不断塞入新岗位说明书一样膨胀,最终抛出 Metaspace OOM。
排查 Metaspace OOM 时,要问的是“谁在不停造新岗位说明书”——通常是反射、代理、热部署带来的类加载器泄漏。方法区与元空间的关系,一句话总结:规范画大饼,实现看 HotSpot。
- 类型信息记录类全限定名、修饰符、父类、接口列表。
- 运行时常量池保存类的字面量与符号引用,是类文件常量池的运行时版本。
- 实现演进Java 8 移除永久代,改用元空间,字符串常量池早已迁往堆。
- 静态变量实体HotSpot 实现中静态变量随 Class 对象存放于堆,元空间只存描述。
程序计数器
员工干活时,手指总点着当前正在读的那一行;被叫去开会(线程切换),回来还能接着看——因为每人的手指都留在自己那一页上。这根“手指”就是程序计数器(PC 寄存器),它是一小块极小的内存,记录当前线程正在执行的字节码指令地址。字节码是 JVM 能直接执行的中间指令,每执行一条,PC 就自动移向下一条。
普通指令顺序执行时,PC 指向下一条指令的地址;遇到 if、循环、异常跳转,PC 会更新为分支目标偏移。因为每条线程各有自己的一根“手指”,线程被挂起再恢复时,就靠各自的 PC 接着读——这也是程序计数器被设计成线程私有的原因:它小到几乎不占内存,却决定了线程切换后能否“无缝续读”。
程序计数器还有两个“最”。其一,它是规范中唯一不会抛出 OOM 的区域——一根手指大不了碰不到天花板;其二,当线程执行 Native 方法(通过 JNI 调用 C/C++ 代码)时,不再由 Java 字节码驱动,PC 的值是 undefined。服务于 Native 方法的栈叫本地方法栈,职能与虚拟机栈类似,但面向的是非 Java 代码,相当于外包专家自带的工具箱。
多线程程序里定位“到底停在哪一行”,看线程 Dump 的栈顶行号——那往往是 PC 留下的轨迹。程序计数器虽小,却是每个线程“读到哪里、该往哪走”的底层指针。
- 普通指令顺序执行时 PC 指向下一条指令。
- 分支跳转遇到 if 或循环,PC 按偏移跳到目标位置。
- Native 调用执行本地方法时字节码停止驱动,PC 无定义。
- 线程恢复线程切换回来时,读取本线程的 PC 继续执行。
对象内存布局与TLAB
从仓库取一件货,发现每件货都用统一纸箱打包:箱体正面贴标签(对象头),箱里码着货物(实例数据),空隙塞泡沫(对齐填充)。标签上写着货号(hashCode)、锁位(锁状态)、搬运次数(GC 分代年龄),还附一张“这是哪类货”的说明书索引(类型指针)。这就是 HotSpot 中对象在堆里的三段式布局。
对象头包含 Mark Word 与类型指针(Klass Pointer)。64 位 JVM 默认开启压缩指针(-XX:+UseCompressedOops)时,Mark Word 占 8 字节、类型指针占 4 字节,普通对象头共 12 字节;关闭压缩则为 16 字节。数组对象还会额外带一个 4 字节的 length 字段。实例数据按字段声明顺序排列,而对齐填充是为了让对象总大小成为 8 的倍数,方便 CPU 与指针运算。
了解了布局,再来看“快速入库通道” TLAB(Thread-Local Allocation Buffer)。仓库收货区(Eden)会给每个线程预划一小块私有缓冲地,线程内 new 对象时直接在自己这块地上放货,完全不用抢锁,大幅提升并发创建对象的吞吐;大对象不走 TLAB,直接进老年代或 Eden 公共区。
用 jol 工具打印对象布局,就像拆箱检查每个对象占几个字节——这是评估内存占用、优化缓存命中率的基本功。对象内存布局与 TLAB 告诉我们:堆里每一个“货箱”都有标准规格,而 TLAB 就是让成千上万的“新货”快速就位的传送带。
- 对象头Mark Word 存 hashCode、锁状态、GC 年龄;类型指针指向元空间中的类元信息。
- 实例数据对象各字段的值按声明顺序紧凑排列。
- 对齐填充HotSpot 要求对象大小是 8 的倍数,不足则补泡沫。
- TLAB 加速在 Eden 内给线程预分私有小块,分配免锁。
OOM排查与调优
仓库爆满叫堆 OOM;便签叠到天花板叫栈溢出;档案室文件漫溢叫元空间 OOM;垃圾回收员累死也腾不出地方,叫 GC overhead limit exceeded。每一种“满”,症状不同、排查手段也不同。对象分配失败本身不是灾难,灾难是“GC 之后还是不够”。
最常见的 OOM 类型有五类:Java heap space(堆内存不足)、GC overhead limit exceeded(GC 时间超 98% 却回收不到 2%)、Metaspace(元空间爆满)、Direct buffer memory(直接内存不足)、unable to create new native thread(线程数受限)。StackOverflowError 严格说不是 OOM,但常作为栈溢出的排查对象一并处理。
排查的标准套路是四步:第一步,启动参数加 -XX:+HeapDumpOnOutOfMemoryError,让 JVM 在 OOM 瞬间自动导出 .hprof 堆快照;第二步,用 MAT 打开 dump,看支配树(Dominator Tree)找出“谁占着最大内存、从哪条引用路径可达”;第三步,用 jmap、jstat 在线上观察各代占用与 GC 频率,区分内存溢出(确实不够)与内存泄漏(用完不还);第四步,根据根因调整 -Xms、-Xmx、-Xmn、-Xss、-XX:MaxMetaspaceSize、-XX:MaxTenuringThreshold 等参数。调大堆只是扩容仓库,泄漏不除,换多大的桶都会再次漏满。
记住那句口诀:先看 dump,再动参数。OOM 排查与调优的终点不是把数字调大,而是找到那个一直往仓库塞货却从不还的手——内存泄漏的根因。
- 堆不足请求分配的大小超过当前可用与本次可回收之和。
- GC 开销超限垃圾回收花费大量时间却只能回收极小比例。
- 元空间爆满类加载器泄漏导致类元信息无限增长。
- 排查闭环dump 导出、MAT 支配树定位、参数针对性调整。
AI 应用场景
AI 推理网关的 JVM 调优
阶段:模型部署与在线推理服务
解决的问题:AI 平台常以 Java 网关(如 Spring Cloud Gateway)承接模型推理请求,做鉴权、路由、限流。高 QPS 下每来一个请求都在堆里创建大量临时对象,Eden 频繁触发 Minor GC,响应延迟抖动。传统做法只加 -Xmx,效果很差。用 JVM 内存模型的知识,可以快速判断“满的是哪块区域”。
具体案例:某 CV 推理平台网关高峰期频繁 Full GC,开启 -XX:+HeapDumpOnOutOfMemoryError 抓取 dump 后用 MAT 分析,发现网关把推理结果的 byte[] 缓存进了无界 Map。改为堆外缓存并限制条数,同时调小 -XX:MaxTenuringThreshold 加速短命对象回收,GC 停顿从 300ms 降到 30ms。
Flink 流处理任务的内存治理
阶段:数据预处理与特征工程
解决的问题:机器学习特征管道常跑在 JVM 上的 Flink / Spark 上,任务 OOM 时很难分清是堆内对象堆积,还是堆外的托管内存、网络缓冲耗尽。理解堆、栈、元空间的分工后,能用 jstat 快速观察各代占用,定位到具体区域。
具体案例:一个流式特征工程任务每隔几小时 OOM,jstat 显示老年代持续爬升、Eden 正常,判定对象泄漏到全局状态——最终定位是状态存储(State)过大且无 TTL。清理状态、调大堆外托管内存后,任务稳定运行一周无 OOM。
用 Arthas 排查模型服务内存泄漏
阶段:线上诊断与稳定性保障
解决的问题:Java 模型服务的会话上下文常被遗忘在集合里,导致堆内存像漏水的桶。用 Arthas 的 dashboard 查看堆内存曲线、用 heapdump 命令实时导出快照,再配合 MAT 的支配树定位泄漏源。
具体案例:某 LLM 服务网关的会话上下文未清理,Arthas heapdump + MAT 支配树定位到 ConcurrentHashMap 中残留的 ChatMemory 对象,为该对象加 TTL 自动回收后,内存水位恢复平稳。
Python 演示
import random class HeapRegion: """模拟堆里的分代区域:Eden / Survivor / Old""" def __init__(self, capacity, name): self.capacity = capacity self.name = name self.objects = [] # 每项: [obj_id, size, age, alive] def used(self): return sum(obj[1] for obj in self.objects) class MiniJVMHeap: """极简 JVM 堆模拟:对象走 Eden->Survivor->Old 的晋升与 Minor GC""" def __init__(self, eden_cap, surv_cap, old_cap, promote_age=3): self.eden = HeapRegion(eden_cap, "Eden") self.survivors = [HeapRegion(surv_cap, "S0"), HeapRegion(surv_cap, "S1")] self.old = HeapRegion(old_cap, "Old") self.promote_age = promote_age self.oid_counter = 0 self.gc_count = 0 def alloc(self, size): """模拟 new 对象:先尝试放 Eden,空间不足就触发 Minor GC""" if self.eden.used() + size > self.eden.capacity: self.minor_gc() if self.eden.used() + size > self.eden.capacity: raise MemoryError("模拟 OOM: java.lang.OutOfMemoryError: Java heap space") self.oid_counter += 1 self.eden.objects.append([self.oid_counter, size, 0, True]) return self.oid_counter def minor_gc(self): """Minor GC 只清理年轻代,存活对象年龄 +1,达标则晋升老年代""" self.gc_count += 1 from_survivor = self.survivors[0] if self.survivors[0].used() > 0 else self.survivors[1] to_survivor = self.survivors[1] if from_survivor is self.survivors[0] else self.survivors[0] to_survivor.objects.clear() promoted = [] for obj in self.eden.objects + from_survivor.objects: obj[2] += 1 # 每次 GC 年龄 +1 obj[3] = random.random() > 0.3 # 模拟可达性分析,70% 存活 if not obj[3]: continue if obj[2] >= self.promote_age: promoted.append(obj) else: to_survivor.objects.append(obj) self.eden.objects.clear() from_survivor.objects.clear() self.old.objects.extend(promoted) if self.old.used() > self.old.capacity: raise MemoryError("模拟 OOM: 老年代已满") print("[Minor GC #%d] 晋升 %d 个对象, 老年代占用 %d/%d" % ( self.gc_count, len(promoted), self.old.used(), self.old.capacity)) def stats(self): print("Eden 占用 %d/%d | Survivor 占用 %d | 老年代占用 %d/%d" % ( self.eden.used(), self.eden.capacity, sum(s.used() for s in self.survivors), self.old.used(), self.old.capacity)) if __name__ == "__main__": heap = MiniJVMHeap(eden_cap=500, surv_cap=200, old_cap=800) try: for i in range(100): heap.alloc(random.randint(20, 60)) if i % 20 == 0: heap.stats() heap.stats() except MemoryError as exc: print("程序终止:", exc)
heap = MiniJVMHeap(eden_cap=500, surv_cap=200, old_cap=800)构造一个迷你堆,三个区域容量对应真实 JVM 的 Eden、Survivor、老年代。if self.eden.used() + size > self.eden.capacity:分配前先看 Eden 剩余空间够不够,这是 JVM 分配对象的第一道检查。self.eden.objects.append([self.oid_counter, size, 0, True])新对象先进 Eden,age=0 且标记为存活,对应new的默认去向。obj[3] = random.random() > 0.3模拟可达性分析:约 70% 的对象能扛过本次 GC,其余被回收。if obj[2] >= self.promote_age:年龄达到阈值则晋升老年代,对应MaxTenuringThreshold机制。raise MemoryError("模拟 OOM: java.lang.OutOfMemoryError: Java heap space")腾不出空间就抛异常,模拟真实堆溢出。heap.alloc(random.randint(20, 60))主循环随机创建大小不一的对象,压力足够时触发多次 Minor GC。
常见误区
误区我一直以为静态变量放在方法区里,元空间的大小约等于项目所有静态变量的总和,调 `MaxMetaspaceSize` 就能控制静态变量占用。
正解规范层面把静态变量抽象归入方法区,但 HotSpot 8+ 的实现中,静态变量的实体(值或引用)是堆上 Class 对象的一部分,元空间实际存放的是类元信息、运行时常量池和方法字节码。判断静态变量占用,应该看堆而不是元空间。好比岗位手册只描述“仓库管理员负责 3 个货架”,而货架本身在仓库里。
为何常见:教材大多按 JVM 规范画图,规范的抽象分层与 HotSpot 的具体实现被混为一谈,面试经常在这里埋坑。
误区方法 return 之后,里面的局部变量应该马上被 GC 回收,否则内存不就泄漏了吗?
正解局部变量表存的是基本类型的值或对象引用,栈帧弹出后引用确实消失;但引用指向的对象在堆里,只要对象还被其他引用持有(比如被 return 给调用方、塞进了全局集合),GC 就不会回收。回收与否取决于“从根出发的可达性”,而不是方法是否已经结束。便签撕了,不等于仓库里的货就报废。
为何常见:把“栈帧弹出”误解成“对象销毁”,没分清引用与对象本体是两个层面的东西。
误区遇到堆 OOM 就直接把 `-Xmx` 调大,堆大了总不会再满了吧?不行就再翻一倍。
正解调大堆只是扩容仓库,能拖延时间,但若存在内存泄漏,堆再大也会再次被打满;而且堆过大还会导致 GC 停顿变长、机器内存吃紧。正确做法是先加 -XX:+HeapDumpOnOutOfMemoryError 生成 dump,用 MAT 找到“谁一直占着内存不还”,再决定是改代码还是调参数。
为何常见:把 OOM 当成根因而不是症状,忽略“分配失败只是结果,空间被谁吃掉才是原因”。
误区元空间用本地内存、默认不设 `MaxMetaspaceSize`,所以它永远不会溢出。
正解“无上限”指不设置 JVM 层面的上限,但物理内存和虚拟内存总量是有限的。反射、CGLIB 动态代理、热部署会不断创建新的类加载器,每个加载器都带着一份类元信息,元空间会持续膨胀并抛出 Metaspace OOM。档案室再大,也经不住不停印刷新手册。
为何常见:把“默认不限制”理解成“无限大”,忽略本地内存总量和类加载器泄漏这两个约束。
误区对象要扛过 15 次 Minor GC 才会进老年代,这是雷打不动的规则。
正解默认 MaxTenuringThreshold=15 只是上限,HotSpot 还有动态年龄判定:如果 Survivor 中同龄对象占用的空间超过 Survivor 容量的一半,大于等于该年龄的对象会提前晋升;超大的对象则通过 PretenureSizeThreshold 直接进老年代。年龄判定是“按规则灵活晋升”,不是“掐表到 15 次”。
为何常见:只背默认值,忽略了 HotSpot 为了节省 Survivor 空间而设计的动态晋升机制,这也是调优参数为什么存在的原因。
面试题
Q1. JVM 运行时数据区中,属于线程共享的区域是?
- A.虚拟机栈与程序计数器两个私密区
- B.堆与方法区两大公共存储区← 正确答案
- C.本地方法栈与虚拟机栈区域
- D.程序计数器与本地方法栈区域
答案:B。共享区 = 堆 + 方法区(元空间),私有区 = 虚拟机栈 + 本地方法栈 + 程序计数器。仓库和手册库人人可用,工位便签与手指只归自己,所以 B 正确。
Q2. 使用 new 创建的对象,通常优先被分配到哪个区域?
- A.老年代区域
- B.元空间区域
- C.Eden 区域← 正确答案
- D.计数器区域
答案:C。new 出来的对象先到年轻代的 Eden “收货区”,只有大对象或多次 GC 存活的才进老年代;元空间放类元信息,计数器记行号。所以选 C。
Q3. 关于 Minor GC 中 Survivor 区的作用,描述正确的是?
- A.大对象必须先移动到 S0 区域存放
- B.存活对象在 S0/S1 间复制防碎片← 正确答案
- C.Survivor 区始终保存类元信息内容
- D.每次 Minor GC 都会清空老年代
答案:B。Minor GC 使用复制算法,存活对象从 Eden + From Survivor 复制到 To Survivor,避免内存碎片。大对象默认直接进老年代,类元信息在元空间,Minor GC 不碰老年代,所以 B 正确。
Q4. 一个 Java 方法调用产生的栈帧中,不包含下列哪一项?
- A.局部变量表与操作数栈两要素
- B.动态链接与方法返回地址信息
- C.对象头中的锁状态与 GC 年龄← 正确答案
- D.局部变量与操作数栈相关数据
答案:C。栈帧四要素是局部变量表、操作数栈、动态链接、返回地址;对象头属于堆中对象的布局,不属于栈帧。选 C。
Q5. 下列哪些运行时数据区属于线程私有?
- A.虚拟机栈与本地方法栈两个栈区← 正确答案
- B.方法区与堆两个共享存储区
- C.程序计数器这个小小的标尺区← 正确答案
- D.字符串常量池所在的堆区域
答案:AC。线程私有 = 虚拟机栈 + 本地方法栈 + 程序计数器;堆与方法区共享,字符串常量池在堆里也属于共享区。A、C 正确。
Q6. Java 8 及之后,永久代被哪个区域替代?
- A.元空间(Metaspace)区域← 正确答案
- B.堆中的老年代空间
- C.虚拟机栈的栈帧空间
- D.直接内存的缓冲空间
答案:A。JDK 8 移除永久代,方法区落地为使用本地内存的元空间。字符串常量池与静态变量在堆里,直接内存与永久代不是替代关系。选 A。
Q7. 64 位 JVM 默认开启压缩指针时,普通对象头的大小为?
- A.8 字节,只有标记字段
- B.12 字节,标记与类型指针← 正确答案
- C.16 字节,含对齐填充
- D.24 字节,含锁记录
答案:B。开启压缩指针时,Mark Word 占 8 字节、Klass Pointer 占 4 字节,对象头共 12 字节;关闭压缩指针才是 16 字节,对齐填充不计入对象头。选 B。
Q8. 关于程序计数器的说法,正确的有哪些?
- A.记录当前线程的字节码执行地址← 正确答案
- B.是唯一规范中不会 OOM 的区域← 正确答案
- C.执行 Native 方法时值为 undefined← 正确答案
- D.由所有线程共同维护一份实例
答案:ABC。PC 记录当前线程执行的字节码位置,线程切换靠它恢复;执行 Native 方法时 PC 无定义;它也是唯一规范中不会抛 OOM 的区域。它不是多线程共享一份,而是每条线程各有一个,D 错误。
Q9. 在 HotSpot 中,静态变量的实体(值或引用)实际存储在哪里?
- A.元空间的类元信息数据结构里
- B.堆上 Class 对象实例区域中← 正确答案
- C.虚拟机栈的局部变量表槽位
- D.程序计数器旁边的保留区域
答案:B。规范把静态变量抽象归入方法区,但 HotSpot 实现中静态变量的实体随 Class 对象放在堆上;元空间存的是类元信息、常量池等描述性数据,栈与 PC 与静态变量无关。规范抽象与具体实现要区分,选 B。
Q10. 排查堆 OOM 的问题,下列哪些做法是正确的?
- A.开启 HeapDumpOnOutOfMemoryError 参数← 正确答案
- B.用 MAT 分析 .hprof 文件定位← 正确答案
- C.只调大 -Xmx 就能彻底解决
- D.用 jmap 观察堆占用与实例← 正确答案
答案:ABD。OOM 排查靠 dump 导出、MAT 支配树定位、jmap/jstat 观察现场;只调大 -Xmx 是拖延不是根治,泄漏不除还会复现。所以 A、B、D 正确。