Java 服务内存占用过高的一次排查过程

1. 缘由

日常敲代码时,运维同事突然把小组人员都拉进了一个群里,说一台线上机器内存耗尽,OOM 导致服务注册的 Mesh 客户端被干掉了,部分服务调用异常。运维同事查看机器负载,发现我们组内一个Java 服务占用的内存有点异常,启动命令-Xmx128m 指定了最大堆内存只有 128M,但是整个进程占用的内存达到了 640M,显然是有问题的

2. 线上排查

运维截图一扔,锅是甩不掉的,老老实实登录到线上机器排查。内存占用过高首先想到的就是发生了内存泄露,使用 jmap -histo $pid > heap.log 输出堆内对象统计情况到文件中,查看文件发现堆中占用内存最多的是各种数组,没有发现明显的问题。没法子,使用 top -H p $pid命令检查该进程内运行的线程状况,终于发现了可疑点,在这个Java 服务里面运行的子线程居然有 5000 个,并且几乎全部都在 Sleeping 状态
在这里插入图片描述
这种情况首先想到的是发生了线程死锁,资源争用导致大量线程被阻塞了。使用 jstack -l $pid > stack.log 将线程栈相关状况输出到文件中,打开文件一搜索却大失所望,根本没有死锁发生。线程状态大都在 TIMED_WAITING,不过随着一行行往下看,也发现了一个可疑点,以下这种 OkHttp ConnectionPool 的线程出现得太多了,线程序号甚至达到了1082707

"OkHttp ConnectionPool" #1082707 daemon prio=5 os_prio=0 tid=0x00007f564c18f000 nid=0x1a4d in Object.wait() [0x00007f5602cb4000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        at java.lang.Object.wait(Object.java:460)
        at okhttp3.ConnectionPool$1.run(ConnectionPool.java:67)
        - locked <0x00000000fc30fb30> (a okhttp3.ConnectionPool)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - <0x00000000fc305f98> (a java.util.concurrent.ThreadPoolExecutor$Worker)

此时回过头来看堆内存文件,发现以下记录,okhttp3.ConnectionPool 这个连接池对象实例占用的内存虽然不多,只有300K,但是实例总数居然有 7876 个,毫无疑问是有问题的

  40:          7876         315040  okhttp3.ConnectionPool

3. 代码排查

可疑点在于 ConnectionPool 这个对象的数量,在项目中搜索 ConnectionPool 对象初始化调用点,发现 OkHttpClient 初始化时会调用 ConnectionPool 的构造方法,也就是每一个 OkHttpClient 实例被创建出来都会伴随着一个连接池 ConnectionPool的创建。而在项目代码中,每次进行 RPC 调用的时候都会重新创建一个 OkHttpClient 对象,至此一切豁然开朗

  • 原因剖析
    OkHttpClient 对象在使用过后会被 JVM 回收,但是 OkHttp源码中ConnectionPool的构造方法里默认最大线程空闲数是5,keepAlive 时间为5分钟,也就是发起一次网络连接后,5分钟内不会断开连接。这样当OkHttpClient 对象被 JVM 回收时,ConnectionPool 因为有线程还保持着与服务端的连接,处于Active状态,5 分钟内不会被回收,自然也不会释放线程资源。线程会占用内存空间,这样随着时间积累,线程数量越来越多,进程占用的内存自然也越来越多了
    /**
    * Create a new connection pool with tuning parameters appropriate for a single-user application.
    * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
    * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
    */
    public ConnectionPool() {
          
          
     this(5, 5, TimeUnit.MINUTES);
    }
    

4. 解决方法

问题根源找到了,修复自然简单。项目代码中每次进行 RPC 调用都重新创建一个 OkHttpClient 对象,这实际上是极大的性能浪费,只要在代码中保存一个静态 OkHttpClient 对象,每次网络请求都复用这个对象就可以了

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/104992561