Session清理
Background 线程
前面我们分析了 Session 的创建过程,而 Session 会话是有时效性的,下面我们来看下 tomcat 是如何进行失效检查的。在分析之前,我们先回顾下 Container
容器的 Background 线程。
tomcat 所有容器组件,都是继承至 ContainerBase
的,包括 StandardEngine
、StandardHost
、StandardContext
、StandardWrapper
,而 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
方法,在调用子容器的后台任务之前,还会调用 Loader
、Manager
、WebResourceRoot
、InstanceManager
的后台任务,这里我们只关心 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 信息进行清理,而这个时候会更新 thisAccessedTime
、lastAccessedTime
时间戳。此外,我们通过调用 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();