
1. 一张图记住 JVM 运行时数据区线程私有和共享千万别混淆如果你翻过《深入理解 Java 虚拟机》大概率最先接触到的就是“运行时数据区”这块内容。说实话刚入门的时候这一大堆区域名称很容易把人绕晕哪些是线程共用的哪些是每个线程自己一份的哪些地方会抛OutOfMemoryError哪些只会抛StackOverflowError。搞不清楚这些后面看 GC 日志、调 OOM、追线上问题都会很吃力。JVM 在执行 Java 程序时会把自己管理的内存划分成若干区域这就是常说的运行时数据区Runtime Data Area。按《Java 虚拟机规范》的划分它包括程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区以及运行时常量池。其中前三个是线程私有的后面几个是线程共享的。用一句人话总结就是线程私有区域管“这个线程正在执行到哪一行、方法调用栈长什么样”线程共享区域管“所有对象和类元数据都放在哪”。1.1 程序计数器唯一不会 OOM 的区域程序计数器Program Counter Register是一块很小的内存空间可以理解为当前线程所执行的字节码的“行号指示器”。为什么需要它因为线程切换之后CPU 要从上次中断的位置继续执行每个线程都得有自己的计数器才能记录状态。所以它是线程私有的。这块区域有几个特点值得记住如果执行的是 Java 方法计数器记录的是正在执行的虚拟机字节码指令地址如果执行的是 native 方法计数器的值是空Undefined唯一一个在《Java 虚拟机规范》里没有规定任何OutOfMemoryError情况的区域。所以在面试里如果被问到“哪些区域可能 OOM哪些不可能”程序计数器就是那个“不可能”的答案。1.2 Java 虚拟机栈方法调用的“后台记录员”Java 虚拟机栈JVM Stack也是线程私有的生命周期和线程一样。它的作用可以用一句话概括每次方法调用都会创建一个栈帧方法结束栈帧就被弹出。一个栈帧里包含的东西值得单独记一下局部变量表存放方法参数和方法内部定义的局部变量。注意它存的是基本类型值、对象引用而不是对象本身。局部变量表的容量单位是 Slotlong 和 double 占两个 Slot其他占一个。操作数栈字节码指令运算时用到的“临时计算区”。比如执行iadd做加法就是把操作数栈最顶部的两个数弹出相加后再压回栈。动态链接每个栈帧里都包含一个指向运行时常量池中该方法的引用用来支撑方法调用过程中的符号引用转换成直接引用。方法出口方法正常返回或异常退出后该回到调用者的哪条指令继续执行。如果线程请求的栈深度超过了虚拟机允许的最大深度会抛StackOverflowError。如果栈在动态扩展时无法申请到足够内存会抛OutOfMemoryError。经典的手写无限递归导致栈溢出就是第一种情况。1.3 本地方法栈给 native 方法用的“专属区域”本地方法栈Native Method Stack和虚拟机栈非常相似区别只是它为 native 方法服务。在 HotSpot 虚拟机里虚拟机栈和本地方法栈直接合二为一所以你在调参时不会单独看到这个区域。理论上它也会抛StackOverflowError和OutOfMemoryError。1.4 Java 堆几乎所有对象实例的“大本营”Java 堆Heap是 JVM 管理的最大一块内存也是 GC 最核心的战场。它在线程启动时创建所有线程共享。按内存回收的角度堆经常被分成新生代Young Generation再细分为 Eden 区和两个 Survivor 区默认比例通常是8:1:1老年代Old Generation存放长期存活的对象或大对象。按内存分配的角度堆又可以进一步切分成线程私有的TLABThread Local Allocation Buffer作用是让每个线程在 Eden 区有自己的分配缓冲区减少并发分配锁冲突。堆的大小是可以动态扩展的默认堆空间如果不够会先尝试扩展扩展不了才抛OutOfMemoryError: Java heap space。调优时最常动的-Xms和-Xmx控制的就是它。1.5 方法区与运行时常量池JDK 8 之后叫 Metaspace方法区Method Area也是线程共享区域主要存类元信息类的全限定名、父类、接口、字段描述、方法描述等运行时常量池编译期生成的字面量字符串字面量、final 常量等和符号引用静态变量JIT 编译后的代码缓存。在《Java 虚拟机规范》里方法区是一个逻辑概念。HotSpot 在 JDK 7 时代用“永久代”实现方法区JDK 8 开始改成“元空间Metaspace”并且把字符串常量池和静态变量挪到了 Java 堆里。这块内容后面会专门展开很多面试题和线上 OOM 都围绕它出现。1.6 直接内存不算规范但影响巨大的“堆外内存”直接内存Direct Memory不在《Java 虚拟机规范》里也不属于运行时数据区但 JDK 的 NIO 使用了它。它通过Unsafe或者DirectByteBuffer在堆外申请内存避免数据在 Java 堆和 native 堆之间来回复制适合高并发 IO 场景。但要注意直接内存不受-Xmx限制只受物理机总内存限制。如果一直申请不释放同样会抛OutOfMemoryError: Direct buffer memory。很多同学排查时只看堆忽略堆外结果绕了一大圈才发现是 NIO 的锅。2. 一个 Java 对象在 JVM 里的“一生”从类加载检查到 TLAB 分配搞清楚内存区域划分之后下一步就是把“内存模型”从静态结构变成动态过程一个对象从new出来到被 GC 回收它到底经历了什么这里我按 HotSpot 虚拟机在 JDK 8 之后的典型流程来讲面试时照着这个顺序说基本不会跑偏。2.1 第一步类加载检查执行new指令时JVM 会先去常量池里找到这个类的符号引用然后检查这个类是否已经被加载、解析、初始化过。如果没加载会先触发类加载过程。在这一步如果类加载失败后面的一切都不会发生。2.2 第二步为对象分配内存类加载通过后JVM 会在堆里为新对象分配内存。分配方式有两种指针碰撞Bump the Pointer如果堆内存是规整的已用内存和未用内存中间放一个指针作为分界点分配就是往空闲方向移动指针。空闲列表Free List如果堆内存不规整JVM 需要维护一个空闲列表分配时从列表里找一块足够大的内存。选择哪种方式取决于垃圾收集器是否带有“压缩整理”功能。比如 Parallel Scavenge、G1 这类能整理内存的通常用指针碰撞CMS 这种基于标记-清除算法的通常要配合空闲列表。2.3 第三步并发分配问题怎么解CAS 和 TLAB对象分配在堆上而且是高频操作如果所有线程都在同一块指针上“抢内存”性能必然崩。HotSpot 的解决方案是TLAB。每个线程在 Eden 区预先分配一小块独立缓冲区线程在自己缓冲区里分配不需要同步。只有 TLAB 不够用的时候才需要同步锁定使用 CASCompare And Swap保证指针更新的原子性。这也是为什么在代码里频繁new小对象时性能通常不会立刻成为瓶颈——大部分分配动作都发生在线程私有的“近路”上。2.4 第四步内存空间初始化零值再设置对象头分配完内存后JVM 会把这块内存不包含对象头按基本类型初始化为零值。这就是为什么你在 Java 里定义一个int[]即使不逐个赋值默认值也是 0对象属性的引用类型默认是 null。接下来 JVM 会设置对象头。对象头是对象放在堆里的“身份证”主要包括Mark Word存对象的哈希码、GC 分代年龄、锁状态标志、线程持有的锁等类型指针指向方法区里的类元数据JVM 靠它知道这个对象是哪个类数组长度如果对象是数组对象头里还会有一块记录数组长度的字段。JDK 6 之后默认开启指针压缩64 位系统下对象头不再是 16 字节而是 12 字节启用-XX:UseCompressedOops后具体大小因 JVM 版本和配置而异。2.5 第五步执行init方法上面的操作都完成JVM 视角里的对象已经已经是一个“内存结构”了但从 Java 程序的视角看构造函数还没执行。JVM 会调用init方法也就是我们写的构造方法和初始化块完成属性赋值、初始化逻辑。注意在这之后对象才会被引用变量持有。如果在构造函数里抛异常前面分配的内存会被回收。2.6 对象怎么被访问句柄和直接指针对象创建好了代码里拿着引用去访问它时底层有两种主流实现句柄访问Java 栈里的引用先指向堆中的一个句柄池句柄池再指向对象实例数据和类元数据。好处是对象移动时只需要改句柄不用改引用。直接指针访问引用直接指向对象实例数据同时在对象头里放类型指针再去找类元数据。好处是速度快少一次间接寻址。HotSpot 默认采用直接指针访问。这也是为什么对象头里要有“类型指针”的设计原因之一。3. 堆和元空间的设计哲学JDK 7 与 JDK 8 以后的内存变化JVM 内存模型不是一成不变的。尤其在 JDK 8 把“永久代”换成了“元空间”这个变化不仅影响内存分布还直接影响很多线上报错和调优方式。3.1 永久代为什么要被替换在 JDK 7 及以前方法区的实现叫永久代。但它有几个明显问题永久代也属于堆的一部分大小受-XX:MaxPermSize限制存储的是类元数据而现实里动态生成类的场景非常多比如 Spring、CGLIB、热部署很容易把永久代撑爆永久代内存回收效率不稳定字符串常量池里的字符串很难被及时清理不是所有 JVM 都有永久代像 JRockit、IBM J9 就没有所以《Java 虚拟机规范》只定义了方法区这个逻辑概念没规定必须用永久代实现。于是 JDK 8 开始HotSpot 彻底放弃永久代改用元空间Metaspace。元空间使用本地内存native memory默认情况下不会再受 JVM 参数里那个固定的“代大小”限制而是使用操作系统可用内存。3.2 字符串常量池为什么搬到了堆里JDK 7 之前字符串常量池在永久代里。永久代空间有限String.intern()这种操作很容易把永久代撑爆。JDK 7 开始字符串常量池被移到了 Java 堆。JDK 8 之后依然在堆里。这意味着什么如果你是开发intern大量字符串的服务可以非常快速地制造Java heap space。字符串常量池里的字符串属于堆的一部分GC 能正常处理调优时用-Xmx就能约束它。3.3 静态变量和类元信息的位置再确认JDK 8 之后类元信息类名、方法描述、字段描述存在元空间运行时常量池存在元空间字符串常量池存在堆里静态变量存在堆里伴随 Class 对象一起。很多文章喜欢说“静态变量在方法区”这个说法在 JDK 7 之前对JDK 7 之后 HotSpot 已经把它挪到了 Java 堆。面试时能主动纠正这个细节会显得你是真正看过新版资料的而不是只背了老八股。3.4 字符串常量池和 OOM 的典型关系因为字符串常量池在堆里你只要写一段不断intern不同字符串的代码很快就能让堆内存爆掉。线上如果遇到Java heap space的报错同时 GC 日志显示老年代一直在增长恰好服务里又大量使用String.intern()就要优先怀疑是字符串去重逻辑设计不合理。4. 分代回收视角下的内存模型对象是怎么被回收的内存模型的最终目的是服务于“内存管理”而内存管理的重头戏就是垃圾回收。这里不展开讲所有 GC 收集器的算法细节但要把“分代回收”和前面说的内存区域对应起来。4.1 可达性分析与 GC Roots对象在堆里分配但判断对象能不能被回收不是看引用计数而是看它能不能从GC Roots出发被遍历到。可以作为 GC Roots 的包括虚拟机栈里的局部变量表引用的对象方法区里的静态变量引用的对象方法区里的常量引用的对象native 方法栈里 JNI 引用的对象被同步锁synchronized持有的对象Java 虚拟机内部的引用系统类加载器、基本类型的 Class 对象、常驻异常对象等。从 GC Roots 出发凡是能到达的对象都视为存活对象剩下的就是可以被回收的对象。这也是 JVM 内存模型里的“可达性分析”思路。4.2 对象分配规则新生代、老年代、大对象对象不是一出生就有一块固定区域而是按规则“入住”的绝大多数对象优先分配在新生代的 Eden 区Eden 区满了触发 Minor GCMinor GC 后依然存活的对象会进入其中一个 Survivor 区每熬过一次 Minor GC对象年龄加 1年龄达到阈值默认 15晋升到老年代大对象比如很大的数组或字符串可能直接进入老年代避免在新生代来回复制如果 Survivor 区里相同年龄所有对象大小总和大于 Survivor 区的一半年龄大于等于该年龄的对象直接晋升这就是“动态年龄判定”如果老年代空间不足以容纳从新生代晋升过来的对象就会触发一次 Full GC。4.3 常见垃圾收集器选型怎么看现代 JDK 里常用的收集器大体可以这样记收集器回收区域算法特点Serial新生代复制单线程适合客户端/小内存Parallel Scavenge新生代复制多线程关注吞吐量Parallel Old老年代标记-整理与 Parallel Scavenge 搭配CMS老年代标记-清除低延迟但会产生碎片G1新生代 老年代Region 化局部复制/全局标记JDK 9 默认平衡吞吐和延迟ZGC全堆并发整理超低停顿JDK 15 起转正如果线上追求吞吐量可以用 Parallel 系列如果追求低延迟优先考虑 G1 或 ZGC。但选择收集器不能只看名字还要看内存多大、停顿时间要求多高。比如堆内存超过 64GBG1 和 ZGC 会比传统分区收集器更有优势。4.4 看一条 GC 日志能读到什么以 JDK 8 开启-XX:PrintGCDetails为例常见日志类似[GC (Allocation Failure) [PSYoungGen: 61440K-8049K(71680K)] 61440K-53483K(232448K), 0.0122354 secs] [Times: user0.03 sys0.05, real0.01 secs]读法很简单YoungGen: 61440K-8049K(71680K)新生代回收前占用 61440K回收后 8049K新生代总容量 71680K括号后面61440K-53483K(232448K)是整堆的回收前后和总容量从堆总量看虽然新生代回收了很多但整堆占用下降不明显说明大量对象留在了老年代Times里的 real 是真实耗时如果持续变大说明 GC 停顿已经影响接口响应时间。GC 日志是调优的第一手资料。原则上先看分配速率、晋升速率再决定调大年轻代还是老年代不要一上来就无脑加-Xmx。5. 八类 OOM 的根因与一次真实排查过程说完了正常情况下的内存流动接下来说说异常情况。生产环境最怕的OutOfMemoryError在 JVM 内存模型里每一种都有对应区域。5.1 OOM 报错类型和根因对照报错类型对应区域/原因典型场景Java heap space堆内存不足对象泄漏堆太小大对象太多GC overhead limit exceeded堆基本耗尽GC 频繁但回收很少系统接近内存死亡状态JVM 连续 GC 超过 98% 时间但只回收不到 2% 堆Metaspace元空间不足动态生成类过多类加载器泄漏Direct buffer memory堆外/直接内存不足NIO 大量使用 DirectByteBuffer未释放或配置过小StackOverflowError虚拟机栈栈深度超限无限递归调用过深unable to create new native thread操作系统无法创建新线程线程数达到系统上限或每个线程栈太大Out of swap space物理内存和交换空间不足宿主机器内存被耗尽Requested array size exceeds VM limit数组长度超过 JVM 能支持的极限创建超大数组注意区分StackOverflowError不是OutOfMemoryError的子类它是VirtualMachineError的另一个分支。面试时别把这两个混为一谈。5.2 一次线上堆内存 OOM 排查链路我之前处理过一个典型的Java heap space问题。服务运行三天后开始频繁响应超时日志大量报 OOM重启后恢复正常但过几天又复发。第一步用jps -l确认进程号不要靠猜。jps -l拿到进程号后先用jstat -gcutil看 GC 趋势jstat -gcutil 12345 1000 20结果发现 Eden 区永远能回收但老年代占用率从 20% 一路涨到 95%Full GC 频次逐渐变密。这说明不是临时流量高峰而是有对象在持续进入老年代且不会被回收。第二步用jmap -histo:live看存活对象分布。jmap -histo:live 12345 | head -50前三名里有一个自定义的SessionWrapper类实例数量大得异常。我立刻去代码里搜谁在创建和持有它结果发现是一个静态并发容器不断加入新的会话对象但缺少对应的清理逻辑。本质是对象被静态引用长期持有GC Roots 可达永远回收不了也就是典型的内存泄漏。第三步为了确认根因在 OOM 时自动导出堆快照。java -Xms2g -Xmx2g -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/oom.hprof -jar app.jar配合 MAT 或者 Eclipse MAT 打开堆快照通过 Dominator Tree 找到大对象持有链可以精确看到对象是怎么被一路引用到静态容器的。第四步修复后用jmap -heap和jstat持续观察。预期结果是老年代占用率趋于平稳或周期性回落不再有持续爬坡曲线。5.3 元空间 OOM 的另一种排查思路如果报错是Metaspace优先怀疑类加载器泄漏。常见原因是热部署框架不停创建新的类加载器或者动态生成代理类、CGLIB 类而没有清理。排查手段jmap -clstats 12345查看类加载器统计观察Metaspace使用量是否只增不减检查是否有自定义类加载器被全局容器持有。如果确实是临时元空间不足需要调大-XX:MaxMetaspaceSize。但根本解法是别让类加载器泄漏。6. 参数调整和工具链jps、jstat、jmap、jstack、Arthas 到底怎么用理论再多最后还是落在参数和工具上。很多同学看到“JVM 调优”就觉得高深其实先从看懂内存参数和会用三个命令行工具开始就够了。6.1 最常用的内存相关参数参数作用示例-Xms初始堆大小-Xms1g-Xmx最大堆大小-Xmx2g-Xmn新生代大小-Xmn512m-XX:NewRatio老年代与新生代比例-XX:NewRatio2表示老年代:新生代2:1-XX:SurvivorRatioEden 区与单个 Survivor 区比例-XX:SurvivorRatio8表示 Eden:Survivor8:1-XX:MetaspaceSize元空间初始大小-XX:MetaspaceSize256m-XX:MaxMetaspaceSize元空间最大大小-XX:MaxMetaspaceSize512m-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动导出堆快照生产环境强烈建议开启-XX:HeapDumpPath堆快照保存路径-XX:HeapDumpPath/data/logs-XX:MaxDirectMemorySize直接内存最大大小-XX:MaxDirectMemorySize1g-Xss单线程栈大小-Xss1m这里有一个常见的调参误区-Xms和-Xmx最好设为相同值避免运行时动态扩容带来的停顿。JDK 8 以后JVM 堆容量变化会触发 Full GC扩大和缩小堆都会带来额外的系统开销直接把初始值和最大值设置成一样更平稳。6.2 设置 IDEA 的 JVM 内存开发环境防 OOM 的实操如果你用 JetBrains 系 IDE 做开发遇到“OutOfMemoryError: Java heap space”导致 IDE 卡死很多时候不是因为项目代码问题而是 IDE 自己的 JVM 堆太小。以 IDEA 为例打开菜单Help - Edit Custom VM Options会打开一个idea.vmoptions文件里面类似这样-Xms2048m -Xmx4096m -XX:ReservedCodeCacheSize512m -XX:UseG1GC把-Xmx从默认值调到 4096m 或 8192m保存后重启 IDEA 即可。同理如果 Maven 或 Gradle 在构建阶段频繁 OOM调的是构建工具的 JVM 参数而不是 IDEA 的。提示IDEA 的堆大小并不是越大越好。如果本机内存只有 8G却给 IDEA 分了 6G操作系统反而会频繁进行内存换页导致整个电脑卡顿。我个人的习惯是8G 内存机器给 IDEA 2G~3G16G 及以上给 4G~6G。6.3 线程池大小、线程数和 JVM 内存的关系热搜词里有句“线程池设置最大线程数是 JVM 剩余可用线程”这个说法不严谨。线程池最大线程数的核心决定因素是任务类型、队列容量和系统吞吐目标单纯用“剩余线程数”来设置肯定不对。但线程数和内存之间确实有关系每一个 Java 线程都会占用独立的虚拟机栈空间默认栈大小一般是 1MB。如果系统同时存在大量线程光线程栈就会吃掉非常多内存。假设-Xss1m一个进程里开了 500 个线程光栈区就是 500MB 左右还没算任何业务对象。所以调优时要同时看线程池里配置的最大线程数单个线程栈大小-Xss堆内存上限-Xmx操作系统能创建的总线程数限制。如果机器并发用户高线程池里的线程数又设置得很大同时堆也拉得很大最后可能不是堆溢出而是申请线程时无法创建新的本地线程。遇到这种情况与其再往上调堆不如合理评估线程池连接数、连接池大小或者适当调小-Xss。6.4 一套实用观察命令jps查看 Java 进程。jps -ljstat监控 JVM 内存和 GC 状态。jstat -gcutil pid 1000 10输出里的S0、S1、E、O、M分别代表幸存区、Eden 区、老年代、元空间的使用百分比。FGC是全 GC 次数FGCT是全 GC 累计耗时。jmap查看堆信息、导出堆快照。jmap -heap pid jmap -histo:live pid | head -20 jmap -dump:live,formatb,fileheap.hprof pidjstack查看线程栈找死锁或线程卡点。jstack pid | grep -A 20 deadlockjconsole图形化监控适合快速看内存曲线、线程数、类加载数。VisualVM比 jconsole 更丰富可以装插件看 GC 插件和 CPU 采样。Arthas阿里开源的 Java 诊断工具生产环境救命神器。curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar进入后常用命令dashboard看全局线程、内存、GC 状态heapdump在线导出堆快照thread -n 3找出 CPU 占用最高的线程trace和watch定位方法调用耗时和参数。Arthas 的价值在于它不需要重启服务就能做在线排查很多线下复现不了的问题只能靠它。7. JVM 内存模型和 Java 内存模型JMM这两个概念为什么总被搞混标题说“JVM 内存模型”但很多人搜着搜着会搜到 JMM。其实它们完全是两码事只是缩写和名字太像。7.1 JVM 内存模型是什么指前面讲的运行时数据区堆、栈、方法区、程序计数器等。它是 JVM 底层“内存结构”层面的划分回答的是“对象存哪里、类元信息存哪里、线程栈存哪里”的问题。7.2 Java 内存模型JMM是什么JMM 是 Java 语言规范里的并发抽象模型它定义的是在并发编程场景下多线程读写共享变量时的内存可见性、原子性和有序性规则。在 JMM 的抽象模型里每个线程有自己的“工作内存”可以类比 CPU 缓存线程对变量的操作都先发生在工作内存再同步回“主内存”。这和生产环境里 JVM 的堆、栈不是完全对应的概念它们是两种不同维度上的描述。JMM 最核心的内容包括可见性一个线程修改了共享变量其他线程能否立即看到原子性一系列操作是否不可分割有序性指令重排是否可能导致结果不一致volatile保证可见性和一定程度的有序性但不保证复合操作的原子性synchronized/Lock保证原子性和可见性final保证初始化安全性Happens-Before 规则如果操作 A 先行发生于操作 B那么 A 的结果对 B 可见。7.3 用一句话区分讨论“内存区域怎么划分、对象放哪里、哪里会 OOM”时说的是JVM 内存模型讨论“多线程共享变量会不会互相看见、重排序会导致什么 bug、volatile 管不管用”时说的是Java 内存模型JMM。如果面试官说“讲一下 JVM 内存模型”你应该先讲运行时数据区如果他说“讲一下 JMM”你应该讲 happens-before、volatile 和共享内存可见性。两者面试都可能被问到千万别答串。8. JVM 内存模型高频面试题速查从背诵到能讲清楚最后整理一份面试题向的清单。这些问题网上到处都有但很多人是背答案。这里我给的不是标准答案而是“怎么讲才能体现出你真的理解”的引导。1. 请画出 JVM 内存模型并说明哪些区域是线程共享的直接画图说五块程序计数器、虚拟机栈、本地方法栈是线程私有的堆、方法区元空间是线程共享的。如果能把直接内存额外提一句会显得知识面更完整。2. 你遇到过哪些 OOM怎么排查优先说自己真实处理过的案例。如果没处理过也一定要按“jps 找进程 - jstat 看 GC - jmap 导堆 - MAT 分析 - 定位引用链”这条链路讲。生产环境最重要的不是背命令而是有一套完整的排查逻辑。3. 永久代和元空间的区别是什么永久代属于 JVM 堆元空间使用本地内存永久代大小受MaxPermSize限制元空间默认受系统内存限制JDK 7 把字符串常量池、静态变量移到堆JDK 8 移除永久代。4. 对象从创建到回收的完整流程类加载检查 - 分配内存TLAB/指针碰撞/空闲列表- 零值初始化 - 设置对象头 - 执行 init 方法 - 栈或静态变量引用 - GC Roots 不可达时被标记回收 - 经过可达性分析和回收器执行回收。5. 为什么 G1 能成为 JDK 9 的默认收集器G1 把堆划分成多个 Region可以做到“在不牺牲太多吞吐量的前提下更可控的停顿时间”。它在全堆范围回收但优先回收垃圾最多的 Region所以叫 Garbage First。适合大内存机器和服务器环境。6. 如何判断对象可以回收用可达性分析法从 GC Roots 出发不可达的对象即可回收。不要提引用计数法HotSpot 没有采用它因为循环引用问题解决不了。最后再多说一句我的习惯面试前与其死背“内存模型”四个字不如亲手写一个会 OOM 的小程序用jmap导出堆再在 MAT 里看一遍对象引用。整个过程大概一小时但它带给你的理解深度比看十遍文章都管用。JVM 的内存模型从来不是一个“背下来就能过面试”的知识点它是你定位线上问题、判断系统容量、设计高并发服务时的底层地图。先把这张地图刻在脑子里之后再谈调优和病根查找都会顺手得多。