现象
- 线上服务一台实例出现OOM
- 个别实例出现内存使用情况较高
问题排查
表象

SSLSessionImpl 占用大部分空间。
查看相关代码,代码中使用了 org.apache.http.impl.client.CloseableHttpClient 。(在某些博客中表示使用 CloseableHttpClient 可解决问题,但此处明显无效)。
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setMaxConnTotal(100);
httpClient = builder.build();
深入 HttpClientBuilder.build()

可以看到当没有指定 SSLContext 时,内部使用了默认的 SSLContext 。
根因
同时,根据MAT的分析报告,我们是 SSLSessionContextImpl 中的 LinkedHashMap 属性造成了内存大量占用。在代码中,我们也是没有相关配置的,深入看下相关的默认配置。

可以看到 SSLSessionContextImpl 中维护了一个 Session 的缓存,同时有两个相关的配置 cacheLimit / timeout ,这两个配置结合官方文档的解释,原因就明了了。
- timeout:默认为
86400s,即 24h。

- cacheLimit:默认为0,即无限制。

结合两个参数,那么默认的 SSLSessionContextImpl 就是对24小时以内的 SSL 连接无限制进行缓存。
疑点
真的是不设置参数,SSLSession 缓存就不会回收了吗?
实际上,sessionCache 是软引用对象(在后续会详细说明),当内存不足时,GC最终还是可以回收掉的。虽然不致死,但它还是会带来问题:
- 频繁触发GC
- GC过程中回收该对象,需要遍历Cache中的
ReferenceQueue和cacheMap - cache 中每个对象对于GC 都是 一视同仁的,所以无论是刚创建的还是已经保留了接近一天的缓存,都会被清除(实际场景中,我们会希望保留刚创建的缓存)
解决方案
综上,实例OOM的原因并非是 SSLSession 的缓存在允许时间内的无限堆积,实际上缓存是可以被回收的。
而由于没有更多的线索,暂无法确定OOM的原因。
此处仅提供了缓兵之策,用于降低 SSLSession 缓存的内存占用。
SSLContext sslContext = SSLContext.getInstance("SSL");
// 根据业务场景,填写一个大于0的合适值
sslContext.getServerSessionContext().setSessionCacheSize(1000);
扩展
CloseableHttpClient 能解决什么问题?
为什么 CloseableHttpClient 不能解决问题?

CloseableHttpClient 实现了 Closeable 接口特性。
而 Closeable 的定义是:
/**
* A {@code Closeable} is a source or destination of data that can be closed.
* The close method is invoked to release resources that the object is
* holding (such as open files).
*
* @since 1.5
*/
public interface Closeable extends AutoCloseable {}
即 Closeable 是定义一个可关闭的资源,类似文件流。但实际上Http 连接是被 SSLSessionContextImpl 的 sessionCache 属性引用了,所以即使连接关了,但由于被引用了,资源还是不能被回收。
SSLSessionImpl 缓存是如何被回收的?
在 SSLSessionContextImpl 中使用了 MemoryCache 作为缓存实现,MemoryCache 里面是使用了 ReferenceQueue 来监听缓存对象引用的状态,会清理即将被回收的 Reference 对象。
Reference 和 ReferenceQueue 的深入分析可参考另一篇文章:Java - Reference 小记
final class SSLSessionContextImpl implements SSLSessionContext {
private Cache<SessionId, SSLSessionImpl> sessionCache;
SSLSessionContextImpl() {
this.sessionCache = Cache.newSoftMemoryCache(this.cacheLimit, this.timeout);
this.sessionHostPortCache = Cache.newSoftMemoryCache(this.cacheLimit, this.timeout);
}
}
public abstract class Cache<K, V> {
public static <K, V> Cache<K, V> newSoftMemoryCache(int var0, int var1) {
return new MemoryCache(true, var0, var1)
}
}
class MemoryCache<K, V> extends Cache<K, V> {
private static final float LOAD_FACTOR = 0.75F;
private static final boolean DEBUG = false;
private final Map<K, MemoryCache.CacheEntry<K, V>> cacheMap;
private int maxSize;
private long lifetime;
private final ReferenceQueue<V> queue;
public MemoryCache(boolean var1, int var2, int var3) {
this.maxSize = var2;
this.lifetime = (long)(var3 * 1000);
if (var1) {
this.queue = new ReferenceQueue();
} else {
this.queue = null;
}
int var4 = (int)((float)var2 / 0.75F) + 1;
this.cacheMap = new LinkedHashMap(var4, 0.75F, true);
}
protected MemoryCache.CacheEntry<K, V> newEntry(K var1, V var2, long var3, ReferenceQueue<V> var5) {
// 当 ReferenceQueue 存在时,则生成软引用节点
// 同时在初始化SoftCacheEntry 时传入了 RQ
return (MemoryCache.CacheEntry)(var5 != null ? new MemoryCache.SoftCacheEntry(var1, var2, var3, var5) : new MemoryCache.HardCacheEntry(var1, var2, var3));
}
// SoftCacheEntry 继承了 SoftReference
private static class SoftCacheEntry<K, V> extends SoftReference<V> implements MemoryCache.CacheEntry<K, V> {
SoftCacheEntry(K var1, V var2, long var3, ReferenceQueue<V> var5) {
// 使用父类初始化方法,指定了 RQ
super(var2, var5);
this.key = var1;
this.expirationTime = var3;
}
}
}