理解ClassLoader

雪域幽狐 2018-12-30 11:08 阅读:8096


类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下图所示。


类的实例化是指创建一个类的实例(对象)的过程;
类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。

ClassLoader作用就是把.class文件加载到JVM中。JVM只认识.class,不认识.java。对于java文件,需要使用javac编译为.class。其他语言编写的程序,通过对应的编译器编译为合法的.class文件,JVM也是可以执行的。比如JRuby程序(.rb)通过jrubyc编译器,也可以编译为.class文件。

类与类加载器
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来自同一个.class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型(Parents Delegation Model)
下文内容限于HotSpot,其他虚拟机实现可能有不同。
系统提供了3种类加载器,如下图所示。

启动类加载器(Bootstrap ClassLoader),该加载器由C++语言实现,无法被Java程序直接引用。负责加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和classes等。另外可以通过-Xbootclasspath来指定。
扩展类加载器(Extension ClassLoader),该加载器由Java语言实现,具体为sun.misc.Launcher$ExtClassLoader,可以直接使用该加载器。负责加载%JRE_HOME%\lib\ext目录中的,或者java.ext.dirs所指定目录中的类库。
应用程序类加载器(Application ClassLoader),该加载器由Java语言实现,具体为sun.misc.Launcher$AppClassLoader,可以直接使用该加载器。负责加载用户类路径上的类库。如果应用程序没有自定义类加载器,一般情况下这个就是程序中默认的类加载器。由于该加载器是ClassLoader.getSystemClassLoader()的返回值,该加载器也称为系统类加载器。
除顶层的启动类加载器外,其余类加载器都应有自己的父类加载器。扩展类加载器的父类加载器为启动类加载器,在Java中不可见,为null。
通过如下代码查看层次关系。
public class TestClassLoader {
    public static void main(String[] args) {
        ClassLoader cl = TestClassLoader.class.getClassLoader();
        System.out.println(cl);

        ClassLoader cl2 = cl.getParent();
        System.out.println(cl2);

        ClassLoader cl3 = cl2.getParent();
        System.out.println(cl3);
    }
}

输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b701da1
null
各个加载器加载内容可通过以下代码查看
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));


双亲委派模型工作过程:
如果一个类加载器收到了类加载的请求,该加载器不会马上自己尝试加载这个类,而是会把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都会传递到启动类加载器中,只有当父加载器返回自己无法完成这个加载请求,子加载器才会尝试自己加载。
源码如下:
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;
        }
    }


使用双亲委派模型的好处是,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就不用再加载一次,保证了Java的基础行为。

自定义类加载器
在JDK 1.2以后,一般继承ClassLoader并重写findClass()。
package com.nowfox.commons.lang;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DiskClassLoader extends ClassLoader {
    Logger logger = LoggerFactory.getLogger(DiskClassLoader.class);
    private String path;

    public DiskClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(path, fileName);
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            byte[] data = new byte[is.available()];
            is.read(data);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            logger.warn("读取文件异常", e);
            throw new ClassNotFoundException(name);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    logger.warn("关闭文件异常", e);
                }
            }
        }
    }

    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if (index == -1) {
            return name + ".class";
        }
        return name.substring(index + 1) + ".class";
    }
}


定义一个接口
package com.nowfox.test.classloading;
public interface TestService {
    void create();
}


实现这个接口
package com.nowfox.test.classloading;

public class TestClass implements TestService {
    @Override
    public void create() {
        System.out.println("create();");
    }
}

注意:编写完TestClass.java,产生TestClass.class(可手动使用javac编译)后,把TestClass.java(可删除)、TestClass.class移到其他目录下,本文为D:/classes目录下
使用自定义ClassLoader
package com.nowfox.test.classloading;

public class TestClassLoader2 {
    public static void main(String[] args){
        ClassLoader loader = new DiskClassLoader("D:/classes/");
        try {
            Class<?> clazz = loader.loadClass("cn.antsaas.test.classloading.TestClass");
            System.out.println(clazz.getClassLoader());
            TestService testObject = (TestService) clazz.newInstance();
            testObject.create();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

如果没有移除TestClass.java,运行TestClassLoader2,显示的是
sun.misc.Launcher$AppClassLoader@18b4aac2
create();
这是因为虽然使用了自定义类加载器,但是根据双亲委派模型,会先由父类加载器加载,DiskClassLoader的父类加载器就是sun.misc.Launcher$AppClassLoader。在没移除TestClass.java和TestClass.class时,AppClassLoader可以找到TestClass.class,因此由AppClassLoader加载。
如果移除了TestClass.java,运行TestClassLoader2,显示的是
com.nowfox.commons.lang.DiskClassLoader@5679c6c6
create();
说明我们自定义类加载器生效了。

破坏双亲委派模型
到目前,双亲委派模型有3次较大规模的“被破坏”情况,JDK 1.2发布以前、线程上下文类加载器以及OSGi。
本文这里主要谈一下线程上下文类加载器。
JNDI、JDBC等目前是Java标准服务,称为服务提供者接口(SPI,Service Provider Interface),在rt.jar里,它的代码由启动类加载器加载。但是这些提供服务的具体实现类在应用的classpath下,启动类加载器不能直接加载这些具体实现类。
因此引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。可以通过java.lang.Thread类的setContextClassLoaderr()方法进行设置,默认继承父线程。

以JDBC为例
public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    private static void loadInitialDrivers() {
///////
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            }
        }
    }
}

public final class ServiceLoader<S> implements Iterable<S>{
    public static <S> ServiceLoader<S> load(Class<S> service) {
          ClassLoader cl = Thread.currentThread().getContextClassLoader();
          return ServiceLoader.load(service, cl);
    }
}

以上两段代码都位于rt.jar,可见加载具体实现类时是通过上下文类加载器来加载的。

参考
1、周志明 《深入理解Java虚拟机》

0条评论

登陆后可评论