什么?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<String, Session> sessions = new<ConcurrentHashMap>();

可以用 arthas 看下线上的情况

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

image.png

通过排查代码发现,我们项目中,在一个 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 GMT

200

可以看到想获取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 GMT

200%

现在返回的 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/...

周梦康
金三银四 还不上车补课?学了就是赚了 [链接]

退隐江湖

8.9k 声望
6.7k 粉丝
0 条评论
推荐阅读
刨根问底 Redis, 面试过程真好使
充满寒气的互联网如何在面试中脱颖而出,平时积累很重要,八股文更不能少!下面带来的这篇 Redis 问答希望能够在你的 offer 上增添一把🔥。

菜农曰15阅读 833

封面图
Java 编译器 javac 及 Lombok 实现原理解析
javac 是 Java 代码的编译器12,初学 Java 的时候就应该接触过。本文整理一些 javac 相关的高级用法。Lombok 库,大家平常一直在使用,但可能并不知道实现原理解析,其实 Lombok 实现上依赖的是 Java 编译器的注...

nullwy10阅读 5.9k

与RabbitMQ有关的一些知识
工作中用过一段时间的Kafka,不过主要还是RabbitMQ用的多一些。今天主要来讲讲与RabbitMQ相关的一些知识。一些基本概念,以及实际使用场景及一些注意事项。

lpe2348阅读 1.8k

封面图
Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 1.8k评论 2

封面图
Redis 发布订阅模式:原理拆解并实现一个消息队列
“65 哥,如果你交了个漂亮小姐姐做女朋友,你会通过什么方式将这个消息广而告之给你的微信好友?““那不得拍点女朋友的美照 + 亲密照弄一个九宫格图文消息在朋友圈发布大肆宣传,暴击单身狗。”像这种 65 哥通过朋...

码哥字节5阅读 1.1k

封面图
NB的Github项目,看到最后一个我惊呆了!
最近看到不少好玩的、实用的 Github 项目,就来给大家推荐一把。中国制霸生成器最近在朋友圈非常火的一个小网站,可以在线标记 居住、短居、游玩、出差、路过 标记后可生成图片进行社区分享,标记过的信息会记录...

艾小仙5阅读 1.5k评论 1

好好的系统,为什么要分库分表?
今天是《分库分表 ShardingSphere 原理与实战》系列的开篇文章,之前写过几篇关于分库分表的文章反响都还不错,到现在公众号:程序员小富后台不断的有人留言、咨询分库分表的问题,我也没想到大家对于分库分表的话...

程序员小富3阅读 1.5k

退隐江湖

8.9k 声望
6.7k 粉丝
宣传栏