[JavaWeb]_[中级]_[Tomcat在关闭时报指向Jfinal框架的错误]

文章讲述了在部署JFinal应用到Tomcat时遇到的内存泄漏问题,主要涉及ThreadLocal的使用。解决方案包括自定义WriterBuffer以避免ThreadLocal变量导致的内存泄漏,以及通过重写CaptchaCache的removeAll方法来关闭内部的Timer,防止定时任务引起的内存占用。文章还提醒注意清理其他可能的定时任务、线程池等资源,确保Tomcat能正常关闭。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

场景

  1. 在部署jfinal程序到tomcat的时候,使用startup.bat启动网站,之后用shutdown.bat关闭网站,发现命令行窗口无法终止退出。报以下错误, 怎么处理?
06-Mar-2023 11:09:24.534 严重 [localhost-startStop-2] org.apache.catalina.loader
.WebappClassLoaderBase.checkThreadLocalMapForLeaks web应用程序[ROOT]创建了一个Th
readLocal,其键类型为[com.jfinal.template.io.WriterBuffer$1](值为[com.jfinal.te
mplate.io.WriterBuffer$1@36d6d87d]),值类型为[com.jfinal.template.io.ByteWriter
](值为[com.jfinal.template.io.ByteWriter@24e46eae),但在停止web应用程序时未能
将其删除。线程将随着时间的推移而更新,以尝试避免可能的内存泄漏
09-Mar-2023 08:49:59.960 WARNING [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [ROOT] appears to have started a thread named [CaptchaCache] but has failed to stop it. This is very likely to create a memory leak. Stack traceof thread:

说明

问题1.

  1. 第一个错误是jfinal里使用了WriterBuffer类,这个类用于缓存输出对象。 看它的源码,它创建了一个ThreadLocal线程本地变量,这个变量的特点有:
    • 变量byteWriters只有它所属的线程可以访问到它的值,也就是ByteWriter, 目的是为了在线程范围内重用ByteWriter缓存。

    • 注意: 有多少个线程调用getByteWriter()就有多少个ByteWriter实例存储在每个线程里。即使tomcat关闭,线程里仍然有这些实例的引用,因此这些实例就不会被释放。

    • 解决办法就是不使用这个线程本地变量,实现一个WriteBuffer的子类。

    • 注意: 这个线程本地变量重用缓存部分一般在服务器端控制大文件输出时才会用到, 输出html并不会重用,因此这个线程本地变量可以去掉。

WriterBuffer.java

package com.jfinal.template.io;

/**
 * WriterBuffer
 */
public class WriterBuffer {
	
	private static final int MIN_BUFFER_SIZE = 64;					// 缓冲区最小 64 字节
	private static final int MAX_BUFFER_SIZE = 1024 * 1024 * 2;		// 缓冲区最大 2M 字节
	
	private int bufferSize = 1024;									// 缓冲区大小
	private int reentrantBufferSize = 128;							// 可重入缓冲区大小
	
	private EncoderFactory encoderFactory = new EncoderFactory();
	
	private final ThreadLocal<ByteWriter> byteWriters = new ThreadLocal<ByteWriter>() {
		protected ByteWriter initialValue() {
			return new ByteWriter(encoderFactory.getEncoder(), bufferSize);
		}
	};
	
	...
	
	public ByteWriter getByteWriter(java.io.OutputStream outputStream) {
		ByteWriter ret = byteWriters.get();
		if (ret.isInUse()) {
			ret = new ByteWriter(encoderFactory.getEncoder(), reentrantBufferSize);
		}
		return ret.init(outputStream);
	}
	
	...
}

MyWriterBuffer.java

import com.jfinal.template.io.*;

public class MyWriterBuffer extends WriterBuffer {

    private int bufferSize = 1024;									// 缓冲区大小

    private int reentrantBufferSize = 128;							// 可重入缓冲区大小
    protected EncoderFactory encoderFactory = new EncoderFactory();

    protected final ThreadLocal<ByteWriter> byteWriters = new ThreadLocal<ByteWriter>() {
        protected ByteWriter initialValue() {
            return new ByteWriter(encoderFactory.getEncoder(), bufferSize);
        }
    };

    public ByteWriter getByteWriter(java.io.OutputStream outputStream) {
        ByteWriter ret = new ByteWriter(encoderFactory.getEncoder(), reentrantBufferSize);
        return ret.init(outputStream);
    }

    public void setEncoderFactory(EncoderFactory encoderFactory) {
        if (encoderFactory == null) {
            throw new IllegalArgumentException("encoderFactory can not be null");
        }
        this.encoderFactory = encoderFactory;
    }
}

问题2

  1. 使用了jfinal自带的验证码类缓存CaptchaCache就会报这个错. 它是通过CaptchaManager.getCaptchaCache()获取的。
    • 报错提示说它创建了一个线程,查看它的源码,发现它在内部创建了一个定时器Timer,而Timer就是一个线程。

    • 这个定时器不是一次性的,不会自己关闭,所以还需要手动关闭这个定时器。

    • timer属性是private的,因此也只能通过反射获取这个属性值。

    • 可以通过子类化重载removeAll方法来获取并关闭timer

CaptchaCache.java

public class CaptchaCache implements ICaptchaCache {
	
	private ConcurrentHashMap<String, Captcha> map = new ConcurrentHashMap<String, Captcha>();
	private int interval = 90 * 1000;	// timer 调度间隔为 90 秒
	private Timer timer;
	
	public CaptchaCache() {
		autoRemoveExpiredCaptcha();
	}
	
	/**
	 * 定期移除过期的验证码
	 */
	private void autoRemoveExpiredCaptcha() {
		timer = new Timer("CaptchaCache", true);
		timer.schedule(
			new TimerTask() {
				public void run() {
					for (Entry<String, Captcha> e : map.entrySet()) {
						if (e.getValue().isExpired()) {
							map.remove(e.getKey());
						}
					}
				}
			},
			interval,
			interval
		);
	}
	...

MyCaptchaCache.java

import com.jfinal.captcha.CaptchaCache;

import java.lang.reflect.Field;
import java.util.Timer;

public class MyCaptchaCache extends CaptchaCache {

    public void removeAll() {
        super.removeAll();

        Field f= null;
        try {
            f = getClass().getSuperclass().getDeclaredField("timer");
            f.setAccessible(true);

            Timer timer = (Timer) f.get(this);
            if(timer != null)
                timer.cancel();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

如何使用?

  1. 可以在配置类JFinalConfig的子类DemoConfig里使用,用两个新类的实例替换默认的实例。
MyWriterBuffer writerBuffer = new MyWriterBuffer();
MyCaptchaCache captchaCache = new MyCaptchaCache();


public void configEngine(Engine me) {
		...
		me.getEngineConfig().setWriterBuffer(writerBuffer);
}

public void configConstant(Constants me) {
		...
		me.setCaptchaCache(captchaCache);
}

@Override
public void onStop() {
	// 异步任务线程池
	//ThreadUtils.get().getExecutor().shutdown();

	// 数据库连接池
	//DemoConfig.stopDruid();

	//BUG: 1. 貌似Jfinal没有释放JDBCDriver.
	//clearJDBCDriver();

	CaptchaManager.me().getCaptchaCache().removeAll();

	// 日志服务
	//LogManager.shutdown();
}

其他

  1. 注意: 如果还有其他定时任务,线程池,Druid数据库连接池或其他线程本地变量也应该在onStop()方法清除掉。

  2. linux上, 可能会使用ubic stop tomcat8工具来关闭tomcat。这时候如果不处理好这些内存泄露的问题会关闭不了,造成网站已关闭的假象,导致如果再次ubic start tomcat8启动网站会报端口占用错误。

参考

  1. 开发JavaWeb网站精讲-基于JFinal框架

  2. JFinal 文档、资料、学习、API,引擎配置

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter(阿斯拉达)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值