Tomcat源码分析-Session源码解析

news/2024/11/8 23:06:59/

Session清理

Background 线程

前面我们分析了 Session 的创建过程,而 Session 会话是有时效性的,下面我们来看下 tomcat 是如何进行失效检查的。在分析之前,我们先回顾下 Container 容器的 Background 线程。

tomcat 所有容器组件,都是继承至 ContainerBase 的,包括 StandardEngineStandardHostStandardContextStandardWrapper,而 ContainerBase 在启动的时候,如果 backgroundProcessorDelay 参数大于 0 则会开启 ContainerBackgroundProcessor 后台线程,调用自己以及子容器的 backgroundProcess 进行一些后台逻辑的处理,和 Lifecycle 一样,这个动作是具有传递性的,也就是说子容器还会把这个动作传递给自己的子容器,如下图所示,其中父容器会遍历所有的子容器并调用其 backgroundProcess 方法,而 StandardContext 重写了该方法,它会调用 StandardManager#backgroundProcess() 进而完成 Session 的清理工作。看到这里,不得不感慨 tomcat 的责任

关键代码如下所示:

ContainerBase.java(省略了异常处理代码)protected synchronized void startInternal() throws LifecycleException {// other code......// 开启ContainerBackgroundProcessor线程用于处理子容器,默认情况下backgroundProcessorDelay=-1,不会启用该线程threadStart();
}protected class ContainerBackgroundProcessor implements Runnable {public void run() {// threadDone 是 volatile 变量,由外面的容器控制while (!threadDone) {try {Thread.sleep(backgroundProcessorDelay * 1000L);} catch (InterruptedException e) {// Ignore}if (!threadDone) {processChildren(ContainerBase.this);}}}protected void processChildren(Container container) {container.backgroundProcess();Container[] children = container.findChildren();for (int i = 0; i < children.length; i++) {// 如果子容器的 backgroundProcessorDelay 参数小于0,则递归处理子容器// 因为如果该值大于0,说明子容器自己开启了线程处理,因此父容器不需要再做处理if (children[i].getBackgroundProcessorDelay() <= 0) {processChildren(children[i]);}}}

Session 检查

backgroundProcessorDelay 参数默认值为 -1,单位为秒,即默认不启用后台线程,而 tomcat 的 Container 容器需要开启线程处理一些后台任务,比如监听 jsp 变更、tomcat 配置变动、Session 过期等等,因此 StandardEngine 在构造方法中便将 backgroundProcessorDelay 参数设为 10(当然可以在 server.xml 中指定该参数),即每隔 10s 执行一次。那么这个线程怎么控制生命周期呢?我们注意到 ContainerBase 有个 threadDone 变量,用 volatile 修饰,如果调用 Container 容器的 stop 方法该值便会赋值为 false,那么该后台线程也会退出循环,从而结束生命周期。另外,有个地方需要注意下,父容器在处理子容器的后台任务时,需要判断子容器的 backgroundProcessorDelay 值,只有当其小于等于 0 才进行处理,因为如果该值大于0,子容器自己会开启线程自行处理,这时候父容器就不需要再做处理了

前面分析了容器的后台线程是如何调度的,下面我们重点来看看 webapp 这一层,以及 StandardManager 是如何清理过期会话的。StandardContext 重写了 backgroundProcess 方法,除了对子容器进行处理之外,还会对一些缓存信息进行清理,关键代码如下所示:

StandardContext.java@Override
public void backgroundProcess() {if (!getState().isAvailable())return;// 热加载 class,或者 jsp Loader loader = getLoader();if (loader != null) {loader.backgroundProcess();}// 清理过期SessionManager manager = getManager();if (manager != null) {manager.backgroundProcess();}// 清理资源文件的缓存WebResourceRoot resources = getResources();if (resources != null) {resources.backgroundProcess();}// 清理对象或class信息缓存InstanceManager instanceManager = getInstanceManager();if (instanceManager instanceof DefaultInstanceManager) {((DefaultInstanceManager)instanceManager).backgroundProcess();}// 调用子容器的 backgroundProcess 任务super.backgroundProcess();

StandardContext 重写了 backgroundProcess 方法,在调用子容器的后台任务之前,还会调用 LoaderManagerWebResourceRootInstanceManager 的后台任务,这里我们只关心 Manager 的后台任务。弄清楚了 StandardManager 的来龙去脉之后,我们接下来分析下具体的逻辑。

StandardManager 继承至 ManagerBase,它实现了主要的逻辑,关于 Session 清理的代码如下所示。backgroundProcess 默认是每隔10s调用一次,但是在 ManagerBase 做了取模处理,默认情况下是 60s 进行一次 Session 清理。tomcat 对 Session 的清理并没有引入时间轮,因为对 Session 的时效性要求没有那么精确,而且除了通知 SessionListener

ManagerBase.javapublic void backgroundProcess() {// processExpiresFrequency 默认值为 6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次count = (count + 1) % processExpiresFrequency;if (count == 0) // 默认每隔 60s 执行一次 Session 清理processExpires();
}/*** 单线程处理,不存在线程安全问题*/
public void processExpires() {long timeNow = System.currentTimeMillis();Session sessions[] = findSessions();    // 获取所有的 Sessionint expireHere = 0 ;for (int i = 0; i < sessions.length; i++) {// Session 的过期是在 isValid() 里面处理的if (sessions[i]!=null && !sessions[i].isValid()) {expireHere++;}}long timeEnd = System.currentTimeMillis();// 记录下处理时间processingTime += ( timeEnd - timeNow );

清理过期 Session

在上面的代码,我们并没有看到太多的过期处理,只是调用了 sessions[i].isValid(),原来清理动作都在这个方法里面处理的,相当的隐晦。在 StandardSession#isValid() 方法中,如果 now - thisAccessedTime >= maxInactiveInterval 则判定当前 Session 过期了,而这个 thisAccessedTime 参数在每次访问都会进行更新

public boolean isValid() {// other code......// 如果指定了最大不活跃时间,才会进行清理,这个时间是 Context.getSessionTimeout(),默认是30分钟if (maxInactiveInterval > 0) {int timeIdle = (int) (getIdleTimeInternal() / 1000L);if (timeIdle >= maxInactiveInterval) {expire(true);}}return this.isValid;

而 expire 方法处理的逻辑较繁锁,下面我用伪代码简单地描述下核心的逻辑,由于这个步骤可能会有多线程进行操作,因此使用 synchronized 对当前 Session 对象加锁,还做了双重校验,避免重复处理过期 Session。它还会向 Container 容器发出事件通知,还会调用 HttpSessionListener 进行事件通知,这个也就是我们 web 应用开发的 HttpSessionListener 了。由于 Manager 中维护了 Session 对象,因此还要将其从 Manager 移除。Session 最重要的功能就是存储数据了,可能存在强引用,而导致 Session 无法被 gc 回收,因此还要移除内部的 key/value 数据。由此可见,tomcat 编码的严谨性了,稍有不慎将可能出现并发问题,以及出现内存泄露

public void expire(boolean notify) {1、校验 isValid 值,如果为 false 直接返回,说明已经被销毁了synchronized (this) {   // 加锁2、双重校验 isValid 值,避免并发问题Context context = manager.getContext();if (notify) {   Object listeners[] = context.getApplicationLifecycleListeners();HttpSessionEvent event = new HttpSessionEvent(getSession());for (int i = 0; i < listeners.length; i++) {3、判断是否为 HttpSessionListener,不是则继续循环4、向容器发出Destory事件,并调用 HttpSessionListener.sessionDestroyed() 进行通知context.fireContainerEvent("beforeSessionDestroyed", listener);listener.sessionDestroyed(event);context.fireContainerEvent("afterSessionDestroyed", listener);}5、从 manager 中移除该  session6、向 tomcat 的 SessionListener 发出事件通知,非 HttpSessionListener7、清除内部的 key/value,避免因为强引用而导致无法回收 Session 对象}

由前面的分析可知,tomcat 会根据时间戳清理过期 Session,那么 tomcat 又是如何更新这个时间戳呢?我们在 StandardSession#thisAccessedTime 的属性上面打个断点,看下调用栈。原来 tomcat 在处理完请求之后,会对 Request 对象进行回收,并且会对 Session 信息进行清理,而这个时候会更新 thisAccessedTimelastAccessedTime 时间戳。此外,我们通过调用 request.getSession() 这个 API 时,在返回 Session 时会调用 Session#access() 方法,也会更新 thisAccessedTime 时间戳。这样一来,每次请求都会更新时间戳,可以保证 Session 的鲜活时间

方法调用栈如下所示:

关键代码如下所示:

org.apache.catalina.connector.Request.javaprotected void recycleSessionInfo() {if (session != null) {  session.endAccess();    // 更新时间戳}// 回收 Request 对象的内部信息session = null;requestedSessionCookie = false;requestedSessionId = null;requestedSessionURL = false;requestedSessionSSL = false;
org.apache.catalina.session.StandardSession.javapublic void endAccess() {isNew = false;if (LAST_ACCESS_AT_START) {     // 可以通过系统参数改变该值,默认为falsethis.lastAccessedTime = this.thisAccessedTime;this.thisAccessedTime = System.currentTimeMillis();} else {this.thisAccessedTime = System.currentTimeMillis();this.lastAccessedTime = this.thisAccessedTime;}
}public void access() {this.thisAccessedTime = System.currentTimeMillis();

http://www.ppmy.cn/news/31146.html

相关文章

一句话设计模式6:享元模式

享元模式:局部单例模式。 文章目录 享元模式:局部单例模式。前言一、享元模式的作用二、如何实现享元模式总结前言 享元模式其实很简单,但是如果用好,确实可以达到减少内存,事半功倍的效果;适合 系统要创建大量相似对象,相同对象等; 一、享元模式的作用 1 享元模式可以解决对象…

【Linux】进程状态(阻塞、挂起、僵尸进程)

文章目录1 阻塞与挂起1.1 阻塞1.2 挂起2 进程状态前言&#xff1a; 当我们在Windows下双击运行一个程序&#xff0c;或是在Linux下通过 ./ 加载运行一个程序&#xff0c;是否就代表对应的进程就一直处在运行状态呢&#xff1f;其实不然&#xff0c;一个进程有许多不同的状态。当…

JDK8新特性宝典

JDK8新特性 ​ Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机&#xff0c;Java 仍是企业和开发人员的首选开发平台 课程内容的介绍 了解Java发展史Lambda表达式…

[Tomcat]解决IDEA中的Tomcat中文乱码问题

目录 1、IDEA 2、VM options 3、IDEA启动程序的存放目录 4、Tomcat 写在前面&#xff1a;此方法亲测有效&#xff01;&#xff01;&#xff01; 1、IDEA 2、VM options 加上这两行&#xff1a; -Dfile.encodingUTF-8 -Dconsole.encodingUTF-8 3、IDEA启动程序的存放目录…

Kubernetes ConfigMap简介

ConfigMap Service是Kubernetes系统中非常重要的一个核心概念&#xff0c;今天来学习另外一个非常重要的资源对象&#xff1a;ConfigMap&#xff0c;我们知道许多应用经常会有从配置文件、命令行参数或者环境变量中读取一些配置信息&#xff0c;这些配置信息我们肯定不会直接写…

华为OD机试 - 端口合并(C 语言解题)【独家】

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 使用说明本期题目:端口合并…

Spring整体架构包含哪些组件?

Spring是一个轻量级java开源框架。Spring是为了解决企业应用开发的复杂性而创建的&#xff0c;它使用基本的JavaBean来完成以前只可能由EJB完成的事情。 Spring的用途不仅限于服务器端的开发&#xff0c;从简单性、可测试性和松耦合的角度而言&#xff0c;任何java应用都可以从…

【备战面试】TCP的三次握手与四次挥手

本篇总结的是计算机网络知识相关的面试题&#xff0c;后续也会更新其他相关内容 文章目录1、TCP头部结构2、三次握手3、四次挥手4、为什么TCP连接的时候是三次&#xff1f;两次是否可以&#xff1f;5、为什么TCP连接的时候是三次&#xff0c;关闭的时候却是四次&#xff1f;6、…