ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

我整理了一份Java性能调优的检查清单

我整理了一份Java性能调优的检查清单 先把结论摆在这儿性能调优不是玄学而是一套可以反复执行的检查流程。我整理这份清单的动机很简单每次线上告警响起团队里总有人凭直觉去猜瓶颈结果往往浪费大半天时间最后发现是连接池配太小或者SQL少了个索引。所以我把过去踩过的坑、翻过的源码、压测得出的数据点全部沉淀成这张检查清单。它不追求覆盖所有极端场景但能保证你在面对一个“慢接口”或“高CPU”问题时有次序、有依据地往下排查而不是东一榔头西一棒槌。清单的第一层永远先看基础设施而不是代码。大多数性能问题的根源是资源配额被静默耗尽而不是某一行代码写得差。建议你先检查CPU、内存、磁盘I/O、网络带宽这四类指标。用top、vmstat、iostat、sar这些老工具瞄一眼确认有没有明显的硬件饱和。我遇到过最离谱的一次应用慢到超时排查半天发现是同一台机器上的日志采集进程把磁盘写满了跟Java程序半毛钱关系没有。所以第一项检查动作就是确认你的JVM进程是不是真正在“抢”资源还是被邻居拖垮。如果你用的是容器还得看cgroup限制top里看到的CPU核数可能是宿主机的而JVM默认的线程池和GC线程数会按照宿主机核心数来初始化这会导致容器内频繁上下文切换。记得用-XX:ActiveProcessorCount2这类参数显式告诉JVM你只有几颗核。确认资源没被偷之后第二层转向JVM内存结构。我建议先看堆内存使用曲线别急着看GC日志。用jstat -gcutil pid 1000连续观察几秒如果EEden区频繁占满且YGC次数飙升说明对象分配速率过高如果Old区持续增长且FGC次数变多那基本可以判断是内存泄漏或大对象堆积。调优的第一原则先判断是对象分配问题还是对象回收问题。分配问题通常代码能救回收问题可能需要调堆大小或换垃圾回收器。查看堆转储前先执行jmap -histo:live看看前几十个类的实例数量和占用字节很多时候一眼就能定位到某个业务对象被异常缓存。如果是Spring Boot应用特别留意ConcurrentHashMap缓存、ThreadLocal未清理、或者事件监听器持有未释放的引用。第三层GC日志是宝藏但多数人没开启。生产环境务必加上-Xlog:gc:filegc.log:time,uptime,level:filecount5,filesize20mJDK 11语法。拿到GC日志后别盯着停顿时间看先计算分配速率和提升速率。分配速率 每次Young GC后Eden回收的内存除以间隔时间这个数字能直接反映你的代码在“生多少垃圾”。如果分配速率持续超过几百MB/s哪怕GC停顿只有50ms吞吐量也会被拖垮。此时优先去代码里找循环内创建大对象、字符串拼接、频繁装箱拆箱。提升速率过高则意味着对象过早晋升到老年代常见原因是新生代太小或大对象直接进入老年代。这时候调整-Xmn或-XX:MaxTenuringThreshold往往比换G1更有效。另外G1的-XX:UseStringDeduplication对大量重复字符串有奇效但别指望它是万灵药。第四层线程状态分析。用jstack连续抓三次线程快照每次间隔几秒然后对比线程栈。如果某个业务线程卡在同一个锁或park上那就是锁竞争如果线程状态一直是RUNNABLE且CPU占用高再看是不是执行了长时间的计算任务。这里有个隐蔽的坑Java的Thread.sleep不会释放锁所以如果你在synchronized块里调sleep其他线程只能干等。更隐蔽的是Object.wait超时后自动重新竞争锁看起来是超时返回实际可能又阻塞了几十毫秒。建议检查代码里有没有用ConcurrentHashMap做锁分段或者用ReentrantLock的tryLock替代同步块。另外jstack里经常能看到ForkJoinPool.commonPool中的线程被某些并行流任务占满导致其他使用parallelStream的代码集体卡死。在线程层面最值得加粗的结论是80%的线程问题不是线程数不够而是任务被某个坏线程堵住了。第五层连接池与外部依赖。很多性能问题在应用内部看不出来一压测就暴露。检查数据库连接池、HTTP连接池、Redis连接池的参数。连接池不是越大越好一个经典的错误是maximum-pool-size设置成50但数据库的max_connections只有100两个服务一接就把库打崩。建议用HikariCP时先看minimumIdle和maximumPoolSize的公式线程数 CPU核数 2 硬盘数这只是一个朴素的起点还得配合实际压测。更关键的是要监控连接获取等待时间。如果从连接池拿连接经常超过几十毫秒说明连接不够或连接泄漏。连接泄漏的排查方式开启leakDetectionThreshold3000日志里会打出哪里获取的连接忘记归还。对于HTTP调用同样要设置connectTimeout和readTimeout否则线程会无限期挂在远程IO上。记住所有外部依赖都可能导致线程耗尽所以必须给每个依赖设置明确的超时与失败降级策略。第六层SQL与数据访问性能。数据库往往是第一个背锅的。检查清单里最狠的一条把慢SQL日志打开统计每个接口总耗时中SQL占比。如果占比超过60%那性能瓶颈在数据层别优化Java代码。用EXPLAIN看执行计划重点关注type是不是ALL或indexrows预估是否巨大有没有文件排序或临时表。索引失效的经典原因包括在索引列上做函数运算、隐式类型转换、like前导通配符。但这些属于基础更高级的坑是N1查询。比如用MyBatis时主查询查出100条订单循环里每个订单去查一次明细就是100次额外的SQL。优化方式是用foreach批量查询或者join。但join也不是越多越好过度join会导致单条SQL复杂度过高建议一个核心查询最多join三张表。此外分页查询别用OFFSET大偏移改用游标分页或限定起始ID。对于写操作考虑批量插入一条INSERT插入多行或者用rewriteBatchedStatementstrue驱动参数。第七层代码层面的热点方法。如果你已经确认堆、GC、线程、连接都没问题那就得用Async Profiler或JFR去采样CPU。核心观点不要盲猜代码性能让采样结果告诉你方法级别的热点。jcmd pid JFR.start再JFR.dump然后用jfr print或者可视化工具打开。重点关注自顶向下的调用树里CPU占比超过20%的方法。常见的热点包括正则表达式反复编译用预编译Pattern、String.format滥用用StringBuilder、序列化框架效率低换成Kryo或ProtoBuf、日志框架在debug级别下拼接字符串。还记得System.out.println在循环里有多慢吗它每次都要同步输出到控制台生产环境等于自杀。别小看这些“小优化”热路径上的一行无厘头代码可能吃掉你30%的吞吐量。但务必记住一切优化必须以压测结果为准先采样再动手。第八层JVM参数调优误区。很多人喜欢抄网上的“标准JVM参数”结果到自家环境翻车。调优的最终目标不是“不GC”而是让GC频率与分配速率匹配。堆设得过大Full GC时间反而更长新生代太小对象频繁晋升。我建议以默认G1为起点然后只调整三个参数-Xmx、-Xms、-MaxGCPauseMillis。先把-Xms和-Xmx设为相同值避免启动时堆扩容MaxGCPauseMillis设置成200ms左右不要追求极端低延迟。如果G1的停顿经常超过目标先去优化对象分配而不是盲目调-XX:ConcGCThreads。另外注意-XX:UseCompressedOops在普通机器上默认开启别乱关。最重要的一条每改一个JVM参数必须压测前后对比且只改一个变量。否则你根本不知道是哪一步起了效果。第九层缓存策略检查。缓存能解决很多性能问题但用错缓存比不用还糟。检查清单里必须包含缓存命中率、缓存穿透、缓存雪崩、缓存一致性的四问。先看本地缓存Caffeine的hitRate低于80%反而损耗性能。如果是分布式缓存Redis确认有没有设置合理的TTL避免“永久缓存”变成“永久脏数据”。热点key过期瞬间会有大量请求打到数据库这叫缓存击穿解决方案是互斥锁或逻辑过期。缓存穿透则建议用布隆过滤器拦截不存在的数据。对于一致性最简单粗暴的做法是“先更新数据库再删除缓存”删除失败怎么办用消息队列重试。缓存最容易被误解的点是缓存不是为了减少数据库压力而是为了降低延迟如果缓存命中率高但接口依然慢那瓶颈在缓存框架本身的序列化。检查你用的Redis客户端是Jedis还是Lettuce序列化用JDK还是Jackson在高并发下差距天壤之别。第十层压测与复盘机制。调优工作没有终点所以清单最后一项是建立持续的性能验证流程。没有压测就上线等于裸奔没有基线数据的调优等于瞎调。建议每次发布前用JMH做微基准用Gatling或wrk做接口压测。压测时必须模拟线上规格的CPU、内存、网络否则结果没有意义。压测结束后保存三份数据吞吐量TPS、响应时间P99、错误率。把这些数据当作下一次调优的起点。再配合实时监控PrometheusGrafana在JVM层暴露GC时间、线程池活跃数、连接池等待时间等指标。性能调优的本质是建立一个反馈循环测量-分析-优化-再测量。我这份清单只是第一步真正的功夫在于每次排查后把根因和修复方案记录下来沉淀成下一个人的检查清单。最后补充一个容易忽略的隐藏层操作系统和文件系统。ulimit -n默认1024时高并发连接直接报Too many open files。TCP的time_wait过多导致端口被占满net.ipv4.tcp_tw_reuse该开就开。JVM临时文件目录如果落在了/tmp而/tmp被定期清理可能造成动态类加载失败。检查清单永远不止于JVM内部你运行的整个环境都是性能的一部分。另外/proc/sys/vm/swappiness如果是默认60JVM堆内存可能被换到磁盘上那种卡顿是GC日志里完全看不出来的。把这些系统参数加进你的启动脚本检查项用sysctl和limit命令先验证一遍再谈代码优化。这份清单写到这里我最后想强调一个反直觉的观点很多性能问题是可以“预防”的而不是“调优”出来的。与其在线上焦头烂额不如在编码阶段就遵循几个铁律循环内别创建重量级对象IO操作必须超时集合初始化给容量日志级别别乱用连接用后即还。我整理这份检查清单不是为了让你背参数而是希望你建立一套系统化的性能思维——当问题出现时你知道该从哪里下手也知道做完之后要如何验证。真正的高手不是比你多知道十个参数而是比你少犯五个愚蠢的错误。如果你看完这份清单发现自己还有几项从未检查过那就从今晚开始把每一个环节都跑一遍。不用急着一次性改完先记录现状再逐步调整。性能调优是一场持久战但每一次成功的调优都会让系统离崩溃远一步让你离真相近一步。希望这份清单能成为你工具箱里那把最顺手的螺丝刀。
返回列表