Linux 环境里的 Java 应用一旦出现响应变慢、吞吐下降或 GC 停顿抖动,问题往往不只在一层。更有效的做法,是按 JVM、代码、系统、并发、数据库和监控六条线逐项排查。下面这份整理不追求罗列参数,而是围绕“为什么调、适合什么场景、怎么判断是否值得做”来展开,方便你把优化动作落到实际服务上。
JVM 参数调优:先稳住内存和回收行为
Java 应用的基础性能,通常先受 JVM 配置影响。尤其是堆大小、垃圾回收器选择、停顿目标和分代比例,往往决定了服务在高负载下是平稳运行,还是频繁抖动。

堆内存建议固定上下限
堆内存是最基础的启动配置,常见做法是把 -Xms 和 -Xmx 设为同一个值,例如 -Xms4g -Xmx4g。这样做的目的,是让 JVM 在启动时一次性申请到位,避免运行过程中动态扩容或收缩堆空间,减少由堆变化带来的性能波动。
如果服务本身负载比较稳定,这种配置通常更容易得到可预期的表现;如果部署环境资源紧张,则需要结合容器或主机实际内存一起评估,避免给 JVM 留得过大,反而挤压系统缓存和其他进程空间。
GC 方案要按堆大小和延迟目标选
垃圾回收器没有“通用最优解”,关键看应用场景。如果堆内存比较大,尤其是超过 4GB,G1 往往是比较均衡的方案,能在吞吐量和停顿时间之间取得不错平衡。

如果业务更看重低延迟,例如接口服务对尾延迟特别敏感,可以继续评估 ZGC 或 Shenandoah,但前提是确认 Java 版本是否支持。至于传统 CMS,虽然过去常用于低停顿场景,但现在已经明显处于被替代的趋势里,新增项目通常不再优先考虑。
停顿目标和分代比例要结合对象生命周期调整
GC 停顿时间可以通过 -XX:MaxGCPauseMillis 设置,例如 -XX:MaxGCPauseMillis=200。这个值的意义更像“目标提示”,而不是硬性上限。它会引导 GC 在吞吐量和停顿时间之间做权衡,因此不能简单理解为“设置了 200 毫秒就一定不会超过”。
如果应用里短生命周期对象很多,还要关注年轻代和老年代的比例。可以通过 -XX:NewRatio 和 -XX:SurvivorRatio 调整,让年轻代拥有更合适的空间,减少对象过早晋升到老年代的情况。
另外,-XX:+UseCompressedOops 在 64 位 JVM 下通常默认开启,它可以把对象引用从 8 字节压缩到 4 字节,从而减少内存占用,并提升一定的访问效率。这个参数本身不算主要调优入口,但理解它的作用有助于判断内存结构表现。
代码层面优化:减少无效开销和锁竞争
JVM 参数决定运行环境的边界,代码质量则直接决定资源是否被浪费。很多性能问题并不复杂,本质上就是高频路径里创建了太多对象、做了太多重复计算,或者让线程在不必要的锁上等待。
高频路径里先减少对象创建
减少对象创建是最常见也最有效的手段之一,尤其是在循环、热方法和高并发接口中更明显。比如在循环里写 String str = new String("abc"),会不断创建新对象;改成 String str = "abc" 后,就可以复用字符串常量池中的对象。

字符串拼接同样值得留意。频繁使用 + 往往会产生不少临时对象,改用 StringBuilder 能更稳定地控制内存分配,尤其适合循环拼接或复杂文本组装场景。
局部变量和循环外提能减少重复成本
局部变量存放在栈中,访问速度通常快于堆中的实例变量。因此,能用局部变量表达的中间状态,就不要反复访问实例字段。像把 this.instanceVar 替换为 localVar,在热点代码里往往是有意义的。
循环内部的不变计算也应该尽量提前。比如:
int size = list.size();
for (int i = 0; i < size; i++) {
// ...
}
相比每次循环都调用 list.size(),这种写法把重复工作压缩成了一次,虽然单次收益不大,但在高频执行时非常划算。
数据结构和同步方式决定上限
容器和算法选型,会直接影响热点操作的复杂度。高频查找场景优先考虑 HashMap,时间复杂度通常是 O(1);需要有序结果时可以使用 TreeMap,复杂度是 O(log n);队列型场景可以用 LinkedList,插入和删除是 O(1)。
Vector、Hashtable 这类同步容器,如果没有明确线程安全要求,通常应尽量避免。它们的同步成本会在高并发下放大。
锁竞争也是代码层的重要瓶颈。把 synchronized 方法改为 synchronized 代码块,可以缩小锁范围,减少锁持有时间。如果业务允许,进一步换成 ConcurrentHashMap、CopyOnWriteArrayList 等并发集合,通常比传统同步容器更适合高并发环境。
系统资源优化:别让 Linux 成为隐性瓶颈
Java 服务跑在 Linux 上,最终还是要受主机内核、文件描述符、磁盘调度和交换空间影响。应用本身写得没问题,但系统层配得保守,一样会出现吞吐受限、连接失败或者磁盘抖动。
内核参数先看网络缓冲和交换倾向
内核参数通常从 /etc/sysctl.conf 入手。比如:
net.core.rmem_max=16777216
net.core.wmem_max=16777216
这类配置可以提高网络缓冲上限,适合网络吞吐压力较大的服务。
另一个经常需要关注的参数是 vm.swappiness。默认值通常是 60,很多服务型场景会建议降到 10,让系统尽量少使用交换空间,避免把热点内存换到磁盘后拖慢响应。对于延迟敏感的 Java 应用,这个调整往往比表面看起来更重要。
高并发服务要提前放开文件描述符
在高并发网络服务里,文件描述符限制经常是被忽略的问题。可以通过 /etc/security/limits.conf 提高 nofile 限制,例如:
* soft nofile 65535
* hard nofile 65535
如果不提前调整,服务在连接数上升时可能直接因为描述符耗尽而报错,表现为连接建立失败、套接字异常或部分请求无响应。
按磁盘类型选 I/O 调度器,并盯住 swap 使用率
I/O 调度器要结合磁盘类型选择。SSD 常见建议是使用 deadline 或 noop,可以通过下面的方式调整:
echo deadline > /sys/block/sda/queue/scheduler
机械硬盘过去更常用 cfq,但它已经在很多场景里逐渐退出主流,因此是否采用还要看系统版本和实际负载特点。
同时要定期检查交换空间使用情况,执行 free -h 查看 swap。如果长期高于 20%,通常说明物理内存吃紧、对象回收不及时,或者系统上存在不必要的后台服务。这个时候仅靠 JVM 参数微调往往不够,最好结合进程占用和系统服务一起排查。
多线程与并发优化:让线程数和模型匹配业务类型
并发优化的核心不只是“多开线程”,而是让线程池规模、同步手段和 I/O 模型跟任务类型相匹配。线程数过少会压不满资源,过多又会让上下文切换和锁竞争吞掉收益。
线程池大小要按 CPU 密集型和 I/O 密集型区分
线程池配置通常以 CPU 核心数为基线,可以通过 Runtime.getRuntime().availableProcessors() 获取。常见经验是:CPU 密集型任务设为 核心数+1,I/O 密集型任务设为 核心数*2。
这个经验值的意义不在于绝对精确,而是在大多数场景下能避免线程开得过多,导致调度开销和上下文切换成本明显上升。对于数据库、网络调用较多的服务,再结合压测结果微调会更可靠。
优先使用 java.util.concurrent 提供的并发工具
并发控制尽量优先使用 java.util.concurrent 包中的成熟工具,而不是手写等待和通知逻辑。比如 CountDownLatch 适合线程同步等待,CyclicBarrier 适合多线程汇合,Semaphore 适合限流,BlockingQueue 适合生产者-消费者模型。
这些工具的优势,不只是写法更清晰,也在于它们通常已经针对可见性、阻塞和调度效率做过充分设计,出问题时也更容易定位。
缩小锁范围,必要时改用无锁或异步方案
减少锁粒度,是提升并发效率最直接的做法之一。不要轻易用 synchronized 修饰整个方法,而应该把非临界区逻辑移到同步块之外,让真正需要互斥的代码尽可能短。
如果数据结构支持,可以进一步采用细粒度锁、分段锁,或者乐观锁方案。例如 ConcurrentHashMap 的并发设计、基于 CAS 的原子操作,以及 AtomicInteger 这类原子类,都是常见替代选择。
面对 I/O 密集型阻塞问题,还可以从线程模型上调整。比如使用 NIO 的 Selector,或者采用 CompletableFuture、Reactive Programming(如 Spring WebFlux)这类异步方案,往往能显著改善等待型场景的响应能力。
数据库访问优化:先减少往返,再优化查询路径
很多 Java 应用看起来是“接口慢”,实际瓶颈却在数据库。数据库层优化不只是加索引,还包括查询字段控制、连接池参数和批量写入方式,目标都是减少无效往返和资源争抢。
SQL 先避免多查、乱查和深层嵌套
SQL 优化是数据库性能的基础动作。首先要避免 SELECT *,只查询必要字段;其次在 WHERE 条件和 JOIN 字段上建立合适索引,让数据库能更快定位数据。
对于复杂查询,可以考虑拆分大查询、引入临时表,或者减少过深的子查询嵌套。很多慢 SQL 并不是单点参数问题,而是查询结构本身让优化器难以走到高效执行路径。
连接池不要只用默认值,重点看上限和超时
连接池方面,HikariCP 仍然是常见优先选择,它的默认配置已经比较成熟。但在线上环境里,仍建议按数据库能力和业务并发量校正 maximumPoolSize,通常不要超过数据库最大连接数的 80%。
connectionTimeout 默认 30 秒,如果网络延迟较高或者数据库偶发抖动明显,可以结合故障恢复策略适当调整。这里的关键不是把超时一味调大,而是让应用在等待连接和快速失败之间取得平衡。
批量操作优先用批处理接口
插入或更新多条记录时,尽量使用批量方式,减少应用与数据库之间的往返次数。典型做法是使用 PreparedStatement 的 addBatch() 和 executeBatch()。
例如批量插入时,可以采用类似下面的方式:
INSERT INTO table VALUES (, ), (, )
相比逐条提交,批量处理往往能明显提高吞吐,尤其在日志写入、数据同步、报表导入这类场景中提升更明显。
性能监控与分析:用数据确认瓶颈是否真的被解决
性能优化最怕“凭感觉改完就算结束”。如果没有监控和验证,很多调整只是把问题从一个层面挪到了另一个层面。真正有效的做法,是让 JVM、系统、日志和压测数据形成闭环。
JVM 工具适合定位 GC、内存泄漏和线程阻塞
排查 JVM 内部状态时,常用工具包括 jstat、jmap 和 jstack。例如:
jstat -gcutil 1000
jmap -dump:format=b,file=heap.hprof
jstack
jstat -gcutil 可以每秒输出一次 GC 统计,适合观察回收频率和停顿特征;jmap 生成堆转储,适合分析内存泄漏;jstack 则用于查看线程栈,排查死锁、阻塞和长时间等待。
如果更偏好图形界面,也可以使用 VisualVM、Java Mission Control、JProfiler 这类工具做辅助分析。
系统工具能快速判断瓶颈落在哪一层
系统层排查可以结合 top、free、df、iotop、netstat 或 ss 使用。它们分别对应 CPU、内存、磁盘空间、磁盘 I/O 和网络连接状态。
这些工具的价值,不只是“看到占用高”,更重要的是帮助你把问题归因。比如 CPU 持续偏高,可能是代码计算密集或线程空转;I/O 持续偏高,则可能是数据库查询慢、日志落盘重或 swap 频繁介入。
日志和压测决定优化是否真正成立
性能优化需要常态化记录和回归验证。可以使用 Logback、Log4j2 记录应用日志,也可以通过 AOP 统计方法执行时间,筛出慢请求和热点链路。
在验证阶段,定期用 JMeter、Gatling 模拟高并发场景,观察优化前后的响应时间、吞吐量和资源占用变化。只有当这些指标一起改善时,才能说明这次调整确实解决了问题,而不是只换来另一个新的瓶颈。







