记一次真实的线上OOM

2 阅读3分钟

背景

晚上快下班,前端告诉我有一个接口迟迟没有响应,可这个项目很久没提过新东西了,更不要说上线了。

排查思路

于是先去登到机器上top,top -Hp一下看下CPU情况,好像并没有什么异常。 然后再free -h看内存,发现确实服务器内存剩余空间不足500MB,那么大概率是内存泄露的问题。 那么直接看下jvm gc情况:

jstat -gcutil 38714 
S0    S1    E      O     M    CCS   YGC  YGCT   FGC     FGCT       GCT 
0.00 0.00 99.99 100.00 88.26 84.01 4494 48.648 127745 30434.755 30483.404
  • 那么问题显而易见
    • Old 区 100%,Eden 区 99.99%老年代和Eden区全部被占满,对象无法分配,也无法回收。
    • FGC = 127745,FGCT = 30434.755 秒Full GC 发生了 12 万次,累计耗时 8.45 小时
    • S0/S1 为 0:因为 Old 区满了,对象从 Young 区晋升不上去,Survivor 区也无法正常工作

确实是很严重的线上问题了,那么立刻通过 jmap 命令 dump内存快照。

jmap -dump:format=b,file=heap.hprof 38714

然后重启服务,保证线上服务是可用的。 本地通过MAT (Eclipse Memory Analyzer) 分析dump文件 打开dump文件可以发现:

image.png

可疑问题对象占了1.5GB堆内存

image.png
Leak Suspects

堆内 jcifs.context.BaseContext 对象实例有19,575个,并且占了87.18%的内存,那这一定是不合理的。 再看 Common Path To the Accumulation Point

image.png 确实问题点都出现在这里。
那么再去看业务代码,看看哪里导致了OOM。


public static List<TreeNode> getPackage() {
        java.util.Properties props = new java.util.Properties();
        props.put("jcifs.smb.client.minVersion", "SMB202");
        props.put("jcifs.smb.client.maxVersion", "SMB311");
        props.put("jcifs.smb.client.disableSMB1", "true");
        props.put("jcifs.smb.client.encrypt", "true");

        CIFSContext baseContext = new BaseContext(new PropertyConfiguration(props));
        NtlmPasswordAuthentication authentication = new NtlmPasswordAuthentication(baseContext, packageIP, packageUser, packagePsw);
        CIFSContext authContext = baseContext.withCredentials(authentication);
        SmbFile remoteFile = new SmbFile(packagePath, authContext);
        ...
}

每次调用此方法都会创建一个BaseContext对象,并且最终没有做close,那么有可能是这里出现了问题。刚好注意到jcifs库有这样一个issue

image.png 大概意思是SmbFileConnection类的构造器会创建一个jcifs.context.BaseContext对象,jcifs.context.BaseContext对象创建时会调用其超类的AbstractCIFSContext的构造方法,AbstractCIFSContext的构造方法中会为jcifs.context.BaseContext对象注册一个shutdown hookSmbFileConnection对象使用结束,会被垃圾回收器回收掉,但是jcifs.context.BaseContext对象还会存在一个shutdown hook的引用链导致不能被回收。
issue中的问题在jcifs 3.9.0版本仍然存在,我们项目中使用的jcifs版本是2.1.7,为了保险,再看一下2.1.7版本的jcifs怎么实现的。

    public abstract class AbstractCIFSContext extends Thread implements CIFSContext {
        private static final Logger log = LoggerFactory.getLogger(AbstractCIFSContext.class);
        private boolean closed;

        public AbstractCIFSContext() {
            Runtime.getRuntime().addShutdownHook(this);
        }
    ...
    }

确实还是有此问题的,那么就可以着手改代码了。

解决问题

其实显示调用close()方法是可以解决此问题的,贴上源码:

    public boolean close() throws CIFSException {
        if (!this.closed) {
            Runtime.getRuntime().removeShutdownHook(this);
        }

        return false;
    }

但是为了防止频繁的创建销毁对象,我选择把此对象做成单例的

private static CIFSContext getBaseContext() {
    if (baseContext == null) {
        synchronized (PackageBranchUtil.class) {
            java.util.Properties props = new java.util.Properties();
            props.put("jcifs.smb.client.minVersion", "SMB202");
            props.put("jcifs.smb.client.maxVersion", "SMB311");
            props.put("jcifs.smb.client.disableSMB1", "true");
            props.put("jcifs.smb.client.encrypt", "true");
            try {
                baseContext = new BaseContext(new PropertyConfiguration(props));
            } catch (jcifs.CIFSException e) {
                log.error("Failed to initialize CIFS context: {}", e.getMessage());
                throw new RuntimeException("Failed to initialize CIFS context", e);
            }
        }
    }
    return baseContext;
}

验证

先压测一把

image.png 再次dump,查看堆区内存

image.png

image.png 可以看到此接口不会导致jcifs.context.BaseContext对象泛滥的情况了,搞定!

如需转载,请私信联系博客作者