一次线程挂掉的排查与GC overhead limit exceeded

本文讨论了在使用多线程分批写入文件时遇到的内存溢出问题,详细解释了问题的原因、如何捕获OutOfMemoryError以及提供了解决方案,包括修改FutureTask的异常捕获逻辑和增加堆内存大小。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在使用多线程分批写入文件时,发生如下异常:

java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.util.concurrent.FutureTask.report(FutureTask.java:122)
        at java.util.concurrent.FutureTask.get(FutureTask.java:192)
        at com.ds.ProductRawDataTask.run(ProductRawDataTask.java:128)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.util.Arrays.copyOf(Arrays.java:3181)
        at java.text.DateFormatSymbols.getShortMonths(DateFormatSymbols.java:433)
        at java.util.Calendar.getFieldStrings(Calendar.java:2241)
        at java.util.Calendar.getDisplayName(Calendar.java:2111)
        at java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1125)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:966)
        at java.text.SimpleDateFormat.format(SimpleDateFormat.java:936)
        at java.text.DateFormat.format(DateFormat.java:345)
        at ...

使用的多线程代码如下:

try{
  List<Future> futures = new ArrayList<Future>();
  //通过线程池写出多个渠道的数据文件到磁盘
  futures.add(executorService.submit(new ProductWeiboRawDataExportTask()));
  futures.add(executorService.submit(new ProductForumRawDataExportTask()));
  futures.add(executorService.submit(new ProductTiebaRawDataExportTask()));
  futures.add(executorService.submit(new ProductNewsRawDataExportTask()));
  futures.add(executorService.submit(new ProductShortVideoRawDataExportTask()));
  executorService.shutdown();
  //阻塞等待所有线程完成
  for(Future f: futures) { f.get(); }  //ProductRawDataTask.java:128

  //将所有渠道的数据文件打包发送到远程ftp服务器
  sendFilesToFTP();   

}catch(Exception e){
  log.error(e.getMessage(),e);
}
        

也就是说,遇到的问题是:在使用线程池执行任务,并且通过Future阻塞等待所有线程执行完成时,因为某个线程抛出了OutOfMemoryError,导致将文件打包发送到远程ftp服务器的操作没有执行。真实原因是因为get()方法抛出了ExecutionException异常,导致跳过了sendFilesToFTP()方法,进入了异常捕获逻辑当中。

首先,让我们理解以下知识点:

1、java.lang.OutOfMemoryError: GC overhead limit exceeded 

Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。

2、一个线程OOM后,其他线程还能运行

3. 怎么捕获OutOfMemoryError

try {
    .....
} catch (Exception e) {
    e.printStackTrace();
}

无法捕获OutOfMemoryError

try {
    .....
} catch (Throwable t) {
   .....
}

只有用catch (Throwable t)捕获OutOfMemoryError

因为,它们的继承关系如下:

4、FutureTask#run()方法会捕获Throwable,并将Throwable设置为内部的outcome变量

  public void run() {
.........
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
//捕获Throwable
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
//将Throwable设置为内部的outcome成员变量
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            .........
        }
    }

5、FutureTask#get()方法会将捕获到的Throwable封装为ExecutionException,并抛出该异常

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

/**
     * Returns result or throws exception for completed task.
     *
     * @param s completed state value
     */
    @SuppressWarnings("unchecked")
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
//如果outcome成员变量为Throwable类型,将Throwable封装为ExecutionException,并抛出该异常
        throw new ExecutionException((Throwable)x);
    }

最终,我们会想到相关解决方法如下:

1、如果可以忽略抛出oom的线程写出的磁盘文件,只要求将其他线程成功写出的文件打包发送到ftp。对FutureTask#get()方法加入try/catch异常捕获块即可。

 executorService.shutdown();
  //阻塞等待所有线程完成
  for(Future f: futures) { 
    try{
       f.get(); 
    }catch(Exception e){
       .....    
    }      
}  

2、增大堆内存,避免oom

<think>嗯,用户现在遇到了Linux环境下Java程序的GC overhead limit exceeded错误,需要解决方案。我之前已经提供过一些关于jstack和grep命令的使用方法,现在用户转向了内存问题。首先,我需要回顾一下用户提供的引用内容,里面有关于这个错误的解决方法。引用1提到禁用GC overhead limit,但需要确定程序内存使用正常。引用2详细分析了原因,包括内存泄漏、堆大小不足、大数据集处理、数据结构问题,并给出了增加堆内存、优化代码、分析内存使用、调整GC策略等方法。引用3则是针对IDEA启动时的解决方案,但用户现在是在Linux环境,可能不适用,但可以借鉴调整内存设置的思路。 用户的需求是解决Linux下的这个问题,所以需要结合引用中的建议,同时考虑Linux环境的特点。首先,我应该分步骤给出解决方案,从最直接的增加堆内存和调整JVM参数开始,然后逐步深入到代码优化和内存分析。可能用户已经尝试过一些基本方法,但问题依然存在,所以需要更深入的排查步骤。 需要注意的是,用户可能不太清楚如何具体实施这些步骤,比如如何分析内存泄漏,如何使用工具如jmap、MAT等。因此,在回答中需要详细说明每个步骤的操作方法,并给出具体的命令示例,方便用户直接复制使用。同时,提醒用户禁用GC overhead limit只是临时解决方案,正确的方法是找到根本原因。 另外,用户之前的问题是关于jstack和线程分析的,可能在内存问题中也需要结合线程堆栈来分析是否有死锁或者资源竞争导致的内存无法释放。可以建议用户结合jstack和内存分析工具一起使用,全面诊断问题。 最后,生成的相关问题要围绕用户可能遇到的后续问题,比如如何分析内存转储、如何选择GC算法、如何预防此类错误等,帮助用户进一步深入理解和解决问题。</think>根据您提供的引用内容和Linux环境特点,以下是解决`GC overhead limit exceeded`错误的完整方案: ### 一、立即缓解措施 1. **调整JVM内存参数** ```bash # 增加堆内存并禁用GC超限保护(生产环境慎用) java -Xmx4g -Xms2g -XX:-UseGCOverheadLimit -XX:+UseG1GC -jar your_app.jar # 推荐组合配置(保持GC监控) java -Xmx4096m -Xms2048m -XX:+UseG1GC -XX:+PrintGCDetails -Xloggc:/var/log/gc.log -jar your_app.jar ``` *参数说明:* - `-Xmx4g`:最大堆内存设为4GB(根据物理内存调整)[^2] - `-XX:+UseG1GC`:使用G1垃圾收集器(特别适合大堆内存)[^2] - `-XX:+PrintGCDetails`:记录GC详细日志 2. **Linux系统级检查** ```bash # 查看物理内存使用情况 free -h # 监控Java进程内存 top -p $(pgrep -f your_app.jar) -o %MEM ``` ### 二、根本原因排查 1. **生成内存转储** ```bash # 获取Java进程PID jps -lv | grep your_app.jar # 生成堆转储文件(需-Xmx值相当的磁盘空间) jmap -dump:live,format=b,file=/tmp/heapdump.hprof <PID> ``` *分析工具推荐:* - Eclipse Memory Analyzer (MAT) - VisualVM(远程连接时需添加JMX参数) 2. **GC日志分析** ```bash # 使用GCViewer分析 java -jar gcviewer-1.36.jar /var/log/gc.log ``` *关键指标:* - Full GC频率 > 1次/分钟 - GC后老年代内存回收率 < 20% - GC停顿时间 > 1秒 ### 三、代码优化方向 1. **常见内存泄漏模式** ```java // 错误示例:静态Map持续增长 public class CacheManager { private static Map<String, Object> CACHE = new HashMap<>(); // 应设置TTL或使用WeakReference } // 正确使用try-with-resources try (BufferedReader br = new BufferedReader(new FileReader(path))) { // 自动关闭资源 } ``` 2. **集合类优化技巧** ```java // 预估容量避免resize List<User> users = new ArrayList<>(100000); // 使用原始类型集合 IntArrayList fastList = new IntArrayList(); // trove4j库 ``` ### 四、生产环境推荐配置 ```bash # G1GC完整参数模板(JDK8+) java -Xmx8g -Xms8g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:InitiatingHeapOccupancyPercent=35 \ -XX:G1ReservePercent=15 \ -XX:ConcGCThreads=4 \ -XX:ParallelGCThreads=8 \ -jar your_app.jar ``` ### 五、自动化监控方案 ```bash # 使用jstat实时监控(每5秒采样) watch -n5 "jstat -gcutil <PID> 5000 1" # Prometheus + Grafana监控模板配置 jmx_exporter配置示例: --- lowercaseOutputName: true rules: - pattern: 'java.lang<type=Memory><>(HeapMemoryUsage|NonHeapMemoryUsage)' name: jvm_memory_usage labels: area: "$1" ``` ### 六、进阶工具链 | 工具 | 用途 | 使用场景 | |-------|-------|----------| | async-profiler | 内存分配分析 | 定位热点对象创建位置 | | jemalloc | 替代内存分配器 | 减少内存碎片化 | | perf | 系统级分析 | 关联JVMOS内存行为 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值