资讯动态

Tomcat JVM内存配置全攻略:从参数设置到OOM排查实战

发布时间:2026/10/24 1:35:15 来源:尧图企业网站定制
Tomcat 线上跑了大半年某天早上突然收到告警服务隔几小时就卡死一次重启后恢复过一阵又卡住。登服务器一看日志满屏的java.lang.OutOfMemoryError: Java heap space。排查了一上午最后定位到的根因其实特别简单Tomcat 启动时用的是 JVM 默认堆内存根本没按实际业务调整过一个低频的批量导出任务就把内存挤爆了。这种问题在 Java Web 项目里太常见了而解决方案就是显式设置 Tomcat 的 JVM 内存大小。这篇文章就是聊清楚 Tomcat 和 JVM 内存配置这件事。不讲虚的直接说清楚三件事JVM 的内存到底怎么分配的、Tomcat 里有哪些地方可以配置 JVM 内存参数、不同场景下内存参数怎么给然后会结合我实际踩过的坑把常用的排查命令和问题定位方法一起列出来。适合正在做 Java Web 应用部署的开发者、刚接手 Tomcat 运维的工程师以及想搞清楚重启治百病背后原因的人。1. 先分清 Tomcat 和 JVM内存配置改的到底是谁很多第一次接触 Tomcat 的人会有个误解以为Tomcat 内存太小是某个 Tomcat 配置文件里的一个叫 memory 的选项。其实不是。Tomcat 本身是个用 Java 写的 Servlet 容器它跑起来以后就是 JVM 里的一个 Java 进程。你说的给 Tomcat 设内存本质上是给这个 Java 进程设置 JVM 运行参数。Tomcat 只是负责把 JVM 参数通过启动脚本传给java命令真正生效的是 JVM 的内存模型。1.1 JVM 内存区域和 Tomcat 的关系JVM 的内存按用途分成几块每块出了问题表现完全不一样堆内存Heap存放 Java 对象实例。你的业务代码里new出来的对象、数据库连接、Session、缓存数据全都住在堆里。堆内存满了抛出Java heap space。栈内存JVM Stack每个线程一个栈存基本类型变量、对象引用和方法调用。栈溢出会抛StackOverflowError一般多见于递归深度过大。元空间Metaspace存放类的元数据。Tomcat 每加载一个 Web 应用、每个类都要在元空间占一份空间。类加载过多或泄漏时抛Metaspace错误。直接内存Direct MemoryNIO、Netty 这类框架使用堆外内存做缓冲区。这块出问题时会抛OutOfMemoryError: Direct buffer memory。在 Tomcat 场景里日常运维最常盯的是堆内存因为它直接影响业务对象的存放但如果部署了很多 Web 应用或者用了大量动态代理元空间也要重点看。后面在问题排查部分我会细讲怎么区分这几类内存溢出。1.2 搞清楚两个关键参数Xms 和 XmxJVM 堆内存有两个最核心的启动参数-Xms堆内存的初始大小JVM 启动时立刻分配的内存。-Xmx堆内存的最大上限JVM 最多能用到这么多。打个比方-Xms是你租办公室时签的合同面积-Xmx是允许你临时扩租的上限。一开始只有 Xms 那么大业务压力上来了JVM 才会向操作系统申请更多内存一直到 Xmx 封顶。为什么两个参数都重要如果只设-Xmx不设-XmsJVM 默认初始堆很小比如机器内存的 1/64启动后内存缓慢增长每扩一次都要执行一次 GC高峰期会出现运行一会就卡一下的现象。所以生产环境我通常建议-Xms和-Xmx设置成相同值让 JVM 启动时就拿到全部可用堆内存省去扩容和收缩的开销。1.3 谁在真正消费 JVM 内存搞清楚内存给谁用配置的时候心里才有数。一个典型的 Tomcat 单机部署内存消费大头大致是这么分布的业务对象Controller、Service 里创建的对象耗时任务的数据占大头。HTTP 连接处理Tomcat 每个并发连接对应一个线程每个线程有自己的栈空间。并发越高线程占的内存越多。连接池数据库连接池、Redis 连接池、HTTP 客户端连接池每个连接在 Java 侧都有缓冲对象。缓存结构本地缓存、静态资源缓存、Session 存储。举个例子一个基于 Spring Boot 打包部署在 Tomcat 的电商后台业务高峰期同时在线用户 3000 人左右堆内存给到 3GB 左右能覆盖绝大多数场景如果并发低但是业务对象大比如图像处理那堆内存不光看并发还得看单次任务的内存消耗。所以给 Tomcat 设置多大内存这件事没有标准答案必须结合部署在同一台机器上的其他进程比如 MySQL、Redis、Nginx内存占用一起算。后文我会给出我自己常用的估算方法。2. 四种配置 Tomcat JVM 内存的途径按场景选Tomcat 设置 JVM 内存的正确姿势取决于你的启动方式。我按实际使用频率从高到低排列也是我自己项目的推荐顺序。2.1 方案一修改 catalina.sh / catalina.bat 脚本Tomcat 的 bin 目录下有两个重要的启动脚本Linux 环境下是catalina.shWindows 环境下是catalina.bat脚本里有一段专门为 JVM 参数预留的位置通常注释写着JAVA_OPTS。找到类似这一行# JAVA_OPTS-Djava.awt.headlesstrue -Dfile.encodingUTF-8 -server -Xms1024m -Xmx2048m -XX:PermSize512m -XX:MaxPermSize512m把注释去掉改成自己的值就可以实现设置 Tomcat JVM 内存的目的。以我的经验Linux 服务器上我一般这样配JAVA_OPTS-server -Xms2048m -Xmx2048m -XX:MetaspaceSize512m -XX:MaxMetaspaceSize512m -Djava.awt.headlesstrue注意几个细节-server表示使用服务端 JVM对于服务型应用特别是部署在服务器上长期运行的场景服务端 JVM 的 GC 策略和性能优化更合适。-Djava.awt.headlesstrue用于无显示器环境下运行涉及 AWT 的组件比如生成图片验证码很多项目不配这个会报HeadlessException。如果 JDK 版本是 8 之前的才有-XX:PermSize和-XX:MaxPermSizeJDK 8 及以后用 Metaspace 替代了持久代。Windows 服务器上对应的就是catalina.bat里面同样有一段JAVA_OPTS的注释改法一样。注意 Windows 的路径环境和权限问题bat 文件改完要检查格式不能带 Linux 换行符不然可能运行时报奇奇怪怪的错误。2.2 方案二使用 setenv.sh 独立配置我强烈推荐用这个方案。Tomcat 官方其实提供了环境隔离的规范方式如果 bin 目录下存在setenv.shLinux或setenv.batWindowsTomcat 启动时会自动加载这个文件把里面的变量作为环境变量生效。具体做法就是新建一个setenv.sh#!/bin/sh JAVA_OPTS-server -Xms2048m -Xmx2048m -XX:MetaspaceSize512m -XX:MaxMetaspaceSize512m -Dfile.encodingUTF-8为什么推荐这个方案因为它不动catalina.sh。你永远不知道团队里有谁都改过那个几百行的主脚本升级 Tomcat 版本时把自己修改的内容和更新包冲突然后互相覆盖最终环境配置丢了。而setenv.sh是独立文件Tomcat 官方设计它是用于存放自定义环境变量的升级 Tomcat 版本也不会把你这个文件覆盖掉。我第一次用这个方案的时候还不熟练踩了个坑直接改完文件就启动报错提示找不到setenv.sh的权限。检查后发现文件没有执行权限。所以提醒一下Linux 下创建完要记得chmod x setenv.sh2.3 方案三在系统环境变量里配置 JAVA_OPTS还有一种方式是不改 Tomcat 目录下的任何文件而是在操作系统层面设置JAVA_OPTS这个环境变量。Linux 下可以在/etc/profile或/etc/environment里加export JAVA_OPTS-Xms2048m -Xmx2048m -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512mWindows 下在系统属性里添加JAVA_OPTS环境变量。这个方案适合的场景是部署工具统一管理了操作系统层面的环境变量或者你不想让 Tomcat 目录内的文件被团队误改。缺点是作用范围搞不清的时候容易误伤如果同一台机器上跑着多个 Tomcat 实例所有实例都会读到同一份 JAVA_OPTS。2.4 方案四Windows Service 方式配置Windows 下如果是把 Tomcat 注册成系统服务运行的比如用 tomcat9w.exe 管理改catalina.bat是无效的需要打开tomcat9w的服务管理器切到 Java 选项卡初始内存池对应-Xms最大内存池对应-Xmx其他 JVM 参数对应JAVA_OPTS里的附加内容填完点确定重启服务生效。这个方式很直观但坑在于服务方式启动的 Tomcat 读的是 Windows 系统服务保存的配置参数任何人改了 Tomcat 目录里的脚本并不会同步到服务管理器里。改完配置后要重启服务两次停止、启动确保 JVM 进程完全退出再重新拉起否则参数可能没生效。3. 内存参数到底怎么给盘点关键的 JVM 调优选项设置 JVM 内存不是只填-Xms和-Xmx就完事了。下面这几个参数是我每次配置 Tomcat 时都会过一遍的每个参数都有它的意义和适用场景。3.1 堆内存参数堆内存相关的参数是基础中的基础参数作用建议值-Xms初始堆大小与-Xmx相同避免运行时动态扩缩-Xmx最大堆大小不超过物理内存的一半左右具体结合其他进程-Xmn新生代大小通常为堆大小的 1/3 到 1/4需结合 GC 调优-XX:MaxDirectMemorySize最大直接内存使用 Netty / NIO 时要显式设定以我常用的配置为例JAVA_OPTS-server -Xms2g -Xmx2g -Xmn768m ...新生代为什么给 768MB因为堆一共 2GB新生代占比在 1/3 左右比较均衡既能让短期存活对象在新生代快速回收又不会把老年代空间挤得太小。这个比例要在压测之后微调不能一上来就套用到所有项目。3.2 元空间与代码缓存JDK 8 之后永久代被元空间替代。元空间使用的是本地内存默认上限是物理内存大小看起来是好事实际上容易变成隐患——如果应用存在类加载器泄漏元空间会不断增长直到把服务器内存吃掉。所以生产环境我习惯显式设置-XX:MetaspaceSize512m -XX:MaxMetaspaceSize512mMetaspaceSize的含义是触发 Full GC 的阈值不是初始占用。也就是说元空间占用达到 512MB 时JVM 会开始执行一次针对元空间的 GC。MaxMetaspaceSize才是上限。代码缓存Code Cache也值得一提。JIT 编译后的本地代码存放在 Code Cache默认值可能太小特别是启动过大量类或者用了很多三方库的重型应用可能出现CodeCache is full警告。我一般加上-XX:ReservedCodeCacheSize256m3.3 GC 策略参数内存给多少和垃圾回收怎么做是紧密相关的。温度打满但总是 Full GC照样卡成幻灯片。JDK 8 默认使用的是 Parallel Scavenge并行收集器这种收集器适合吞吐量优先的批处理。但对于响应时间敏感的业务我更倾向用 G1-XX:UseG1GC -XX:MaxGCPauseMillis100 -XX:ParallelGCThreads4MaxGCPauseMillis是对 GC 停顿时间的期望上限G1 会尽量控制在这个范围。ParallelGCThreads控制并行 GC 线程数线程数不是越多越好4 核虚机上设置 4 就够。用 G1 后如果发现吞吐量下降可以调-XX:G1HeapRegionSize和-XX:InitiatingHeapOccupancyPercent。这套组合拳能让 GC 趋向低延迟但还是要以实际压测为准不同业务模型差异很大。3.4 内存配置的估算方法最后总结我自己的内存估算经验。一台物理机或虚机总共 16GB 内存部署 MySQL、Redis 和 Tomcat。我通常会这样分操作系统和基础服务预留 2GBMySQL分配 4GBRedis分配 1GBNginx 等辅助进程预留 1GB剩余给 Tomcat大约 8GB但是 8GB 不能全部给到-Xmx。Tomcat 进程除了堆内存还有线程栈、元空间、直接内存等配套开销。经验法则是Xmx 上限不超过机器可用内存的 50% 到 60%在这个例子里给 4GB 到 5GB 是稳妥区间。另一个需要重点考虑的因素是 Tomcat 并发线程数。Tomcat 连接器默认maxThreads是 200每个线程栈默认 512KB64位系统用 1MB200 个线程光栈空间就要占 200MB 左右。所以如果并发大这部分开销也要算进内存预算里必要时调整maxThreads。4. 完整实操从查看当前内存到配置生效配置完了得验证验证过程中还得会读状态。我经常见到有人改完配置服务重启后以为万事大吉实际根本没生效。所以我每次配置都会按照下面的流程走一遍。4.1 第一步确认当前 JVM 实际运行参数修改配置前先搞清楚现在 Tomcat 到底用了多大的内存。找到 Tomcat 进程 PIDps -ef | grep tomcat假设 PID 是 12345然后查看实际的 JVM 参数jcmd 12345 VM.flags或者jinfo -flags 12345如果 JVM 已经开启了远程调试或者本地有权限这两个命令能看到运行中的 JVM 生效参数。如果运行环境和 JDK 不同jcmd找不到命令可以先调用jps确认 Java 进程清单再对 PID 执行jcmd。还有一种查看方式是从启动脚本回显./bin/catalina.sh start启动日志里会带有完整的 Java 命令其中的-Xms、-Xmx参数清晰可见。多实例部署时需要注意区分多个 Tomcat 进程别拿 A 实例的内存参数去判断 B 实例。4.2 第二步备份并修改配置修改setenv.sh或者catalina.sh之前养成备份的习惯cp bin/setenv.sh bin/setenv.sh.bak.$(date %Y%m%d)实际修改内容时不建议直接删掉原有参数而是在原参数基础上追加或调整。很多 Tomcat 版本里本身带了一些默认 JAVA_OPTS直接覆盖会丢失默认设定比如某些需要-Djava.util.logging.config.file参数的项目会受影响。改完以后不要急着启动先做语法检查bash -n bin/setenv.sh报错的话会直接告诉你哪一行语法有问题这是在改完重启之前最便宜的一道保险。4.3 第三步重启并验证重启务必彻底停止再启动bin/shutdown.sh # 等待30秒确认进程完全退出 ps -ef | grep tomcat bin/startup.sh有些老手习惯于直接 kill -9 进程但我一般不推荐在没确认情况下就用强杀因为 Tomcat 的shutdown.sh会触发 JVM 的正常关闭钩子应用里如果有未完成的事务或需要在关闭时释放的资源能有善后机会。如果 30 秒后进程还在再用 kill 命令。重启后立刻看启动日志tail -f logs/catalina.out日志里会显示 JVM 启动参数看到-Xms2048m -Xmx2048m这样的字样就说明参数已经生效了。4.4 第四步多实例部署时的一案通用配置方法一台机器上跑多个 Tomcat 实例的场景下所有实例共用catalina.sh但每个实例有独立的setenv.sh可以放在各自的 CATALINA_BASE 下。打个比方如果机器上有/opt/tomcat-instance-1和/opt/tomcat-instance-2两个目录分别给每个实例建立 bin/setenv.sh配置各自的内存参数互不干扰。此时还需检查一个参数-Dcatalina.base是否指向了正确的实例目录否则配置写对了Tomcat 却读错了目录。这种问题出问题后极容易绕弯需要先确认环境变量。5. 内存溢出排查实录遇到问题如何精准定位内存参数设置再合理也免不了在实际运行中出现问题。这一部分我整理一下真正有用的排查思路和常用命令。5.1 三种常见内存溢出的现场面貌线上日志里出现的 OOM不能看到OutOfMemoryError就急着加内存先分清楚是哪种问题Java heap space堆内存满了。可能是 Xmx 太小也可能是内存泄漏或大对象过多。Metaspace元空间溢出。多见于动态生成类的框架如 CGLIB 代理创建过猛或者 Web 应用热部署次数过多类加载器泄漏。Unable to create new native thread这不是内存区满而是操作系统线程数限制或者内存碎片问题多见于疯狂创建线程的场景。只有第一种情况加内存往往是有效果的。第二种和第三种加内存解决不了根本问题反而可能让情况更糟。5.2 用 jstat 观察 GC 表现发现服务变慢先看 GC 频率。jstat 命令是我必用的工具jstat -gcutil 12345 1000 20每隔 1 秒打印一次共 20 次。输出中重点看FGCFull GC 次数。FGCTFull GC 消耗的总时间。GCTGC 总时间。如果 Full GC 频繁且 FGCT 很高说明老年代一直在艰难腾挪空间。这时要配合 dump 文件确定是内存不足还是对象泄漏。5.3 获取堆 Dump 文件当出现疑似内存问题不要盲目重启先保留现场。给 JVM 加上两个参数下次 OOM 时自动导出堆快照-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/logs/tomcat_heap_dump.hprof以后 OOM 发生时指定目录会自动生成.hprof文件这个文件拿到本地后可以用分析工具比如 Eclipse MAT打开直接就能看到哪些对象占用了最多的内存一条一清二楚的引用链能帮你迅速定位到业务代码里的可疑位置。我遇到的一个经典案例就是这样某个项目上线后堆内存慢慢涨始终没有回落的趋势。从 dump 文件里看到java.util.HashMap$Node[]占了大头顺着引用链追下去发现是业务代码里一个静态 Map 把每次请求的用户对象都缓存住没有清理。加内存只能延长时间清掉这个缓存引用后内存立刻平稳下来。5.4 用 Arthas 排查运行时问题如果说 jstat 和 dump 是事后处理那 Arthas 就是现场直播。用 Arthas 连接线上进程执行dashboard可以看到实时的内存占用、GC 次数、线程状态。执行memory可以列出堆、非堆、直接内存等各区域的使用情况对于确认直接内存是否爆满很有帮助。Arthas 在某些受限环境比如容器内缺少操作权限可能启动失败但本地常规部署一般没问题。我通常把这套工具放到本机而不是生产机需要诊断时通过 ssh 连接执行。5.5 一份常见内存问题速查表最后整理一个速查表实践中出了内存问题对着排查现象可能原因优先操作启动后不久就Java heap spaceXmx 配置过小调大 Xmx检查是否有一次性加载大文件运行几天后慢慢变慢FGC 频繁对象引用泄漏导出 dump分析大对象引用链频繁热部署后 Metaspace 溢出类加载器泄漏减少热部署频率调大 MaxMetaspaceSize并发升高后抛native thread线程数过多或栈内存过大检查线程转储调低 maxThreads 或线程栈大小使用了 Netty 后 Direct buffer 溢出直接内存不足设置 MaxDirectMemorySize并检查堆外缓存释放逻辑修改参数后重启无变化配置写错脚本或实例混淆用 jcmd 核对实际参数检查 setenv.sh 文件权限6. 最后一个容易被忽略的点容器环境下的内存配置现在很多 Tomcat 应用跑在 Docker 容器里这里有个大坑需要单独说。在容器里直接设置-Xmx4g但容器的内存上限只有 2GBJVM 会因为超出容器内存限制被 OOM Killer 干掉。反过来设置了-Xmx2g但容器内存上限 8GB多业务混部时 JVM 可能感知不到整体内存约束有隐性风险。比较稳的做法是给容器设置明确的--memory上限。JVM 参数里使用-XX:MaxRAMPercentage代替固定-Xmx比如JAVA_OPTS-server -XX:MaxRAMPercentage50 -XX:InitialRAMPercentage50 -XX:MinRAMPercentage50这个参数会让 JVM 根据容器限制计算可用内存按比例分配堆内存。这个方式比硬编码-Xmx更灵活特别是在弹性伸缩场景下实例规格会自动调整固定参数反而成了瓶颈。如果你是传统方式部署裸机或虚机上直接跑 Tomcat用固定-Xmx没问题。但 Docker 部署越来越普及这里给不熟悉的人提个醒先搞清楚容器的 memory limit再决定用哪种配置方式。7. 我在配置 Tomcat 内存时的几条实操心得配置 Tomcat JVM 内存这件事门槛不高但细节多。把经常踩的坑写出来希望能让大家少走点弯路。第一不要把-Xms和-Xmx设置成天差地别的两个值。比如初始 256MB 最大 4GBJVM 会在运行中不停扩缩容频繁触发 GC看起来内存利用率很低实际性能很差。两者设置成一致是目前生产环境的主流做法。第二改完配置后不要马上认为万事大吉。启动日志里看到参数生效只是第一步后续要在业务高峰期持续观察 GC 表现和内存占用趋势。如果内存是一条斜向上的直线不回头那是泄漏如果是锯齿状正常回收那就说明问题不大。第三团队协作时把内存配置放进启动脚本或 setenv.sh 交给配置管理工具统一管理。不要在每台机器上手敲命令改时间一长配置必然漂移。我见过有系统 A 的 Tomcat 内存 2GB、系统 B 的 4GB同一个应用行为却完全不同排查问题时浪费了大量时间。第四JDK 版本升级后要跑一遍内存配置验证。从 JDK 8 升到 JDK 11 或 17默认 GC 器变了Metaspace 行为也有差异原来的-XX:PermSize参数在新版本里直接无法识别导致启动失败的情况太常见了。第五不要忽视线程栈大小和直接内存这类非主流参数。堆内存配置得当但线程栈太小或直接内存不够照样会出现奇怪的服务不稳定。完整的内存配置 堆 元空间 栈 直接内存四个维度都要照顾到。按照上面的方法把 Tomcat 内存配置好绝大多数内存问题都能在源头避免。学会配置和排查只是第一步养成通过日志、jstat、dump 逐步定位问题的习惯才是真正值钱的能力。下次线上告警再亮起的时候你就有底气从容处理而不是重启了事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑