Tomcat学习笔记(7)- Loader(类加载器)

1. 什么是类加载器

详情可了解

所谓类加载器,就是用于加载Java类到Java虚拟机中的组件,它负责读取Java字节码,并转换成java.lang.Class类的一个实例,使字节码.class文件得以运行。

类加载器有如下三种分类:

  • 启动类加载器(Bootstrap ClassLoader):加载对象是Java核心库,把一些核心的Java类加载进JVM中,这个加载器使用原生代码(C/C++)实现,并不是继承java.lang.ClassLoader,它是所有其他类加载器的最终父加载器,负责加载<JAVA_HOME>/jre/lib目录下JVM指定的类库。其实它属于JVM整体的一部分,JVM一启动就将这些指定的类加载到内存中,避免以后过多的I/O操作,提高系统的运行效率。启动类加载器无法被Java程序直接使用。
  • 扩展类加载器(Extension ClassLoader):加载的对象为Java的扩展库,即加载<JAVA_HOME>/jre/lib/ext目录里面的类。这个类由启动类加载器加载,但因为启动类加载器并非用Java实现,已经脱离了Java体系,所以如果尝试调用扩展类加载器的getParent()方法获取父加载器会得到null。然而,它的父类加载器是启动类加载器。
  • 应用程序类加载器(Application ClassLoader):亦叫系统类加载器(System ClassLoader),它负责加载用户类路径(CLASSPATH)指定的类库,如果程序没有自己定义类加载器,就默认使用应用程序类加载器。它也由启动类加载器加载,但它的父加载类被设置成了扩展类加载器。如果要使用这个加载器,可通过ClassLoader.getSystem ClassLoader()获取。

双亲委派模型:

在这里插入图片描述
双亲委派模型会在类加载器加载类时首先委托给父类加载器加载,除非父类加载器不能加载才自己加载。按照如上图的模型向上层委派。

通过这个机制,保证了Java应用所使用的都是同一个版本的Java核心库的类,同时这个机制也保证了安全性。

设想如果应用程序类加载器想要加载一个有破坏性的java.lang.System类,双亲委派模型会一层层向上委派,最终委派给启动类加载器,而启动类加载器检查到缓存中已经有了这个类,并不会再加载这个有破坏性的System类。

在JVM中,一个类由完全匹配类名和一个类加载器的实例ID作为唯一标识。也就是说,如果包名、类名都相同的类,只要它们由两个不同的类加载器加载,那么在JVM层面这两个类不是同一个类。这就是所谓的隔离机制。

加载过程:

要加载一个类,类加载器先判断此类是否已经加载过(加载过的类会缓存在内存中),如果缓存中存在此类,则直接返回这个类。否则,获取父类加载器,如果父类加载器为null,则由启动类加载器载入并返回Class。如果父类加载器不为null,则由父类加载器载入,载入成功就返回Class,载入失败则根据类路径查找Class文件,找到就加载此Class文件并返回Class,找不到就抛出ClassNotFoundException异常。

2. 破坏双亲委派

先来看一个例子:

 public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(filename);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        };

        Class<?> aClass = myClassLoader.loadClass("com.lyq.boot.lexicalanalysis.A");
        Object o =  aClass.newInstance();
        A a = new A();
        System.out.println(o.getClass().equals(a.getClass()));
    }


上面代码应该输出的是false。

默认的loadClass方法实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时,才由自己加载。这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败时才会由自己加载。它并没有优先交给父类加载器,这就打破了双亲委派机制。

所以,只要自定义类加载器每次不是往上递归交给父类,这样就破坏了双亲委派。

3. Tomcat的类加载器

Tomcat的类加载器需要完成的功能:

  • 同一个Web服务器里,各个Web项目之间各自使用的Java类库要互相隔离。
  • 同一个Web服务器里,各个Web项目之间可以提供共享的Java类库。
  • 为了使服务器不受Web项目的影响,应该使服务器的类库与应用程序的类库互相独立。
  • 对于支持JSP的Web服务器,应该支持热插拔(HotSwap)功能。

很容易我们就可以看出,Web项目可能不止一个,那么类加载器应该也有很多个。


在Tomcat中,最重要的一个类加载器是Common类加载器,它的父类加载器是应用程序类加载器。负责加载$CATALINA_BASE/lib、$CATALINA_HOME/lib两个目录下所有的.class文件与.jar文件。

在这里插入图片描述

org.apache.catalina.startup.Bootstrap#initClassLoaders

   private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // 没有配置文件,默认为这个加载器——我们可能在一个“单一”环境中。
                commonLoader = this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

首先创建一个Common类加载器,再把Common类加载器作为参数传进createClassLoader方法里,在这个方法里面会根据catalina.properties中的server.loader和share.loader属性是否为空判断是否另外创建新的类加载器。如果属性为空,则把常见类加载器直接赋值给Catalina类加载器和共享类加载器。如果默认配置满足不了你的需求,可以通过修改catalina.properties配置文件满足需要。

其次,从WebApp ClassLoader的名字来看,就大概知道它主要用于加载Web应用程序。它的父类加载器是Common类加载器,Tomcat中一般会有多个WebApp类加载器实例,每个类加载器负责加载一个Web程序。

对照这样的一个类加载器结构,看看上面需要解决的问题是否解决。由于每个Web应用项目都有自己的WebApp类加载器,很好地使多个Web应用程序之间互相隔离且能通过创建新的WebApp类加载器达到热部署。这种类加载器结构能有效使Tomcat不受Web应用程序影响,而Common类加载器的存在使多个Web应用程序能够互相共享类库。

4. ClassLoaderFactory

Java虚拟机利用类加载器将类载入内存的过程中,类加载器要做很多的事情,例如,读取字节数组、验证、解析、初始化等。而Java提供的URLClassLoader类能方便地将Jar、Class或网络资源加载到内存中。Tomcat中则用一个工厂类ClassLoaderFactory把创建类加载器的细节进行封装,通过它可以很方便地创建自定义类加载器。

在这里插入图片描述

利用createClassLoader方法并传入资源路径和父类加载器即可创建一个自定义类加载器,此类加载器负责加载传入的所有资源。

其中有个内部类Repository:

    public static class Repository {
        private final String location;
        private final RepositoryType type;

        public Repository(String location, RepositoryType type) {
            this.location = location;
            this.type = type;
        }

        public String getLocation() {
            return location;
        }

        public RepositoryType getType() {
            return type;
        }
    }
}
    public enum RepositoryType {
        DIR,
        GLOB,
        JAR,
        URL
    }

对于RepositoryType :

  • DIR:表示整个目录下的资源,包括所有Class、Jar包及其他类型资源。
  • GLOB:表示整个目录下所有的Jar包资源,仅仅是.jar后缀的资源。
  • JAR:表示单个Jar包资源。
  • URL:表示从URL上获取的Jar包资源。

如下是一个创建类加载器的例子:

在这里插入图片描述

5. ClassNotFoundException

Tomcat会创建Common类加载器、Catalina类加载器和共享类加载器三个类加载器供自己使用,这三个其实是同一个类加载器对象。
Tomcat在创建类加载器后马上就将其设置成当前线程类加载器,即Thread.currentThread().setContextClassLoader (CatalinaLoader),这里主要是为了避免后面加载类时加载不成功。

为什么会抛出ClassNotFoundException:

  1. 在JVM中,一个类由完全匹配类名和一个类加载器的实例ID作为唯一标识。
  2. 在类加载器加载某个类时,一般会在类中引用、继承、扩展其他的类,于是类加载器查找这些引用类也是一层一层往父类加载器上查找的,最后查看自己,如果都找不到,将会报出找不到此类的错误。
    这里得解释:如果一个类A由M类加载器加载,那么其子类B由N加载时,它会在N加载的类中找A,而N中的类是没有加载A的,所以会抛出异常。
  3. 每个运行中的线程都有一个成员ContextClassLoader,用来在运行时动态地载入其他类。在没有显式声明由哪个类加载器加载的类(例如在程序中直接新建一个类)时,将默认由当前线程类加载器加载,即线程运行到需要加载新类时,用自己的类加载器对其进行加载。系统默认的ContextClassLoader是系统类加载器,所以一般而言,Java程序在执行时可以使用JVM自带的类、$JAVA_HOME/jre/lib/ext/中的类和$CLASSPATH/中的类。

如何解决?

因为两个类被两个类加载器加载而导致找不到类,那么最简单的解决方法就是使这两个类统一由一个类加载器来加载。在tomcat中查看当前线程类加载器,并设置出现问题的类的加载器为同一加载器。这个设置的时机一般在Tomcat启动时就设置。
至于如何使其是同一个加载器来加载,可以利用传递为双亲委派的特性设置父类加载器。

一般在启动时把Common类加载器设置为线程上下文类加载器。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值