ARTICLE DETAIL

资讯详情

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

JVM内存模型深度解析:运行时数据区、GC与OOM排查实战

JVM内存模型深度解析:运行时数据区、GC与OOM排查实战 每个做Java的人迟早都会遇到一次关于JVM内存模型的灵魂拷问。可能是在面试现场被问到“JVM运行时数据区有哪些”也可能是线上服务突然报出OutOfMemoryError时的手忙脚乱。我第一次系统梳理这块知识就是因为一次半夜的线上告警——一个定期任务把内存打满接口大面积超时而我对着堆栈日志完全不知道从哪里下手。从那以后我就意识到内存模型不是面试题而是一张地图搞懂它排查问题、做调优才谈得上章法。这篇文章我尽可能把JVM内存模型讲透从运行时数据区的划分、堆内存的分代设计到垃圾回收和参数调优配合实际踩坑经验一起输出。适合准备Java面试的人也适合已经在写业务代码但想提升排查能力的开发者。不求你一次记住所有细节但希望你读完能建立起自己的分析框架再遇到内存问题至少知道该往哪个方向看。1. 为什么JVM内存模型值得反复咀嚼很多人写了好几年Javanew对象、调接口、存数据库都驾轻就熟但问他“一个对象从创建到回收经历了什么”他只能说出“用完了就GC了”。这就像开车开了五年却从没打开过引擎盖车能跑但一旦亮故障灯就只能干瞪眼。JVM内存模型说的是Java程序运行时内存到底被划分成了哪些区域每个区域负责什么对象在这些区域里怎么流转以及它们什么时候被回收。这套模型的直接来源是《Java虚拟机规范》官方文档对运行时数据区有明确划分但说实话光看规范容易犯困因为它是法律条文式的写法。我更建议把这套规范和技术实战结合起来读一边看区域定义一边对照自己的线上配置和报错日志。理解内存模型的收益主要体现在三件事上。第一是定位问题的能力OOM分好几种堆溢出和栈溢出、元空间溢出的报错信息、排查思路完全不同第二是调优的方向感JVM参数动辄上百个如果你不知道堆内存内部有新生代和老年代之分就理解不了为什么-Xms和-Xmx只是起点后面还有一堆比例参数要配第三是底层功底的积累GC算法、类加载机制、并发编程的可见性问题全都和内存模型纠缠在一起。可以说内存模型是Java后端知识体系里承上启下的那一环。还有个更现实的原因面试高频。你去翻那些大厂的Java面试题“JVM内存模型”“jvm内存模型”“GCjava内存模型优化”几乎轮着出现。但我的建议是别为了面试死记硬背。你就想象自己是在给一台运行的Java服务做体检每一块区域都是待检查的器官这样学下来的知识比背二十条面试答案都有用。2. 运行时数据区Java程序真正落地的地方Java程序运行时的内存区域划分由JVM规范定义HotSpot虚拟机是市面上最常用的实现Oracle JDK和OpenJDK用的都是它所以下文都以HotSpot为例展开。规范里画了五块“正式挂牌”的区域程序计数器、虚拟机栈、本地方法栈、堆、方法区。另外还有直接内存这种“编外区域”虽然不归堆管但用得多了也经常引发问题。这五块区域按照线程归属可以分为两类线程私有的和线程共享的。程序计数器、虚拟机栈、本地方法栈是跟着线程走的一个线程一份天然不会产生并发竞争堆和方法区是进程级别的所有线程共享垃圾回收的主战场就在这两块。先把这条线索拎清楚后面看GC和并发问题就容易多了。2.1 程序计数器最小却最不能出错的区域程序计数器是JVM里最小的内存区域它就像线程的执行进度记号。CPU在执行Java代码时靠字节码解释器不停地在指令之间跳转循环、分支、异常跳转都需要知道“下一步在哪一行”这个记录下一条指令地址的东西就是程序计数器。这块区域有非常独特的性质它是唯一一个在规范里没有规定任何OutOfMemoryError情况的区域。原因也好理解每个线程的计数器存储的只是一个行号或者空指针容量需求非常固定不会被撑爆。但它又有一种很经典的异常场景如果Java方法执行了native本地方法那程序计数器的值是未定义的这也是为什么在用JNI写混合代码时要格外小心。你平时写业务代码基本感知不到它的存在但它贯穿了方法调用的全过程。理解程序计数器的另一个意义在于它是“线程切换后能恢复执行”的关键支撑。JVM的多线程是靠抢占式调度实现的一个线程随时可能被切走等它再拿到CPU时间片时就要靠程序计数器找回上次执行到哪一行。如果这块区域设计的太复杂线程切换的成本就会直线上升。所以它就简简单单存个地址不做任何花哨的事。2.2 虚拟机栈玩的就是压栈弹栈虚拟机栈描述的是Java方法执行时的线程内存模型每次方法调用都会创建一个栈帧压在栈顶方法结束就弹出。栈帧里装着局部变量表、操作数栈、动态链接、方法返回地址等数据。我打个比方这就像玩俄罗斯方块方法调用就是不断落下方块调用结束就是消除一行栈永远在有限的高度里来回伸缩。最容易出问题的是局部变量表和操作数栈。局部变量表里存的是方法参数和方法内部定义的局部变量注意这里存的是基本数据类型、对象引用不是对象本身。对象本体在堆里栈里只有握着它的指针。这个区分特别重要很多搞混内存模型的开发就是栽在“栈里到底有什么”这个问题上。如果一个线程请求的栈深度超出了JVM允许的范围就会抛StackOverflowError这是最常见的栈异常。但还有一种不那么常见的可能虚拟机栈扩容时申请不到足够内存会抛OutOfMemoryError。比如某些容器环境给线程栈设置的-Xss参数过小同时方法是重度递归调用就有可能出现这种OOM。平时写代码遇到递归一定要先估算深度或者干脆用循环替代别让栈去承担不该它承担的压力。2.3 本地方法栈被忽视的第三块地盘本地方法栈和虚拟机栈的作用非常相似唯一区别是虚拟机栈为Java方法服务本地方法栈为native方法服务。在HotSpot里这两者被合并成了同一个但规范层面它们是分开的。为什么单独划一块出来因为Java不是完全封闭的很多底层能力要通过JNI调用C/C编写的本地库比如文件操作、网络socket底层处理、以及各种底层图形库。调用本地方法时参数需要压入本地方法栈方法执行过程中还可能自己再申请栈内存。这块区域同样可能抛StackOverflowError和OutOfMemoryError只是平时遇到得少。值得关注的是本地方法栈常常成为内存问题的隐身帮凶。比如一个本地库申请了内存却没有及时释放而你从Java层面看内存明明很健康这种情况定位难度直接上一个档次。排查的时候如果发现堆和元空间都正常、但进程RSS内存一直涨就该考虑是不是native层的内存泄漏了到时候光调JVM参数是没有用的。2.4 堆JVM内存的绝对主角堆是Java内存管理的核心区域也是所有线程共享的一块内存。几乎所有的对象实例和数组都在这里分配。它的容量大小直接决定了Java程序能支撑多大的数据量我们平时调的-Xmx参数管的就是这块区域的上限。早年间技术社区流传过一个说法“Java对象都在堆上分配”这句话在绝大多数场景下成立直到现代JVM引入了逃逸分析和栈上分配优化少数对象可以不经过堆直接在线程栈里分配。但这是优化层面的特例正常情况下你仍然可以认为堆就是对象的大本营。堆内部分区在垃圾回收视角下可以分为新生代和老年代新生代里面又分为Eden区、From Survivor区、To Survivor区。为什么要搞得这么复杂后面专门用一节来讲。这里你先记住一个总原则堆是GC工作的重点区域JVM对堆的管理粒度是“分代”不同年龄段的对象用不同的回收策略。线上频繁出现的OutOfMemoryError如java.lang.OutOfMemoryError: Java heap space就是堆空间不足的直接信号。这种问题常见于一次性加载了超大集合、大对象数组、或者存在内存泄漏让老年代被持续填满。看到这个报错第一件事不是加内存而是先拿堆转储文件做分析搞清楚是谁占用了空间、有没有被错误持有的引用。2.5 方法区与元空间7到8的版本变迁方法区也是线程共享的它用来存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。在JDK 8以前方法区的落地实现叫“永久代”很多资料里直接叫它PermGenJDK 8之后HotSpot把永久代搬了出去取而代之的是“元空间”Metaspace。你去看最新搜索热点里“jvm内存模型”相关的内容几乎都会提到这个版本差异因为面试题太喜欢问了。永久代和元空间的核心区别在于永久代有大小限制而且就放在堆内存里调-XX:MaxPermSize来管理一不小心就溢出元空间则使用了本地内存也就是进程内存默认情况下上限受物理内存限制不再作为堆内存的一部分。这带来的直观影响是JDK 8以后类元数据导致的OOM变少了但也不是完全免疫如果加载了海量动态代理类或者热部署次数过多元空间依然会扛不住。方法区里还有一个重要的信息运行时常量池。字符串驻留、Class文件的常量表都会被加载到这里。由于JVM呢JDK 7开始把字符串常量池从永久代移到了堆这直接导致后面很多关于字符串相等性的面试题有了新说法。你问“两个用new创建的String是否相等”答案永远不能脱离“常量池在堆里”这个前提。3. 堆内存分代设计与GC触发时机堆内部搞分代本质上是基于一个经验事实大多数对象的生命周期非常短活不过几轮垃圾回收少数对象能活得很久比如Spring容器的单例Bean、缓存对象。如果让短命对象和长寿对象混在一个空间里每次GC都要扫描全堆代价太高。分代之后短命对象集中放在新生代用高频次、低成本的Minor GC清理长寿对象挪到老年代用低频次、重量级的Full GC兜底。这套设计直接影响了调优时的思维。你设置堆内存参数时不只是在设置“总空间大小”更是在调整新生代和老年代的结构比例、以及Eden和Survivor的比例。每个参数的选择都是在跟业务对象生命周期特征做匹配。3.1 新生代的三块区域怎么配合新生代分为一块Eden区和两块Survivor区字面上叫Eden、From Survivor、To Survivor。绝大多数新对象被创建后首先进入Eden区。当Eden区快满时触发Minor GC把Eden和From Survivor里还存活的对象复制到To Survivor同时对象年龄加一。之后原本的From和To角色互换保证总有一块Survivor区是空着的以便下一次复制。这个过程用到的复制算法代价就是必须牺牲一块Survivor区的空间当作“逃生通道”。HotSpot默认的Eden和Survivor比例是8:1:1也就是新生代里90%的空间归Eden和Survivor共用10%的Survivor用来放复制后的存活对象。这个比例可以通过-XX:SurvivorRatio调整。我见过不少人调优时一上来就动这个比例但大多数场景根本没必要。比例设置的核心依据是一次Minor GC后Eden里存活下来的对象大约占多大空间。如果你发现Survivor区容量偏小对象频繁晋升到老年代那才需要考虑调大Survivor比例。贸然把Eden扩得很大、Survivor缩得很小反而会让存活对象被提前晋升老年代负载加重。3.2 晋升老年代的三个条件对象什么时候告别新生代、进入老年代主要由三条规则决定。第一是年龄阈值。每熬过一次Minor GC对象年龄加一默认到15岁就会被晋升到老年代。这个阈值可以通过-XX:MaxTenuringThreshold修改但动手前要想清楚太大的阈值会让长期存活对象反复复制浪费性能阈值太小又会把本该留在新生代的对象提前挤去老年代。第二是动态年龄判定。JVM并不会严格等对象长到15岁才晋升它会根据Survivor区里同龄对象的总大小做一个估算。如果某个年龄段的对象总大小超过了Survivor空间的一半那大于等于这个年龄的对象会直接晋升。这条规则的目的就是防止Survivor空间被占满保证复制算法有地方“倒手”。第三是大对象直接进入老年代。设置-XX:PretenureSizeThreshold参数后超过阈值的对象直接在老年代分配。这样设计是为了避免大对象在Eden区和两个Survivor区之间来回复制浪费不少性能。但注意这个参数只对Serial和ParNew两款收集器生效追求性能的G1不太吃这一套。我之前接手过一个服务最典型的毛病就是短任务创建超大数组结果大对象在新生代反复复制Minor GC频率异常高。看了GC日志后直接给大数组开了“老年代快速通道”GC压力瞬间小了很多。3.3 GC类型和触发时间点按收集范围分常见的GC类型大概分四种新生代收集Minor GC、老年代收集Major GC、全堆收集Full GC、以及G1这种混合收集Mixed GC。平时见到最多的报错和调优场景集中在前三类。Minor GC触发时机是Eden区不够用了它执行速度快对应用停顿影响小。Major GC和Full GC的触发条件就复杂多了老年代空间不足、元空间不足、显式调用System.gc()虽然设置了-XX:DisableExplicitGC后可以屏蔽都可能导致Full GC。如果JDK 8之后的默认垃圾收集器是G1那Full GC的触发还可能涉及G1特有的并发失败场景比如并发标记周期未完成时堆内存就被耗尽。在排查GC问题时最忌讳猜。一定要开GC日志看每一条事件的实际原因。你如果看到Full GC调用里System.gc()频繁出现而代码里确实有人写了这行那基本可以从“其实我不需要在这里强制回收”开始讲了。很多Dev写了System.gc()是为了释放内存结果反而触发整堆停顿造成更大的延迟抖动。4. 从一次线上OOM开始反推内存模型纸上谈兵聊完了运行时数据区接下来用三次真实的问题场景把堆、栈、元空间串起来。这几个场景并不稀奇但每次都能在群里引出一堆讨论因为它们把内存模型从“八股知识”变成了“排查路径”。4.1 一次典型的堆溢出排查过程先说我遇到最多次的Java heap space溢出。场景是这样一个运维平台的前端上传接口在用户上传Excel文件后服务端需要把整个文件解析成一堆行对象再做批量校验。业务高峰期一次性上千个文件并发上传每个Excel最多三万多行一瞬间堆就被塞满了。当时的处理链路是先用jstat -gcutil看各个区域的内存使用率和GC次数发现Eden区占满后对象无法晋升到Survivor区Survivor空间看起来够但老年代增长速度也很快Full GC频繁。接着用jmap -dump:formatb,fileheap.bin把堆快照拉下来配合MAT分析很快就看到问题对象是Excel行解析时保留的原始行列表被一个静态缓存持有着根本没释放。修复方案有两条线同时走代码侧解决引用持有问题因为静态缓存的存在导致解析后对象永远无法被回收配置侧给新生代适当加大因为大批量临时对象确实需要更大的Eden空间来缓冲。修复后GC日志明显平缓了Full GC从每分钟好几次降到几乎一天一两次。这个案例我记了很久因为它很好地说明了堆溢出不全是“内存给少了”很多时候是对象生命周期管理出了问题。如果你是第一次遇到堆OOM建议按这个顺序排查先确认OOM类型然后拉GC日志判断GC频率和触发条件再拉堆转储找大对象和引用链。一套流程走完至少能把问题范围缩小到某个类或某个集合上。千万别上来就加-Xmx你很可能只是把爆炸时间点往后拖延了。4.2 栈溢出的常见姿势栈溢出在Web应用里不算高频但一旦出现定位往往非常直观。最常见的触发场景就是没有终结条件的递归调用。比如一个解析树状结构的工具类写递归时忘了处理环数据一跑就直接怼穿栈。StackOverflowError的特点是报错信息里会带着一长串递归调用栈最后一行是你递归方法的入口。那么多个栈帧压在一起最直接的原因就是方法嵌套深度超过了栈的允许深度。加-Xss参数可以增大单个线程的栈容量但治标不治本你能提高某次调用的深度上限却改变不了代码本身无限递归的事实。遇到这类问题我不会先去调参数而是会先检查递归终止条件和数据预校验。树状结构遍历前先确认节点之间的父子关系没有成环递归深度如果已知很大直接改成显式的循环加栈数据结构或者换成迭代器写法从根上规避栈深度风险。还有一点很多人不知道线程池里的线程栈也会受-Xss影响。如果你为线程池设置了很大的核心线程数同时每个线程栈都开得很深那光线程栈本身就可能消耗掉数百MB的进程内存。这属于栈内存引发的间接OOM排查时容易被忽视。4.3 元空间溢出的冷门陷阱JDK 8以后元空间溢出的报错是java.lang.OutOfMemoryError: Metaspace。它的触发条件通常是加载了太多类或类加载器本身无法被回收导致它们加载的类元数据一直堆积在元空间。最常见的热部署场景就是一个很好的例子。Web容器每次重新加载应用都会创建新的类加载器如果旧类加载器没有被清理那它加载的类就不会释放反复热部署几次元空间就炸了。还有一个隐蔽场景是动态代理和大量反射每生成一个代理类JVM都要在元空间里生成对应的类元数据如果某个框架不加节制的创建代理类元空间就会被塞满。排查这类问题的思路和堆溢出类似但对象换成了类。用jcmd GC.class_stats或者JDK自带工具查类数量如果发现某个前缀的代理类动辄几万个那基本坐实了动态类生成失控。修复方向不外乎两种让类加载器可回收以及控制动态代理的缓存策略别每次调用都生成新代理。4.4 内存泄漏和内存溢出的区别很多人把这两个词混着用但排查思路上差别非常大。内存溢出是真正申请不到内存了属于急性病内存泄漏是某个对象没用了却还被人持有引用垃圾回收拿它没办法属于慢性病时间久了也会拖成溢出。内存泄漏的经典案例包括静态集合里不断塞数据却不清理、监听器注册了但忘记注销、线程池里任务持有重量级对象、以及各种缓存组件设计了超时淘汰却因为误操作失效。判断是不是泄漏一个通用办法是连续做多轮同样的操作然后观察堆占用是否持续攀升。如果操作本身不应该长期保留对象但堆占用一步步涨上去那基本就是泄漏。做Java开发的人如果能把“溢出是急性病、泄漏是慢性病”这个框架想清楚问诊的时候就不会跑偏。面试官问你“什么情况会导致OOM”你从泄漏和瞬时流量两个维度展开答案的层次感就出来了。5. 把内存模型变成调优参数模型不落到参数上应用价值少一半。这里整理一组我实际调优时最常用的JVM参数和选择逻辑。不对着具体业务空谈参数但给一个可以“抄作业”的参考框架。5.1 核心参数的意义和设计思路以下是我经常在Server模式下使用的一组基础参数参数作用我的建议-Xms堆初始大小建议和Xmx设成一样避免运行期堆扩容引发性能抖动-Xmx堆最大大小根据业务数据和QPS评估不要超过物理内存的一半左右-Xmn新生代大小这里的值同时影响Eden和两个Survivor的总容量-XX:SurvivorRatioEden和Survivor比例默认8别轻易改除非你有明确的存活对象占比数据-XX:MaxTenuringThreshold对象晋升年龄阈值默认15多数场景不用动-XX:HeapDumpOnOutOfMemoryErrorOOM时自动生成堆转储强烈建议所有线上服务开启-XX:HeapDumpPath堆转储文件路径要指定到有足够磁盘空间的位置-XX:MetaspaceSize / MaxMetaspaceSize元空间初始/最大大小防止元空间无限膨胀给一个合理的上限这里最想强调的是Xms和Xmx两者设成一样能避免JVM在扩容和缩容间反复横跳。开发环境无所谓但线上高并发服务堆大小每动一次垃圾收集器都要重新调整内部结构停顿风险直线上升。所以我的线上配置基本都遵守“堆的长宽一开始就给定不留给JVM自我调整的空间”。MetaspaceSize也要给个明确上限。虽然元空间默认吃本地内存但如果你不设上限遇到动态类加载失控的情况别说服务本身了整台机器都可能被拖垮。给个上限至少能让JVM在元空间耗尽时抛出可识别的OOM异常方便后续定位。5.2 从参数组合反推业务性质调参的底层逻辑其实是“让堆的分区形态符合对象的生命周期”。如果业务里大量短平快任务创建完就扔那就让新生代大一点给临时对象足够的缓冲如果服务是重缓存型的大量对象长期驻留那老年代的空间就要充足新生代反而可以保守一点。举个例子一个典型的IO密集型服务连接请求频繁但处理逻辑简单每次请求创建的对象普遍活不过一两秒。那就可以把新生代调大SurvivorRatio适当降低让对象尽量死在新生代避免无意义晋升。反过来一个数据仓库服务一个查询就要加载数万行数据到内存做聚合计算这些大对象几乎逃不过晋升老年代的命运那新生代再大也白搭还不如把老年代空间给足同时配一个低停顿的垃圾收集器。所以说参数没有绝对最优只有匹配业务职责的最合适。调优之前先问自己“这个服务的内存大头是谁那些对象的生命周期长什么样”答案直接影响你对堆内部分区的预判。5.3 用GC日志验证参数参数改完之后验证手段就是GC日志。JDK 9以前配置日志项用-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/var/log/gc.logJDK 9以后统一日志框架用-Xlog:gc*info:file/var/log/gc.log:time,uptime,level。两种写法我都用过现在新项目基本走统一日志选项更干净。拿到GC日志后看两个关键指标GC频率和单次GC停顿时间。如果你调整了新生代大小Minor GC次数明显下降单次GC时间还在可接受范围内那这次调整是正向的。如果GC频率确实下降了但单次Full GC停顿时间反而变长那就要警惕新生代变大导致老年代被压缩对象晋升后的老年代空间更局促了Full GC的扫描压力并未缓解。我最近在优化一个批处理系统时就是通过GC日志发现Full GC每次耗时超过五秒。用jstat -gccause看了上一次GC的原因才发现不是堆空间不足而是Metadata GC Threshold这个锅元空间触发了Full GC。把MetaspaceSize调高后Full GC直接消失。这类问题不看日志靠拍脑袋很难定位。6. 面试中最容易答错的几个内存模型细节讲完运行时数据区和GC最后再聊几个大部分人容易栽跟头的细节。这些点不是纯粹面试技巧因为它们在真实的代码评审和问题排查中就是这么出现的。6.1 栈帧里的引用和对象本体很多人以为“对象是分配在栈上的”这是误解的来源之一。栈里存的是对象的引用不是对象本体。基本类型变量、对象引用、返回值、局部变量表都在栈帧里而对象实例本体在堆里。除非开启了逃逸分析并满足标量替换等条件JVM才可能把对象拆散后分配到栈上但这是编译器优化不是运行时数据区的默认行为。这个概念清晰之后看很多问题就通了。比如方法的局部变量被赋值为null只能让局部变量表里那个槽位不再指向堆里的对象帮助GC更快回收但如果你把这个引用传给了某个静态集合那置空局部变量根本没有用因为堆里那个对象还是被静态集合引用着GC照样回收不了。这就是“引用在栈、对象在堆”的直接实战意义。6.2 逃逸分析到底躲过了什么逃逸分析是JIT编译阶段做的一项优化分析判断一个对象在方法内创建后有没有可能被方法外部“看到”。如果没有逃逸出方法作用域JVM可以将对象的字段拆开分别放在栈帧的局部变量槽位里甚至省去对象头的开销这项优化叫标量替换。必须强调的是逃逸分析处理的是分配位置的问题但它并不是运行时数据区里那块区域的改变。很多人把逃逸分析说成“对象可以去栈上”这种表述不够严谨。更准确地说经过逃逸分析后JVM不再按默认方式在堆上创建那个对象而是通过标量替换直接在栈帧里展开。遇到面试问这个点能把“逃逸分析标量替换”说完整回答的深度就上去了。这个优化在具体业务代码里难以直接观测但它的收益是真实的。热点方法里创建的短生命周期小对象如果满足逃逸条件会因为标量替换减少大量堆分配也减轻垃圾回收压力。所以学内存模型的时候不妨把它和JIT编译知识放在一起看别孤立记忆。6.3 引用计数和可达性分析判断对象是否“已死”主流JVM用的是可达性分析不是引用计数。引用计数算法虽然实现简单但解决不了循环引用的问题——两个对象互相引用外部没有引用指向它们引用计数器却永远不会清零对象就泄漏了。Java里用的可达性分析从一组叫作GC Roots的根对象出发沿着引用链往下走能走到的对象就是活着的走不到的就是可回收的。GC Roots包括栈帧里的局部变量引用、静态变量引用、JNI引用、以及活跃线程的引用等等。这也解释了为什么内存泄漏常出在“根引用没断开”的场景一个对象被静态集合持有就相当于从GC Roots一路挂着即使业务上已经永远用不到它了可达性分析依然会认为它活着。把这个机制和前面说的栈帧引用串起来你就能完整回答“一个对象什么时候被回收”这类问题了不是它被置空了就算而是没有任何GC Root能通过引用链找到它才算真正进入可回收状态。这条链路清晰后对GC的理解就是你自己的了而不是背来的。最后分享一个我自己的学习方法不要一次性把JVM内存模型的所有细节都背下来而是先搭骨架——线程私有的三块、线程共享的两块、堆内部的分代、GC根可达的判定把这些记住之后再遇到异常报错和调优需求就往骨架上挂具体知识。技术这东西尤其是JVM光看永远学不扎实去线上服务里看一眼GC日志、调一次参数、解一次真实的OOM比闷头啃十篇文章都有用。
返回列表