okhttp导致的内存溢出(OOM)sun.security.ssl.SSLSocketImpl

  • 使用分析工具:MAT(Memory Analyzer Tool)、JvisualVM
  • 占用内存:sun.security.ssl.SSLSocketImpl

一、 项目场景:

功能:一个定时任务(xxl-job)采用线程池的方式多线程请求第三方拉取数据,网络框架使用okhttp3。
问题:执行job时,内存短时间内暴增,导致OOM


二、问题描述

  • 定时任务执行时,突然内存激增,OOM导致项目重启。
  • 下面这张图是重启后再次执行定时任务的内存监控

    image.png

三、原因分析:

3.1 查看堆栈信息

使用MAT查看堆栈信息,sun.security.ssl.SSLSocketImpl这个东西占了62%
image.png

点击Details ,可以看到有9k多个对象

image.png

使用OQL查询sun.security.ssl.SSLSocketImpl,发现其中的host都是请求第三方的地址

select * from sun.security.ssl.SSLSocketImpl

image.png
image.png

到这里,基本可以定位到是由于请求第三方资源没有释放,导致内存暴增。接下来查看请求第三方的代码

3.2 查看代码

看到底层工具类OkHttpClientUtil工具类中获取OkHttpClient对象的代码是这样的,每次请求都是new一个OkhttpClient对象,可能是每次都是new一个OkhttpClient的问题,于是在本地复现

   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()是我新写的

@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;

    }

}

测试类

@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对象)

控制打印可以看到不断的发出请求

image.png

使用jvisualvm工具(位于jdk bin目录下) 分析堆情况

执行后,发现堆在不断增大

image.png

点击菜单上的线程,看到一堆的等待线程OkHttp connectionPool(连接池)

image.png

将堆信息下载下来,用MAT分析

点击右上角堆Dump下载堆信息

image.png

使用MAT分析

发现最大占用的两个部分别是:sun.security.ssl.SSLSocketImplokhttp3.ConnectionPool(连接池),场景基本复现。

image.png

image.png
image.png

使用OQL查看

image.png

host地址是百度地址,基本复现

image.png

4.2.2 使用单例模式

使用getHttpClientSingleton()方法获取HttpClient对象(每次请求都new一个新的HttpClient对象)

使用jvisualVM监控

堆稳定,不会不断增加

image.png

等待线程也不多

image.png

4.3 为什么每次请求都创建OkHttpClient会导致内存溢出

分析完知道导致问题的原因是每次请求都去new一个OkHttpClient,那为什么会导致内存溢出呢?
路径:okhttp3.Dispatcher#executorService可以看到这块代码

  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实例,而不是每次都去创建

以下两种都可以

  • 使用单例模式
  • 使用静态代码块,只加载一次。
这个错误信息"anzhuo Could not initialize class sun.security.ssl.SSLContextImpl$DefaultSSLContext"通常与Android应用程序中的SSL/TLS连接初始化失败有关。这个问题可能由多种原因引起,以下是一些可能的原因和解决方案: 1. 缺少必要的加密库: 确保你的项目中包含了所有必要的加密库。某些设备可能缺少特定的加密算法实现。 2. 兼容性问题: 某些Android版本可能存在已知的SSL/TLS实现问题。检查你的应用是否兼容目标设备的Android版本。 3. 权限问题: 确保你的应用具有必要的网络权限。在AndroidManifest.xml中检查是否包含<uses-permission android:name="android.permission.INTERNET" />。 4. 证书问题: 检查服务器证书是否有效,是否被信任。可能需要更新CA证书或添加自定义信任管理器。 5. ProGuard混淆: 如果你使用了ProGuard混淆,确保正确配置了规则,以防止混淆关键的SSL相关类。 6. 设备特定问题: 某些设备制造商可能对Android系统进行了修改,影响了SSL/TLS实现。尝试在不同的设备上重现问题。 7. 代码中的错误: 检查你的代码,确保正确初始化SSLContext。例如: ```java SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, new java.security.SecureRandom()); ``` 8. 库冲突: 确保你的项目中没有包含多个版本的SSL库,这可能导致类加载冲突。 如果以上建议都不能解决问题,你可以尝试以下步骤: 1. 捕获并记录更详细的错误信息。 2. 使用调试器单步执行代码,检查SSLContext初始化失败的具体原因。 3. 在不同的设备和Android版本上测试应用程序。 4. 考虑使用更新的网络库,如OkHttp,它可能提供更好的SSL/TLS支持。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值