1、背景

有一微服务,当系统没有用户在使用时,内存占用率也很高,导致实际可用内存大大减小。

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

微服务中有一个拦截器,当http请求过来,获取请求的header信息,然后在内存中组装出一个对象,放到ThreadLocal对象或属性中,方便后面的Controller、Service层等地方去使用。

2、快照文件分析

overview点击最大的一块,List Object

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

找到线程的HandlerMetthod:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

成功在description中找到可疑的类和方法:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

在直方图和支配树中按深堆排序:发现了UserDataContextHolder类的UserData类:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

问题点基本定位。

3、相关源码与本地复现

有一个用户上下文的处理类,其有一个ThreadLoacl的属性,UserData中用一个10m的byte数组模拟存入了用户信息:

public class UserDataContextHolder {
    public  static ThreadLocal<UserData> userData = new ThreadLocal<>();

    public static class UserData{
        byte[] data = new byte[1024 * 1024 * 10]; //模拟保存10m的用户数据
    }
}

写个拦截器,模拟组装出一个对象,放到ThreadLocal:

/**
 * 拦截器的实现,模拟放入数据到threadlocal中
 */
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDataContextHolder.userData.set(new UserDataContextHolder.UserData());
         return true;
    }

    /**
     * 坑在:若前面的代码执行过程抛出异常,则postHandle方法不执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserDataContextHolder.userData.remove();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}


接口中模拟中途出错:

@RestController
@RequestMapping("/threadlocal")
public class DemoThreadLocal {

    @GetMapping
    public ResponseEntity test() {
        error();  //模拟发生错误或者异常
        return ResponseEntity.ok().build();
    }

    private void error() {
        throw new RuntimeException("出错了");
    }

}

Jmeter中模拟50并发:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP
最后发生OOM

4、解决思路

import com.itheima.jvmoptimize.practice.demo.common.UserDataContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 拦截器的实现,模拟放入数据到threadlocal中
 */
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        UserDataContextHolder.userData.set(new UserDataContextHolder.UserData());
         return true;
    }

    /**
     * 坑在:若前面的代码执行过程抛出异常,则postHandle方法不执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //UserDataContextHolder.userData.remove();
    }

    /**
     * 这次改在afterCompletion里去remove
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserDataContextHolder.userData.remove();
    }
}


5、补充

web请求过来,tomcat中的线程池等长时间不用了,线程被回收,ThreadLocal也就被回收了,按理说不会有内存泄漏。但当前项目中,部分配置如下:

server:
  port: 8881
  tomcat:
    threads:
      min-spare: 50
      max: 500

以上对tomcat的线程配置做了定制化,即最大线程500个,100个核心线程数,这100个线程,即使空闲下来,也不会去做回收。因此上面50个线程过来,最后会有500m的空间一直被占着。上面本地复现只给了100M的堆内存,直接oom了,现在改成600m,看看空间占用:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

和之前分析的一样,请求结束后,JVM堆内存占用依旧很高:

【开发篇】九、系统不处理业务时也占用大量内存-LMLPHP

当然,你也可以将配置里的tomcat的核心线程数改成0,这样,内存一段时间后也会被回收。但改成0,这个配置不会生效,可以取一个最小值(10)。

03-27 11:23