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

资讯详情

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

一文读懂JVM内存模型与GC机制,掌握线上问题排查与调优

一文读懂JVM内存模型与GC机制,掌握线上问题排查与调优 搞Java开发久了你要是问我哪块内容最绕我估计很多人会先想到JVM内存模型和垃圾回收机制。尤其是最近《我的世界》Java版发布新版本社区里又一次聊起“JVM管着游戏、所以吃内存还会偶尔卡顿”这个话题——其实这个锅不能全甩给JVM但确实能说明一套成熟的运行时数据区和GC机制对应用的表现影响有多大。说白了JVM干三件事把字节码跑起来、把内存管起来、把垃圾收干净。今天我就把自己这些年啃JVM内存模型、排查各种内存问题、整理垃圾回收机制的过程沉淀成一篇能看“懂”也能用“起来”的内容不堆概念只讲为什么要有这些设计以及真到线上出问题时的排查思路。这篇内容适合谁看我想三类人最合适一是快面试Java岗位、正在啃JVM面试题的同学这篇文章把高频考点的原理和场景都讲透二是项目上线后一遇到Full GC频繁或者内存溢出就抓狂的后端开发你会在这里看到一套实用的排查工具链和调优思路三是想搞清楚jre、jvm、jdk之间关系想入门Java运行原理的初学者。咱们不追求把每行参数背下来而是先建立一个完整的内存模型地图再顺着垃圾回收机制的脉络往下走最后落到实操工具和场景上。1. 运行时数据区JVM到底把内存分成了哪几块JVM在启动的时候会向操作系统申请一块内存然后把这块内存按用途划分成几块区域这就是所谓的运行时数据区。很多新手看资料时会被“方法区、堆、栈”这些词绕晕其实只需要抓住两条线一条是“哪些区域是线程私有的”一条是“哪些区域是线程共享的”。线程私有意味着这条线程自己用别人碰不着生命周期跟着线程走线程共享则意味着所有线程都能访问也是并发控制里最容易出问题的地方。1.1 线程私有区程序计数器、虚拟机栈、本地方法栈先讲程序计数器。它是所有区域里最“小”也最“倒霉”的一个——因为JVM规范明确规定它是唯一不会抛出OutOfMemoryError的区域。程序计数器存的是当前线程正在执行的字节码行号或者说是“下一条指令的地址”。为什么需要它因为线程在执行的过程中随时可能被切出去切回来总得知道刚才执行到哪了吧。这个“位置信息”就是程序计数器在干的事儿。要是执行的是本地方法native方法那程序计数器的值就是空Undefined因为本地方法不走字节码解释执行这套逻辑。再讲虚拟机栈。它是Java方法执行时的内存模型每个方法从调用到执行结束的过程就对应一个栈帧从入栈到出栈的过程。一个栈帧里装了什么主要是局部变量表、操作数栈、动态链接、方法出口这些信息。局部变量表存放的是基本数据类型、对象引用和返回地址注意这里的“对象引用”只是指针不是对象本身对象本体在堆里。操作数栈就是方法在执行时算数运算和调用指令的工作台。平时我们经常听到的“StackOverflowError”就发生在这里——线程请求的栈深度超过了虚拟机允许的深度。循环调用死递归、无限嵌套调用基本都会把这个区打爆。反过来如果栈扩展时申请不到足够内存则会抛OutOfMemoryError。第三个线程私有区域是本地方法栈。这个区域服务的是native方法也就是那些用C/C编写的非Java方法。HotSpot虚拟机比较偷懒直接把本地方法栈和虚拟机栈合并了所以你在用标准JDK时几乎感受不到这两个区域的差异。但在其他JVM实现上或者你去看规范文档时它们是被分开定义的。平时写Java代码很少碰到这里的异常一旦碰到基本就是native层出了问题排查起来比较痛苦。1.2 线程共享区Java堆与元空间Java堆是JVM内存里最大的一块也是垃圾回收的主战场。所有通过new创建出来的对象实例和数组都会在这里分配内存。堆由所有线程共享所以这里的并发安全问题最突出。为了更高效地回收堆又被分成了新生代和老年代新生代里又有Eden区、Survivor的S0和S1区。这一整块的结构就是垃圾回收机制最核心的舞台。平时咱们调优时设置的-Xms和-Xmx参数控制的就是堆的初始大小和最大大小。很多线上OOM要么是堆真的大到不够用了要么是某个对象把自己“钉”在了堆里一直没有释放。方法区在概念上存的是类信息、常量、静态变量以及即时编译器编译后的代码缓存但从JDK8开始HotSpot把方法区的实现从“永久代”改成了“元空间”。永久代在JDK8之前是堆的一部分有大小上限一不小心就抛PermGen Space异常元空间挪到了本地内存里不再受堆内存大小限制而是直接使用操作系统的本地内存默认情况下上限就是物理内存的大小。这个改动的好处是非常明显的减少了元数据OOM的概率也让字符串常量池等数据在JDK7时就已经迁到了堆中。元空间如果再OOM那基本就是类加载器泄漏、动态生成类太多或者反射加载类无节制导致的。1.3 一张表看懂各区域的作用、生命周期与异常为了方便对照记忆我整理了一张运行时数据区速查表平时面试前复习我也基本只看这份表格区域是否线程私有存储内容主要异常说明程序计数器是当前线程字节码执行行号无唯一不抛OOM的区域虚拟机栈是栈帧局部变量表、操作数栈等StackOverflowError、OOM深度过大或扩展失败时触发本地方法栈是native方法调用信息StackOverflowError、OOMHotSpot中与虚拟机栈合并Java堆否对象实例、数组OutOfMemoryErrorGC主战场分新生代和老年代方法区/元空间否类元信息、常量、静态变量、JIT代码缓存OutOfMemoryErrorJDK8后以元空间实现使用本地内存理解这张表之后再去看网上那些JVM调优教程会突然觉得容易很多。比如为什么有人会调整-Xmn这个新生代大小参数因为新生代太小会导致短命对象频繁进入老年代造成Full GC次数上升新生代太大又会压缩老年代空间反过来影响老年代容量。这些都是内存模型在实际工程里的直接延伸。2. 垃圾回收的第一性问题对象怎么判定生死垃圾回收机制要跑起来第一个要解决的问题就是到底哪个对象是垃圾判断对象还有没有用JVM用的不是简单引用计数而是可达性分析算法。另外还要理解四种引用类型因为它们在判定生死时的影响路径完全不同理解了引用类型你对缓存、ThreadLocal这些常见工具的底层原理也会通透很多。2.1 可达性分析与GC Roots引用计数算法是最朴素的想法给每个对象加一个计数器被引用就加1引用失效就减1计数器为0就认为是垃圾。听起来挺美但它有一个致命问题——循环引用。两个对象互相引着对方但外部已经没有任何引用指向它们计数永远不为0垃圾就永远收不掉。这就是为什么主流JVM都不采用引用计数法。现在的JVM都用可达性分析算法。思路是从一组被称为GC Roots的根节点出发沿着引用链往下搜索能遍历到的对象就是“活着的”没被遍历到的对象就标记为可回收。整个过程有点像从你家的门出发沿着所有的路往下走走到的地方都还有人住走不到的荒宅就等着被清理。那哪些对象能当GC Roots这块是面试高频点必须记牢。主要包括虚拟机栈中局部变量表引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象还有Java线程等活跃对象。在实际运行时GC Roots的集合并不固定但这份清单已经覆盖了绝大多数场景。值得注意的是即使一个对象被标记成了可回收它也不一定立刻被物理删除还可能在finalize阶段被“救”回来但依赖finalize是极其不推荐的写法JVM规范里也明确说“不要用它做资源清理”。2.2 四种引用类型和典型使用场景Java里引用不只有强引用一种从强到弱依次是强引用、软引用、弱引用、虚引用。垃圾回收机制对它们的处理策略完全不一样。强引用就是我们平时写代码最多的情况比如Object obj new Object()。只要强引用还存在JVM就算OOM了也不会去回收这个对象。软引用描述一些“有用但非必需”的对象在内存充足时它们正常活在将要OOM之前JVM会把这些对象纳入回收范围。经典应用就是缓存框架比如很多图片缓存、对象缓存就用了SoftReference既要利用缓存提速又不想把内存打爆。弱引用比软引用更“短命”下次GC一跑不管内存够不够只要只存在弱引用对象就会被回收。ThreadLocal的内部实现就是一个典型场景——ThreadLocalMap里的Entry继承了WeakReferencekey是弱引用就是为了避免ThreadLocal对象被强引用钉死导致内存泄漏。这里有个经典坑虽然key是弱引用但value是强引用ThreadLocal用完不调用remove的话value仍然会让对象活着从而引发内存泄漏。之前大家说ThreadLocal和线程池一起用容易出事根源就在这。虚引用最弱跟没有引用几乎一样。它存在的意义不是控制对象生命周期而是让对象被回收时收到一个系统通知。比如NIO里用来跟踪堆外内存、DirectByteBuffer的清理时机就是基于虚引用和引用队列做的。理解了这四类引用你再去看那些“JVM内存泄露查看工具”的报告能更清楚地分辨哪些对象是因为缓存设计不当、哪些是因为ThreadLocal没清理而一直活着。2.3 三大基础回收算法优缺点与适用场景如果说可达性分析是“判定生死”的裁判那回收算法就是把“已死”对象清出去的执行者。主流的三个基础算法是标记-清除、复制、标记-整理。标记-清除算法分两步走先把所有需要回收的对象做个标记然后统一清理。它最大的问题是有两个一是效率不高标记和清除都要遍历大量对象二是会产生大量不连续的内存碎片。碎片多了之后以后再想给一个大对象分配连续内存就会发现明明内存总量够却找不到足够大的连续空间触发下一次GC。这个场景很像磁盘碎片化文件很多但都分散成小碎块大文件就存不下了。复制算法解决了碎片问题。它把内存分成大小相等的两块每次只使用其中一块。GC的时候把活对象复制到另一块然后把当前这块一次性清空。这样只用移动指针就能完成内存分配而且没有碎片性能非常稳定。但代价也是明摆着的可用内存缩小了一半。那为什么新生代还会用复制算法因为新生代对象“朝生夕死”的比例极高绝大多数对象第一次GC就会被清走只需要复制那少数活对象就行。HotSpot对复制算法的改进是把新生代拆成一块较大的Eden和两块较小的Survivor默认比例8:1:1每次只用Eden和一块Survivor回收时把存活对象复制到另一块Survivor这样浪费的空间就降到10%了不再是50%。标记-整理算法则是为了老年代量身定做的。老年代里的对象存活率高如果还用复制算法就得复制大量活对象成本太高如果只用标记-清除又解决不了碎片。标记-整理的思路是让所有存活对象往堆的一端移动然后直接清理掉边界以外的内存。这样既消除了碎片又避免了高频复制大量对象。代价是移动对象需要更新引用停顿时间会比标记-清除更差一些但这对于老年代这种回收频率低、存活对象多的场景来说总体还是更划算的。3. 分代收集与主流垃圾收集器选型有了基础的回收算法还不够不同的应用场景对GC的需求差别非常大。一个批量处理的离线任务它可能更关心吞吐量而不是单次停顿一个在线交易系统它可能更在意每次GC的停顿能不能控制在几十毫秒以内。为了兼顾这些差异JVM把堆分成了年龄不同的区域并配套设计了一整套从Serial到G1再到ZGC的收集器家族。3.1 为什么JVM要分代设计堆上一节提到绝大多数对象都是“朝生夕死”的。统计表明在实际业务中新生对象里98%左右都要很快被回收只有少数对象会活得很久。如果所有对象不分年龄地混在一起每次GC都得把所有对象扫描一遍效率会很差。于是JVM就把堆按对象年龄拆成了新生代和老年代。新生代放短命对象回收频率高用复制算法性价比最高老年代放经历了多次GC仍存活的对象回收频率低用标记-整理或标记-清除来兜底。对象在新生代里是怎么流转的一个对象先在Eden区分配第一次Minor GC后如果还活着就移动到S0年龄加1。再经历一次GC如果还是活着就挪到S1同时年龄再加1。对象每次从一块Survivor移动到另一块Survivor年龄都会增加。当年龄达到阈值默认15时它就会晋升到老年代。除了年龄到了会晋升JVM还有两条晋升路径一条是动态年龄判断即Survivor中相同年龄所有对象大小总和超过Survivor空间一半时年龄大于等于这个值的对象直接进老年代另一条是大对象直接进老年代这个用-XX:PretenureSizeThreshold参数控制目的是避免大对象在Eden和Survivor之间复制消耗大量资源。理解了分代结构你就能理解很多线上问题的根因。比如代码里写了死循环往集合里塞数据Eden很快被打满Minor GC一直在跑但新对象又不断地涌进来Survivor根本装不下部分对象被迫提前晋升到老年代。老年代空间持续增长最终触发Full GC。这个过程反映了内存模型设计和垃圾回收机制是密不可分的一个整体。3.2 经典收集器Serial、Parallel、CMSSerial是最古老的收集器单线程工作GC时必须暂停所有应用线程也就是STWStop The World。单核CPU时代它还能接受现在基本只会用在客户端模式或一些小工具上。Parallel是它的多线程版本也是JDK8默认的新生代收集器目标是把多核CPU用起来尽可能提高吞吐量。如果你的应用对暂停时间不敏感而更在乎单位时间处理的任务量Parallel是稳妥的选择。CMSConcurrent Mark Sweep是老一代大名鼎鼎的低延迟收集器设计目标就是减少老年代GC时的停顿。它整个流程分为初始标记、并发标记、重新标记、并发清除四个阶段其中只有初始标记和重新标记需要STW耗时的标记和清除阶段都能和业务线程并发执行。听起来很美好但CMS也逃不过三个老毛病一是它属于标记-清除算法运行久了会产生大量碎片极端情况会触发Full GC时的“Concurrent Mode Failure”退化成Serial Old来做一次完整收集二是它不能完全并发并发阶段会抢占CPU对CPU资源敏感三是它没法处理并发标记阶段产生的新垃圾只能等下一次GC再处理这部分称为浮动垃圾。到了JDK9CMS被公开标记为废弃JDK14正式移除它的时代基本结束但它把并发思路带进了GC设计包括G1在内的后续收集器都受了它的启发。3.3 G1与ZGC面向大堆和低延迟的演进G1从JDK9开始成为默认收集器它的核心创新在于把整个堆划分成许多大小相等的Region每个Region在逻辑上可以属于新生代也可以属于老年代物理上不再强制连续。G1的回收思路是“追踪各个Region里垃圾堆积的价值”优先回收垃圾最多、收益最大的Region这样停顿时间是可预测、可控制的通过-XX:MaxGCPauseMillis参数来设置目标暂停时间。G1还引入了记忆集RSet来记录跨Region引用关系从而避免每次全堆扫描。G1适合堆内存较大、多核CPU、既要保证吞吐量又希望能控制停顿的场景。但它也不是银弹如果 Region 内对象引用关系太复杂RSet维护成本会很高如果应用线程本身对内存分配压力极大G1的停顿还是可能超过目标值。复杂的混合回收Mixed GC机制也让它的调优参数比传统收集器多出不少。再往后是ZGC。ZGC主打的是超低停顿目标是任何堆大小下都能把STW时间控制在10毫秒以内。它用了染色指针、读屏障、内存多重映射等一系列黑科技把大部分原来需要STW的工作变成了可并发执行的阶段。ZGC从JDK11开始实验性引入JDK15转正JDK21之后已经支持分代收集。如果你的项目堆内存动不动就几十GB而且对响应延迟特别敏感那ZGC就是当前技术栈里最能打的选择。不过它适合“大内存、低延迟”场景小内存项目上ZGC不一定能发挥出优势选型时还要结合业务实际。3.4 收集器选型与常用参数速查表下面这张表整理了几个主要收集器的定位和关键参数方便大家做选型时快速对照收集器适用代际线程模型主要目标关键参数/说明Serial新生代单线程简单场景-XX:UseSerialGCSerial Old老年代单线程CMS失败兜底标记-整理算法Parallel Scavenge新生代多线程高吞吐量-XX:UseParallelGCJDK8默认Parallel Old老年代多线程高吞吐量与Parallel配套使用CMS老年代多线程并发低延迟JDK9废弃JDK14移除G1全堆分区多线程并发可预测停顿-XX:UseG1GCJDK9默认ZGC全堆分区多线程并发超低停顿-XX:UseZGCJDK15支持选型逻辑其实就一句话在小堆上Parallel的吞吐量表现很优秀在中等偏大的堆上G1能给出比较好的延迟和吞吐平衡在大堆且对延迟极度敏感的场景下ZGC是更合适的方向。实践中很多人问“要不要把默认收集器换掉”我的建议是先别急着换绝大多数应用用默认的G1就够真出现大停顿先去查代码、查大对象、查GC日志不要一开始就陷入收集器参数的适配里。4. 从问题出发内存泄漏排查与JVM调优实操前面对内存模型和垃圾回收机制的理解最终都要落到“线上出问题怎么办”上。我遇到最多的JVM问题无非四类Full GC频繁、内存持续增长、CPU飙高、以及各种OutOfMemoryError。处理这些问题有一套固定的工具链和流程不需要凭空猜关键是学会用数据定位。4.1 排查工具清单和基础命令排查JVM问题第一反应应该是用JDK自带的命令行工具轻量且基本上所有环境都有。我把平时最常用的命令和用途整理成了一张表命令作用常用示例jps查看Java进程IDjps -ljstat查看GC统计、内存使用jstat -gcutil pid 1000jmap查看堆内存、导出堆快照jmap -heap pid / jmap -dump:formatb,fileheap.hprof pidjstack导出线程快照排查死锁和CPU高jstack pidjcmd综合诊断命令jcmd pid GC.heap_dump /path/heap.hprofjconsole/jvisualvm图形化监控本地或JMX远程连接MAT分析堆dump文件安装后打开.hprof文件分析Arthas线上诊断利器dashboard、thread、heapdump等命令在两个工具里我特别想强调jstat和MAT。jstat -gcutil能直接看到各区使用比例、YGC/FGC次数以及GC耗时是判断当前GC状态的第一手资料。MAT则能把堆dump文件里的“大对象”、“支配树”、“可疑泄漏”一目了然地显示出来。很多情况下拿到一份堆dump后用MAT的Leak Suspects看一眼问题对象就直接指向了。4.2 一次Full GC频繁的真实排查记录之前我处理过一个线上服务表现是接口响应偶尔特别慢监控告警显示Full GC每隔几分钟就一次。我接到问题的第一步是登到机器上看GC统计执行jstat -gcutil pid 1000结果看到老年代O占比一直徘徊在90%以上而且FGC次数稳定往上走。这就说明老年代里的对象一直在快速增长已经没有剩余空间。第二步要确认到底是什么对象占着老年代不走。我用jmap -dump:formatb,fileheap.hprof pid导出了堆快照然后在MAT里打开。Leak Suspects直接指向了一个业务缓存类里面的Map对象占了整个堆的70%以上。再顺着支配树往下看发现这个Map的key一直在增加value是数据库查询返回的大对象而且缓存没有做容量上限和过期清理。问题定位到了业务侧为了提速把整表查询结果塞进了一个静态Map上线后数据量膨胀老年代被这个缓存焊死最终导致持续Full GC。第三步做验证和止血。先在代码里把缓存改成基于Caffeine的本地缓存并设置最大容量和过期时间然后重启服务观察GC曲线。改完之后的jstat -gcutil显示老年代占比稳定在30%以下FGC次数基本不再增长响应耗时也回到正常水平。这个案例特别典型因为它说明了一个核心道理很多JVM层面看起来很吓人的GC问题底层其实是业务代码对内存模型理解不够把本应该被GC的短命对象硬生生变成了老年代的常住居民。4.3 调优思路先看监控再改参数JVM调优最容易犯的错误就是“上来就改参数”。没有监控数据做基础调优就是拍脑袋改完还可能引入新的问题。我习惯的调优顺序是先确定目标是追求高吞吐量还是低停顿时间再收集数据用GC日志、jstat、APM监控把当前GC频次、停顿时间、各区占比摸清楚最后才动手改参数而且一次只改一个参数改完观察一段时间的表现。新手可以从这几组参数开始尝试-Xms和-Xmx设置为相同值避免堆在运行过程中动态扩容带来的性能抖动-XX:MetaspaceSize设置一个初始元空间阈值避免元数据扩容触发不必要的Full GC加-XX:HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath/path让OOM时自动导出堆快照打印GC日志用-Xlog:gc*JDK9的写法把GC细节记录下来后续分析就有据可依。很多“gcjava内存模型优化”的文章里推荐的参数组合本质都是围绕“让对象在新生代被回收、减少老年代压力、避免Full GC”这条主线来的。参数只是工具理解内存模型才是根基。4.4 用混沌工程验证JVM稳定性关于验证GC稳定性我还想分享一个比较新的玩法混沌工程。像ChaosBlade这类工具就提供了JVM场景的故障注入能力常见的是通过blade create jvm系列命令在目标JVM进程上注入内存溢出、方法执行延迟、类加载异常等故障。比如你想验证线上OOM告警和自动重启机制靠不靠谱就可以在压测环境对某个服务注入OOM故障观察监控曲线有没有及时推送告警大盘有没有反映出堆内存的暴涨服务降级兜底有没有生效。这套做法的价值在于它不依赖“线上碰巧出事”而是主动制造问题来检验你整个稳定性体系的反应。我把故障注入熟悉了以后明显感觉到自己对GC参数的理解更深了——以前只是看文档知道“Full GC会让STW”等真在压测中注入一次老年代打满的场景看到请求一瞬间全部停顿、线程堆积你对那一段垃圾回收机制的文字描述就有了真实的肌肉记忆。当然混沌工程一定要在测试环境做别拿生产环境开玩笑而且要先准备好回滚和恢复预案。5. 面试高频考点与常见问题速查JVM是Java后端面试里必问的一块围绕运行时数据区、内存模型、垃圾回收机制出题的花样很多但核心考点其实相对固定。把这个章节当成一个自查清单用你能快速发现自己哪里还有盲区。5.1 面试官最爱问的几个JVM问题第一个高频题是“讲一下JVM运行时数据区”。面试官想听的不是你把八个区域背一遍而是想看你能否说清楚线程私有和线程共享的区别、JDK8之后方法区改成元空间的原因、以及每个区域会出现什么样的异常。我建议按“私有区三件套”和“共享区两件套”来组织回答再补上元空间改动的动机这样逻辑完整又有深度。第二个高频题是“GC怎么判断对象可以回收”。收答案时重点看可达性分析算法和GC Roots的组成。回答时最好主动提到“不是引用计数”并说明循环引用的问题然后列出常见的GC Roots集合。如果有余力可以再提一句finalize机制虽然能复活对象但实践中绝不推荐使用。第三个高频题是“对象在内存中的晋升过程”。这个问题考的是分代收集的具体执行流程。你可以从Eden分配、Minor GC、Survivor区复制、年龄递增讲到动态年龄判断、大对象直接进老年代再把默认晋升年龄15这个参数和相关JVM参数说明白。能把这块讲清楚说明你对内存模型和垃圾回收机制的衔接是有真实理解的。第四个高频题是“CMS和G1的区别”。以前的版本喜欢考CMS近几年更倾向考G1和ZGC。回答时可以围绕“是否物理分代”、“并发模型”、“停顿目标”、“碎片处理”几个维度做对比并强调G1用Region和RSet解决了旧算法全堆扫描的问题。如果能顺带提一句“CMS使用标记-清除因此有碎片G1从整体上通过复制Region内的对象来避免碎片”这一题的深度就出来了。第五个问题经常出现在项目面试环节“你们线上JVM怎么调的”。这个问题没有标准答案但有一条万能思路先说明业务场景比如是网关、订单、还是大数据任务再给出监控手段GC日志、PrometheusGrafana、Arthas然后举一个真实调过的参数比如改堆大小、换GC收集器、调整新生代比例最后说明调优前后的数据变化。就算你调优经验不多也能通过这套回答展示完整的调优方法论。5.2 常见JVM异常与“no suitable jvm”类启动问题排查下面把JVM相关问题里最容易遇到的异常和解决方向也整理成一份速查表异常/问题表示什么排查方向java.lang.OutOfMemoryError: Java heap space堆空间不足用jmap/MAT看堆dump检查是否有大对象或内存泄漏java.lang.OutOfMemoryError: Metaspace元空间不足检查类加载器是否泄漏、动态代理/反射生成类是否过多java.lang.OutOfMemoryError: GC overhead limit exceededGC几乎回收不出内存大概率堆内存严重不足或泄漏立刻dump堆java.lang.StackOverflowError虚拟机栈深度超限查递归、无限嵌套调用、深层次循环依赖Full GC频繁老年代压力大或晋升异常看jstat各区占比找存活对象是否被错误保活no suitable jvm was found to start the application启动器找不到可用JVM检查JAVA_HOME或JRE_HOME变量是否设置正确、路径是否存在重新安装匹配位数的JDK/JRE“no suitable jvm was found to start the application”这个问题在Windows上启动一些安装型Java应用时很常见原因通常是安装程序要求的JRE/JDK版本和系统环境变量不一致或者注册表指向了已卸载的JDK。处理思路很简单先确认装了哪个版本的JDK再去系统环境变量里把JAVA_HOME指到正确路径顺手看一下PATH里有没有残留的旧Java路径。还有一点容易忽略——32位应用必须配32位JVM64位应用必须配64位JVM版本位数不匹配也会报“no suitable jvm”。我个人在实际排查JVM问题的过程中最深的体会是不要迷信某一条命令或某个参数真正帮你定位问题的一定是“运行时数据区的知识GC日志的数据工具的分析”三者叠加。尤其是当你理解了垃圾回收机制为什么会停顿、为什么老年代会膨胀、为什么Metaspace会溢出之后很多报错在你眼里就不再是黑盒而是一条可以推理的线。最后再分享一个小技巧平时写代码时就刻意做一些能减轻GC压力的习惯比如避免在循环里创建大对象、及时关闭资源、合理设计缓存容量和过期策略。这些功夫花在源头往往比事后调优更省钱也更省心。希望这篇关于JVM内存模型和垃圾回收机制的梳理能帮你少走一些我当年走过的弯路。
返回列表