使用JMH测试含CompletableFuture并行逻辑的Java方法时遇线程OOM问题
解决JMH测试含并行逻辑代码时的线程创建OOM问题
你在使用JMH测试build方法性能时,该方法内部通过CompletableFuture创建多线程并行加载内存对象Dictionary,但当前的基准方法每次迭代或预热都会重复执行,导致抛出java.lang.OutOfMemoryError: unable to create new native Thread错误。以下是针对这类场景的最佳测试实践:
1. 复用线程池,避免线程爆炸
LocalFSDictionaryCreator的build方法每次执行时,不要每次都创建新线程或依赖无界线程池(比如默认的ForkJoinPool.commonPool在高并发调用下也会过载)。应该在类中预先初始化固定大小的线程池,所有并行任务复用该池中的线程:
public class LocalFSDictionaryCreator implements DictionaryCreator { // 根据CPU核心数设置合理的线程池大小 private static final ExecutorService FIXED_POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); @Override public Dictionary build() { // 并行任务指定使用复用的线程池 CompletableFuture<Void> loadTask1 = CompletableFuture.runAsync(this::loadMetadataPart1, FIXED_POOL); CompletableFuture<Void> loadTask2 = CompletableFuture.runAsync(this::loadMetadataPart2, FIXED_POOL); // 等待所有任务完成 CompletableFuture.allOf(loadTask1, loadTask2).join(); return assembleDictionary(); } // 其他加载和组装逻辑... }
2. 复用DictionaryCreator实例,避免重复初始化资源
当前基准方法每次调用都会新建LocalFSDictionaryCreator实例,这会重复初始化线程相关资源甚至文件句柄。使用JMH的@State注解管理实例生命周期,确保整个测试周期内只初始化一次:
@State(Scope.Benchmark) public class DictionaryBenchmark { private DictionaryCreator creator; // 每个测试批次(Trial)前执行一次初始化 @Setup(Level.Trial) public void initCreator() { creator = new LocalFSDictionaryCreator("metadata_avro_files"); } @Benchmark public Dictionary buildDictionary() { return creator.build(); } }
@State(Scope.Benchmark)保证实例在整个基准测试中唯一,@Setup(Level.Trial)确保初始化逻辑只在测试批次开始时执行,避免重复创建对象带来的资源浪费。
3. 合理配置JMH的测试参数
- 控制测试线程数:如果使用
@Threads注解设置JMH测试线程,要确保测试线程数 + 内部并行线程数不超过系统可承受的线程总数,比如先设置@Threads(1)做单线程基准测试,验证稳定性后再调整。 - 调整预热和迭代次数:减少不必要的预热迭代,比如设置
@Warmup(iterations = 3, time = 1)(3次预热,每次1秒),避免过多预热导致线程资源耗尽。示例配置:
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3, time = 1) @Measurement(iterations = 5, time = 1) @Threads(1) @Fork(1) @State(Scope.Benchmark) public class DictionaryBenchmark { // ... 测试代码 }
4. 确保资源正确清理
如果build方法中打开了文件流、临时连接等资源,要在方法执行完毕后及时关闭,避免资源泄漏间接引发内存或线程问题。也可以通过JMH的@TearDown注解在测试结束后统一清理资源:
@TearDown(Level.Trial) public void cleanup() { // 关闭线程池或其他资源 ((LocalFSDictionaryCreator) creator).shutdownPool(); }
内容的提问来源于stack exchange,提问作者Ravi Kapoor
相关产品推荐
相关产品推荐

