记一次 OOM:GC overhead limit exceeded排查

1.背景

  • 产品大大对程序员小亮抱怨某个计算收益类型的任务每次都要2+小时才能跑完,希望该任务能快速优化下。小亮嘴上嘟嚷道:可以做,要先提需求单(内心写照如下图)。

2.分析

2.1 流程

  • 1.程序员小亮对着收益类型的任务一顿分析,心想这还不简单(单线程改成多线程不就完事了)。遇事不要慌,开局先来一副流程图:

2.2 TTL多线程

- 多线程使用这还不简单,直接ThreadPoolExecutor走起,但是想到组内建议使用阿里的ThreadLocal Transmittable ThreadLocal(TTL,下同),TTL主要解决在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
- 那么TTL优化了哪些地方呢?针对JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把==任务提交给线程池时的ThreadLocal值传递到 任务执行时==(请注意高亮这段话,这也是本次OOM的根源所在,后面详解)。

2.3 上代码

  • 1.初始化线程池
private ExecutorService executor;

@PostConstruct
private void initExecutor() {
    executor = TtlExecutors.getTtlExecutorService(
            new ThreadPoolExecutor(6, 12, 5, TimeUnit.MINUTES, newLinkedBlockingQueue<>(),
                                   new ThreadFactoryBuilder()
                                          .setNameFormat("xxx-Thread-Pool-%d").uild(),
                                   newThreadPoolExecutor.CallerRunsPolicy());
}
  • 2.多线程执行任务,考虑到我们需求是汇总所有计算结果,再做其他事。所以我们主线程需要等待多线程计算完后,然后执行其他结果(针对多线程同步,我们可以使用CountDownLatch、Barrier、(Future配合sumbit)等方式来完成同步操作(具体使用方法可以自行百度哈),这里示例我们使用CountDownLatch)。
public void process(){
    // 其他代码
    
    CountDownLatch countDownLatch = new CountDownLatch(fileLineNum);
    while(lineIterator.hasNext()){
        String line=lineIterator.next();
        exector.submit(()->{
            // 具体执行计算逻辑
            calculateLine(line);
            countDownLatch.countDown();
        });
    }
    
    // 主线线程等待多线程执行完毕
    countDownLatch.await();
    // do other things
}
  • 3.OOM异常
  • 小亮开心的把多线程换上了,数据也飞快的跑起来了,多线程牛逼啊。

  • 可没过一会就处理速度就骤降了,也多了很多full gc的log

  • 啪,然后就OOM,超出了GC开销限制。(程序猿的快乐没了)。
  • 4.保留现场
  • 小亮哪里见过这异常啊,赶紧咨询组里的大牛。大牛让小亮赶紧登陆机器看看原因(这里也推荐使用阿里自带得诊断工具Arthas):
    • 1.查看下java线程CPU和内存运行情况
    // 查看机器CPU运行情况,其中PID=52950为当前java运行线程PID
    $ top -n 5 
    
    // 或查看java线程内存使用情况
    $ jstat -gcutil 52950 1000 ##其中 52950为PID,1000为刷新间隔(1s)
    
    • 从上图可知,full gc的次数多达300+次,而且中的full gc时间也曾高达200+s,所以肯定是发生了内存泄漏,导致full gc超限,引发的OOM。
    • 2.dump日志(线上生产可千万不能直接jdump日志)。当然你也可以在jvm参数中配置了如下参数(或线上找PE帮忙dump下),来自动保存OOM的日志:
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps 
    -Xloggc:./gc.log 
    -XX:+HeapDumpOnOutOfMemoryError 
    -XX:HeapDumpPath=自定义的路径
    
    • dump日志命令:
    // dump日志后,会得到java_pid52950.hrof文件
    $ jdump -F jump:format=b,file=heapDump 52950
    
    • 3.dump文件分析,本打算使用jhat命令来分析一番,结果半天没啥反应。于是只能使用java自带的分析神器VisualVM(当然也可以使用MAT),打开命令如下:
    $ jvisualvm
    
    • 4.大内存对象
    • 可以看到String和char []两个对象占据内存的Top2.
    • 于是点开String和char[]对象,来看看里面都是些啥?看到langue、en-US、IP是不是很熟悉,有点类似我们的http请求头的数据结构。
    • ps:如果此时还不能定位到具体代码,可以使用OOL查询具体引用如下图可参考此文章排查具体代码行
    • 5.怀疑
    • 因为我们使用的是TTL线程,而且整个请求是会带上链路trace信息的,trace里面的字段正好和大内存对象里面的结构神似,所以怀疑就是TTL把父线程的trace信息拷贝了子线程。
    • 6.源码
    • 代码不会骗人,直接看TTL源码。看看TTL到底对我们的多线程做了什么。先看下TTL时序图,可知在4.1.2的时候进行copy。
    • copy流程代码(代码1),注释的意思是在任务在创建时从源线程执行拷贝,也就意味着当我们对task进行sumit时,就已经从主线程的threadlocal进行了相关拷贝(拷贝如代码2)。
    // 代码1
     /**
     * Computes the value for this transmittable thread-local variable
     * as a function of the source thread's value at the time the task
     * Object is created.
     * <p>
     * This method is called from {@link TtlRunnable} or
     * {@link TtlCallable} when it create, before the task is started.
     * <p>
     * This method merely returns reference of its source thread value(the shadow copy),
     * and should be overridden if a different behavior is desired.
     *
     * @since 1.0.0
     */
    public T copy(T parentValue) {
        return parentValue;
    }
    
    // 代码2
    private static HashMap<ThreadLocal<Object>, Object>captureThreadLocalValues() {
        final HashMap<ThreadLocal<Object>, Object> threadLocal2Value= new HashMap<ThreadLocal<Object>, Object>();
        for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry: threadLocalHolder.entrySet()) {
            final ThreadLocal<Object> threadLocal = entry.getKey();
            final TtlCopier<Object> copier = entry.getValue();
            
            // 进行拷贝操作
            threadLocal2Value.put(threadLocal,copier.copy(threadLocal.get()));
        }
        return threadLocal2Value;
    }
    

3.解决方案

  • 原因:既然知道是因为TTL在主线程提交任务时,对threadlocal进行了拷贝。结合本次收益文件大约有50W行,也就意味着在2.3.2中会提交约50W个任务,当然拷贝的对象也就多了,必然OOM了。
  • 解决方案:
    • 1.针对业务需要,是否需要传递主线程threadlocal信息至多线程,若不需要则可采用new ThreadPoolExecutor(xx,xx)来创建原生得线程池、或合理调整Jvm内存参数Xmx、Xms。
    • 2.对于任务可以分批处理,即让每个线程处理多条数据,代码如下:
       for(int i=0;i<fileLineNum;i+=batchSize){
           // 分批处理,减少task任务数量,从而减少threadlocal的拷贝次数
           executor.submit(()->{processLines(lines)});
       }
    
    • 3.【强制】设置合理得阻塞队列长度和拒绝策略,本次小亮使用的时无界阻塞队列并没有初始化长度,那么CallerRunsPolicy拒绝策略相当于无效。

4.总结

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
当在使用Eclipse的Build Project功能时出现"GC overhead limit exceeded"错误时,这意味着JVM的内存溢出了,在GC(垃圾回收)过程中所花费的时间过长,而回收的堆内存只占很小的比例。这个错误是Hotspot VM 1.6定义的一个策略,旨在提前抛出异常以防止发生OOM(OutOfMemoryError)。 为了解决这个问题,你可以尝试以下几个方法: 1. 增加JVM的内存限制,可以通过在启动Eclipse时添加参数"-XX:MaxPermSize=1024m"来增加最大内存限制。这样在编译文件时将占用更多的内存。 2. 检查你的项目的依赖和资源使用情况,确保没有过多的资源占用内存。 3. 尝试升级Eclipse版本或使用最新的更新来修复可能存在的bug或问题。 4. 如果以上方法都不起作用,你可能需要考虑增加你的计算机的物理内存来提供更大的内存供应。 希望这些方法能够帮助你解决"GC overhead limit exceeded"错误。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Eclipse:An internal error occurred during: "Build Project"... GC overhead limit exceeded](https://blog.csdn.net/weixin_34321753/article/details/90149104)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值