性能瓶颈分析:CPU密集型 vs IO密集型、内存泄漏排查、GC调优、数据库连接池优化

各位同学,咱们今天聊点硬核的。性能瓶颈分析,说白了就是给系统“看病”。我做了这么多年风控,见过太多系统“死”得不明不白。有的CPU飙到100%,有的内存泄漏像水管漏水,还有的数据库连接池直接罢工。嗯,今天我就把这几类典型问题,掰开揉碎了讲清楚。

一、CPU密集型 vs IO密集型:你得先分清“敌人”是谁

我个人习惯,拿到一个性能问题,第一件事不是看代码,而是先判断:这到底是CPU累,还是IO累?你想想看,方向错了,调优就是白费劲。

1. CPU密集型

说白了,就是计算太多。比如风控里的复杂规则引擎、大量加解密、模型推理。这类任务,CPU一直处于“忙死”状态,上下文切换少,但每个任务都占着CPU不放。

典型特征:

  • CPU利用率持续 > 90%
  • 线程数不多,但每个线程都在疯狂计算
  • 系统响应时间随并发数增加而线性增长

我在项目中遇到过,一个实时风控规则引擎,每次请求要跑200多条规则。上线后CPU直接拉满。怎么解?

  • 减少计算量:规则预编译、缓存中间结果
  • 并行计算:用ForkJoinPool把规则拆成子任务
  • 升级硬件:换更高主频的CPU,或者增加核心数

2. IO密集型

这类问题更隐蔽。系统看起来CPU不高,但就是慢。为什么?因为线程都在等IO——等数据库、等Redis、等外部接口。

我的经验:IO密集型系统的CPU利用率通常只有20%-40%,但线程数可能上千。这时候加CPU没用,得加线程池大小。

我曾经调过一个风控系统,每次请求要查5个外部数据源。IO等待时间占了总耗时的80%。优化方案:

  • 异步化:用CompletableFuture并行发起IO请求
  • 连接池调大:让更多线程同时等待IO
  • 缓存:对高频查询做本地缓存

二、内存泄漏排查:别等OOM了才后悔

内存泄漏,我愿称之为“慢性毒药”。系统跑着跑着,内存一点点涨上去,最后OOM。嗯,这里要注意,OOM不一定是内存泄漏,也可能是内存真的不够用。但泄漏,一定是代码的锅。

排查工具链

工具 用途 我的用法
jstat 查看GC情况 先看老年代是不是一直在涨
jmap dump堆内存 OOM时自动dump,或者手动触发
MAT / JProfiler 分析dump文件 找GC Root引用链,定位泄漏点

避坑指南:我曾经在生产环境直接jmap dump,结果系统卡死了一分钟。后来我学乖了,用jmap -dump:live只dump存活对象,或者用-XX:+HeapDumpOnOutOfMemoryError自动触发。

常见泄漏场景

  • ThreadLocal未清理:线程池里的线程复用,ThreadLocal里的对象一直不被回收。我见过一个风控系统,每次请求往ThreadLocal里塞用户信息,结果内存里堆了几十万个用户对象。
  • 静态集合类:HashMap、ArrayList定义为static,只增不减。说白了就是“只吃不吐”。
  • 资源未关闭:数据库连接、文件流、网络连接,用完不close。这类问题在try-catch里特别常见。

三、GC调优:别让垃圾回收成了“垃圾”

GC调优,很多人一上来就调各种参数。我个人建议,先搞清楚GC在干什么。说白了,GC就是帮我们打扫内存垃圾。但如果打扫得太频繁,或者打扫一次太久,系统就卡了。

GC调优的核心指标

  • 吞吐量:应用运行时间 / (应用运行时间 + GC时间)。我一般要求 > 99%
  • 暂停时间:每次GC导致应用停顿的时间。实时风控系统,我要求 < 50ms
  • GC频率:Young GC每秒不超过1次,Full GC每10分钟不超过1次

我的调优步骤

  1. 先看日志:加上-XX:+PrintGCDetails -XX:+PrintGCDateStamps,分析GC日志
  2. 确定目标:是吞吐量优先,还是低延迟优先?风控系统,我选低延迟
  3. 选GC器:JDK 8用G1,JDK 17+用ZGC。我最近在项目里用ZGC,暂停时间基本在1ms以内
  4. 调参数:比如-XX:MaxGCPauseMillis=50,-XX:G1HeapRegionSize=4m

一个真实案例:我之前调一个风控系统,Full GC每3分钟一次,每次暂停2秒。分析发现,老年代里有个巨大的HashMap,存了所有用户的规则缓存。解决方案:把缓存拆成多个小Map,并设置过期时间。Full GC降到了每30分钟一次。

四、数据库连接池优化:别让连接成了瓶颈

数据库连接池,看着简单,坑却不少。你想想看,连接池太小,请求排队;连接池太大,数据库扛不住。我见过一个系统,连接池设了200,结果数据库连接数打满,直接挂了。

连接池大小怎么定?

有个经典公式:连接数 = (CPU核心数 * 2) + 有效磁盘数。但这是理论值。实际项目中,我一般这样算:

  • 先压测,看单个连接的QPS
  • 再算目标QPS,反推连接数
  • 留20%的余量
场景 推荐连接数 说明
纯查询,无事务 10-20 连接复用率高
混合读写,短事务 20-50 根据TPS调整
长事务,复杂查询 5-10 连接占用时间长,不宜过多

HikariCP的配置经验

我现在基本只用HikariCP。它的性能确实好。几个关键参数:

  • maximumPoolSize:别超过数据库最大连接数的80%
  • minimumIdle:建议等于maximumPoolSize,避免频繁创建连接
  • connectionTimeout:30000ms,超过就报错,别让请求死等
  • maxLifetime:1800000ms(30分钟),比数据库的wait_timeout短一点

我的小技巧:在HikariCP里加上leakDetectionThreshold参数,设为60000ms。如果连接被占用超过60秒,日志里会打印堆栈信息。我曾经靠这个参数,抓到一个在事务里调外部接口的坑。

知识体系总览

下面这张图,是我自己总结的性能瓶颈分析框架。你照着这个思路走,基本不会跑偏。

性能瓶颈分析框架 性能瓶颈 CPU密集型 IO密集型 内存泄漏 减少计算量 并行计算 异步化 连接池调优 ThreadLocal 静态集合 GC调优 数据库连接池优化 选GC器 调参数 HikariCP 连接数计算 从问题分类到具体措施,一步步定位和解决性能瓶颈

好了,这一章的内容就到这里。性能瓶颈分析,说白了就是“望闻问切”。先分清楚是CPU还是IO,再查内存有没有泄漏,然后调GC和连接池。每一步都有章可循。你按照这个框架去排查,大部分问题都能找到根因。


公众号:蓝海资料掘金营,微信deep3321