一、内存溢出场景
线上某次促销活动,监控到内存持续增长,接口响应时间越来越长,到最后无响应,内存溢出OOM
1.紧急解决方案
扩容,重启机器增加堆内存分配
2.原因分析
由于jvm参数中有配置jvm参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/aliyun/logs/jvm/
该参数配置代表发生OOM时,生成一份堆快照文件
从服务器中获取到堆快照文件java_pid3088.hprof
使用jvisualvm打开该快照,如图:
如图所示,除一些基本数据类型外,内存溢出时快照中存在较多的java.security.Provider ServiceKey对象与java.security.Provider Service对象。
Provider主要用于加密解密时用到,项目中原本相关的代码就很少,所以这里已经对部分问题接口有了猜测。然后观测内存增长比较高的时间段,查询日志该时间段接口访问比较频繁的接口,发现有一个单点登录接口比较符合上边的情况,对该接口重点压测分析,如没问题对其余相关接口压测分析。
注:相关的截图为后续复现截图
3.jemter压测问题接口
线下复现问题,对单点登录接口进行压测,发现该接口请求响应时间越来越长,直到最后完全没有响应,发生OOM,与线上情况基本吻合。
(1).jemter压测响应情况
(第一张刚刚压测截图为后续重新压测补充的,所以时间不一致,但是效果一致)
压测整体响应情况如下:通过刚刚压测阶段->压测一小段时间->压测一段时间->压测最后阶段,可以发现响应时间越来越长,到最后完全无响应,对应报错OOM。此时基本可以确认问题出在该接口。
2.jvisualvm观测内存与垃圾回收情况
压测时打开jdk自带jvisualvm工具,查看其内存与垃圾回收情况。
整体监视情况如下
垃圾回收情况如下:刚刚开始压测一小段时间:老年代已经在持续增长了,eden已经出现了多次minorGC。
压测一段时间后,eden执行了非常多次minorGC,而老年代一直在持续增长,已经到达了最大内存。
压测较长后,eden执行了非常多次,而老年代在到达了最大内存后也执行了一次fullGC。
压测很久很久后,可以老年代在一次fullGC后,并没有缓解情况,后续还在持续增长,探后再次打满内存,导致OOM。
3.jvisualvm观测堆快照--定位内存泄漏的是哪个对象
可以发现java.security.Provider ServiceKey对象与java.security.Provider Service对象在经历了无数次gc仍然存活,截图时为75次,占用了大量堆空间,其依赖的基本数据类型同样占用大量堆空间,所以这里再次断定Provider对象出现了问题。
4.jvisualvm观测线程快照--定位内存溢出的是哪个线程方法
通过线程快照可以定位到具体类与方法,查看该方法为:
Cipher.getInstance("RSA", new BouncyCastleProvider());
由此得出结论,单点登录接口,做加密操作时,创建了大量的BouncyCastleProvider对象,且该对象一直不能被回收,导致内存溢出。
下面对代码详细分析
4.代码分析
简化demo,通过上述定位到了接口/oom/test,方法定位到了buildParam的Cipher.getInstance方法,对象则定位到了Provider,下面分析下demo。
@RestController
public class OOMController {
@RequestMapping("/oom/test")
public String testOOM() {
JSONObject data = new JSONObject();
data.put("AppId", "APPID");
data.put("Secret", "HASHKEY");
data.put("TransCode", "TRANSCODETOKEN");
String rsaContent = buildParam(data.toString());
return rsaContent;
}
/**
* 这是个实际遇到的问题,关于api调用加密解密的参数拼接导致内存泄漏
* 简化了业务,只保留了问题关注点代码,也就是new BouncyCastleProvider()
*
* @param data
* @return
*/
private String buildParam(String data) {
try {
// 怀疑这里new BouncyCastleProvider可能会有问题
Cipher cipher = Cipher.getInstance("RSA", new BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
JSONObject value = new JSONObject();
value.put("Data", data);
value.put("Access_Token", "token");
JSONObject content = new JSONObject();
content.put("content", value);
return content.toString();
}
}
对Cipher.getInstance("RSA", new BouncyCastleProvider());进行深层次的分析,创建Cipher时,会验证Provider是否合法,验证Provider合法过程中,会将Provider放到一个全局map-verificationResults中,所以一直存在引用,即Provider和全局verificationResults不会被回收,到这里问题基本就定位到了,下边有一些debug时verificationResults的一些截图。
public static final Cipher getInstance(String var0, Provider var1) throws NoSuchAlgorithmException, NoSuchPaddingException {
if (var1 == null) {
throw new IllegalArgumentException("Missing provider");
} else {
Exception var2 = null;
List var3 = getTransforms(var0);
boolean var4 = false;
String var5 = null;
Iterator var6 = var3.iterator();
while(true) {
while(true) {
Cipher.Transform var7;
Service var8;
do {
do {
if (!var6.hasNext()) {
if (var2 instanceof NoSuchPaddingException) {
throw (NoSuchPaddingException)var2;
}
if (var5 != null) {
throw new NoSuchPaddingException("Padding not supported: " + var5);
}
throw new NoSuchAlgorithmException("No such algorithm: " + var0, var2);
}
var7 = (Cipher.Transform)var6.next();
var8 = var1.getService("Cipher", var7.transform);
} while(var8 == null);
if (!var4) {
// 验证Provider是否支持该算法
Exception var9 = JceSecurity.getVerificationResult(var1);
if (var9 != null) {
String var12 = "JCE cannot authenticate the provider " + var1.getName();
throw new SecurityException(var12, var9);
}
var4 = true;
}
} while(var7.supportsMode(var8) == 0);
if (var7.supportsPadding(var8) != 0) {
try {
CipherSpi var13 = (CipherSpi)var8.newInstance((Object)null);
var7.setModePadding(var13);
Cipher var10 = new Cipher(var13, var0);
var10.provider = var8.getProvider();
var10.initCryptoPermission();
return var10;
} catch (Exception var11) {
var2 = var11;
}
} else {
var5 = var7.pad;
}
}
}
}
}
/**
*
*/
private static final Map verificationResults = new IdentityHashMap();
/**
* 每次将Provider放到全局变量verificationResults map中,所以会一直被引用,不会被垃圾回收
*/
static synchronized Exception getVerificationResult(Provider var0) {
Object var1 = verificationResults.get(var0);
if (var1 == PROVIDER_VERIFIED) {
return null;
} else if (var1 != null) {
return (Exception)var1;
} else if (verifyingProviders.get(var0) != null) {
return new NoSuchProviderException("Recursion during verification");
} else {
Exception var3;
try {
verifyingProviders.put(var0, Boolean.FALSE);
URL var2 = getCodeBase(var0.getClass());
verifyProviderJar(var2);
// 这是一个全局变量,并且每次调用该方法都会新增,所以会一直在增加,
// 且属于不能被回收的对象
![image.png](https://upload-images.jianshu.io/upload_images/15829707-77445ee4e469d6b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
.put(var0, PROVIDER_VERIFIED);
var3 = null;
return var3;
} catch (Exception var7) {
verificationResults.put(var0, var7);
var3 = var7;
} finally {
verifyingProviders.remove(var0);
}
return var3;
}
}
通过对verificationResults截图可以发现该map中BouncyCastleProvide作为key存在,且BouncyCastleProvide对象本身属于键值对且占用内存比较多
5.最终解决
问题发现了,解决方案则比较简单,这里其实不需要每次都创建一个BouncyCastleProvide对象,只需要创建一个全局BouncyCastleProvide对象就可以。
解决后相关内存与垃圾回收截图献上。
注:文中相关截图都是后续复现中所截,且并非同一次压测截图完成。
库存导出频繁fullgc与内存泄露问题
1.poi替换easyexcel,一次捞所有数据替换为分页查 解决内存泄露
2.metaspace达到阈值导致fullgc
其他:
文档推荐,g1fullgc排查思路,gc日志解读
https://blog.csdn.net/liaomingwu/article/details/124813934
https://segmentfault.com/a/1190000021875196
https://zhuanlan.zhihu.com/p/267388951
https://www.jianshu.com/p/ac1ba3479c08