☰
一次线上故障,重新审视JVM堆、栈、方法区内存参数
2026/10/20 9:09:23 网站建设 项目流程

一次线上故障,让我重新审视“堆、栈、方法区”这三个内存参数

前阵子处理了一个服务的频繁Full GC问题,现象很典型:接口RT(响应时间)从几十毫秒飙到几秒,CPU在垃圾回收上烧掉一大半,旧生代内存曲线像心跳一样规律起落。排查下来,根本原因不是代码内存泄漏,而是启动参数里堆内存、栈大小、元空间这些基础参数从来没有认真配过,全用的默认值。

很多同学觉得JVM内存参数是“装完JDK就不管的东西”,实际上,堆、栈、方法区这几个区域的容量设置,直接决定了服务在压力下是稳如老狗还是秒变死狗。这篇文章我会从实际调优案例出发,把堆、栈、方法区(元空间)的大小设置讲透,包括参数选型、估算思路、踩坑记录,不搞虚的,全是能直接落地的东西。

1. 整体设计:先说清楚这几个内存区域是干什么的,再谈怎么设

1.1 JVM运行时内存区域,一张图就能讲明白

JVM把运行时内存划分为几个区域,最核心的划分方式是“线程私有”和“线程共享”。

线程私有的区域有:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)。这三块跟着线程走,线程创建就分配,线程销毁就回收,数据量小但线程数量多时累积效应很可观。

线程共享的区域有:堆(Heap)和方法区(在JDK 8及以后,方法区的实现叫元空间Metaspace)。堆是Java对象的主要“住所”,也是垃圾回收的主战场;元空间存的是类结构、类加载器信息、常量池等类元数据。

很多人容易把“栈”和“堆”混在一起。你申请一个new Object(),对象实例在堆上,而指向这个对象的引用存在栈上的局部变量表里。栈管“执行流程的上下文”,堆管“对象存储”,元空间管“类信息”。三者职责不同,调优侧重点也完全不同。

1.2 内存参数设置的核心原则:别拍脑袋,要基于业务模型来配

调参之前先想清楚三个问题。

第一,你的服务是CPU密集型还是IO密集型?这决定了线程数上限,进而决定了栈内存总量。

第二,你的对象是“朝生夕死”还是“长期存活”?这决定了新生代和老年代的比例。

第三,你的服务有没有动态生成类、使用反射、加载大量JSP之类的行为?这决定了元空间要给多大。

所有的内存参数设置,本质上就是回答这三个问题后,在“物理机总内存”的约束下做预算分配。我见过很多调优文档直接丢出“堆给2G、栈给512K、元空间256M”这种万能配方,这是糊弄人的。不同业务的服务,内存画像差异巨大,照搬配置只会留下隐患。

1.3 我的调优总体路线:先观测,再建模,最后调参

我自己的调优流程比较笨,但胜在稳妥:先开着默认参数跑一段时间(容器里可以加-XX:+PrintGCDetails和-XX:+PrintGCDateStamps),用 jstat、jmap 这些工具观察内存水线和对象分配速率,然后基于观测数据建模,算出合理的区域容量,再改参数、压测、验证。整个过程走下来,基本不会出现“调完参数反而更卡”的情况。

2. 堆内存:核心战场,参数最多,坑也最多

2.1 堆的分代模型,决定了你要调哪些参数

堆内存不是一块完整的大区域,而是被分成了新生代(Young Generation)和老年代(Old Generation),新生代里又细分为Eden区和两个Survivor区(S0、S1)。

新创建的对象默认进入Eden区。Minor GC发生时,Eden里存活的对象会被挪到Survivor区,并记录年龄。每熬过一次GC,年龄加1,涨到阈值(默认15)后,就会被晋升到老年代。这个设计假设了“大多数对象朝生夕死”,让垃圾回收只扫描新生代就能清理掉大部分死对象。

和堆相关的核心参数有这么几个:

参数作用默认值(以Java 8为例)
-Xms堆初始大小物理内存的1/64
-Xmx堆最大大小物理内存的1/4
-Xmn新生代大小堆的1/3左右
-XX:NewRatio老年代与新生代比例2(老年代是新生代2倍)
-XX:SurvivorRatioEden与Survivor区比例8(Eden:S0 = 8:1)
-XX:MaxTenuringThreshold晋升老年代的年龄阈值15

不要漏了-Xss和-XX:MaxDirectMemorySize这两个间接影响堆可用内存的参数,后面会细讲。

2.2 -Xms 和 -Xmx,为什么不建议设置成不同值?

很多人会在开发环境把-Xms调小、-Xmx调大,理由是“启动快、省内存”。这个思路在开发环境可以理解,但生产环境强烈建议把两者设为同一个值。

原因很简单:JVM在运行过程中,如果发现堆内存不够用,会动态扩展堆;如果发现堆内存大量空闲,又会动态收缩。这个扩容和收缩的过程涉及堆内存的重新分配,代价高昂,而且内存收缩后再次扩容时,容易引发性能抖动。生产环境里,让堆大小保持恒定,避免GC时机的不可预测,比省那点初始内存要划算得多。

我习惯的写法是:

java -Xms4g -Xmx4g -jar app.jar

如果是容器化部署,注意一定要给容器预留出堆之外的内存余量。我用一个经验公式:容器内存 = 堆最大内存 + 元空间 + 线程栈总内存 + 直接内存 + 操作系统保留内存,后面这三个没有预留就很容易触发容器OOM Kill。

2.3 新生代大小,决定GC频率和晋升压力

新生代设置的黄金判断标准是:Minor GC频率不高,且GC停顿时间可接受。如果新生代太小,对象很快塞满,Minor GC频繁触发,业务停顿增多;如果新生代太大,一方面留给老年代的空间变小,大对象和晋升对象容易把老年代撑爆,另一方面Minor GC停顿时间本身会变长。

-Xmn和-XX:NewRatio都能控制新生代大小。我个人的习惯是优先用-Xmn显式指定,因为它更直观,不会被其他参数的默认值干扰。举个例子:

4G堆,“朝生夕死”现象非常明显的接口服务,我会这样设:

java -Xms4g -Xmx4g -Xmn2g -XX:SurvivorRatio=8 -jar gatewayserver.jar

这样设置后,新生代2G,其中Eden区约1.6G,两个Survivor区各0.2G。每次Minor GC后,存活对象在S0和S1之间复制,低于晋升阈值的继续在新代周转,达到阈值的晋升老年代。

这里有个非常容易踩的坑:-XX:SurvivorRatio=8的含义是 Eden:S0 = 8:1,也就是Eden占新生代的8/10,S0和S1各占1/10。如果你以为它代表Eden占全部堆的8/10,那算出来的Eden区容量就会和实际差出一个数量级。我当初也干过这事,按错误的计算值去估 jeunes 区的分配速率,结果压测时GC频率比估算的高了一倍。

2.4 晋升机制与“老年代被撑爆”的典型案例

对象是怎么进入老年代的?除了年龄阈值,还有两种情况会直接进老年代:一是大对象(-XX:PretenureSizeThreshold,默认值为0,表示不启用该限制,大于阈值的大对象直接分配到老年代),二是“动态年龄判断”——Survivor区中所有相同年龄对象的总大小超过Survivor区容量的一半时,大于等于该年龄的对象直接晋升老年代。

我处理过一个线上案例:某服务堆设置成3G,新生代1G,接口每次返回一个很大的JSON数组,里面有几百个长度几千的字符串。这些大对象在Eden区一放,直接触发大对象晋升,很快涌入老年代,导致老年代短短几个小时内就从30%涨到95%,Full GC变成家常便饭。

解决思路也很简单:如果确认大对象不可避免,可以启用-XX:PretenureSizeThreshold=524288(大于512KB的对象直接进老年代),这样大对象就不会反复在新老年代之间折腾,反而减少了复制开销。注意,这个阈值设置需要谨慎,如果老年代空间本来就不宽裕,贸然启用只会加快老年代触顶。

2.5 堆调参后的老年代容量重建

改完新生代,老年代容量会跟着变化,务必要重新评估。比如堆总容量4G,新生代从1.5G改成2.5G,老年代就从2.5G缩到1.5G。如果这个服务长期存活的对象很多,1.5G老年代就危险了。

我调参时会看三个指标:

  • jstat 输出的老年代使用量(OU列)变化趋势;
  • Full GC发生频率和平均耗时;
  • 接口RT在GC前后的波动幅度。

只有这三个指标同时健康,我才认为堆参数调到位了。

3. 栈内存:被严重低估的“固定开销”

3.1 虚拟机栈到底是干嘛的,默认值有多大?

每个Java线程创建时,JVM会为它分配一个独立的虚拟机栈,栈里由一个个栈帧(Stack Frame)组成。每个方法从调用到执行完毕,都对应一个栈帧入栈和出栈。栈帧里放着局部变量表、操作数栈、动态链接、方法出口等信息。

栈大小默认值因平台而异,在64位Linux上,HotSpot虚拟机默认是1MB。这个默认值对多数业务线程够用,但线程数上去了,这个积少成多的开销就非常可怕。

我算给你看:一个服务开了500个线程,每个线程栈1MB,光线程栈就占500MB内存,这还没算线程自己的堆栈信息和JVM内部开销。很多同学在容器里明明把堆设置得很合理,却频繁被系统OOM Kill,查来查去,最后发现是线程栈叠加的总内存超过了容器限额。

3.2 -Xss 参数怎么设,踩过的一个真实案例

-Xss 用来设置每个线程的栈大小,可以这样用:

java -Xss512k -jar app.jar

这个值不能盲目调小。栈太小时,递归调用深度稍微大一点就抛出StackOverflowError,排查起来非常痛苦。

我调优过一个计算密集型的服务,服务里用了递归做树形遍历,树的深度最多也就十几层。当时我把-Xss从默认1MB调成256KB,压测150个并发线程,没跑多久就冒出一批StackOverflowError。原因是虽然每个方法帧本身不大,但线程在处理一些复杂查询时,会连续调用很多层工具类方法,导致栈帧数量远超预期。

后来我把-Xss调整到512KB,并发400线程压测,问题消失。栈内存总开销从1MB x 400 = 400MB降到512KB x 400 = 200MB,节省了一半内存,给堆留出了更多空间。

强烈建议:设置-Xss后,把递归深度最深的场景写成单元测试跑一遍,或者直接在压测阶段验证,不要上线后再等报错。

3.3 线程数估算公式:栈内存没有“标准答案”,只有“预算模型”

当你想在有限内存下支撑更多并发线程,栈大小的设定可以用一个简单的预算模型来算。

假设容器内存4G,你计划分配堆2G、直接内存256M、元空间256M,操作系统和JVM其他开销保留约800M,那么留给线程栈的“预算”是:

4G - 2G - 256M - 256M - 800M ≈ 768M

如果你希望服务能撑到1000个并发线程(通常指的是处于活跃等待状态的线程数),那么单个线程栈需要控制在768KB左右。但要注意,线程栈与其他内存区域不一样——堆内存可以动态调整但不能“让出”物理内存,线程栈却会在线程销毁后真正释放。所以预算模型里的线程栈大小不是死的,可以根据业务生命周期灵活调整。

3.4 栈大小与虚拟内存、容器限额的三角关系

最后提一个很容易被忽略的坑:JVM申请栈空间时,是向操作系统申请虚拟内存。在容器里,虚拟内存也要受容器限额约束(很多容器内核参数启用了vm.overcommit限制)。一个服务如果线程开得极多,即使堆内存设置不大,虚拟内存总量也可能突破容器限制,表现为“无法创建线程”异常。

遇到这种“无法创建线程”的情况,优先排除三个方向:第一,线程栈是否过大;第二,容器允许的pid/cgroup数量上限是否太低;第三,虚拟内存是否被耗光。别一上来就想着加内存,先算清楚总账。

4. 方法区与元空间:最容易“静悄悄出事”的角落

4.1 从永久代到元空间:JDK 8之后发生了什么?

很多人还停留在“方法区=永久代”的旧印象里。JDK 8之后,HotSpot把永久代(PermGen)彻底移除,类元数据被移到“元空间”(Metaspace)。

关键区别在于,永久代是JVM堆的一部分,受堆大小限制;而元空间使用的是本地内存(Native Memory),不受堆容量约束。这意味着,理论上元空间可以无限扩张,一直到操作系统物理内存耗尽。听起来很宽松,是不是?恰恰相反,这个“宽松”成了最危险的隐患:一旦服务的类加载器出现泄漏,元空间就会无限吞噬宿主机的内存,最终触发操作系统级别的内存耗尽。

元空间相关核心参数:

参数作用备注
-XX:MetaspaceSize元空间触发GC的初始阈值首次到达阈值时触发Full GC
-XX:MaxMetaspaceSize元空间最大容量上限建议必设,否则无限增长
-XX:MinMetaspaceFreeRatio扩容后可空闲比例下限调低可减少扩容频率
-XX:MaxMetaspaceFreeRatio扩容后空闲比例上限调低可减少空间浪费

4.2 MetaspaceOOM 不是“类太多”,而是“类加载器泄漏”

我第一次遇到java.lang.OutOfMemoryError: Metaspace时,第一反应是“类太多”,于是无脑调大MaxMetaspaceSize。可事实是,调大后问题依然存在,因为根因是某个动态代理工具在运行时不断生成新的类,而这些类由同一个类加载器加载后,类加载器又无法被垃圾回收,元空间里的类元数据堆积成山。

排查类加载器泄漏有两条重要经验:

第一,用jmap -clstats <pid>查看每个类加载器的加载类数量,如果某个类加载器对应的类数量持续增长,恭喜,基本锁定了泄漏点。

第二,配合jcmd <pid> GC.class_stats可以进一步分析类元数据占用,但注意,这个命令在高版本JDK里可能因为权限或模块限制不可用,需要降级到 JDK 8 或者开启相应诊断接口。

我当时就在某项目X里跑了一个模拟动态代理Demo,每秒钟生成100个代理类,不到20分钟元空间就涨到了400M,而最大容量设置的是256M,于是Full GC疯狂触发。用jmap -clstats一看,代理类加载器类数量直线上升,裸眼可见的教科书级泄漏。

4.3 元空间大小设置建议:必须设上限,但别设太紧

生产环境中,我强烈建议显式设置-XX:MaxMetaspaceSize。没有上限,就像开车不踩刹车,迟早出事。

设置多少合适?需要评估业务场景:如果框架使用大面积动态代理(Spring、Dubbo这类框架都会),ORM映射类较多,起步可以给256M到512M。如果还有热部署、JSP编译,那上限甚至要给到1G。

另外,-XX:MetaspaceSize的默认值在JDK 8里大概在20-21M左右,这个值偏低。元空间使用量达到MetaspaceSize后,JVM会触发一次GC(第一次往往是Full GC),如果业务一启动就加载了大量类,很容易在启动阶段就引发一次不必要的Full GC,导致启动变慢。我会把这个值调得高一些,比如512M,相当于提示JVM“我没那么容易触顶”,减少不必要的GC时机。

4.4 永久代相关的兼容问题:还在用JDK 7?小心字符串常量池

如果把项目从JDK 7升到JDK 8,有一个历史遗留坑值得知道:在JDK 7及之前,字符串常量池存放在永久代中,永久代空间不够会抛OutOfMemoryError: PermGen space。升级到JDK 8后,字符串常量池被移到堆中,PermGen相关的-XX:MaxPermSize参数直接失效,代之以-XX:MaxMetaspaceSize。

如果在升级后发现“永久代溢出”变了脸,先确认是不是还在使用失效参数。很多老项目的启动脚本里躺着一行-XX:MaxPermSize=256m,JDK 8下这行是无效的,但虚拟机不会报错,容易让人误以为永久代问题仍然存在。实际GC日志和监控数据才能反映真实压力。

5. 直接内存与堆外内存:不调会吃亏,一调又容易过头

5.1 直接内存是什么,为什么会间接影响堆内存设置?

直接内存(Direct Memory)不是运行时数据区的标准组成部分,但在Java NIO和某些框架(Netty、RocketMQ)中,它是主力内存区域。它通过DirectByteBuffer在堆外分配内存,由JVM帧栈上的一个引用指向底层操作系统映射的内存块。

为什么调堆参数时要惦记它?因为直接内存最大的特点是“不受堆大小控制”。如果你把-Xmx设成4G,但直接内存不设上限,默认等于堆最大上限,外部框架的NIO操作会把宿主机内存吃掉一大截,最终同样导致系统内存耗尽。

一个我处理过的案例:某服务堆设了2G,但没有任何直接内存参数,用了Netty做文件上传下载,压测到200并发时,宿主机内存直接快满了。用-XX:MaxDirectMemorySize=512m锁定上限后,内存水线稳定,整体行为正常。

5.2 直接内存排查方法论:jstat 看不到,但文件描述符能说明问题

棘手的点在于,直接内存无法用 jmap 或 jstat 直接看到使用量。排查思路通常是反推:查看系统内存占用和堆、元空间、线程栈的差值,大致估算直接内存消费量;再结合lsof或osquery查看进程打开的临时文件、socket缓冲区大小,判断是否有异常的直接内存分配。

这个思路对应一句经验:调优不要只盯着JVM内部区域,多看看系统监控面板上的RES(常驻内存),对比JVM参数预算和实际占用的差值,才能发现真正的堆外泄漏。

5.3 如何设定直接内存的限制上限?

生产环境建议总是显式设置-XX:MaxDirectMemorySize。它的默认值等于堆大小(更准确地说,在NIO类库请求时才初始化)。如果堆是2G,默认直接内存上限就是2G,而且这是“可用即得”的虚拟内存地址空间,不是实际物理分配。等到大量分配时,就可能发生分配成功但操作系统无法提供足够物理内存的情况。

我一般这样推算:容器总内存中,堆占60%-70%,元空间+线程栈+直接内存合计占20%-25%,其余留给操作系统和JVM自身。比如4G容器,堆给2.5G,元空间256M,线程预算512M,直接内存给512M,剩下约1.2G是安全余量。这种做法比我刚入行时“堆给满,其他不管”的做法,稳态明显好很多。

6. 常见问题与排查技巧:遇到OOM,先别慌,按顺序来

6.1 四种常见内存溢出的速查表

在实际支持过程中,项目方最常问的一个问题就是“报OOM了,该怎么办”?先把常见的OOM类型和排查方向列成一张速查表:

异常信息主要含义排查起点
Java heap space堆内存不足jmap -heap 看堆使用与分区占比
StackOverflowError递归/栈帧过多结合-XX:ThreadStackSize与递归深度检查
Metaspace元空间超限jmap -clstats 看类加载器与类数量
Direct buffer memory直接内存超限系统内存、lsof、NIO框架使用分析
GC overhead limit exceededGC回收效率极低检查堆大小是否过小,对象是否存在泄漏

这张表解决的是“见到异常第一步看哪里”的问题。只有先分清楚种类,后面的处理才不会跑偏。

6.2 定位问题:启动参数、GC日志、jstat观察三板斧

排查任何内存问题,先做三件事:

第一步,确认启动参数。执行jcmd <pid> VM.flags或jinfo -flags <pid>,把真实的堆、栈、元空间参数捞出来,防止“配置在文件里但实际没生效”的情况。

第二步,看GC日志。如果启动时加了-Xlog:gc*(JDK 11+)或-XX:+PrintGCDetails(JDK 8),可以从日志里看到每次GC前后内存使用详情,判断是“水位持续上升”还是“周期性波动”。

第三步,用jstat连续观测:

jstat -gcutil <pid> 1000 20

关注四列:E(Eden使用率)、O(老年代使用率)、YGC(Young GC次数)、FGCT(Full GC累计毫秒数)。如果O持续超过70%且FGCT在快速增加,基本可以断定老年代压力巨大,要么参数不合理,要么对象内存泄漏。

6.3 实战复盘:一个堆、栈、元空间都出了问题的“假死”服务

说一个我印象深刻的综合案例,模拟项目X是个消息推送系统,部署在4C8G的容器里,使用方反馈服务隔三差五“假死”,线程能力断崖下降。

初始启动参数是这样的:

java -Xms2g -Xmx2g -jar pushservice.jar

现场排查步骤:

  • 第一步看进程状态,CPU居高不下,线程dump发现大量线程卡在Full GC的Stop The World阶段。
  • 第二步看堆内存,GC日志显示老年代使用率一直在高位线徘徊,并且每次Full GC只能回收不到5%的空间,说明大部分对象逃过了Minor GC还长期存活,晋升压力极大。
  • 第三步看元空间,gc日志里元空间容量从200M一路涨到500M多,但当时没有设置MaxMetaspaceSize,所以没炸,却拖长了每次GC的扫描时间。

最终修改方案:

java -Xms4g -Xmx4g -Xmn2g -XX:SurvivorRatio=8 -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=256m -Xss512k -XX:MaxDirectMemorySize=512m -jar pushservice.jar

还顺手在代码里改了一处定时任务创建新线程却长时间不销毁的问题,把线程数峰值从600降到200。最后压测结论:接口RT的P99从2600ms降到230ms,GC总耗时占比降到不到1%。

这个案例给我的启发特别大:内存参数设置从来不是孤立行为,它要和代码特征、线程模型、容器限额一起联动着看,才能让服务真正稳定。

6.4 避坑清单:这些看起来合理的配置,其实是隐患

再整理几条我自己或者其他项目同学常踩的坑,每条都是有人付出过代价的:

  • 坑一:-Xmx设置为物理内存的80%,不留任何余地。容器环境里这会直接挤占文件缓存和操作系统的页面缓存,导致读写变慢甚至OOM Kill。堆内存不能贪心,要留余量。
  • 坑二:同时设置-Xmn和-XX:MaxTenuringThreshold但不知道两者联动。新生代越小,对象晋升越快,晋升阈值形同虚设。调整-Xmn后要重新评估晋升统计。
  • 坑三:使用默认-XX:+UseParallelGC时没有考虑CMS或G1的并发线程数。用G1时-XX:ConcGCThreads和-XX:ParallelGCThreads参数会影响GC线程占用的CPU,如果没注意容器CPU限制,明明设置了4个GC线程,实际可用CPU只有2核,反而触发调度抖动。
  • 坑四:设置了-Xss512k但内容里有大量深层次的Jackson反序列化或字符串操作,导致栈上局部变量表和操作数栈溢出。此类问题在压测时才暴露,最好提前用小线程压一轮“最大栈深路径”。

7. 工具链与实操建议:用什么工具观测、怎么让调优数据说话

7.1 常用JVM观测命令,手机里也要存一份

以下命令是内存参数调优过程中最高频使用的,每个都值得熟练掌握:

命令用途示例
jstat -gcutil观测GC利用率jstat -gcutil 1234 1000 30
jmap -heap打印堆模型与参数jmap -heap 1234
jmap -clstats观察类加载器统计jmap -clstats 1234
jcmd VM.flags打印生效的JVM参数jcmd 1234 VM.flags
jcmd GC.heap_dump触发堆转储jcmd 1234 GC.heap_dump /tmp/app.hprof
jstack获取线程栈快照jstack 1234 > /tmp/jst.txt

注意三点:

  • 高版本的JDK(9+)里,jmap -heap和jmap -clstats可能提示“不支持”,建议改用jcmd系列命令。
  • 在容器里执行jmap/jcmd极容易因为未知安全策略失败,这时考虑通过JMX MBean或者在线监控Agent(比如Arthas这类工具)替代,但一定要避开在严苛安全环境里随意启用远程诊断端口。
  • 线上转储heap dump文件通常巨大,最好在低峰期操作,并确保磁盘空间充足。

7.2 参数改完怎么验证?压测有讲究

改完JVM参数,最忌讳直接上生产。我自己的标准化验证流程是:先单机压测,看看GC频率和RT有没有明显改善;再把压测机数量扩到集群规模,观察多实例间的内存水位是否一致;最后再灰度上线1-2台机器,同时对比原配置和现配置的日志曲线。

压测场景要覆盖两个方向:一个是正常流量下的稳态,另一个是突发大流量下的峰值。峰值压测能暴露晋升风暴、线程栈溢出和元空间不足这些问题,这些在低负载下根本不会现形。

模拟峰值时,我用一个外部压测工具(比如Locust或wrk)发出周期性波峰请求,持续运行15-30分钟,实时盯住jstat和进程内存,就能看到参数到底能不能抗住。

7.3 关于监控与告警:参数是死的,曲线是活的

没有监控的内存参数就是盲人摸象。哪怕在开发环境,我也建议至少接入基础的JVM监控数据,观察三个核心指标:堆内存使用、Full GC频率和耗时、Metaspace空间使用率。

告警阈值可以这样设:Full GC发生次数超过每小时5次,或Metaspace使用率超过80%保持5分钟以上,就该人工介入。实测下来这个阈值对多数微服务是合理起点,既不会过度打扰,也不会让问题潜伏太久。

8. 最后聊点实在的个人经验

这几年的调优经验总结下来,最核心的一条其实是:JVM内存参数永远服务于两点——业务特征和物理资源。没有万能配方,但有可靠的思路。

首先,一定要把堆、栈、方法区、直接内存这四项连在一起做预算,单独调其中一项,往往按下葫芦浮起瓢。

其次,调优过程一定要用数据说话,不要靠猜。观测、建模、配置、压测,四步闭环,缺一不可。

最后,要养成查看GC日志的习惯。哪怕不调优,线上服务日常运维时,GC日志里的每一条记录都在默默告诉你服务的内存健康状况。学会读它,比背一万个参数名都有用。诚心推荐大家有空的时候,把自己服务的启动参数全部打印出来,一项项对照这篇文章过一遍,一定会有当年没想到的收获。

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

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

立即咨询