拒绝策略里的对象晋升:探秘 Java 线程池不当配置引发的 Full GC 根源
2026/6/9 18:34:01 网站建设 项目流程

拒绝策略里的对象晋升:探秘 Java 线程池不当配置引发的 Full GC 根源

前言

兄弟们,说实话,搞技术这条路真是各种坑。咱们做开发的,说白了就是要不断踩坑、不断成长,这才是技术人的常态。
在 Java 高并发编程中,线程池的参数配置直接影响到系统的稳定性和性能。当任务队列满且线程数达到 Limits 时,不当的拒绝策略(如 CallerRunsPolicy)可能导致任务执行线程被阻塞,进而引发老年代对象晋升甚至频繁 Full GC。本文将深度剖析拒绝策略导致对象异常晋升的底层机理,并提供最佳实践与规避策略。

一、底层原理

1.1 核心机制

Java 线程池的状态流转,其实像个严格的“国企晋升体系”。

它一共有 5 种状态:运行、关闭、停止、整理、终止。

正常业务都在“运行”状态里打转。

一旦线程池被 shutdown,它就开始拒绝新任务。

这时候,拒绝策略就登场了。

常见的拒绝策略有 4 种:抛异常、调用者运行、丢弃、丢弃最老。

问题往往出在“自定义拒绝策略”上。

很多兄弟为了记录日志,或者为了重试,在拒绝策略里写了大量逻辑。

这些逻辑会创建新的对象,比如日志记录器、重试任务对象。

如果这些对象存活时间稍长,就会从 Eden 区晋升到 Survivor 区。

如果 Survivor 区也塞满了,它们就会直接“移民”老年代。

老年代空间有限,一旦填满,JVM 只能启动 Full GC 进行大扫除。

这个过程就像餐厅后厨忙不过来,服务员还在旁边搞装修。

不仅没帮上忙,还占用了过道,导致传菜员(GC 线程)寸步难行。

graph TD A["线程池状态(运行中)"] -->|任务队列满 | B["触发拒绝策略"] B -->|执行拒绝逻辑 | C["创建临时对象"] C -->|对象存活 | D["Eden 区"] D -->|Minor GC | E["Survivor 区"] E -->|长期存活 | F["老年代"] F -->|空间不足 | G["Full GC 触发"] G -->|内存回收失败 | H["系统卡顿/崩溃"] style A fill:#f9f,stroke:#333,stroke-width:2px style H fill:#ff6b6b,stroke:#333,stroke-width:2px

1.2 与同类方案的对比

很多同事觉得,线程池满了直接抛异常最省事。

其实不同拒绝策略对内存的影响天差地别。

我们拿生产环境常见的几种方案做个对比。

拒绝策略类型内存消耗系统影响适用场景
AbortPolicy抛出异常,中断流程必须保证任务执行,不允许丢失
CallerRunsPolicy调用者线程执行,降低提交速度需要削峰填谷,允许延迟
自定义日志策略创建日志对象,加剧 GC不推荐,除非日志极轻量
DiscardPolicy静默丢弃,无反馈允许数据丢失,如实时视频流

你看,自定义策略如果不小心,就是内存杀手。

二、快速上手

为了复现这个问题,我写了一个最小可运行的 Demo。

这个 Demo 模拟了线程池满后,拒绝策略疯狂创建大对象的情况。

你只需要 3 分钟,就能亲眼看到内存是怎么被吃掉的。

import java.util.concurrent.*; import java.util.ArrayList; import java.util.List; public class ThreadPoolGCdemo { public static void main(String[] args) throws InterruptedException { // 创建一个极小的线程池,核心线程 2 个,最大 2 个,队列容量 5 // 这样很容易触发拒绝策略 ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5) ); // 设置自定义拒绝策略,这里模拟创建大对象 executor.setRejectedExecutionHandler(new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // ⚠️ 警告:这里创建大对象是 Full GC 的元凶 // 实际业务中,可能是打印日志、记录数据库、发起重试 byte[] largeData = new byte[1024 * 1024]; // 1MB 数组 // 模拟处理逻辑,让对象存活时间变长 System.gc(); System.out.println("任务被拒绝,已创建 1MB 临时对象"); } }); // 疯狂提交任务,填满线程池 for (int i = 0; i < 100; i++) { final int taskId = i; executor.submit(() -> { try { Thread.sleep(500); // 模拟任务耗时 System.out.println("任务 " + taskId + " 执行中"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 等待任务执行完毕 executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); System.out.println("所有任务处理完成,请观察 GC 日志"); } }

运行这段代码,配合-XX:+PrintGCDetails参数。

你会看到老年代的使用率像坐火箭一样往上窜。

三、核心 API / 深水区

3.1 核心方法速查

排查线程池问题,这几个 API 是你必须熟记的。

API 方法作用生产建议
getPoolSize()当前线程数监控是否达到最大值
getQueue().size()队列当前大小预警队列积压
getCompletedTaskCount()已完成任务数计算吞吐量
allowCoreThreadTimeOut()核心线程超时回收节省空闲资源

3.2 生产级配置

在生产环境,线程池配置绝对不能“拍脑袋”。

拒绝策略必须做到“轻量级”。

如果一定要记录日志,请用异步日志框架,别在拒绝策略里同步写文件。

超时控制也要做好,防止任务无限期阻塞线程。

// 生产级线程池配置示例 ThreadPoolExecutor safeExecutor = new ThreadPoolExecutor( 10, // 核心线程数 20, // 最大线程数 60L, // 空闲线程存活时间 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000), // 有界队列,防止 OOM new ThreadFactory() { // 自定义线程工厂,方便排查 private int count = 0; public Thread newThread(Runnable r) { return new Thread(r, "业务线程-" + (++count)); } }, new ThreadPoolExecutor.CallerRunsPolicy() // 使用 CallerRuns 削峰 ); // 拒绝策略里千万别写耗时操作 // 如果必须记录,请丢给另一个专用日志线程池

3.3 高级定制

有些场景下,默认的拒绝策略不够用。

你可以实现RejectedExecutionHandler接口,做更复杂的逻辑。

比如,把被拒绝的任务存入 Redis,等系统空闲了再取回来处理。

但记住,存入 Redis 这个动作本身也要控制时间。

别为了救火,把消防栓也给堵了。

四、实战演练

这次故障的真实场景是“订单超时取消”。

我们有一个定时任务线程池,负责扫描 30 分钟未支付的订单。

随着流量激增,线程池队列满了。

当时的拒绝策略是:记录一条数据库日志,标记任务失败。

问题在于,每条日志都关联了一个巨大的“订单快照对象”。

这个快照对象有几百个字段,序列化后好几 KB。

高并发下,每秒被拒绝的任务成千上万。

这些快照对象瞬间塞满了老年代。

Full GC 频繁触发,导致数据库连接池也被占满。

整个系统形成了“内存满 -> GC -> 卡死 -> 任务堆积 -> 内存更满”的恶性循环。

我们当时的修复方案是:

  1. 拒绝策略只记录订单 ID,不记录完整对象。
  2. 将重试逻辑改为消息队列异步处理。
  3. 增加线程池监控报警,队列超过 80% 就通知。

修复后,Full GC 频率从每分钟 1 次降到了每天 1 次。

五、避坑指南与最佳实践

5.1 💡 技巧:监控先行

不要等报警了才去看线程池。

getQueue().size()getPoolSize()接入 Prometheus。

设置阈值,比如队列使用率超过 70% 就发警告。

5.2 ⚠️ 警告:拒绝策略别干重活

拒绝策略里只能做“记录”或“丢弃”。

严禁在拒绝策略里发起 RPC 调用、查数据库、写大文件。

这就像火灾发生时,你不仅不灭火,还在现场搞装修。

5.3 ✅ 推荐:有界队列

永远不要用LinkedBlockingQueue的无界构造器。

一定要指定容量,防止内存溢出。

如果队列满了,宁可拒绝,也不要让内存爆炸。

5.4 💡 技巧:优雅停机

应用关闭时,调用shutdown()后,给线程池一点时间。

使用awaitTermination等待任务执行完。

避免强制shutdownNow()导致任务丢失或数据不一致。

六、综合实战演示

下面是一套经过生产验证的线程池封装代码。

它包含了异常处理、超时控制、优雅停机以及安全的拒绝策略。

你可以直接拿去复用,但要根据业务调整参数。

import java.util.concurrent.*; import java.util.logging.Logger; public class SafeThreadPoolManager { private static final Logger logger = Logger.getLogger(SafeThreadPoolManager.class.getName()); private final ThreadPoolExecutor executor; public SafeThreadPoolManager() { // 初始化线程池 this.executor = new ThreadPoolExecutor( 5, // 核心线程 10, // 最大线程 60L, // 超时时间 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(500), // 有界队列 new ThreadFactory() { private final AtomicLong threadCount = new AtomicLong(0); @Override public Thread newThread(Runnable r) { return new Thread(r, "SafeBiz-Thread-" + threadCount.incrementAndGet()); } }, // 自定义拒绝策略:记录轻量日志,不创建大对象 new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // ✅ 推荐:只记录任务特征,不持有任务引用 logger.warning("任务被拒绝,线程池已满,任务哈希: " + r.hashCode()); // 这里可以投递到死信队列,方便后续补偿 } } ); // 允许核心线程超时,节省资源 executor.allowCoreThreadTimeOut(true); } public void submitTask(Runnable task) { try { executor.submit(task); } catch (Exception e) { // 捕获提交异常,防止主线程崩溃 logger.severe("任务提交失败: " + e.getMessage()); } } public void shutdown() { // 优雅停机 executor.shutdown(); try { // 等待 30 秒,看任务能不能跑完 if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { // 如果没跑完,强制关闭 executor.shutdownNow(); // 再等 10 秒 if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { logger.severe("线程池未能正常关闭"); } } } catch (InterruptedException e) { // 恢复中断状态 executor.shutdownNow(); Thread.currentThread().interrupt(); } } }

这段代码的关键在于拒绝策略的克制。

它只打印了任务的哈希码,没有持有任务对象的引用。

这样垃圾回收器就能立刻回收被拒绝的任务,不会造成内存泄漏。

七、总结

线程池是 Java 并发的基石,也是内存泄漏的重灾区。

这次故障告诉我们,拒绝策略不是“垃圾桶”,不能往里扔重东西。

监控要到位,队列要有界,拒绝要轻量。

把复杂的问题想简单,把简单的细节做到极致。

这才是我们作为架构师该干的事。

下次再看到 Full GC 频繁,先查查线程池的拒绝策略吧。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询