前两天朋友老张找我吐槽,说他公司那个后台管理系统,一到月底跑报表就卡得像老牛拉车,客户投诉不断。他们团队折腾了一圈,最后发现是Java应用没调好,堆内存天天爆,GC频繁得跟闹钟似的。这事儿让我想起这些年踩过的坑,干脆聊聊Java调优那些实际用得上的招儿。
别等出事才想起调优
很多人觉得调优是系统崩了才该干的事,其实不然。就像你不会等到车抛锚了才去保养,Java应用也得定期“体检”。比如启动时加上-XX:+PrintGCDetails,看看日志里GC频率和耗时。要是发现Young GC一秒好几次,或者Full GC动不动停几百毫秒,那基本就是信号了。
堆内存不是越大越好
有个误区是“机器内存多,-Xmx直接给8G、16G”,结果反而更慢。大堆意味着GC时间更长。我见过一个服务把堆从16G降到4G,配合合理的新生代比例,延迟直接降了一半。关键在于平衡:业务能承受的停顿时间是多少?数据对象生命周期多长?
比如这个配置:
-Xms4g -Xmx4g -Xmn1.5g -XX:SurvivorRatio=8 -XX:+UseG1GC
设成固定大小避免动态扩缩带来的波动,G1在大堆下表现更稳,适当调大新生代,减少对象过早进入老年代。
对象创建也是成本
代码里常见这种写法:
for (int i = 0; i < list.size(); i++) {
String msg = "Processing item: " + i;
logger.debug(msg);
}
看着没啥问题,但每次循环都生成新字符串,debug关了也白搭——+操作先创建StringBuilder再拼接。改成懒加载判断:
if (logger.isDebugEnabled()) {
logger.debug("Processing item: {}", i);
}
既省对象,又少拼接。
线程池别瞎配
看到太多人newFixedThreadPool(100),以为并发上去了。结果数据库连接池才20,大量线程堵着,CPU空转。合理做法是根据下游能力反推,比如接口平均耗时50ms,要扛1000QPS,那理论线程数 ≈ 1000 × 0.05 = 50。再留点余量,设60-70足矣。
监控比调参更重要
上线后别撒手不管。JVM层面用Prometheus+Grafana看GC、堆使用、线程状态;代码里关键路径打点,比如一个订单处理从进来到出去花了多久。有次我们发现某个缓存加载总超时,追下去是序列化用了默认的Java原生,换成FastJSON后耗时从80ms降到8ms。
调优不是一锤子买卖,更像是持续观察、微调的过程。就像煮咖啡,豆子、水温、研磨度都得匹配,才能出那一口顺滑。”}