tomcat启动失败时的ClassNotFoundException

tomcat在部署应用时,如果部署失败,该应用会被stop,但如果该应用的WebappClassLoader被其它线程持有并且继续使用的话,可能发生异常。

应用部署失败被stop的时候,classLoader也被stop(因为WebappClassLoader实现了Lifecycle接口),stop的时候,相关的 files/jars/resoures/repositories/parent 等等都被清除掉了,同时标识应用是否启动的started状态也被设置为false;基本上该classloader已经不可用了。(除了还可以委托给j2seClassLoader这个bootstrap classloader,这个classloader实际运行时是ExtClassLoader)

假设应用里有一个类自己启动了一个线程,逻辑大致如下:

static {
    new Thread(){
        public void run(){
            // 1) 先sleep一段时间,确保应用完成初始化
            // 2) 然后执行一些逻辑
        }
    }.start();
}

上面线程在执行后续逻辑的时候,使用WebappClassLoader去加载类,如果在sleep的阶段app在tomcat中没有启动成功,被stop了,则后边的逻辑有些意思。

看一下loadClass方法:

    public synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {

    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class<?> clazz = null;

    // Log access to stopped classloader
    if (!started) {
        try {
            throw new IllegalStateException(); //这里抛出异常,又被自己捕获
        } catch (IllegalStateException e) {
            log.warn(sm.getString("webappClassLoader.stopped", name));
        }
    }

    // (0) Check our previously loaded local class cache
    ...

    // (0.1) Check our previously loaded class cache
    ...

    // (0.2) Try loading the class with the system class loader, to prevent
    //       the webapp from overriding J2SE classes
    ...

    // (0.5) Permission to access this class when using a SecurityManager
    ...

    // (1) Delegate to our parent if requested
    ...

    // (2) Search local repositories
    ...

    // (3) Delegate to parent unconditionally
    ...

    throw new ClassNotFoundException(name);

}

loadClass方法里判断了started状态,未启动的情况下,抛出异常,又自己捕获,仅仅记录了一下日志。然后继续后边的逻辑,先从cache里找,然后委托j2se classloader,再委派parent classloader,以及从本地仓库寻找。找不到抛出ClassNotFoundException

前面提过应用在stop时webappclassloader里的cache/parent等有关资源都被清空了,如果应用里启动的线程后续逻辑要求找的类,不是j2seclassloader可以找得到的类的话,就会抛出异常。

很多情况下,开发人员总是被这个ClassNotFoundException所迷惑, 如果仔细看日志的话,已经给出了应用被stopped的信息:

2014-6-17 21:58:42 org.apache.catalina.loader.WebappClassLoader loadClass  
信息: Illegal access: this web application instance has been stopped already.    

Pandora的启动优化

pandora3进行了很多重构,上周花了几天时间对启动过程做了一些优化。官方的tomcat只部署一个及简单的servlet应用,在我的机器上启动过程大概在1秒左右,如果应用使用了spring-mvc框架,启动过程大约在2000-2400毫秒左右。

如果使用Ali-Tomcat,启动过程中要先启动pandora容器,同一个spring-mvc的样例应用,总启动时间在4-5秒左右。优化的手段主要有2点:1) 部分模块异步化加载(前提是其它模块以及应用的启动对该模块没有依赖)。 2) 在自定义的classloader.loadClass方法里针对java7开启并行加载(在我的场景下所加载的类大约1000-2000个,使用4个线程,性能上约有20-30%的提升)。

java7的classloader所支持的并行加载,实质是把原先loadClass方法原先粗粒度的锁synchronized(this)换成了细粒度的锁synchronized (getClassLoadingLock(name)),为每个class都分配一个锁。这部分特性其实可以移植到自定义的Classloader实现里来,使得jdk6上也可以享用并行加载。

自定义的classloader要使用jdk7的并行加载机制,需要在静态构造块里注册为可并行加载:

static {
    ClassLoader.registerAsParallelCapable();
}

注意它要求当前classloader所有的父类都进行注册才行,比如我们在自定义的 MyClassLoader中覆盖了loadClass方法, 假设它的父类是 ClassLoaderBase,而ClassLoaderBase又继承自系统的URLClassLoader,那么要求URLClassLoaderClassLoaderBase 都必须在静态块中有注册声明可并行加载才行(URLClassLoader及其父类在jdk代码中是有注册的)。

另外,使用spring-mvc框架的应用,之所以比普通的servlet启动时慢,很大一部分原因是在于servlet3的web-fragment.xml合并的过程比较耗时。这个过程是在Pandora3启动后,输出应用启动日志过程中体会到的一个明显的“卡顿”:

2014/06/18 10:36:09:588 [INFO] Pandora - Container started. time elapsed: 439 ms
2014-06-18 10:36:10,740 org.springframework.web.servlet.FrameworkServlet initServletBean

这中间居然耗费了将近1.2秒,通过开启日志debug,发现主要的耗时在 WebXml.merge 方法上。原因是servlet3引入了web模块化配置,对原先的web.xml可以由多个“片段”组成,这些片段使用web-fragment.xml来描述,可以对一组servlet/Filter/Listener + web-fragment.xml 打成一个jar,即一个web模块。

这种做法带来一定灵活性,但要求应用在启动时,必须对WEB-INF/lib下的jar,以及其它路径下classloader可访问的jar(这部分可配置,默认包含)进行检查,合并web-fragment.xml片段。

Tomcat对jar中的web-fragment.xml的扫描主要通过JarScanner实现的,它也提供了配置来忽略哪些jar文件;在conf/catalina.properties里定义了tomcat.util.scan.DefaultJarScanner.jarsToSkip对哪些jar文件忽略。

使用spring-mvc框架的应用,依赖了一堆spring的jar,而这些jar没有在默认的忽略列表里,在扫描和合并web-fragment.xml的过程中会比较耗时。

最近看过的电影(3)

《梦想阿根廷》一部带有魔幻现实主义色彩的电影,故事背景发生在1976-1983年的军事独裁期间。里面有一个场景是主人公找他失踪的妻子时到了一个偏僻的农场,那个农场养着非常多的、各种种类的鸟儿;农场的主人是犹太人,曾被纳粹关在集中营里过,二战结束后来到了阿根廷。农场主说他们在集中营的时候,电线上时不时飞来一些鸟儿,这些鸟儿带给他们一些自由的幻想。后来纳粹士兵在电线上通了电,一些鸟儿被电死,之后鸟儿们再也不敢飞过去,他们就像失去了对自由的希望一样。所以当他来到阿根廷后,买下那块农场后在当地的市场上买下了所有的鸟儿,让这些鸟儿在这里自由生长。

googleapis被墙,更换了一下博客主题

本来一直使用wordpress默认的主题,风格也挺好的,简洁、干净;但最近页面打开的总是很慢,在排除了服务器端的问题之后,通过浏览器定位发现每次都是因为某个css里要访问googleapis下的内容,而googleapis被墙掉了,导致资源加载非常慢。

要么修改默认主题里的css不要访问font.googleapis,要么换一个不会访问googleapis的主题。尝试了几个主题,现在用的这个还凑合,就是展示代码的效果不太好。先用着吧,有好的主题欢迎推荐给我。