- 使用分析工具:MAT(Memory Analyzer Tool)、JvisualVM
- 占用内存:sun.security.ssl.SSLSocketImpl
一、 项目场景:
功能:一个定时任务(xxl-job)采用线程池的方式多线程请求第三方拉取数据,网络框架使用okhttp3。
问题:执行job时,内存短时间内暴增,导致OOM
二、问题描述
-
定时任务执行时,突然内存激增,OOM导致项目重启。
-
下面这张图是重启后再次执行定时任务的内存监控
三、原因分析:
3.1 查看堆栈信息
使用MAT查看堆栈信息,sun.security.ssl.SSLSocketImpl
这个东西占了62%
点击
Details
,可以看到有9k多个对象
使用OQL查询sun.security.ssl.SSLSocketImpl,发现其中的host都是请求第三方的地址
select * from sun.security.ssl.SSLSocketImpl
到这里,基本可以定位到是由于请求第三方资源没有释放,导致内存暴增。接下来查看请求第三方的代码
3.2 查看代码
看到底层工具类OkHttpClientUtil
工具类中获取OkHttpClient对象的代码是这样的,每次请求都是new一个OkhttpClient对象,可能是每次都是new一个OkhttpClient的问题,于是在本地复现。
c
private static OkHttpClient getHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(obtainConnectTimeOut(), TimeUnit.MILLISECONDS)
.writeTimeout(obtainWriteTimeOut(), TimeUnit.MILLISECONDS)
.readTimeout(obtainReadTimeOut(), TimeUnit.MILLISECONDS)
.build();
}
四、场景复现:
模拟生产,采用线程池方式多线程请求,请求地址改为百度,数据随便塞一点只要正常相应就行。
4.1代码
OkHttpClientUtil
工具类,getHttpClient()
是之前的,getHttpClientSingleton()
是我新写的
java
@Slf4j
public class OkHttpClientUtil {
private static final MediaType TYPE_JSON = MediaType.parse("application/json; charset=utf-8");
private volatile static OkHttpClient okHttpClient;
public static OkHttpClient getHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(30000, TimeUnit.MILLISECONDS)
.writeTimeout(1800000, TimeUnit.MILLISECONDS)
.readTimeout(1800000, TimeUnit.MILLISECONDS)
.build();
}
/**
* 单例双重检测
*
* @return
*/
public static OkHttpClient getHttpClientSingleton() {
if (null == okHttpClient) {
synchronized (OkHttpClient.class) {
if (null == okHttpClient) {
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30000, TimeUnit.MILLISECONDS)
.writeTimeout(1800000, TimeUnit.MILLISECONDS)
.readTimeout(1800000, TimeUnit.MILLISECONDS)
.build();
}
}
}
return okHttpClient;
}
}
测试类
java
@Slf4j
@SpringBootTest
public class SpringAmqpTest {
@Bean(name = "banksAssetTaskExecutor")
public TaskExecutor assetTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(20);
// 设置最大线程数
executor.setMaxPoolSize(100);
// 设置队列容量
executor.setQueueCapacity(1000);
// 设置默认线程名称
executor.setThreadNamePrefix("AssetTaskExecutor-api-thread");
// 设置线程池拒绝策略:抛弃旧的
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.initialize();
return executor;
}
@Resource
private TaskExecutor assetTaskExecutor;
@Test
public void test() throws Exception {
final CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
assetTaskExecutor.execute(() -> {
//每个线程执行1000个请求
for (int j = 0; j < 10000; j++) {
try {
long l1 = System.currentTimeMillis();
Response response = requestBaidu();
long l2 = System.currentTimeMillis();
log.info("线程id{},请求响应时间{},相应内容{},", Thread.currentThread().getName(), l2 - l1, response);
} catch (Exception e) {
log.info("执行失败Excetion:", e);
}
}
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("执行完成!!!!");
}
private Response requestBaidu() throws IOException {
// //获取OkHttpClient对象(getHttpClient()\getHttpClientSingleton())
OkHttpClient okHttpClient = OkHttpClientUtil.getHttpClient();
Map<String, String> map = new HashMap<>();
map.put("江", "哈哈");
String json = JSONObject.toJSONString(map);
RequestBody body = RequestBody.create(TYPE_JSON, json);
Request request = new Request.Builder().url("https://baidu.com/").post(body).build();
Response response = okHttpClient.newCall(request).execute();
return response;
}
}
4.2 测试结果
4.2.1 每次都new HttpClient
使用getHttpClient()方法获取HttpClient对象(每次请求都new一个新的HttpClient对象)
控制打印可以看到不断的发出请求
使用jvisualvm工具(位于jdk bin目录下)
分析堆情况
执行后,发现堆在不断增大
点击菜单上的
线程
,看到一堆的等待线程OkHttp connectionPool(连接池)
将堆信息下载下来,用MAT
分析
点击右上角
堆Dump
下载堆信息
使用MAT分析
发现最大占用的两个部分别是:
sun.security.ssl.SSLSocketImpl
和okhttp3.ConnectionPool
(连接池),场景基本复现。
使用OQL查看
host地址是百度地址,基本复现
4.2.2 使用单例模式
使用getHttpClientSingleton()方法获取HttpClient对象(每次请求都new一个新的HttpClient对象)
使用jvisualVM
监控
堆稳定,不会不断增加
等待线程也不多
4.3 为什么每次请求都创建OkHttpClient会导致内存溢出
分析完知道导致问题的原因是每次请求都去new一个OkHttpClient,那为什么会导致内存溢出呢?
路径:
okhttp3.Dispatcher#executorService
可以看到这块代码
java
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
从这里可以知道每个okHttpClient
对象在请求的时候都会创建一个线程池(连接池),而且线程池的keepAliveTime是1分钟;
由于之前的代码是每次请求都new一个OkHttpClient
对象,所以每次请求都会new一个新的线程池,在一分钟内大量进行请求的会,内存会在短时间内暴涨。
解决办法依就是只使用一个OkHttpClient
五、解决方案:
解决方法就是只使用一个OkHttpClient实例,而不是每次都去创建
以下两种都可以
- 使用单例模式
- 使用静态代码块,只加载一次。