01前言这天review项目的老代码发现这么一段代码ListObject paramList request.getParamList(); paramList.forEach(x - {... ...}); // 一些简单操作 paramList.forEach(x - {... ...}); // 又一些简单操作 paramList.forEach(x - {... ...}); // 双一些简单操作哎这就一个集合遍历为啥要遍历三遍嘞如果集合数据量比较多合并为一个遍历接口性能会不会就能提升一些paramList.forEach(x - { ... ...// 一些简单操作 ... ...// 又一些简单操作 ... ...// 双一些简单操作 });送上门的优化不能不要啊于是就在本地试一下。结果发现一次循环遍历处理果然比三次循环处理提升了不少了毫秒级别。于是兴冲冲修改部署测试环境却发现性能没啥提升。为什么嘞本地测试明明提升不小有意思了那就去探究一下。02本地测试测试环境jdk 版本1.8.0_9164 位测试硬件Intel i7-1185G74 核 8 线程、16GB DDR4、256GB SSD测试数据ArrayList 存储 1000 万条 Integer 数据元素值 0~9999999测试方法空循环遍历一千万数据每次只执行单次代码多次执行取平均值。测试指标遍历全部元素的总耗时单位毫秒测试循环类型// 1. 普通for循环 for (int i 0; i list.size(); i) {} // 2.增强for循环 for (Integer num : list) {} // 3. list.forEach list.forEach(num - {}); // 4. stream.forEach list.stream().forEach(num - {});测试代码public static void main(String[] args) { // 初始化数据 ListInteger list new ArrayList(); for (int i 0; i 1000_0000; i) { list.add(i); } long s System.currentTimeMillis(); ... ... //执行测试空循环遍历 System.out.println(cost: (System.currentTimeMillis() - s)); }测试结果循环方式执行耗时ms普通for循环4增强for循环17list.forEach48stream.forEach51根据结果得到一个问题同样是顺序循环遍历为什么差距这么大呢forEach耗时甚至是普通for循环的10倍以上接下来我们分析一下。03底层分析1. 普通 for 循环普通 for 循环是最底层的手动迭代模式arr[i]直接通过「基地址 偏移量」计算元素内存地址。源码public E get(int index) { Objects.checkIndex(index, size); // 边界检查 return elementData(index); // 直接访问底层数组 } // 底层数组访问无额外逻辑 E elementData(int index) { return (E) elementData[index]; }字节码0: iconst_0 // 推送 int 常量 0 到操作数栈 1: istore_1 // 把栈顶的 0 存入局部变量表索引 1 → 初始化循环变量 i0 2: iload_1 // 加载 i 的值0到操作数栈 3: aload_0 // 加载局部变量表索引0的值 4: invokeinterface #15, 1 // 调用list.size()方法常量池#15对应接口方法 1 表示方法参数个数 9: if_icmpge 18 // 比较栈中两个 int 值若 i ≥ list.size()循环结束否则继续执行 12: iinc 1, 1 // i 自增 15: goto 2 // 继续循环 18: return // 返回普通for循环代码非常简洁 无方法调用开销无额外对象创建、无多余方法调用仅通过基础语法层面的索引操作完成遍历。2. 增强for循环增强 for 循环是迭代器Iterator的语法糖jdk5 引入核心依赖Iterable接口所有可遍历集合均实现该接口。源码// ArrayList的迭代器实现 private class Itr implements IteratorE { int cursor; // 下一个要返回的元素索引 int lastRet -1; // 上一个返回的元素索引用于remove int expectedModCount modCount; // 期望的修改次数防止并发修改 public boolean hasNext() { return cursor ! size; // 索引未到集合大小则有下一个元素 } public E next() { checkForComodification(); // 校验并发修改 int i cursor; if (i size) throw new NoSuchElementException(); Object[] elementData ArrayList.this.elementData; if (i elementData.length) throw new ConcurrentModificationException(); cursor i 1; // 移动指针 return (E) elementData[lastRet i]; // 直接访问数组返回元素 } // 并发修改校验 final void checkForComodification() { if (modCount ! expectedModCount) throw new ConcurrentModificationException(); } }字节码0: aload_0 // 加载局部变量表索引0的值当前对象: List 实现类实例 1: invokeinterface #11, 1 // 调用 List 接口的 iterator() 方法常量池#11对应方法返回 Iterator 接口实例 6: astore_1 // 把栈顶的 Iterator 实例存入局部变量表 7: aload_1 // 加载 Iterator 实例到栈 8: invokeinterface #12, 1 // 调用 Iterator 的 hasNext() 方法常量池#12对应方法 返回是否还有下一个元素 13: ifeq 29 // 判断栈顶hasNext()的返回值若为 0循环结束返回否则继续执行 16: aload_1 // 加载局部变量Iterator 实例到栈 17: invokeinterface #13, 1 // 调用 Iterator 的 next() 方法常量池#13对应方法 获取下一个元素返回 Object 类型 22: checkcast #14 // Object类型强转Integer 常量池#14对应 Integer.class 25: astore_2 // 把强转后的 Integer 元素存入局部变量表索引2 → 保存当前遍历元素 26: goto 7 // 继续循环 29: return // 返回编译器会自动将增强 for 循环转换为「获取迭代器→hasNext()判断→next()获取元素」的逻辑无需手动管理索引。3. list.forEachList.forEach是 jdk8 引入的函数式遍历定义在Iterable接口中List继承自Iterable核心依赖Consumer函数式接口。源码public void forEach(Consumer? super E action) { Objects.requireNonNull(action); final int expectedModCount modCount; final E[] elementData (E[]) this.elementData; final int size this.size; for (int i0; modCount expectedModCount i size; i) { // 调用Consumer处理当前元素 action.accept(elementData[i]); } if (modCount ! expectedModCount) { throw new ConcurrentModificationException(); } }字节码0: aload_0 // 加载局部变量表索引0的值ListInteger 实例 1: invokedynamic #16, 0 // 动态绑定 Lambda 表达式核心指令jdk 7 用于 Lambda/函数式接口绑定 // - #16常量池索引对应「Lambda 引导方法Bootstrap Method」和相关元数据如目标接口、方法签名 // - 0参数个数 → 表示 Lambda 未捕获外部变量 // - 目标生成一个 ConsumerInteger 接口实例因 forEach 参数是 Consumer绑定 Lambda 的核心逻辑即下面的 lambda$test3$0 方法 // - 执行后栈顶压入生成的 Consumer 实例 6: invokeinterface #17, 2 // 调用 List 接口的 forEach 方法常量池#17对应方法 // - 方法签名List.forEach(Consumer? super E) → void接收 Consumer 接口遍历集合元素并执行 accept 方法 // - 参数2个接口方法调用的参数个数 调用者thisList实例 显式参数Consumer实例 // - 执行逻辑List 遍历每个元素调用 Consumer 的 accept 方法即 Lambda 逻辑 11: return // 返回 // 私有静态方法 lambda$test3$0 private static void lambda$test3$0(java.lang.Integer); Code: 0: return // 空逻辑方法直接返回如上lambda 表达式会编译为匿名内部类实例每次循环调用action.accept(elementData[i])。4. Stream.forEachStream.forEach是 jdk8 引入的流式遍历终端操作基于「Stream 管道模型」和「惰性求值」机制。核心依赖三个组件• Stream流式操作的载体封装数据来源集合 / 数组和中间操作链• Spliterator分割迭代器负责数据的分割与遍历支持并行• Sink数据转发器连接中间操作如filter与终端操作forEach传递元素并执行逻辑。其底层逻辑是「管道化处理」中间操作如filter会被封装为Sink链终端操作forEach触发时Spliterator遍历数据并通过Sink链传递最终由Consumer处理元素。源码// Stream.forEach的底层实现ReferencePipeline.forEach public void forEach(Consumer? super P_OUT action) { Objects.requireNonNull(action); // 封装终端操作的Sink最终处理元素 SinkP_OUT sink new Sink.ChainedReferenceP_OUT, P_OUT(Sink.cancellationRequested()) { Override public void accept(P_OUT u) { action.accept(u); // 调用用户传入的Consumer } }; // 触发遍历Spliterator遍历数据并通过Sink链传递 evaluate(sink); } // 执行遍历 final S extends SinkP_OUT S evaluate(S sink) { return isParallel() ? parallelEvaluate(sink) : sequentialEvaluate(sink); } // 串行遍历 private S extends SinkP_OUT S sequentialEvaluate(S sink) { // 构建Sink链将中间操作的Sink与终端Sink链接 SinkP_OUT chainedSink opWrapSink(StreamOpFlag.NOT_CANCELABLE, sink); // Spliterator遍历数据通过Sink链传递 spliterator().forEachRemaining(chainedSink); // 结束 chainedSink.end(); return sink; }字节码0: aload_0 // 加载局部变量表索引0的值ListInteger 实例 1: invokeinterface #18, 1 // 调用 List 接口的 stream() 方法常量池#18对应方法 // - 方法签名List.stream() → StreamE将 List 转为顺序流 StreamInteger // - 参数个数1接口方法调用的参数包含调用者 this无额外显式参数 // - 执行后栈顶压入返回的 StreamInteger 实例 6: invokedynamic #19, 0 // 动态绑定 Lambda 表达式jdk 7 新增用于函数式接口实例化 // - #19常量池索引对应「Lambda 引导方法Bootstrap Method」 元数据目标接口 Consumer、方法签名 // - 0参数个数 → 表示 Lambda 未捕获任何外部变量无状态 Lambda // - 目标生成 ConsumerInteger 接口实例绑定 Lambda 核心逻辑即下面的 lambda$test4$1 方法 // - 执行后栈顶压入生成的 Consumer 实例 11: invokeinterface #20, 2 // 调用 Stream 接口的 forEach 方法常量池#20对应方法 // - 方法签名Stream.forEach(Consumer? super T) → void遍历流中元素执行 Consumer 的 accept 方法 // - 参数个数2接口方法调用的参数 调用者stream 实例 显式参数Consumer 实例 // - 执行逻辑Stream 遍历每个元素调用 Consumer.accept(Integer)即 Lambda 逻辑 16: return // 返回 // 私有静态方法 lambda$test4$1 private static void lambda$test4$1(java.lang.Integer); Code: 0: return // 空逻辑方法直接返回可以看到stream.forEach相比list.forEach还多了维护流水线相关操作。四种循环方式对比循环类型底层依赖组件遍历方式核心开销来源并发修改校验方式普通 for 循环无仅语言语法 ArrayList 数组 拆箱方法索引直接访问无额外开销仅边界检查无需手动控制增强 for 循环Iterable Iterator迭代器 hasNext ()/next ()接口调用hasNext/nextIterator 的 expectedModCount 校验List.forEachConsumer 集合自身遍历数组直接遍历 / 迭代器遍历Consumer.accept () 方法调用集合自身 modCount 校验Stream.forEachStream Spliterator Sink管道化 Sink 链传递Stream 创建 Sink 链转发 Spliterator 分割Spliterator 的 modCount 校验04结果总结插入知识点JIT 是Just-In-Time Compiler的缩写「即时编译器」是 Java 虚拟机JVM中核心的性能优化组件 —— 它的核心目标是解决 “解释执行 Java 字节码速度慢” 的问题通过将 “频繁执行的热点代码” 编译为高效的机器码让 Java 程序运行速度接近原生语言如 C/C。JIT 是 “惰性优化”—— 不启动就优化所有代码只优化 “频繁执行的热点代码”平衡 “启动速度” 和 “运行速度”。通过底层代码和字节码的分析可以总结测试现象的原因1. 普通 for 循环最快核心优势无额外开销 JIT 深度优化• 关键步骤无冗余1. 循环条件判断i list.size()→size是 ArrayList 的成员变量直接读取耗时可忽略2. 数组访问list.get(i)→ 直接通过下标elementData[i]获取元素O (1)无中间操作3. 拆箱Integer → int所有遍历都需执行无差异4. 循环变量自增i→ 局部变量操作耗时可忽略。• JIT 优化放大优势边界检查消除JVM 可证明i size不会越界删除get(i)中的数组边界检查循环展开将多次循环合并为一次减少循环判断 / 自增的次数缓存友好连续内存访问CPU 缓存命中率接近 100%。耗时核心仅数组访问 拆箱无任何额外开销。2. 增强 for 循环次快核心开销迭代器的「fail-fast 检查」 方法调用• 关键步骤比普通 for 多 2 个核心开销1. 迭代器创建new Itr()→ 初始化cursor0、expectedModCountmodCount耗时低但需创建对象2. 循环条件it.hasNext()→ 仅判断cursor ! size简单耗时低3. 元素获取it.next()→ 核心开销点•checkForComodification()判断modCount expectedModCount防止遍历中修改集合一千万次 int 比较的累积开销•cursor 数组访问和普通 for 一致4. 拆箱和普通 for 一致。• 优化局限性• 迭代器的hasNext()/next()是方法调用即使 JIT 内联仍比普通 for 的直接操作多一层开销•modCount检查无法消除fail-fast 机制的必须步骤。耗时核心一千万次modCount检查 迭代器方法调用的累积开销。3. list.forEach较慢核心开销Consumer 接口方法调用 循环内modCount检查• 关键步骤比增强 for 多 1 个核心开销1.Consumer对象创建若用 lambda 表达式如e - {}会编译为匿名内部类实例jdk8 用invokedynamic优化但仍有对象创建开销2. 并发修改检查同增强 for 循环modCount expectedModCount3. 接口方法调用JVM 每次执行accept(x)时都需要通过「动态分派」找到 Lambda 动态生成的accept()实现虽然类已初始化但方法调用仍需查表jdk 8 的 JIT 对「动态生成的 Lambda 方法」内联支持极差 因为 Lambda 的accept()是动态生成的JIT 无法在编译期识别其逻辑只能按普通虚方法处理无法消除调用开销。4. 自动拆箱和其他方式一致。耗时核心一千万次accept()方法调用 循环内modCount检查。4. stream.foreach最慢核心开销Stream 框架额外开销 Spliterator 管理• 关键步骤比 list.forEach 多框架级开销1. Stream 创建list.stream()→ 生成 SpliteratorArrayList 的索引型 Spliterator、创建 Stream 管道ReferencePipeline→ 固定框架开销2. Spliterator 遍历spliterator.tryAdvance(action)→ 需管理 Spliterator 的索引范围、状态比直接下标遍历多一层包装3. 元素处理action.accept()→ 和 list.forEach () 一致的方法调用开销4.modCount检查Spliterator 同样会维护expectedModCount防止并发修改5. 拆箱和其他方式一致。• 优化局限性• Stream 框架代码复杂JIT 难以像普通 for 那样做深度优化如循环展开、边界检查消除• 即使是串行 Stream仍会加载并行相关的逻辑无实际执行但有初始化开销。耗时核心Stream 框架初始化 Spliterator 管理开销。性能差距的核心根源总结四种遍历的耗时排序普通 for 增强 for list.forEach stream.foreach本质是 「额外开销的叠加」「JIT 优化的递减」性能影响因素普通 for增强 forlist.forEachstream.foreach中间对象迭代器 / Stream 等无迭代器ConsumerStreamSpliteratormodCount 检查无有next () 中有循环条件中有Spliterator 中额外方法调用无hasNext()/next()accept()tryAdvance()accept()JIT 优化程度最好较好中等较差jdk 8 中forEach和stream.forEach的慢本质是「函数式编程的抽象层开销」在大数据量下的持续叠加 ——Lambda 的虚方法调用、Stream 的 Spliterator 遍历这些开销在 jdk 8 中无法被 JIT 优化消除而普通 for 循环因「无抽象层 JIT 深度优化」成为大数据量遍历的最优选择。05再次测试还是jdk8下每种循环都先执行一遍空循环遍历再次执行记录第二次耗时。结果 普通for循环和增强for循环基本没什么变化。 但是forEach和stream.forEach的耗时却增加很多和增强for循环基本相同了。为什么呢 这是JIT的功劳。JIT 对 list.forEach 的核心优化第二次提速关键根据上边分析可知list.forEach第一次慢的核心是「接口多态调用」和「Consumer 对象创建开销」第二次 JIT 会针对性解决这两个问题1. 去虚拟化消除 Consumer.accept() 的多态开销第一次执行时Consumer是 lambda 表达式对应的匿名内部类实例如Lambda$1xxxaction.accept()是接口多态调用JIT 不确定Consumer的具体实现无法内联只能通过接口查找实现类开销高。第二次执行前JIT 会通过「类型 profile」记录方法调用的实际类型发现当前Consumer只有唯一实现就是那个空逻辑的 lambda。此时 JIT 会做「单态去虚拟化」—— 直接将多态调用替换为「具体实现类的方法调用」甚至进一步内联。优化前后对比伪代码• 第一次解释执行action.accept(element)→ 接口查找实现 → 调用Lambda$1.accept()多态开销• 第二次编译执行JIT 直接内联Lambda$1.accept()的逻辑空逻辑此时list.forEach的核心开销只剩「循环控制」和「modCount 检查」与增强 for 循环Itr.hasNext()Itr.next()的开销接近。2. 逃逸分析Escape Analysis消除 Consumer 对象创建开销第一次执行时lambda 会被实例化为Consumer对象堆上分配有对象创建和垃圾回收的潜在开销。第二次 JIT 会通过「逃逸分析」发现这个Consumer对象只在list.forEach方法内使用没有 “逃逸” 到方法外部比如没有被存储到全局变量、没有被其他线程访问。此时 JIT 会做「栈上分配」或「标量替换」—— 直接消除Consumer对象的堆分配把对象的字段拆成局部变量甚至直接消除对象进一步降低开销。JIT 对 stream.forEach 的核心优化第二次提速关键stream.forEach第一次慢的原因是「Stream 初始化 Spliterator 多层调用」第二次 JIT 会优化这两层额外开销1. 优化 Stream 初始化开销第一次执行list.stream()时会创建ReferencePipeline.HeadStream 管道头、ArrayListSpliterator拆分迭代器等对象初始化逻辑有一定开销。第二次 JIT 会• 通过「逃逸分析」消除 Stream 相关对象的堆分配这些对象仅在遍历期间使用无逃逸• 编译优化 Stream 初始化的冗余逻辑比如合并常量、消除不必要的参数检查使得list.stream()的初始化开销几乎可以忽略。2. 内联 Spliterator.tryAdvance () 方法stream.forEach依赖ArrayListSpliterator.tryAdvance(action)迭代第一次执行时这是一层额外的方法调用tryAdvance()→accept()。第二次因为tryAdvance()是热点方法且ArrayListSpliterator是具体类非接口JIT 会直接将其「内联」到 Stream 的forEach终端操作中此时两层方法调用tryAdvance()accept()被合并为一层甚至直接内联为循环内的逻辑开销大幅降低。3. 同 list.forEach 的去虚拟化优化Consumer.accept()的多态调用同样被 JIT 去虚拟化并内联消除接口调用开销。优化后其开销与list.forEach、增强 for 循环基本一致。06再再次测试这次使用jdk17版本其他条件不变还是只测试一遍空循环遍历 得到结果:循环方式单次执行耗时ms普通for循环4增强for循环19list.forEach20stream.forEach22可以发现jdk17下forEach和stream.forEach的性能已经和增强for循环基本相同了那jdk偷偷做了哪些升级呢性能提升原因分析JDK 17 作为长期支持LTS版本针对 Stream API 和 Lambda 表达式的执行效率实现了全方位优化核心原因可归结为1. C2 编译器对函数式代码的深度优化JDK 17 默认沿用 HotSpot 的 C2 编译器服务端模式并继承了 JDK 9-16 期间的优化成果其对函数式编程相关优化逻辑进行了大幅增强•lambda 内联优化C2 能将 Consumer.accept() 等函数式接口方法与 Stream 内部迭代逻辑直接内联消除虚函数调用的性能开销•Stream 流水线扁平化智能识别无中间操作的 Stream如 DATA.stream().forEach(...)直接优化为普通迭代逻辑跳过 Stream 流水线封装带来的额外开销•逃逸分析增强将简单 Consumer 等函数式接口实例判定为“不逃逸”通过标量替换技术避免堆分配效果等同于分配在栈上减少垃圾回收GC压力。•GraalVM 可选增强若部署环境采用 GraalVM可进一步利用其先进的全局优化算法在特定场景下获得比 C2 更优的函数式代码执行性能。2. Stream API 的底层实现优化JDK 17 继承了自 JDK 9 以来对 Stream 内部实现的持续微调• 流水线简化针对无中间操作或简单链式调用如 stream().forEach()直接复用集合的迭代器避免额外封装• Spliterator 优化改进了迭代器的分割与遍历逻辑提升遍历效率• 类结构精简内部 ReferencePipeline 等类简化减少了对象创建的隐性成本。3. 装箱与拆箱的性能提升JDK 17 对包装类如 Integer的自动装箱/拆箱处理更高效结合 JIT 的类型推断减少了部分场景下的方法调用开销。若使用基本类型流IntStream优化效果更显著可直接访问底层基本类型数据避免 intValue() 拆箱开销。4. 并行 Stream 的调度优化jdk17 优化了 Fork/Join 框架并行 Stream 的底层依赖的线程调度• 减少线程创建和切换开销• 优化任务拆分策略避免小任务的过度拆分• 利用 CPU 缓存局部性提升并行执行效率。5. JVM 层面的整体优化JDK 17 在逃逸分析、方法内联、循环展开等方面均有增强这些优化对所有循环类型都有增益但对 Stream 这类复杂逻辑的提升更为明显。到这也就知道了文章开头的优化没什么效果的原因 测试环境使用的是jdk17且参数数据量比较小这部分代码耗时占接口调用占比极小。草率了07场景选择结合四种循环的特性、jdk 版本差异与性能表现开发中可遵循 “性能优先、兼顾可读性” 的原则按以下场景选型优先 传统 for 循环 的场景1. 遍历数组或支持随机访问的集合ArrayList且需要索引如修改元素、跳过 / 反向遍历2. 对性能要求极致如高频遍历、海量数据处理且 jdk 版本低于 113. 需手动控制循环进度如多线程场景下的分段遍历。优先 增强 for 循环 的场景1. 遍历集合List、Set或数组无需索引追求代码简洁2. 需兼容低版本 jdk且可读性优先级高于极致性能3. 遍历非随机访问集合如 LinkedList传统 for 的get(index)会导致 O (n²) 性能问题增强 for迭代器更高效。优先 List.forEach 的场景1. jdk8 环境遍历 List 且无需索引追求函数式编程风格2. 遍历逻辑简单如打印、简单过滤方法引用out::println可进一步简化代码3. 需兼顾可读性与性能jdk11 环境下性能接近增强 for。优先 Stream.forEach 的场景1. 需对数据进行链式操作过滤、映射、排序、聚合如stream().filter().map().forEach()2. 海量数据处理可通过parallelStream()利用多核 CPU注意线程安全与锁开销3. jdk17 环境性能已接近传统循环且代码可读性、扩展性更优4. 函数式编程场景如结合 Optional、Stream API 构建流式数据处理管道。08总结一下Java 循环的演进本质是 “性能与可读性的平衡”从传统 for 的极致性能到增强 for 的简洁再到 List.forEach 与 Stream.forEach 的函数式灵活每一种循环都对应特定的开发场景。随着 jdk 版本的升级尤其是 jdk17 的性能飞跃函数式遍历的性能短板已大幅弥补开发中可更自由地在 “性能” 与 “代码优雅” 之间做出选择。最终循环选型的核心不是 “哪一种更快”而是 “哪一种更适合当前场景”—— 在满足性能需求的前提下简洁、易维护的代码才是长期价值的关键。