什么(JVM 老年代内存不断上涨竟是因为获取 ServletContext 姿势不对)
前几日一直在筹备一个比较大的项目,发现一个问题,还好流量不是非常非常大,不然又得提桶跑路了。在线上运行的时候发现当并发过高的情况,会出现老年代内存上涨的情况。
定位问题
【什么(JVM 老年代内存不断上涨竟是因为获取 ServletContext 姿势不对)】我找了个台机器 dump 了堆内存了,交给了我组攻坚小队长 LAY
,使用类似于 MAT
一样的内存分析工具,发现内存泄露的最大可能性是ConcurrentHashMap
,进一步在支配树
中发现,ConcurrentHashMap
存的都是org.apache.catalina.session.StandardSession
,基本定位是 tomcat 的 session 机制导致的。
但是我们项目是不依赖 tomcat 的 session 机制的,所有的会话保持都通过 ticket 去和账号中心交互,在我们系统不存储。怎么还是会用到 tomcat 的默认 session 对象呢?十万个问号在头脑里显现。
查看 org.apache.catalina.session.ManagerBase
代码,可以看到 session
是一个 ConcurrentHashMap
:
protected Map sessions = new();
可以用 arthas 看下线上的情况
watch org.apache.catalina.session.ManagerBase findSession '{params,returnObj,throwExp,target.sessions.size()}'-n 1-x 3
文章图片
通过排查代码发现,我们项目中,在一个 filter 里面写了一段如下代码
ServletContext sc = httpServletRequest.getSession().getServletContext()
搜代码是一个简单方案,但是不是 100% 有用,
如果搜不到代码,最好是通过 arthas stack
查看调用生成 session 的调用栈
好家伙,为什么获取ServletContext
要从 session 绕一圈呢?直接就能拿啊 httpServletRequest.getServletContext()
。Demo 验证 然后通过一个小实验可以发现当代码里不显性的调用 session 则不会存储 session 的。
@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 GMT200
可以看到想获取
ServletContext
其实不需要通过session
去取,两种方式获取到的是同一个对象。所以httpServletRequest.getSession().getServletContext()
方式大可不必。@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 GMT200%
现在返回的 header 里面就没有了
JSESSIONID
了。解决方案 虽然 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/...
推荐阅读
- 为什么你的路演总会超时()
- 眼光要放高远
- 时间老了
- 财商智慧课(六)
- 子龙老师语录
- 异地恋中,逐渐适应一个人到底意味着什么()
- 前任
- 做一件事情的基本原理是什么()
- 今天写一些什么
- 七老修复好敏感、角质层薄、红血丝