JVM内存模型全解析:从运行时数据区到OOM排查实战
2026/9/18 10:24:14 网站建设 项目流程

JVM对每一位Java开发来说,都是一座绕不开的“山”。平时写代码、调接口、排查线上问题,绕来绕去最终都会撞上内存相关的话题。面试更不用说,JVM内存模型几乎是必考项,但很多人对这一块的理解停留在“背八股”的阶段——能说出堆、栈、方法区,却说不清对象在内存里到底怎么流转,一个变量到底存在哪里,OOM到底是怎么回事。

这篇文章我不想讲成教科书式的知识点罗列,而是想从“内存这张地图上到底有什么”“对象的一生怎么走”“满了之后怎么回收”“出问题了怎么排查和调优”这几个角度,帮你把JVM内存模型梳理成一个能直接用于实战的体系。不管你是准备面试的求职者,还是工作中经常被线上内存问题折磨的开发,这篇文章都值得花二十分钟认真读一遍。

1. 先给内存画张地图:运行时数据区到底存了什么

JVM内存模型的核心,就是Java虚拟机规范里定义的“运行时数据区”。理解它之前,先建立一个整体概念:JVM启动后,会从操作系统申请一块内存,这块内存被划分成几个区域,不同的区域存放不同类型的数据,并且承担不同的职责。

这个划分方案不是随便定的,它体现了两条非常重要的设计思路。一是线程隔离:每个线程都有自己的私有空间,互不干扰,保证线程安全。二是线程共享:所有线程公用的大仓库,存放类的元数据、实例对象、静态变量等全局级数据。

搞清楚这个基础概念,后面看对象分配、垃圾回收、内存溢出都会顺很多。

1.1 线程私有的三块地:程序计数器、虚拟机栈、本地方法栈

这三块区域随着线程的创建而创建,随着线程的消亡而释放,不需要垃圾回收器操心。

程序计数器是三者中最小的,它记录的是当前线程正在执行的字节码指令地址。为什么需要它?因为多线程切换时,CPU要能恢复每个线程之前的执行位置,程序计数器就是干这个活的。有个细节很多人不知道:如果执行的是Java方法,计数器记录的是字节码指令地址;如果执行的是native方法,计数器值为空。它是唯一一个在Java虚拟机规范里没有规定任何OutOfMemoryError情况的区域。

虚拟机栈描述的是Java方法执行时的线程内存模型。每个方法从调用到结束,对应一个栈帧的入栈和出栈。栈帧里装着局部变量表、操作数栈、动态链接、方法返回地址等信息。我经常跟人打比方:栈就像一个弹夹,每个方法调用就是压入一颗子弹,返回就是弹出一颗,压得太深弹不出来就会爆——也就是StackOverflowError。

面试中高频的“局部变量存在哪里”这个问题,答案就在局部变量表里。它存放了方法参数和方法内部定义的局部变量,单位是槽(Slot),long和double占用两个槽,其他类型占一个。注意,局部变量表在编译期就已经确定了大小,不会运行时动态变化。

本地方法栈和虚拟机栈非常相似,区别在于虚拟机栈为Java方法服务,本地方法栈为native方法服务。HotSpot虚拟机把两者合并成了一个,但这只是实现选择,不等于规范规定,面试被问到别答错。

1.2 线程共享的两大区域:堆与方法区

是JVM管理的最大一块内存,也是垃圾回收的主战场。几乎所有对象实例和数组都在这里分配。它被设计成线程共享,所以多个线程同时创建一个对象,就涉及并发安全的问题——这也是后面要讲的TLAB机制存在的原因。

堆的内部逻辑上被划分为新生代和老年代,新生代又细分为Eden区和两个Survivor区。为什么要这么划分?用一个生活中的场景来理解:大多数人年龄不大就“走”了,只有少数能“长寿”。把对象按生命周期分放到不同区域,垃圾回收器就能分区处理,不同区域采用不同的回收策略,效率远高于每次都扫描全部内存。关于对象在堆中的流转,我下一章详细展开。

方法区存放的是类的元数据信息,包括类的结构信息(字段、方法、接口)、运行时常量池、静态变量、即时编译器编译后的代码缓存等。JDK 7及之前,方法区的实现叫永久代;JDK 8开始,永久代被移除,改为元空间(Metaspace),并且元空间不再使用JVM堆内存,而是使用本地内存(直接内存)。

这个改动对实际开发影响巨大,很多人应该印象很深:以前经常报java.lang.OutOfMemoryError: PermGen space,升级到JDK 8之后这类问题少了,因为元空间默认只受物理内存限制,写动态生成类的框架(比如CGLIB、反射、热部署场景)时没那么容易把方法区挤爆。

1.3 一张表看懂:谁存什么、出了错报什么

把以上内容汇总成一张表,方便你对照记忆,无论是复习还是面试前突击都很好用:

区域是否线程共享存什么可能出现的异常
程序计数器字节码执行地址
虚拟机栈局部变量表、操作数栈、动态链接、返回地址StackOverflowError、OutOfMemoryError
本地方法栈native方法调用StackOverflowError、OutOfMemoryError
对象实例、数组OutOfMemoryError: Java heap space
方法区/元空间类元信息、常量、静态变量OutOfMemoryError: Metaspace

这里有个重要补充:除了上面这五大区域,JVM还使用一块直接内存。它不属于运行时数据区,也不是Java虚拟机规范中定义的部分,但JDK的NIO类库经常用。直接内存的分配不经过堆,而是通过native函数库直接分配堆外内存,再用一个存储在堆里的DirectByteBuffer对象操作这块内存。它的好处是省去了一次从堆内复制到堆外的过程,在IO密集场景下性能提升明显;坏处是如果不受管控地使用,容易出现OutOfMemoryError: Direct buffer memory,而且肉眼很难排查,因为堆内存明明很健康。

2. 对象的一生:从new出来到被回收要经过哪些站

理解了内存地图,接下来回答一个核心问题:一个对象从被new出来那一刻起,它在内存里是怎么一步步走完一生的?这个过程直接决定了你会不会遇到内存问题,也是解决各种JVM疑难杂症的底层基础。

2.1 new一个对象时,内存里发生了什么

很多人以为new Object()就是简单地在堆里划一块空间,实际上步骤比想象中的复杂。

第一步,类加载检查。虚拟机遇到new指令时,先检查这个类是否已被加载、解析和初始化过。如果没有,必须先执行类加载过程,把类的元信息放进方法区。

第二步,为对象分配内存。对象所需内存大小在类加载完成后就可以完全确定,接下来就是在堆中划分一块区域。分配方式有两种:如果堆内存是规整的(使用标记-整理算法的回收器,如Serial、ParNew),采用指针碰撞,把空闲内存的指针向空闲方向移动所需大小;如果堆内存不规整(使用标记-清除算法的回收器,如CMS),采用空闲列表,从列表中找到一个足够大的空间分给对象,并更新列表。

第三步,内存空间初始化。虚拟机将分配到的内存空间(不包括对象头)都初始化为零值,这样对象的实例字段在Java代码中不赋值就能直接使用默认值,底层靠的就是这一步。

第四步,设置对象头。对象头里存储这个对象的哈希码、GC分代年龄、锁状态标志、类元数据指针等信息。这些信息在对象生命周期中会被频繁更新,也是后面理解偏向锁、轻量级锁的基础。

第五步,执行构造方法,也就是<init>方法,一个真正符合程序员视角的对象才诞生。

2.2 对象在堆中的分区流转:Eden、Survivor与老年代

绝大多数对象创建后很快就不再使用,这就是“朝生夕灭”的特性。为了高效回收,HotSpot把堆分成了新生代和老年代。

新生代中,Eden区是对象诞生的主战场,大多数对象直接在这里分配。新生代还包含两个Survivor区,习惯叫S0和S1(也叫From和To),两者大小相等。为什么要搞两个Survivor区,而不是一个?这是为了避免内存碎片化。回收时,把Eden和某个Survivor中存活的对象复制到另一个空的Survivor中,然后一次性清空Eden和刚才那个Survivor,这样始终有一个Survivor是空的,内存始终是规整的。

对象每经历一次Minor GC(新生代回收),年龄加1,默认达到15岁(可通过-XX:MaxTenuringThreshold设置)就会被移到老年代。但这不是唯一路径,有两类对象会直接进入老年代:一是大对象(可以通过-XX:PretenureSizeThreshold指定大小阈值),因为大对象在Eden和两个Survivor之间来回复制成本太高;二是Survivor区放不下的大龄对象。

2.3 两个影响对象分配的隐藏机制:TLAB与逃逸分析

先说TLAB(Thread Local Allocation Buffer,线程本地分配缓冲)。堆是线程共享的,多个线程同时在Eden区创建对象,就必须加锁同步,这严重影响效率。HotSpot的做法是:为每个线程在Eden区分配一小块独立空间,线程就在自己的TLAB里分配对象,互不干扰,只有TLAB用完并重新申请时才需要加锁。你可以把TLAB理解成每个线程的“私人小仓库”。

再说逃逸分析。JVM通过逃逸分析判断对象是否会被外部访问,如果对象只在方法内部使用,没有被“逃逸”出去,JVM就会做优化:一是栈上分配,把对象分配在虚拟机栈的局部变量表里,方法结束栈帧弹出,对象直接销毁,连垃圾回收都不用参与;二是标量替换,把对象的字段拆散成一个个局部变量,不创建对象实体;三是锁消除,对象不会逃逸时,加在它上面的同步锁可以被安全地去掉。

这两个机制解释了为什么你new了很多小对象却很少出现性能问题,也解释了一些“玄学”——同样的代码,为什么某些情况下GC压力差异巨大,很可能就是逃逸分析是否生效造成的。

3. 垃圾回收视角:判定标准、收集器选型与Stop The World

内存模型运转起来之后,堆里会积累大量不再使用的对象。JVM需要一套机制把这些“垃圾”找出来并清除,腾出空间给新对象。这一章我从三个维度把GC机制讲透:怎么判断一个对象该回收、有哪些回收器可选、回收时对业务线程有什么影响。

3.1 什么样的对象该回收:可达性分析

判断对象是否存活,主流JVM采用可达性分析算法。思路是:从一组称为GC Roots的根对象出发,沿着引用链向下搜索,搜索走过的路径叫引用链。当一个对象到GC Roots没有任何引用链相连时,就证明此对象不可达,可以被回收。

GC Roots包括哪几类?

  • 虚拟机栈中引用的对象(方法参数、局部变量)
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象
  • Java虚拟机内部的引用(基本类型对应的Class对象、常驻的异常对象、系统类加载器等)
  • 被同步锁(synchronized)持有的对象

注意一个细节:不可达不等于立即回收。对象要真正被回收,至少要经历两次标记。第一次标记后,如果对象没有覆盖finalize()方法或者finalize()已被调用过,就不会再执行;如果覆盖了且没执行过,对象会被放进一个低优先级的队列里,等某个线程触发finalize()。如果在这个方法里对象重新与GC Roots建立了引用链,就能“自救”一次。不过现在的实践强烈不推荐依赖finalize(),它性能不稳定、执行时机不确定,早就是被官方标记为废弃的机制了。

和可达性分析对应的还有引用计数算法,原理是给对象加一个计数器,每被引用一次加1,失效减1,为0就回收。这个算法简单高效,但解决不了循环引用问题——两个对象互相引用,却没有任何外部引用时,它们各自计数器都不为0,永远无法被回收。Java没有采用它,这个知识点是面试高频题。

3.2 重要补充:四种引用类型与回收的关系

可达性分析判断的是“对象能否被回收”,但能不能被更快回收、什么时候回收,还取决于引用类型。

引用类型回收时机典型使用场景
强引用永不回收,OutOfMemoryError也不回收new出来的对象
软引用内存不足时回收缓存实现(MyBatis等框架常用)
弱引用下一次GC时回收ThreadLocal的ThreadLocalMap、WeakHashMap
虚引用随时可能回收,主要用于跟踪对象回收NIO堆外内存回收通知、监控对象回收

拿ThreadLocal举例:它的Entry继承了WeakReference,key(即ThreadLocal实例)是弱引用。所以ThreadLocal使用完,只要把强引用断开,下次GC时key就会被回收,但value是强引用,还挂在ThreadLocalMap里,这就造成了value无法被访问却无法被回收的内存泄漏。这也是为什么大家强调使用完ThreadLocal后要调用remove()清除数据。

3.3 常见垃圾回收器怎么选:Serial、Parallel、CMS、G1到ZGC

JVM的垃圾回收器不是只有一种,不同场景有不同选择。我按发展脉络来讲,这样你更容易理解为什么会有这么多选择。

Serial回收器是最古老的单线程回收器,GC时必须暂停所有工作线程(Stop The World),直到回收完成。它简单高效,在单核CPU或小内存场景下反而表现优秀。客户端模式下默认使用。

Parallel回收器是Serial的多线程版本,利用多核CPU并行回收,JDK 8默认使用的是它。它关注的是高吞吐量,适合后台计算任务,对响应时间要求不高的场景。

CMS回收器(Concurrent Mark Sweep)是第一个真正意义上的并发回收器,目标是“低停顿”。它主打并发收集,GC线程和工作线程尽量同时运行。但它的缺点也不少:基于标记-清除算法,会产生大量内存碎片;对CPU资源敏感;无法处理浮动垃圾,可能触发Concurrent Mode Failure,进而退化成Serial Old进行Full GC。

G1回收器(Garbage First)从JDK 9开始成为默认回收器。它的核心设计是:不再严格区分物理上的新生代和老年代,而是把堆划分为一个个Region,逻辑上仍然保留分代。GC时优先回收价值最大的Region,也就是“垃圾最多、回收成本最低”的区域,这样能控制停顿时间,同时兼顾吞吐量。G1可预测的停顿模型让它在服务端广泛使用。

ZGC回收器从JDK 11引入、JDK 15转正,核心能力是把停顿时间控制在10毫秒以内,且不随堆大小增长。它通过读屏障、染色指针、内存多重映射等多项技术,实现了几乎全并发。如果线上业务对延迟极度敏感,堆又很大(几十GB甚至上百GB),ZGC是目前最值得考虑的方向。

总的来说,选择逻辑非常简单:追求吞吐量选Parallel,追求低延迟选G1(默认)或ZGC,老系统继续用CMS也别急着改,稳定优先。没有最好的回收器,只有最适合当前场景的回收器。

3.4 Stop The World到底是什么,以及它为什么躲不开

几乎所有回收器在进行某些操作时,都不得不暂停工作线程,这个暂停就是Stop The World(简称STW)。为什么一定要停?因为GC需要枚举根节点、标记对象、整理内存,如果工作线程还在不停创建新对象、修改引用,内存状态一直在变,标记结果就不可信了。

现代JVM做了大量优化来缩短STW时间。在枚举根节点时,HotSpot使用OopMap记录栈上和寄存器中哪些位置是引用,GC时直接查询,不用逐条遍历字节码。但不可能为每条指令都生成OopMap,于是JVM只在“安全点”记录。安全点是程序执行中能让所有线程都暂停的特殊位置,一般选在方法调用、循环跳转、异常跳转等指令处。线程在安全点检查是否需要GC时,如果线程处于阻塞或睡眠状态,无法响应中断,就要靠安全区解决——只要线程进入安全区,GC就不用等它进入安全点了。

理解STW的意义在于:它决定了线上GC停顿的可见影响。如果Full GC频繁且耗时高,业务接口的延迟曲线就会很难看。因此,排查性能问题时,先看一眼GC日志,往往能快速定位是不是STW在作祟。

4. 实战视角:从内存参数配置到OOM排查的完整链路

理论讲了一堆,最终都要落到“怎么调、怎么查”上。这一章我按实际工作中最常走的链路来讲:先配置合理的JVM参数,再用工具定位问题,最后从编码习惯上根治内存问题。这条链路走通了,大部分内存相关的线上故障都能对付。

4.1 内存参数先搞懂再抄:-Xms、-Xmx、-Xmn、-XX:MetaspaceSize

JVM启动参数很多,但内存相关的核心参数就那几个,先把它们彻底搞懂。

参数作用默认值
-Xms堆初始大小物理内存的1/64
-Xmx堆最大大小物理内存的1/4
-Xmn新生代大小由JVM自行分配
-XX:MetaspaceSize元空间初始大小约21MB
-XX:MaxMetaspaceSize元空间最大大小无上限
-XX:MaxDirectMemorySize直接内存最大大小等于-Xmx

实际部署时最大的一个原则是:-Xms和-Xmx务必设置成相同的值。原因很简单,如果初始堆和最大堆不一致,JVM在运行期间会根据需要动态扩展和收缩堆,这个过程涉及堆内存的重新分配,会造成不必要的性能开销,也让GC表现不稳定。把两者设置一致,堆大小固定,虽然牺牲了一点灵活性,但换来了可预测的性能表现,这在服务端实践中是公认的稳妥做法。

新生代大小怎么定?一般经验是占堆的1/3到1/2。新生代太小,短命对象频繁进入老年代,导致老年代频繁GC;新生代太大,老年代空间变小,Full GC压力加大。如果你拿不准,先维持JVM默认比例跑一段时间,用GC日志观察Minor GC和Full GC频率再调整,不要一开始就凭感觉设置。

元空间默认没有上限,这听起来很宽松,但风险在于:如果应用动态生成类(CGLIB、反射、Groovy脚本等),且代码有Bug导致类无法卸载,元空间就会无限制增长,最终把系统物理内存耗尽。生产环境建议设置-XX:MaxMetaspaceSize,给元空间一个天花板,防止极端情况拖垮整台机器。

4.2 用工具说话:jps、jstat、jmap、jstack、jconsole、visualvm、arthas

参数配好只是第一步,真正的功夫在于能用工具定位问题。我把最常用的排查工具串成一个完整流程。

先从jps开始,列出当前机器上的Java进程及其ID,相当于对应用“点名”。

拿到PID之后,用jstat观察内存和GC情况:

# 每隔1000毫秒输出一次GC信息,共输出10次 jstat -gc 1234 1000 10

输出里有几个关键列:YGC(新生代GC次数)、YGCT(新生代GC总耗时)、FGC(Full GC次数)、FGCT(Full GC总耗时)。如果FGC频繁且FGCT持续走高,基本可以断定老年代空间紧张或元空间不足。

接着用jmap导出堆内存快照:

# 查看堆内存使用情况 jmap -heap 1234 # 导出堆转储文件 jmap -dump:format=b,file=/tmp/heap.bin 1234

导出的heap.bin文件可以用MAT(Memory Analyzer Tool)或VisualVM分析。MAT打开后重点看“Leak Suspects”报告,它会自动找出占用内存最高的对象和可能的泄漏路径,这一步是排查内存泄漏的黄金路径。

jstack用于线程分析,虽然不直接查内存,但排查OOM时经常需要配合。如果堆内存充足却报OOM,可能是线程数爆了——每个线程默认占用1MB栈空间,几万个线程就把地址空间耗尽了。jstack 1234输出线程快照,重点看线程状态是否大量卡在同一个地方。

jconsole是JDK自带的图形化监控工具,可以实时查看堆内存使用曲线、线程数、类加载数,适合开发环境做趋势观察,但不建议在生产环境长时间开着。

arthas是目前排查线上问题最推荐的利器,它是阿里开源的一款Java诊断工具。几个最实用的命令你直接记住:

  • dashboard:总览线程、内存、GC情况
  • memory:查看各内存区域使用情况
  • heapdump:和jmap一样导出堆快照
  • thread -n 3:打印CPU占用最高的三个线程栈

在无法重启应用的场景下,arthas比前面几个JDK自带工具好用得多。做过线上问题排查的人应该都有体会:jmap在大堆场景下触发Full GC的问题很烦,arthas可以配合,但也要注意在低峰期执行。

4.3 从编码上预防OOM:一些值得长期坚持的习惯

工具能解决的是“出了问题怎么查”,但更高阶的做法是从编码阶段就避免问题发生。根据我踩过不少次坑的经验,这几个习惯非常值得养成。

第一,设置线程池参数时,别只看核心线程数和最大线程数,要同时关注阻塞队列的容量。线程池满了,新任务默认会进入无界队列(比如new LinkedBlockingQueue()),任务无限堆积,最终导致OOM。在生产环境,队列一定设置边界,拒绝策略也要明确。

第二,处理批量数据时,避免一次加载全量到内存。有一次用MyBatis查几十万条记录做批量更新,直接selectList一把梭,结果OOM。改成流式查询、分页查询后问题立刻消失。

第三,使用缓存时设置容量上限和过期策略。本地缓存(如ConcurrentHashMap当缓存用)最容易出现内存泄漏,因为Map自己不会淘汰数据。用Caffeine或Guava等自带淘汰策略的缓存框架,并设置合理的最大容量。

第四,注意IO流和连接的正确关闭。流没关导致的资源泄露,虽然不一定直接OOM,但日积月累文件描述符耗尽、堆外内存泄漏的案件我见过不少。JDK 7之后的try-with-resources语法能用就用。

第五,谨慎使用ThreadLocal,用完后调用remove。上一章解释过ThreadLocalMap的value是强引用,尤其在线程池场景下,线程复用时间长,value一直挂在线程上,泄漏问题会被放大。

5. 个人排查经历:一次由堆外内存引发的故障复盘

讲完工具和习惯,分享一个我印象很深的真实案例,整个过程很能说明“内存问题不一定是堆的问题”。

那是一个数据同步服务,运行一段时间后接口响应越来越慢,最后直接不响应了。第一反应查堆内存,jstat看GC情况,YGC和FGC都没有明显异常,堆内存使用率也不高,大概稳定在40%左右。当时有点疑惑——堆明明很健康,为什么服务越来越慢?

后来听了运维反馈,说是容器内存报警,整个进程的RSS(常驻内存)已经接近容器上限。这提示问题很可能出在直接内存上。用arthas执行memory命令,果然direct内存占用已经非常夸张,远超正常范围。

进一步排查代码,发现问题出在一个批量下载文件的逻辑里:用ByteBuffer.allocateDirect()分配了直接内存,但在异常分支里没有释放。DirectByteBuffer本身是被堆中的对象引用的,GC时可以通过Cleaner机制回收,但前提是这个DirectByteBuffer对象能被GC到。由于代码错误地把它存进了一个静态Map里,对象永远存活,堆内存不会被撑爆,但直接内存却被一点点蚕食殆尽。

那次修复方案很简单:代码里去掉静态Map的持有,同时用try-with-resources或显式调用sun.misc.Cleaner(新版可以用Unsafe或直接依赖GC)来管理直接内存的生命周期。更重要的是,在那之后我把-XX:MaxDirectMemorySize参数引入了所有服务,给直接内存加了硬性上限。

这个案例教给我一件事:排查OOM类问题,一定要先确认异常信息里出现的是哪块区域。Java heap space是堆溢出,Metaspace是元空间溢出,Direct buffer memory是直接内存溢出。区域不同,根因和排查路径完全不同,开头方向错了,后面全白搭。

6. 面试中的JVM内存模型:几个高频问题的正确打开方式

作为一篇希望“通俗易懂”的JVM内存模型文章,最后用面试场景收尾我觉得最合适,因为很多人学这块的第一动力就是面试。这里挑几个出现频率最高的问题,讲一下面试官真正想听到的得分点在哪里。

“说一下JVM内存模型。”

这是一个开放题,答得好能展示知识广度。不要只报出五个区域的名字。正确姿势是:按线程私有和线程共享分类介绍,每个区域补充“存什么、什么时候出问题”,最后落到GC主战场是堆、元空间在JDK 8的变化上。如果能顺手提一句直接内存,并且说清楚DirectByteBuffer与堆外内存的关系,就远超平均水平了。

“JVM怎么判断一个对象可以被回收?”

只答“可达性分析”四个字只能得基础分。能说清楚GC Roots的几大类、两次标记与finalize自救的关系、为什么不用引用计数算法,就说明你真的理解而不是背的。如果还能补充一句“不同引用类型影响回收时机”,面试官会认为你有实际应用的理解。

“哪些内存区域会发生OutOfMemoryError,哪些不会?”

这是个很能考底细的问题。程序计数器不会OOM,这是规范明确规定的。虚拟机栈和本地方法栈既可能StackOverflowError也可能OutOfMemoryError。堆、方法区/元空间、直接内存都可能OOM,但异常信息里的关键词不一样:Java heap spaceMetaspaceDirect buffer memory。把这三类关键词说清楚,面试官会立刻意识到你处理过真实问题。

“了解哪些垃圾回收器,生产环境用什么?”

先按发展阶段把Serial、Parallel、CMS、G1、ZGC各自的特点说一遍,重点突出CMS的低延迟但碎片问题、G1的Region分治与可预测停顿、ZGC的染色指针与几乎零停顿。最后落到生产环境选型逻辑上——默认G1、追求吞吐用Parallel、追求极致延迟上ZGC。如果还能说出“没有最好的回收器,只有最合适当前场景的”,结尾就很漂亮。

我个人在实际排查和面试别人时最深的一点体会是:JVM内存模型不是背出来的,是用问题喂出来的。你踩过一次堆外内存的坑,对直接内存的理解胜过看十篇文章;你亲眼看到一次Full GC导致的口袋告警,对STW的理解就刻进骨头里了。所以把这篇文章当成一张地图,遇到具体问题的时候回来对照,理解会逐步加深。如果是刚接触JVM不久的读者,建议先动手跑几次jstat和jmap,配合本地的Demo程序,比纯看文章效率高得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询