Java的类加载过程与双亲委派机制

类的加载过程详解

Java中类的加载过程主要分为五个个阶段:

  1. 加载:

    在加载阶段,JVM需要完成三件事:

    1. 通过类的全限定类名来获取该类的二进制字节流
    2. 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表该类的Class对象,作为方法区该类的各种数据访问入口
  2. 链接:

    连接阶段分为三个步骤:验证、准备、解析

    1. 验证:检查Class文件的字节流中的信息是否符合Java虚拟机规范的约束要求

      其中检查内容为:文件格式验证、元数据验证、字节码验证、符号引用验证

    2. 准备:为类中定义的静态变量分配内存空间并设置变量的初始值(零值)

      特别地,final修饰的成员会被直接初始化为其具体的值。

    3. 解析:

      JVM将常量池中的符号引用替换为直接引用,其中包括:类或接口的解析、字段解析、方法解析、接口方法解析

  3. 初始化:

    JVM真正将执行类中的编写的Java程序,将主导权移交给应用程序。

    该阶段会执行类构造器中的()方法,该方法是由编译器自动收集类中的所有类变量的复制动作和静态代码块中的语句合并产生的,该方法并不是程序员在Java代码中编写的方法,是由Javac编译器自动生成的。

    初始化顺序为: (父类)初始化代码块 ->(父类)构造函数、

    ​ (子类)初始化代码块 ->(子类)构造函数、

    ​ (父类)静态成员、静态代码块 ->(子类)静态成员、静态代码块

    注意非静态成员并不会被初始化赋值,非静态成员会在对象创建时进行初始化。

Class.forName和loadClass 方法区别

面试的时候常问,为什么JDBC连接数据库时使用forName进行类的加载,而不使用loadClass方法,这两者有什么区别?

Class.forName不仅会将类加载进内存,并且还会进行链接、初始化操作,也就是会执行上述顺序的初始化内容,从而执行初始化代码块和静态代码块中的代码;而loadClass方法只会进行加载,并不会链接该类。所以在JDBC操作数据库时会使用forName来加载类,同时让其自动执行静态代码块中注册驱动的相关代码。

ClassLoader源码分析

ClassLoader是Java定义的抽象类,这是一个没有抽象方法的抽象类,用户自定义的类加载器都必须继承自该类。

其中核心的方法是loadClass (String name, boolean resolve),该方法用于类的加载。常用其重载形式的loadClass(String name) 方法。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查该类是否已经被加载过了。
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();//用于加载耗时统计
                try {
                    //如果当前加载器的parent不为空,就交给parent进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //如果parent为空,则交给引导类加载器(启动类加载器)
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //如果父类加载器没有找到,那么就调用本类对象的findClass方法
                    //寻找该类
                    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();
                }
            }
            //是否要解析该类(类加载过程中的连接link阶段,调用本地方法resolveClass0),使用loadClass加载类并不会连接该Class文件
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

protected Object getClassLoadingLock(String className) {
    Object lock = this;
    //如果该类加载器支持并发加载,就返回一个并发锁对象,并将该锁以要加载的类名为key存到map中,
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}

// Maps class name to the corresponding lock object when the current
// class loader is parallel capable.
// Note: VM also uses this field to decide if the current class loader
// is parallel capable and the appropriate lock object for class loading.
private final ConcurrentHashMap<String, Object> parallelLockMap;

可以看到,使用loaderClass进行类加载操作时,仅仅会将类的二进制字节流加载进内存中,而不会进行链接。

Class.forName(String name)

类加载器分类

在Java虚拟机规范中定义了两种不同的类加载器:启动类加载器(Bootstrap ClassLoader)——C语言实现、自定义类加载器——Java语言实现。

实际开发中,普遍将类加载器分为四类类:

  1. 启动类加载器:负责加载位于/JAVA_HOME/lib目录下的类(核心类库)
  2. 扩展类加载器:负责加载位于/JAVA_HOME/lib/ext目录中的类
  3. 应用类加载器:也被称为系统类加载器,负责用户类路径下的所有的类库
  4. 用户自定义类加载器:顾名思义,用户自定义的类加载器

双亲委派机制

工作过程

如果一个类加载器收到了类加载请求,它首先会将这个类委派给自己的父类加载器去完成,每一个加载器类都是这样,因此所有的加载请求最终都会来到顶层的启动类加载器中,只有父类加载结果返回为空时,即父类无法进行加载时,子类才会尝试自己完成加载。

使用委派机制原因和效果

双亲委派模型对于保证Java程序的稳定性极为重要,可以保证同一个类在程序的各种类加载器环境中都能够保证是同一个加载器加载,是同一个类。防止核心类库的恶意篡改。例如,用户编写的与rt.jar 类库中重名的Java类(例如自定义java.lang.String 类),将会发现它可以正常编译,但是永远不会被加载。

破坏双亲委派模型

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。

OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1)将以java.*开头的类,委派给父类加载器加载。

2)否则,将委派列表名单内的类,委派给父类加载器加载。

3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。

4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。

5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。

6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。

7)否则,类查找失败。

上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。

关于Java Web服务器的自定义的类加载器

主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere都实现了自己定义的类加载器。一个功能健全的web服务器要解决一下几个问题:

  1. 部署在一个服务器上的两个web应用程序所使用的Java类库可以实现互相隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用
  2. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。
  3. 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。目前,有许多主流的Java Web服务器自身也是使用Java语言来实现的。因此,服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库相互独立
  4. 支持jsp应用的Web服务器,大多数都需要支持HotSwap功能。我们知道,jsp文件最终要编译成Java Class才能由虚拟机执行,但jsp文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无需重启作为一个很大的“优势”来看待,因此,“主流”的Web服务器都会支持JSP的热替换,当然也有“非主流”的,如运行在生产模式下的WebLogic服务器默认就不会处理JSP文件的变化

Tomcat的类加载规则

在其目录结构下有三组目录(“/common/*”、“/server/*”、“/shared/*”)可以存放Java类库,另外还可以加上Web应用程序本身的目录“/WEB-INF/*”,一共4组,把Java类库放置在这些目录中的含义分别如下:

1)放置在/commom目录中:类库可被Tomcat和所有的Web应用程序共同使用

2)放置在/server目录中:类库可被Tomcat使用,对所有的Web应用程序都不可见

3)放置在/shared目录中:类库可被所有的Web 应用程序所共同使用,但对Tomcat自己不可见

4)放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

CommonClassLoader、CatalinaClassLoader、SharedClassLoader和 WebappClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。

截取自官网 关于Tomcat9.0的类加载文档说明:

WebappX 是单个Tomcat实例中部署的每个Web应用程序创建一个类加载器。 Web应用程序的/ WEB-INF / classes /目录中的所有解压缩的类和资源,以及Web应用程序的/ WEB-INF / lib目录下的JAR文件中的类和资源,对于此Web应用程序都是可见的,但对其他该服务器中的其他Web应用不可见。

Web应用程序类加载器与默认的Java委托模型有所不同(根据Servlet规范2.4版,第9.7.2节Web应用程序类加载器中的建议)。 当处理从Web应用程序的WebappX类加载器加载类的请求时,该类加载器将首先在本地存储库中查找,而不是在查找之前委派。 也有例外。 属于JRE基类的类不能被覆盖。 有一些例外,例如可以使用适当的JVM功能覆盖XML解析器组件,JVM功能是Java <= 8的认可标准重写功能,而Java 9+是可升级的模块功能。 最后,对于由Tomcat(Servlet,JSP,EL,WebSocket)实现的规范,Web应用程序类加载器将始终首先委托JavaEE API类。 Tomcat中的所有其他类装入器都遵循通常的委托模式。
因此,从Web应用程序的角度来看,类或资源的加载按以下顺序查找以下存储库:

  1. JVM的Bootstrap类
  2. / WEB-INF / classes / 目录下的网络应用程序类(程序员自己编写的Java类)
  3. Web应用程序的/WEB-INF/lib/*.jar
  4. 系统 System类加载器(如上所述)
  5. 通用 Common类加载器(如上所述)

除此之外,还可以通过设置conf/catelina.properties文件的server.loader或者shared.loader来实现更为复杂的架构:

其中Server类加载器只对Tomcat服务器本身可见,对部署的应用程序不可见。

Shared类加载器对所有的应用程序可见以便于在所有的应用中共享代码,但是,任何有关shared中的代码修改都需要重启Tomcat才能是修改生效。

posted @ 2020-05-21 15:46  RYUJUNG  阅读(205)  评论(0)    收藏  举报