什么?JVM 老年代内存不断上涨竟是由于获取 ServletContext 姿式不对

2021年11月25日 阅读数:14
这篇文章主要向大家介绍什么?JVM 老年代内存不断上涨竟是由于获取 ServletContext 姿式不对,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前几日一直在筹备一个比较大的项目,发现一个问题,还好流量不是很是很是大,否则又得提桶跑路了。在线上运行的时候发现当并发太高的状况,会出现老年代内存上涨的状况。java

定位问题

我找了个台机器 dump 了堆内存了,交给了我组攻坚小队长 LAY,使用相似于 MAT 同样的内存分析工具,发现内存泄露的最大可能性是ConcurrentHashMap ,进一步在支配树中发现,ConcurrentHashMap 存的都是org.apache.catalina.session.StandardSession,基本定位是 tomcat 的 session 机制致使的。apache

可是咱们项目是不依赖 tomcat 的 session 机制的,全部的会话保持都经过 ticket 去和帐号中心交互,在咱们系统不存储。怎么仍是会用到 tomcat 的默认 session 对象呢?十万个问号在头脑里显现。 segmentfault

查看 org.apache.catalina.session.ManagerBase 代码,能够看到 session 是一个 ConcurrentHashMaptomcat

protected Map<String, Session> sessions = new<ConcurrentHashMap>();

能够用 arthas 看下线上的状况bash

watch org.apache.catalina.session.ManagerBase findSession '{params,returnObj,throwExp,target.sessions.size()}'  -n 1  -x 3

image.png

经过排查代码发现,咱们项目中,在一个 filter 里面写了一段以下代码session

ServletContext sc = httpServletRequest.getSession().getServletContext()
搜代码是一个简单方案,可是不是 100% 有用, 若是搜不到代码,最好是经过 arthas stack 查看调用生成 session 的调用栈

好家伙,为何获取ServletContext要从 session 绕一圈呢?直接就能拿啊 httpServletRequest.getServletContext()并发

Demo 验证

而后经过一个小实验能够发现当代码里不显性的调用 session 则不会存储 session 的。app

@RestController
@Slf4j
public class ApiController {

    @Autowired
    HttpServletRequest httpServletRequest;

    @RequestMapping("/hello")
    public String hello() {

        ServletContext sc1 = httpServletRequest.getServletContext();
        ServletContext sc2 = httpServletRequest.getSession().getServletContext();

        if (!sc1.equals(sc2)) {
            return "500";
        }

        return "200";
    }

}
$ curl -i http://127.0.0.1:8080/hello
HTTP/1.1 200 
Set-Cookie: JSESSIONID=7893E89199F79E2EC1BA0EB25D1DCD47; Path=/; HttpOnly
Content-Type: text/plain;charset=UTF-8
Content-Length: 3
Date: Mon, 25 Oct 2021 05:48:24 GMT

200

能够看到想获取ServletContext其实不须要经过session去取,两种方式获取到的是同一个对象。因此httpServletRequest.getSession().getServletContext()方式大可没必要。curl

@RestController
@Slf4j
public class ApiController {

    @Autowired
    HttpServletRequest httpServletRequest;

    @RequestMapping("/hello")
    public String hello() {

        ServletContext sc1 = httpServletRequest.getServletContext();

        return "200";
    }

}
$ curl -i http://127.0.0.1:8080/hello
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 3
Date: Mon, 25 Oct 2021 05:49:09 GMT

200%

如今返回的 header 里面就没有了JSESSIONID了。jvm

解决方案

虽然 tomcat 自己是有过时 session 清理方案的,ContainerBackgroundProcessor 线程专门作这件事,能够经过配置

server.tomcat.background-processor-delay = 10

来启动,默认是不开启的。不过并发太高的状况,也于事无补,毕竟默认是30分钟的过时时间。而且咱们如今的应用都不依赖 tomcat 默认的 session 因此不要调用 getSession 的方法是最好的。

为何是老年代

年轻代其实分为两部分,分别是1个Eden区和2个Survivor区(分别叫from和to),默认比例是8:1:1,通常状况下,新建立的对象都会被分配到Eden区,(除非一些特别大的对象会直接放到老年代),当Eden没有足够的空间的时候,就会触发jvm发起一次Minor GC,若是对象通过一次Minor GC还存活,而且又能被Survivor空间接受,那么将被移动到Survivor空间当中,对象在Survivor区中每熬过一次Minor GC,年龄就会增长一岁,当它的年龄增长到必定程度(15岁)时,就会被移到老年代中,固然晋升老年代的年龄能够经过-XX:MaxTenuringThreshold来设置。

https://zhuanlan.zhihu.com/p/...