你的代码可能并没有你想象中那么快。哪怕你用着最新的JDK 21写着漂亮的Stream API做着优雅的Lambda只要踩中一个常见的性能陷阱几十毫秒的延迟、几M的内存泄漏就会像温水煮青蛙一样一点一点吞噬你的系统吞吐量。别急着甩锅给JVM或者GC先看看下面这十个你很可能每天都在犯的错误以及解决它们最简单直接的方法。无脑使用String 拼接让亲儿子 StringBuilder 当场失业无数教科书和博客都警告过String是不可变的你用“”拼接字符串编译器会悄悄把它翻译成new StringBuilder().append(...).toString()。在循环体内这么做每次迭代都会新建一个StringBuilder循环1000次就多产生999个临时对象GC压力直接飙升。更隐蔽的是你写String s a b c;编译器会直接优化为 abc但一旦换成变量比如s prefix : suffix就变成了三段式拼接。解决办法只要在循环内拼接老老实实自己new一个StringBuilder然后append。如果循环超过几十次这种手动预分配缓冲区长度的收益非常可观。哪怕你用了JDK 15也别指望编译器能包办一切显式使用 StringBuilder 仍然是最可控的性能选择。ArrayList 无初始容量扩容每次搬家都翻倍搬家费ArrayList默认初始容量是10当你持续添加元素超过10个时系统会创建一个新数组容量扩展为原来的1.5倍JDK 8然后把旧数组所有元素搬过去。频繁扩容导致大量的数组复制和垃圾产生。如果你事先知道数据量大概在1万左右却从10开始慢慢扩容中间会触发大约 log(1.5, 10000/10) ≈ 14 次扩容复制。每次复制都涉及CPU密集的System.arraycopy。解决办法预估初始容量直接 new ArrayList(10000)。别偷懒宁愿多估一点也不要少估。同理HashMap也有类似问题甚至更严重默认负载因子0.75扩容翻倍还会导致rehash。创建集合时指定初始大小是为数不多不改变逻辑却能立竿见影的优化手段。无脑使用stream.parallel()反被并行拖垮并行流看起来很美但很多场景下它比串行还慢。原因之一是拆箱装箱带来的大量对象创建比如IntStream.range(0, 100000).parallel().sum()这种因为每个元素都需要从原始int转换为Integer如果中间做了其他操作导致装箱再加上ForkJoinPool的线程调度开销最终结果可能比串行慢几倍。另一个常见坑是共享可变状态在并行流里对同一个数组写操作或者用并行流处理ArrayList这样的非线程安全容器会导致大量锁竞争甚至数据错误。解决办法并行流只用于计算密集型、无状态、数据量巨大至少几十万以上且不涉及频繁拆箱的操作。实际工作中多线程用线程池显式控制比偷懒用parallel流可靠得多。记住并行不是银弹它只是让你更容易写出bug且性能更差的代码。捕获全家桶异常catch (Exception e)的隐性成本捕获异常会带来栈回溯填充、对象创建、性能损耗。如果捕获范围过大比如把所有异常都catch住那么任何不符合预期的小问题都会触发这个开销。更可怕的是一旦在循环内部catch异常每次迭代都可能产生完整异常栈。曾经有项目把NumberFormatException当作逻辑判断的一部分循环处理几百万行数据时应用直接卡死。解决办法只捕获你明确要处理的异常而且永远不要在循环内捕获无关异常。如果必须处理先预判输入合法性比如用正则或类型检查把异常留给真正意外的情况。异常处理的成本比你想象的高至少两个数量级别拿它当if-else用。日志打印正酣磁盘IO成为隐形杀手很多人习惯在业务代码里写log.debug(订单处理完成参数 param)哪怕日志级别设为INFO这条字符串拼接依然会被执行因为参数已经在调用log方法前计算完毕。如果调用频繁这会产生大量StringBuilder对象。更致命的是日志框架的同步写磁盘操作比如Logback的RollingFileAppender默认是同步的会直接拖慢业务线程。解决办法使用占位符式日志log.debug(订单处理完成参数{}, param)这样参数只在相应级别启用时才被格式化。同时生产环境日志级别设为WARN或ERROR不要因为调试日志没关就把磁盘写残。对于高并发应用异步日志Appender比如Logback的AsyncAppender必须用上否则日志就是性能杀手。反射、动态代理滥用让JVM开销翻倍Spring和MyBatis大量使用CGLIB或JDK动态代理这是框架层面的权衡。但如果你在业务代码里频繁调用Method.invoke()或者用反射获取/设置字段那就惨了。反射调用比直接调用慢几十倍因为它需要安全检查、方法查找和参数包装。即使是动态代理如果每次请求都生成新的代理类比如用Proxy.newProxyInstance每次都创建也会因为类加载和验证消耗资源。解决办法能用接口调用就用接口能使用Lambda或方法引用就别用反射。如果一定要用反射比如框架自省缓存Method和Field对象避免重复查找。另外Spring的AOP尽量切在接口上用JDK代理而非CGLIB能避免很多不必要的开销。使用System.currentTimeMillis()记录耗时高并发下变慢System.currentTimeMillis()内部调用gettimeofday或类似系统调用在Linux上这是一个快速操作大约几十纳秒。但它会引发一次用户态到内核态的切换。如果在高并发场景下频繁调用比如每个请求都要记录开始和结束时间乘以N次那么这些系统调用的累计开销会非常可观。曾经有团队在每秒几万QPS的接口里用这种方式做日志记录结果系统时间花费占用CPU达到5%。解决办法对于高并发下的时间戳使用System.nanoTime()测量时间差它更精确且开销略低对于绝对时间考虑用Netty的Epoch缓存或者定期从NTP拉取一次。如果只是需要日志里带时间留给日志框架自己去填充比你自己调用好得多。大对象直接进入老年代GC频发当你创建了一个几十MB的大数组或大对象并且它很快就用完了比如作为临时缓冲区但JVM的“直接进入老年代”机制通过-XX:PretenureSizeThreshold控制可能会导致这个临时大对象直接分配到老年代。然后GC时发现老年代多了一个几十MB的垃圾触发Full GC。或者因为对象太大在TLAB线程本地分配缓冲区放不下直接在堆上分配引发锁竞争。解决办法尽量避免创建临时大数组尤其是网络IO中用buffer pool如Netty的ByteBuf池复用缓冲区。如果无法避免考虑调大年轻代或者调整-XX:PretenureSizeThreshold让自己可控。对大对象说“不”是Java程序员对GC最起码的尊重。线程池参数拍脑袋导致任务排队死锁创建线程池时很多人图省事直接newFixedThreadPool(10)然后提交任务。但如果任务之间形成了依赖比如TaskA需要等待TaskB完成而TaskB却在队列里排队线程池已满就会形成线程饥饿死锁。更常见的是你用线程池执行异步IO操作结果IO等待占用了线程后续任务全部排队CPU却空转。这种场景下线程池的队列长度设置不当拒绝策略用错都会导致性能雪崩。解决办法明确线程池使用场景IO密集型任务用大线程池如2CPU核心数1计算密集型用小线程池CPU核心数1异步任务考虑用额外的线程池隔离。使用有界队列并设置合理的拒绝策略比如调用者运行策略CallerRunsPolicy能缓解压力而不是直接丢弃。最关键的是不要在一个线程池里同时混用CPU密集阻塞和IO密集等待的任务。自动拆装箱引发的“微型垃圾海”ListInteger里加1000万个int每个元素都要从原始int装箱成Integer对象这会产生1000万个对象。遍历时再拆箱成int又产生大量临时对象。在短时间循环里这些对象的创建和回收会给GC带来巨大压力。很多人以为“Java自动处理了”其实它只是帮你写了Integer.valueOf(i)和i.intValue()而valueOf虽然有缓存-128到127但超出范围还是会新建对象。解决办法对于大量数值处理使用IntArrayList第三方库如Eclipse Collections、TIntArrayList或直接使用原始类型数组。JDK的IntStream和LongStream也避免了大部分装箱但要注意使用原始流不要和对象流混用。记住自动拆装箱是糖吃多了会蛀牙。你遇到的问题可能远不止这十个。性能优化从来不是一蹴而就的从代码规范的层面提前规避陷阱比事后用Profiler抓热点更高效。下一次当你写完一段代码不妨反问自己这里面有没有隐性的对象分配有没有不必要的同步有没有滥用高级特性如果有改掉它你的生产环境会感谢你。