目录

ThasBlog

学无止境

记一次线上 JAVA 程序 OOM 事件

依赖三方包:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <!-- 阿里云SLS提供的SDK -->
        <dependency>
            <groupId>com.aliyun.openservices</groupId>
            <artifactId>aliyun-log</artifactId>
            <version>0.6.56</version>
        </dependency>

代码:

public class DefaultServiceClient2 extends DefaultServiceClient {

    // 问题的主要原因出在这个 DefaultServiceClient 上, 放置一个大型成员变量扩大OOM效应
    private byte[] bytes = new byte[1024 * 1024 * 20];

    public DefaultServiceClient2() {
        super(new ClientConfiguration());
    }
}
public class MemoryLeakDemo {

    private static MemoryLeakClass memoryLeakClass;

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10000; i++) {
            DefaultServiceClient2 defaultServiceClient2 = new DefaultServiceClient2();
            defaultServiceClient2.shutdown();
        }

    }
}

java -Xmx100m -Xms100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof MemoryLeakDemo

运行之后直接 OOM:

image.png

单纯看上述代码, 前一段还运行正确着, 正常 shutdown , 没有任何对象被强引用, 照理说后面也应该正常执行, 所有对象随着 shutdown 释放完成, 都会被GC掉, 但最后却还是 OOM了.

接着使用 VisualVm 看 dump 分析:

image.png

毫无疑问, 是因为我们定义的 DefaultServiceClient2 内部的 20M 的字节数据撑爆了内存, 那么为什么这个字节数据没有被回收掉呢?

一直向下追溯引用链:

image.png

一直追溯到 java.lang.ref.Finalizer, PoolingHttpClientConnectionManager 仍然被 Finalizer 队列引用着, 因此, 引用链上的对象, 没有被回收掉.

第一个问题:

明明是 DefaultServiceClient2 引用了 PoolingHttpClientConnectionManager, 为什么会被反过来引用着?

查看引用关系:

image.png

DefaultServiceClient2 的 this$0 对象被 TrustStrategy引用着, 明显是个匿名内部类

image.png

实例方法创建的匿名类对象, 会持有当前实例的 this 指针, 通过这个匿名类对象中的指针, DefaultServiceClient2 被反过来一直引用着.

第二个问题:

为什么 GC 的 Finalizer 队列一直引用着 PoolingHttpClientConnectionManager 没有释放?

PoolingHttpClientConnectionManager 覆写了 finalize 方法, 因此 GC 的时候会加入 Finalizer 等待执行 finalize 方法, 但是 Finalizer 是一个低优先级的线程, 因此并不能保证及时的执行完队列中的全部对象的 finalize 方法. 当 主线程/业务线程繁忙时(尤其是线上业务, 业务线程高达上百个), Finalizer 线程可能无法得到足够多的CPU时间片, 当 finalize 执行速度跟不上其他业务线程生产 PoolingHttpClientConnectionManager 对象时, OOM 就发生了.

线上出现的 OOM 问题不是像 Demo 中这样的超大对象, 而是 PoolingHttpClientConnectionManager 被创建了几十万个, 每一个对象虽然占用很小, 但是堆积起来就很大了, 最终的深堆分析, PoolingHttpClientConnectionManager 占据了超过 90% 的 dump 文件的堆空间.

OOM 的最主要原因还是错误的使用了 SLS SDK 的 DefaultServiceClient, 导致 PoolingHttpClientConnectionManager 被创建的太多.


标题:记一次线上 JAVA 程序 OOM 事件
作者:thas
地址:https://thas.cc/articles/2021/06/03/1622734827959.html