1、背景
有一微服务,当系统没有用户在使用时,内存占用率也很高,导致实际可用内存大大减小。
微服务中有一个拦截器,当http请求过来,获取请求的header信息,然后在内存中组装出一个对象,放到ThreadLocal对象或属性中,方便后面的Controller、Service层等地方去使用。
2、快照文件分析
overview点击最大的一块,List Object
找到线程的HandlerMetthod:
成功在description中找到可疑的类和方法:
在直方图和支配树中按深堆排序:发现了UserDataContextHolder类的UserData类:
问题点基本定位。
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并发:
最后发生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,看看空间占用:
和之前分析的一样,请求结束后,JVM堆内存占用依旧很高:
当然,你也可以将配置里的tomcat的核心线程数改成0,这样,内存一段时间后也会被回收。但改成0,这个配置不会生效,可以取一个最小值(10)。