简述Java类加载机制

类加载本质

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行验证、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制。

类加载过程

  • 加载:加载阶段,虚拟机需要完成以下三件事情:
  1. 通过一个类的全限定名来获取定义此类的二进制字节流; 
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

其实,虚拟机并没有明确规定类的二进制字节流从哪里获取,这也是Java虚拟机强大的原因,而许多Java技术的基础也是建立在这个基础之上的。比如:

  1. 从本地获取
    1. 通常的类加载过程:根据对应类的Jar文件中,直接加载类的二进制字节流;
    2. SPI类加载过程:根据实际使用需要,加载特定的实现策略类;如JDBC针对不同数据库的驱动
  2. 从网络获取
    1. Applet应用
  3. 运行时计算生成
    1. 动态代理技术:通过反射技术,在运行时生成特定接口的代理类的二进制字节流
  4. 从其他文件生成
    1. JSP技术:由JSP文件生成对应的Class类
  • 验证:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备:这一阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
  • 解析:这一阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化:类初始化是类加载的最后一步,初始化阶段才真正开始执行类中定义的Java程序代码,这一阶段主要执行类构造器<clinit>()方法的过程。

类加载器

虚拟机设计团队将类加载阶段的类加载过程中的加载动作,放到虚拟机外部实现,也就是可以运行开发人员自己觉得如何获取所需要的类,这个加载动作的实现就是类加载器。

双亲委派模型

虚拟机提供了一个非强制性的类加载机制,即双亲委派模型,他的过程是如果一个类加载器收到了类加载的请求,它首先不会自己取尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该首先传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己取加载。

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    synchronized (getClassLoadingLock(name)) { // 类加载静态锁        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) {                long t1 = System.nanoTime();                c = findClass(name);  // 调用当前类加载器
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                sun.misc.PerfCounter.getFindClasses().increment();            }        }        if (resolve) {            resolveClass(c);        }        return c;    }}

其实对于虚拟机来说,只有两种加载器,一种是启动类加载器,是有C++语言实现的,是虚拟机的一部分;另外一部分是所有的其他类加载器,由Java语言编写,独立于虚拟机之外,都继承自抽象类java.lang.ClassLoader。

另外,双亲委派模型还要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这就形成了一种加载器的父/子关系。

类加载器的分类及作用

  • 启动类加载器:负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的类库。如:rt.jar;
  • 扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路劲中的所有类库;
  • 应用程序类加载器:负责加载用户类路径(ClassPath)上的所指定的类库;
  • 自定义类加载器:根据程序的需要,编写特定的类加载逻辑。

类加载器的补充和应用场景

类的唯一性是由类的全限定名和加载它的类加载器一同确定的,也就是被不同类加载器加载的两个相同的类,也必定不相等。

上下文类加载器

双亲委派模型可以解决大部分类加载的问题,但不能解决所有场景。虚拟机允许灵活的设置类加载器,从而使类加载体系更加灵活。接下来我们先了解一下线程上下文加载器,以及使用上下文加载器的一些场景。

SPI机制与JDBC驱动类加载

我们都知道Java的SPI机制是一种JDK内置的服务提供发现机制,比如java.sql.Driver接口,可以由不同的数据库厂商实现此接口,来提供具体数据库的驱动,并且需要按照SPI的规范,在jar包的META-INF/services/目录里,创建一个以服务接口命名的文件。

我们知道java.sql.Driver接口是在rt.jar下,也就是由启动类加载器负责加载,但是各种厂商的实现类在ClassPath下,不应由启动类加载器加载,这样就违背了双亲委派模型,这时我们看DriverManager.java的源码:

private static void loadInitialDrivers() {    String drivers;    try {        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {            public String run() {                return System.getProperty("jdbc.drivers");            }        });    } catch (Exception ex) {        drivers = null;    }
    AccessController.doPrivileged(new PrivilegedAction<Void>() {        public Void run() {            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);            Iterator<Driver> driversIterator = loadedDrivers.iterator();            try{                while(driversIterator.hasNext()) {                    driversIterator.next();                }            } catch(Throwable t) {                // Do nothing            }            return null;        }    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    if (drivers == null || drivers.equals("")) {        return;    }    String[] driversList = drivers.split(":");    println("number of Drivers:" + driversList.length);    for (String aDriver : driversList) {        try {            println("DriverManager.Initialize: loading " + aDriver);            Class.forName(aDriver, true,            ClassLoader.getSystemClassLoader());        } catch (Exception ex) {            println("DriverManager.Initialize: load failed: " + ex);        }    }}
public static <S> ServiceLoader<S> load(Class<S> service) {    ClassLoader cl = Thread.currentThread().getContextClassLoader();    return ServiceLoader.load(service, cl);}

使用到ServiceLoader.load方法去加载具体的驱动实现,而ServiceLoader内部使用的是Thread.currentThread().getContextClassLoader();获取的类加载器,这个加载器就是上下文类加载器,完美的补充了双亲委派模型不能加载SPI机制实现类的问题。上下文类加载器默认就是AppClassLoader,当然我们可以自己实现上下文类加载器(TCCL),并通过Thread.currentThread().setContextClassLoader(ClassLoader cl)来灵活指定。

Tomcat类加载

Tomcat作为Web服务器,通常允许部署多个应用,那么多个应用之间怎么去做同一个类的隔离呢,这就需要由不同应用中不同类加载器来加载,才能实现隔离,而且多个应用也存在共用部分,下面我们结合Tomcat源码分析一下Tomcat的类加载器。

public void init() throws Exception {
    initClassLoaders();    Thread.currentThread().setContextClassLoader(catalinaLoader);    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // Load our startup class and call its process() method    if (log.isDebugEnabled())    log.debug("Loading startup class");    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");    Object startupInstance = startupClass.getConstructor().newInstance();
    // Set the shared extensions class loader    if (log.isDebugEnabled())        log.debug("Setting startup class properties");        String methodName = "setParentClassLoader";        Class<?> paramTypes[] = new Class[1];        paramTypes[0] = Class.forName("java.lang.ClassLoader");        Object paramValues[] = new Object[1];        paramValues[0] = sharedLoader;        Method method =        startupInstance.getClass().getMethod(methodName, paramTypes);            method.invoke(startupInstance, paramValues);
        catalinaDaemon = startupInstance;}
ClassLoader commonLoader = null;ClassLoader catalinaLoader = null;ClassLoader sharedLoader = null;
private void initClassLoaders() {    try {        commonLoader = createClassLoader("common", null);        if( commonLoader == null ) {            // no config file, default to this loader - we might be in a 'single' env.            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);    }}

Bootstrap.java中初始化Tomcat类加载器,分别是commonLoader、catalinaLoader、sharedLoader。通过设置上下文类加载器可以灵活的使用不同类加载器加载应用中的类,完成多应用部署和热部署。

commonLoader是Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;

catalinaLoader是Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;

sharedLoader是各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;

WebappClassLoader是各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃。当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

P.S. Tomcat不是彻底打破了双亲委派模型,而是灵活的使用上下文类加载器来解决热部署、多个应用部署等场景,顶层类加载还是遵循双亲委派模型的。

Dubbo类加载

Dubbo也是基于SPI机制实现的架构,JDK默认的SPI机制类加载器ServiceLoader,会在META-INF/services下获取接口的所有实现类,虽然也提供了延迟加载,但也基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

Dubbo对JDK的SPI机制进行了扩展:

  1. 使用K、V形式的列表,不再依赖提供者类的全限定名;
  2. 使用扩展点载入器ExtensionLoader,用于加载Dubbo中的各种可配置组件,如:ProxyFactory、LoadBalance、Protocol、Filter、Container、Cluster、RegisterFactory等;
  3. 扩展点加载器实现了IOC功能,通过动态代理形式,为依赖注入代理对象,再通过调用参数的不同,自动引用不同的实现。
  4. 扩展点加载器实现类AOP功能,增加了功能增强、自动包装实现。

OSGI类加载

OSGI是Java模块化标准,而OSGI实现模块化热部署的关键则是它自定义的类加载器机制的实现,每个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热部署。在OSGI环境下,类加载器不再是双亲委派模型中的树形结构,而是进一步发展为更加复杂的网状结构,笔者没有做过OSGI相关开发,这里类加载过程不做赘述。


参考资料:

  1. 《深入理解Java虚拟机》
  2. 《Java虚拟机精讲》
  3. https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
  4. https://www.cnblogs.com/chenerzhu/p/9741883.html
  5. https://segmentfault.com/a/1190000017517197
  6. https://blog.csdn.net/yangcheng33/article/details/52631940
  7. https://blog.csdn.net/qq_38182963/article/details/78660779
  8. https://www.jianshu.com/p/7daa38fc9711

原文发布于微信公众号 - BanzClub(banz-club)

原文发表时间:2019-04-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券