Java中使用ScheduledExecutorService触发OOM,求正确使用方式
Hey there! Let's walk through what's causing your OutOfMemoryError and those endless Full GC logs, plus cover the right way to use ScheduledExecutorService.
1. 为什么会出现OOM和持续Full GC?
你设置的JVM堆内存太小了:-Xms5m -Xmx5m只给了5MB的堆空间。现代Java应用哪怕只是启动基础类、线程池、任务调度框架,都需要远超这个大小的内存。当堆被占满后,JVM会不断触发Full GC试图回收空间,但几乎没什么可回收的(你的任务本身很轻,但框架和系统开销已经占满了堆),所以会陷入"GC循环",最后抛出OutOfMemoryError。
看你的GC日志:5099K->5099K(5632K)意味着Full GC后几乎没释放任何内存,堆已经被完全占满了。
2. 你的代码里的小问题(不直接导致OOM,但影响逻辑)
- 拼写错误:
jobNmae应该是jobName(虽然不影响运行,但代码规范要注意) scheduleWithFixedDelay的参数错误:你传入的是period(2秒)而不是period1(4秒),这会导致job2的执行间隔变成2秒,和job1一样,不符合你的预期。
3. ScheduledExecutorService的正确使用方式
a. 合理配置JVM内存
先把堆内存调大,比如改成:
java -Xms256m -Xmx256m -XX:+PrintGC ScheduleExecutorTest > ./output.txt
这个大小足够支撑你的测试任务,实际生产环境可以根据业务需求调整。
b. 手动创建线程池(比Executors更可控)
Executors.newScheduledThreadPool()是快捷方式,但缺乏自定义配置。建议直接用ScheduledThreadPoolExecutor构造器,指定线程池大小、拒绝策略等,避免资源耗尽:
// 自定义线程池,指定核心线程数,还可以配置线程工厂、拒绝策略 ScheduledExecutorService service = new ScheduledThreadPoolExecutor( 10, Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 任务满时直接抛出异常,也可以选其他策略 );
c. 确保任务执行时间不超过间隔(针对scheduleAtFixedRate)
scheduleAtFixedRate是固定频率执行,如果任务执行时间超过了period,任务会堆积在队列里,占用内存。如果你的任务可能耗时较长,建议用scheduleWithFixedDelay(固定延迟,上一个任务结束后再等delay执行下一个),或者确保任务执行时间远小于间隔。
d. 任务结束后关闭线程池
如果是临时任务(比如你的测试代码),记得在合适的时机关闭线程池,不然JVM会一直运行(线程池的非守护线程会阻止JVM退出):
// 比如测试时,运行一段时间后关闭 Thread.sleep(30000); // 运行30秒 service.shutdown(); // 等待所有任务完成,或者超时 if (!service.awaitTermination(10, TimeUnit.SECONDS)) { service.shutdownNow(); }
4. 修改后的完整测试代码
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ScheduleExecutorTest implements Runnable { private String jobName = ""; public ScheduleExecutorTest(String jobName) { super(); this.jobName = jobName; } @Override public void run() { System.out.println("execute : " + jobName); } public static void main(String[] args) throws InterruptedException { // 自定义线程池,更可控 ScheduledExecutorService service = Executors.newScheduledThreadPool(10); long initialDelays = 4; long period = 2; // 固定频率执行:初始延迟4秒,之后每2秒执行一次 service.scheduleAtFixedRate(new ScheduleExecutorTest("job1"), initialDelays, period, TimeUnit.SECONDS); long initialDelays1 = 4; long period1 = 4; // 固定延迟执行:初始延迟4秒,上一个任务结束后等待4秒执行下一个 service.scheduleWithFixedDelay(new ScheduleExecutorTest("job2"), initialDelays1, period1, TimeUnit.SECONDS); // 测试运行30秒后关闭线程池 Thread.sleep(30000); service.shutdown(); if (!service.awaitTermination(10, TimeUnit.SECONDS)) { service.shutdownNow(); } } }
5. 补充:GC日志说明
那些[Full GC (Ergonomics)]日志是JVM的GC自适应调节机制触发的,当JVM检测到堆内存不足时,会自动触发Full GC尝试回收。但因为你的堆太小了,回收后几乎没有可用空间,所以会反复触发,直到最后抛出OOM。
内容的提问来源于stack exchange,提问作者yue mu

