背景
晚上快下班,前端告诉我有一个接口迟迟没有响应,可这个项目很久没提过新东西了,更不要说上线了。
排查思路
于是先去登到机器上top,top -Hp一下看下CPU情况,好像并没有什么异常。 然后再free -h看内存,发现确实服务器内存剩余空间不足500MB,那么大概率是内存泄露的问题。 那么直接看下jvm gc情况:
shell
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内存快照。
shell
jmap -dump:format=b,file=heap.hprof 38714
然后重启服务,保证线上服务是可用的。 本地通过MAT (Eclipse Memory Analyzer) 分析dump文件 打开dump文件可以发现:

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

Leak Suspects
堆内 jcifs.context.BaseContext 对象实例有19,575个,并且占了87.18%的内存,那这一定是不合理的。 再看 Common Path To the Accumulation Point
确实问题点都出现在这里。
那么再去看业务代码,看看哪里导致了OOM。
java
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
大概意思是SmbFileConnection类的构造器会创建一个jcifs.context.BaseContext对象,jcifs.context.BaseContext对象创建时会调用其超类的AbstractCIFSContext的构造方法,AbstractCIFSContext的构造方法中会为jcifs.context.BaseContext对象注册一个shutdown hook 。SmbFileConnection对象使用结束,会被垃圾回收器回收掉,但是jcifs.context.BaseContext对象还会存在一个shutdown hook的引用链导致不能被回收。
issue中的问题在jcifs 3.9.0版本仍然存在,我们项目中使用的jcifs版本是2.1.7,为了保险,再看一下2.1.7版本的jcifs怎么实现的。
java
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()方法是可以解决此问题的,贴上源码:
java
public boolean close() throws CIFSException {
if (!this.closed) {
Runtime.getRuntime().removeShutdownHook(this);
}
return false;
}
但是为了防止频繁的创建销毁对象,我选择把此对象做成单例的
java
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;
}
验证
先压测一把
再次dump,查看堆区内存

可以看到此接口不会导致jcifs.context.BaseContext对象泛滥的情况了,搞定!
注
如需转载,请私信联系博客作者