记一次 JAVA 的内存泄露分析
第一时间阅读精彩文章!
https://github.com/jasonGeng88/blog
背景
前不久,上线了一个新项目,这个项目是一个压测系统,可以简单的看做通过回放词表(http请求数据),不断地向服务发送请求,以达到压测服务的目的。在测试过程中,一切还算顺利,修复了几个小bug后,就上线了。在上线后给到第一个业务方使用时,就发现来一个严重的问题,应用大概跑了10多分钟,就收到了大量的 Full GC 的告警。
针对这一问题,我们首先和业务方确认了压测的场景内容,回放的词表数量大概是10万条,回放的速率单机在 100qps 左右,按照我们之前的预估,这远远低于单机能承受的极限。按道理是不会产生内存问题的。
线上排查
首先,我们需要在服务器上进行排查。通过 JDK 自带的 jmap 工具,查看一下 JAVA 应用中具体存在了哪些对象,以及其实例数和所占大小。具体命令如下:
jmap -histo:live `pid of java`
# 为了便于观察,还是将输出写入文件
jmap -histo:live `pid of java` > /tmp/jmap00
jmap -dump:format=b,file=heap.dump `pid of java
离线分析
1.maven 依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.3</version>
</dependency>
2.HttpAsyncClient 客户端
public class HttpAsyncClient {
private CloseableHttpAsyncClient httpclient;
public HttpAsyncClient() {
httpclient = HttpAsyncClients.createDefault();
httpclient.start();
}
public void execute(HttpUriRequest request, FutureCallback<HttpResponse> callback){
httpclient.execute(request, callback);
}
public void close() throws IOException {
httpclient.close();
}
}
主要逻辑:
public class ReplayApplication {
public static void main(String[] args) throws InterruptedException {
//创建有内存泄露的回放客户端
ReplayWithProblem replay1 = new ReplayWithProblem();
//加载一万条请求数据放入缓存
List<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);
//开始循环回放
replay1.start(cache1);
}
}
回放客户端实现(内存泄露):
public class ReplayWithProblem {
public List<HttpUriRequest> loadMockRequest(int n){
List<HttpUriRequest> cache = new ArrayList<HttpUriRequest>(n);
for (int i = 0; i < n; i++) {
HttpGet request = new HttpGet("http://www.baidu.com?a="+i);
cache.add(request);
}
return cache;
}
public void start(List<HttpUriRequest> cache) throws InterruptedException {
HttpAsyncClient httpClient = new HttpAsyncClient();
int i = 0;
while (true){
final HttpUriRequest request = cache.get(i%cache.size());
httpClient.execute(request, new FutureCallback<HttpResponse>() {
public void completed(final HttpResponse response) {
System.out.println(request.getRequestLine() + "->" + response.getStatusLine());
}
public void failed(final Exception ex) {
System.out.println(request.getRequestLine() + "->" + ex);
}
public void cancelled() {
System.out.println(request.getRequestLine() + " cancelled");
}
});
i++;
Thread.sleep(100);
}
}
}
内存分析:
2.visualVM 中前后3分钟的内存对象占比情况:
3.visualVM 中前后3分钟的GC情况:
核心代码分析:
代码优化
找到问题的原因,我们现在来优化代码,验证我们的结论。因为List<HttpUriRequest> cache1中会保存回调对象,所以我们不能缓存请求类,只能缓存基本数据,在使用时进行动态的生成,来保证回调对象的及时回收。
代码如下:
public class ReplayApplication {
public static void main(String[] args) throws InterruptedException {
ReplayWithoutProblem replay2 = new ReplayWithoutProblem();
List<String> cache2 = replay2.loadMockRequest(10000);
replay2.start(cache2);
}
}
public class ReplayWithoutProblem {
public List<String> loadMockRequest(int n){
List<String> cache = new ArrayList<String>(n);
for (int i = 0; i < n; i++) {
cache.add("http://www.baidu.com?a="+i);
}
return cache;
}
public void start(List<String> cache) throws InterruptedException {
HttpAsyncClient httpClient = new HttpAsyncClient();
int i = 0;
while (true){
String url = cache.get(i%cache.size());
final HttpGet request = new HttpGet(url);
httpClient.execute(request, new FutureCallback<HttpResponse>() {
public void completed(final HttpResponse response) {
System.out.println(request.getRequestLine() + "->" + response.getStatusLine());
}
public void failed(final Exception ex) {
System.out.println(request.getRequestLine() + "->" + ex);
}
public void cancelled() {
System.out.println(request.getRequestLine() + " cancelled");
}
});
i++;
Thread.sleep(100);
}
}
}
结果验证
2.visualVM 中前后3分钟的内存对象占比情况:
总结
以上,便是今天的分享,希望大家喜欢,觉得内容不错的,欢迎点击「在看」支持,谢谢各位
喜欢文章,点个 在看