《深入理解Java虚拟机》——类加载器

429 阅读7分钟

类加载器的作用

  • 类加载的作用是实现类的加载动作,也就是实现类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”。
  • 对于任意一个类,都由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。也就是说,在用Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceOf()方法判断时,只有被同一个类加载器加载的类才相等。

类加载器的分类

image.png

双亲委派模型

应用程序是由这几种类加载器互相配合进行加载的,这几种类加载器之间的层次关系如下图。这种层次关系,称为双亲委派模型。

image.png

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

注意:这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。

// ClassLoader源码
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    ...
    
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

双亲委派模型的工作过程:

如果一个类加载器收到了类加载的请求,它首先把这个请求委派给父类加载器去完成,每个层次的父类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
使用双亲委派模型的好处是java类随着它的类加载器一起具备了一种带有优先级的层级关系。

// 双亲委派模型的实现在java/lang/ClassLoader的loadClass()方法中,
// 过程是:先检查是否被加载,如果没有,则调用父类的loadClass()加载,若父类加载失败,再调用自己的findClass()去加载

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

破坏双亲委派模型的例子

1、SPI

双亲委派模型并不是一个强制性的约束模型,而是java设计者推荐给开发者的一种类加载器实现方式。在java世界中大部分的类加载器都遵循这个模型,但也有例外。比如:java中涉及SPI的加载动作,如JNDI、JDBC等,这些代码由jvm提供统一标准,然后调用由独立厂商实现并部署在应用程序的ClassPath下的SPI代码,但是启动类不可能认识应用程序的代码。
为了解决这一问题,java设计团队引入了线程上下文类加载器Thread Context ClassLoader。这个类加载器可以通过java.lang.Thread的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它将从父线程中继承,如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器
JDBC就是使用线程上下文类加载器去加载的SPI代码。

// DriverManager # getConnection()

// 获取连接时,获取线程上下文加载器,然后使用Class.forName()指定类加载器来加载数据库驱动。

    //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
           if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        ...
    }
    
    private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }

JDBC的加载过程可以参考: JDBC驱动加载机制

2、Tomcat

Tomcat也是破坏双亲委派模型的典型例子。
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

  • 1.部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离;

  • 2.部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享;

  • 3.Web容器需要尽可能地保证自身的安全不受部署的Web应用程序影响;

  • 4.支持JSP应用的Web容器,要支持JSP的热替换。 为解决上面的问题,tomcat为每个应用程序提供了一组classpath,每个classpath都有一个自定义的类加载器去加载下面的类库。
    在Tomcat目录结构中,有3组目录(“/common/”、“/server/”和“/shared/”)可以存放Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/”,一共4组,把Java类库放置在这些目录中的含义分别如下:

    - 放置在/common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
    
    - 放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见。
    
    - 放置在/shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
    
    - 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。   
    

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如下图所示。 image.png

如果一个jar在应用程序的classpath和/common/*目录下都存在,应用程序会自己来加载这个类,传统的双亲委派模型是用父加载器去加载的,这就是违背双亲委派模型的地方。

3、OSGi 更加灵活的类加载器模型

OSGi的类加载器之间的关系不再是像双亲委派模型那样简单的树形结构,而是发展成一种更复杂的、运行时才能确定的网状结构。
OSGi的各Bundle类加载器之间只有规则,没有固定的委派关系。例如,如果一个Bundle声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会委派给发布它的Bundle加载器去完成。不涉及某个具体的Package时,各Bundle类加载器是平级关系,只有具体使用某个Package和class时,才会根据Package导入导出定义来构造Bundle之间的委派和依赖。

参考:《深入理解Java虚拟机》