Java 应用跑在 CentOS 上时,响应时间突然变长、CPU 长时间维持在高位,是很多线上服务都会碰到的问题。真正麻烦的地方在于,表面看像是“程序变慢了”,实际可能牵扯线程热点、GC 频繁、锁竞争、系统参数不合适,甚至后台服务和磁盘 I/O 一起拖后腿。
这类问题不适合靠经验盲调。更稳妥的做法,是先把瓶颈定位清楚,再分别检查 JVM、代码实现和系统资源,最后用监控与压测验证调整是否真的生效。按这个顺序处理,通常比一上来改参数更容易找到有效解法。
先定位瓶颈,别急着改配置
Java 性能优化最忌讳“先调一轮再说”。如果不知道问题到底卡在 CPU、线程、GC 还是 I/O,后面的调整很容易南辕北辙。先把热点找出来,后续动作才有依据。

先用 top 找到高占用进程
top 是最直接的起点。它可以实时查看系统 CPU、内存占用情况,先确认是不是某个 Java 进程把资源吃满了。找到对应 PID 之后,后面的线程和堆栈分析才能继续展开。
再用 ps -mp 和 jstack 追到具体线程
确认 PID 后,可以用下面的命令查看线程运行状态,并按 CPU 占用排序:
ps -mp <PID> -o THREAD,tid,timesort-rn
这样能比较快地定位到到底是哪一个线程在持续消耗 CPU。
拿到线程 ID 后,再结合 jstack 导出线程快照:
jstack -l <PID> | grep <TID> -a60 > thread_log.txt
如果堆栈里大量出现 BLOCKED 或 WAITING,通常就要重点排查锁竞争、线程阻塞,甚至死锁问题。
GC 指标也要同步观察
有些慢不是线程打满,而是 GC 过于频繁。可以用下面的命令按秒观察 GC 状态:
jstat -gcutil <PID> 1000
通过这个输出,可以看到 GC 次数、停顿情况,以及新生代、老年代的使用率。如果 GC 频率过高、停顿明显偏长,问题往往就在堆配置或垃圾回收策略上。
JVM 参数为什么经常是性能问题源头
很多 Java 应用在开发环境里跑得没问题,一到生产环境就开始变慢,核心原因往往是 JVM 仍在沿用默认参数。默认值通常偏保守,不一定适合线上负载。

堆内存先避免动态扩缩容
比较常见的做法,是把初始堆和最大堆设置为相同值,减少运行期扩容带来的开销。例如:
-Xms4g -Xmx4g
这样 JVM 在启动时就完成堆内存分配,避免服务运行过程中频繁调整堆大小,影响响应稳定性。
低停顿场景优先考虑 G1GC
如果服务内存较大,同时又希望控制停顿时间,G1GC 通常是更常见的选择。可以先启用:
-XX:+UseG1GC
然后再结合业务目标继续细调,例如:
-XX:MaxGCPauseMillis=200
-XX:NewRatio=3
这里 -XX:MaxGCPauseMillis=200 用来约束目标停顿时间,-XX:NewRatio=3 表示新生代占 1/4。是否合适,还得结合实际对象分配速率和 GC 表现来判断。
GC 日志一定要开
只看瞬时监控,往往很难判断 Full GC 到底多不多、单次停顿是否异常。建议开启 GC 日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
拿到日志后,可以配合 GCViewer 之类的工具分析,重点看 Full GC 次数、停顿时长和回收效率。很多 JVM 参数是否有效,最后都要回到日志证据上来确认。
代码层面的慢,通常藏在对象和并发细节里
如果 JVM 和系统指标看起来还算正常,但吞吐还是上不去,就要回头看代码实现。性能问题经常不在“大逻辑”上,而是积累在循环、对象分配、容器选型和锁粒度这些细节里。
先减少不必要的对象创建
临时对象创建过多,会直接增加 GC 压力。一个典型例子,就是在循环里使用:
str += "x"
这种写法会不断创建新字符串,代价很高。改成 StringBuilder 往往就能明显改善。类似地,数据库连接、线程池这类资源也应该复用,而不是每次都重新 new 一个。
数据结构和算法选错,复杂度会放大成本
容器和算法的选择会直接决定性能上限。比如频繁随机访问时,ArrayList 更合适;频繁插入删除时,LinkedList 更适合当前场景。排序也是同理,数据量一大,冒泡排序的成本会迅速失控,换成快速排序或归并排序,差距会非常明显。
高并发下要重点看锁竞争
线程多、请求密时,锁竞争常常是响应抖动的关键原因。能用 ConcurrentHashMap,就尽量不要把共享容器长期放在 synchronized HashMap 上;能缩小锁范围,就不要把整个方法都锁住。锁粒度越粗,高并发下的等待成本越明显。
内存泄漏需要用堆转储确认
还有一类问题不一定马上表现为 CPU 飙高,而是服务越跑越慢,最后堆被撑满。连接没关闭、集合对象长期持有引用,都是常见原因。可以先导出堆文件:
jmap -dump:live,format=b,file=heap.hprof <PID>
然后再用 MAT(Memory Analyzer Tool)分析大对象、引用链和可疑泄漏点,定位哪些对象没有被正常释放。
系统资源和内核参数也可能拖慢 Java
排查到这里,如果应用本身没有明显热点,就要看 CentOS 是否把资源浪费在了别处。线上性能不稳,很多时候并不是 Java 单独造成的。

先清理不必要的后台服务
可以先列出系统开机自启服务:
systemctl list-unit-files --type=service
如果确认某些服务并不需要,比如 bluetooth,可以直接禁用:
systemctl disable bluetooth
对资源紧张的服务器来说,回收这部分 CPU 和内存,往往比想象中更有价值。
按场景调整内核参数
网络连接、交换分区和连接队列长度,都会影响 Java 服务的整体表现。可以编辑 /etc/sysctl.conf,加入以下配置:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
vm.swappiness = 10
net.core.somaxconn = 1024
tcp_tw_reuse 和 tcp_tw_recycle 用于复用和快速回收 TIME_WAIT 连接,vm.swappiness = 10 表示系统更倾向优先使用物理内存,somaxconn 增大后则有助于承接更高并发的 TCP 连接请求。修改完成后执行:
sudo sysctl -p
需要注意的是,这类参数是否适用,仍然要结合内核版本、网络环境和业务连接模型来验证。
文件系统和存储介质也会影响延迟
如果应用对磁盘 I/O 较敏感,文件系统与挂载参数同样重要。XFS 更适合大文件和高并发场景,挂载时加上 noatime 和 nodiratime,可以减少访问时间记录带来的额外 I/O。硬件条件允许的话,从机械盘切换到 SSD,通常是最直观的性能提升手段之一。
优化后怎么判断真的有效
性能优化不是改完参数就结束。真正有价值的结果,必须通过监控和压测来验证,否则很容易只是把问题从一个位置转移到另一个位置。
先用基础监控盯住 CPU、内存和 I/O
最常用的几组命令依然很实用:
top
htop
vmstat 1
iostat 1
top 和 htop 适合看 CPU、内存整体趋势;vmstat 1 可以观察虚拟内存状态,如果 si/so 偏高,说明交换分区使用频繁;iostat 1 则可以看磁盘 I/O,await 偏高意味着 I/O 等待时间过长。
需要深挖时再上 JVM 可视化工具
如果基础监控还不够,可以继续用 VisualVM 做图形化观察,查看 JVM 内存、线程和 GC 变化;也可以用 JProfiler 分析 CPU 热点和内存泄漏。对于大规模部署环境,Prometheus 配合 JMX Exporter,则更适合持续采集 JVM 指标并统一可视化。
最后用压测结果闭环
调优完成后,最好用 JMeter 模拟高并发访问,并结合监控数据一起看结果。如果 TPS 提升了、响应时间下降了、CPU 和 GC 也更平稳,说明这轮优化方向基本正确。反过来,如果压测一上来问题又出现,就要回到前面的定位步骤继续收敛瓶颈。
Java 在 CentOS 上跑得慢,通常不是单一参数造成的,而是多个环节叠加。先定位,再分层优化,最后用数据验证,才是更适合生产环境的处理方式。









