JVM的艺术—类加载器篇(二)

原创
10/26 12:00
阅读数 4.2K

点击上方蓝色“奇客时间”,选择“设为星标

回复“面试宝典”获取美团、滴滴、阿里2020面试真题


 

引言

今天我们继续来深入的剖析类加载器的内容。上节课我们讲了类加载器的基本内容,没看过的小伙伴请加关注。今天我们继续。

什么是定义类加载器和初始化类加载器?

  • 定义类加载器:假设我们的某一个类是由ExtClassLoader加载的,那么ExtClassLoader称为该类的定义类加载器

  • 初始化加载器:能够返回Class对象引用的都叫做该类的初始类加载器,比如类A是由我们的ExtClassLoader加载,那么

    ExtClassLoader是该类的定义类加载器,也是该类的初始类加载器,而我们的AppClassLoader也能返回我们A类的引用

    那么AppClassLoader也是该类的初始类加载器。

什么是类加载器的双亲委派模型?

上篇文章我们提到了类加载器的双亲委派模型,也可以称为双亲委托模型。今天这篇文章我们就来把这个概念给讲明白。

概念:用一种简单的方式去描述双亲委托的概念。可以分为两个部分去理解

1委托:

jvm加载类的时候是通过双亲委派的方式去加载,自下而上的去委托。

自定义类加载器需要加载类时,先委托应用类加载器去加载,然后应用类加载器又向扩展类加载器去委托,扩展类加载器在向启动类加载器去委托。

如果启动类加载器不能加载该类。那么就向下加载

2加载:

jvm加载类的时候是通过双亲委派的方式去加载委托,但是加载的时候是由上向下去加载的,当委托到最顶层启动类加载器的时候,无法在向上委托,那么

启动类加载器就开始尝试去加载这个类,启动类加载器加载不了就向下交给扩展类加载器去加载,扩展类加载器加载不了就继续向下委托交给应用类加载器

去加载,以此类推。

如果文字描述你还不清楚什么是双亲委托机制,那么我画了一幅图可以更清楚类加载的过程。如下:

通过上图,我们知道更能清楚的知道,双亲委托模型的工作机制,用一句简单的话说,就是需要加载一个类的时候,向上委托,向下加载。

注意:在双亲委派机制中,各个加载器按照父子关系形成树型结构,除了根加载器以外,每一个加载器有且只有一个父加载器。

接下来,我也从jdk底层源码的角度给大家画了一张类加载的主要过程,图如下:

以上就是类加载器加载一个类的重要过程步骤。希望各位小伙儿可以结合源码的方式,仔细再研究一下。其实还挺好理解的。

下面咱们再说说,java采用双亲委托的方式去加载类,这样做的好处是什么呢?

  • 双亲委派模型的好处

    总所周知:java.lang.object类是所有类的父类,所以我们程序在运行期间会把java.lang.object类加载到内存中,假如java.lang.object类

    能够被我们自定义类加载器去加载的话,那么jvm中就会存在多份Object的Class对象,而且这些Class对象是不兼容的。

    所以双亲委派模型可以保证java核心类库下的类型的安全。

    借助双亲委派模型,我们java核心类库的类必须是由我们的启动类加载器加载的,这样可以确保我们核心类库只会在jvm中存在一份

    这就不会给自定义类加载器去加载我们核心类库的类。

    根据我们的演示案例,一个class可以由多个类加载器去加载,同时可以在jvm内存中存在多个不同版本的Class对象,这些对象是不兼容的。

    并且是不能相互转换的。

什么是全盘委托加载?

解释:假如我们的Person类是由我们的系统类APP类加载器加载的,而person类所依赖的Dog类也会委托给App系统类进 行加载,这个委托过程也遵循双亲委派模型。代码如下

person类代码中创建Dog实例

public class Person {

  public Person(){
  
      new Dog();
  }

}

public class Dog {

    public Dog(){
        System.out.println("Dog 的构造函数");
    }
}
  • 测试类

    public class MainClass02 {
    
        public static void main(String[] args) throws Exception {
            //创建自定义类加载器的一个实例,并且通过构造器指定名称
            Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
            myClassLoader.setPath("I:\\test\\");
            Class<?> classz = myClassLoader.loadClass("com.test.Person");
            System.out.println(classz.getClassLoader());
            System.out.println(Dog.class.getClassLoader());
        }
    }
    
    
    运行结果:
    
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$AppClassLoader@18b4aac2
    
    Process finished with exit code 0
    

    从上面的运行结果,我们可以看出,当我们用自定义类加载器去加载我们的Person的时候,根据双亲委托模型,我们的Person并没有被自定义类加载(Test01ClassLoader)加载,而是被AppClassloader加载成功,同时根据全盘委托规则,我们的Dog类也被AppClassLoader加载了。所以大家一定要记住这个至关重要的结论。为我们后面的学习打下坚实的基础。

下面我们再看一个例子。我们把类路径下的Person.class文件删除掉,然后再运行一下上面的main函数,看看结果。代码如下:

通过那行结果我们看出,Person类是由我们的自定义类加载器加载的。那为什么Dog类没有进行全盘委托的,这是因为双亲委托模型的缘故,我们的类路径下并没有Person类,故此AppClassLoader是无法加载我们的路径I:\\test\\下的com.test.Person.class文件的。所以Person类是由我们自定的类加载器加载的。再看Dog类,由于它的加载要遵循双亲委托模型,因为类路径下有Dog.class文件,所以AppClassLoader就可以加载Dog类。故此加载Dog类的ClassLoader是AppClassLoader。写到这里,大家对类加载已经有了一个非常深刻的理解。那么java为什么使用双亲委托模型的好处我相信已经不言而喻了。那么下面来说说双亲委托模型,有没有他的弊端呢,或者说有什么不好的地方嘛?我们可以打破这种双亲委托的方式去加载类嘛?下面我们来看一个例子。

类加载器的命名空间

说到双亲委托模型的弊端,那我就离不开命名空间的概念。

类加载器的命名空间 是由类加载器本身以及所有父加载器所加载出来的binary name(full class name)组成.

①:在同一个命名空间里,不允许出现二个完全一样的binary name。

②:在不同的命名空间中,可以出现二个相同的binary name。当然二者对应的Class对象是相互不能感知到的,也就是说Class对象的类型是不一样的。

解释:同一个Person.class文件 被我们的不同的类加载器去加载,那么我们的jvm内存中会生成二个对应的Person的Class对象,而且这二个对应的Class对象是相互不可见的(通过Class对象反射创建的实例对象相互是不能够兼容的不能相互转型**

③:子加载器的命名空间中的binary name对应的类中可以访问 父加载器命名空间中binary name对应的类,反之不行

下面准备了一张图,以便于大家的理解。

上面这张图就很好的解释了命名空间的概念。大家可以再好好的体会一下。

我们光画图,光用嘴说并不是一种很有力的证据,就如同我写在这篇博文的时候所提,我们在学习和掌握某个概念的时候,就必须要拿出有力的证据,来证明自己的猜想或者是观点,那我们就举一个例子。来验证一下我们上面的理论是否正确。代码如下:

这是Person类的代码。

package com.test;

public class Person {

    public Person() {
        new Dog();
        System.out.println("Dog的classLoader:-->"+ Dog.class.getClassLoader());
    }

    static{
        System.out.println("person类被初始化了");
    }
}

这是Dog类的代码。

package com.test;

public class Dog {

    public Dog(){
        System.out.println("Dog 的构造函数");
    }
}

具体的验证思路是这样的,首先我们把Person类的Class文件放到启动类加载器的加载目录下(C:\Program Files\Java\jdk1.8.0_144\jre\classes 这是启动类加载器的加载目录)来达到Person类交给启动类加载器加载的目的。

然后呢,我们让Dog类去被AppClassLoader(系统类加载器去加载)。然后我们在Person类中去访问Dog类。看看能否访问成功。

测试环境:把我们的Person.class放置在C:\Program Files\Java\jdk1.8.0_131\jre\classes这个目录下,那么我们的Person.class就会被我们的启动类加载器加载,而我们的Dog类是被AppClassLoader进行加载,我们的Person类 中引用我们的Dog类会抛出异常.

创建main方法进行测试:

package com.test;

import java.lang.reflect.Method;

/**
 * jvm 类加载器 第一章
 * @author 奇客时间-时光
 * 自定义类加载器——命名空间
 * 测试父加载所加载的类,不能访问子加载器所加载的类。
 */
public class MainClass02 {

    public static void main(String[] args) throws Exception {

        System.out.println("Person的类加载器:"+Person.class.getClassLoader());

        System.out.println("Dog的类加载器:"+Dog.class.getClassLoader());

        Class<?> clazz = Person.class;
        clazz.newInstance();


    }
}

运行结果:
    
"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=59226:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass02
Person的类加载器:null
Dog的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
person类被初始化了
Exception in thread "main" java.lang.NoClassDefFoundError: com/test/Dog
 at com.test.Person.<init>(Person.java:7)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 at java.lang.Class.newInstance(Class.java:442)
 at com.test.MainClass02.main(MainClass02.java:20)

Process finished with exit code 1

总结:通过上面的代码我们就可以看出来,我们在Person中去new一个Dog的实例的时候,并没有创建成功,而是抛出了Exception in thread "main" java.lang.NoClassDefFoundError: com/test/Dog这样的异常,这也就证明了,我们上面所说的结论(父加载器所加载的类,不能访问子加载所加载的类。)

即启动类加载器所加载的类,不能访问系统类加载器所加载的类(AppClassLoader)。

那么肯定会有人问,我们的子加载器所加载的类,可以访问父加载器所加载的类嘛?我们不妨来证实一下,我们只需要改动一下MainClass02这个类的代码即可,让AppClassLoader去加载Dog类,让我们的自定义类加载器去加载我们的Person类。并在Person类中去访问Dog类。然后将之前C:\Program Files\Java\jdk1.8.0_131\jre\classes目录下的Person中的Class文件删除掉,另外还有把我们类路径下的Person文件删除掉,并且在I:\test\目录下添加com.test.Person.class文件。代码如下:

package com.test;

import java.lang.reflect.Method;

/**
 * jvm 类加载器 第一章
 * @author 奇客时间-时光
 * 自定义类加载器
 * 测试子类加载器所加载的类,能否访问父加载器所加载的类。
 */
public class MainClass02 {

    public static void main(String[] args) throws Exception {
        //创建自定义类加载器的一个实例,并且通过构造器指定名称
        Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
        myClassLoader.setPath("I:\\test\\");
        Class<?> classz = myClassLoader.loadClass("com.test.Person");
        System.out.println(classz.getClassLoader());

        System.out.println("Dog的类加载器:"+Dog.class.getClassLoader());

        classz.newInstance();


    }
}

运行结果:
"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=60588:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass02
自己的类加载器被加载了
com.test.Test01ClassLoader@677327b6
Dog的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Dog 的构造函数

Process finished with exit code 0

从上面的结果可以看出,Person是由我们的Test01ClassLoader自定义类加载器所加载的,那么它的父亲加载器是AppClassLoader,显然Dog类是由我们的AppClassLoader所加载的。故此代码正常运行,没有抛出异常,从而得出结论:

1:父加载器所加载的类,不能访问子加载器所加载的类。

2:子加载器所加载的类,可以访问父加载器所加载的类。

双亲委托模型的弊端

  • 我们先看一段我们非常熟悉的数据库连接相关的代码片段。

    Class.forName("com.mysql.jdbc.Driver");
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/RUNOOB","root","123456");
    Statement stmt = conn.createStatement();
    

案例分析

  • 在上述图中的第五步为什么会用线程上下文加载器进行加载呢?

  • 在双亲委托模型的机制下,类的加载是由下而上的。即下层的加载器会委托上层进行加载。有些接口是Java核心库(rt.jar)提供的例如上面的createStatement接口,而Java核心库是由启动类加载器进行加载的。而这些接口的具体实现是来自不同的厂商(Mysql)。而具体的实现都是通过依赖jar包放到我们项目中的classPath下的。Java的启动类加载器/根类加载器是不会加载这些其他来源的jar包。

  • 我们都知道classPath下的jar包是由我们系统类加载器/应用加载器进行加载,根据我们双亲委托的机制父类加载器是看不到子类(系统类加载器)所加载的具体实现。createStatement 这个接口是由根类加载器进行加载的 而具体的实现又加载不了。在双亲委托的机制下,createStatement这个接口就无具体的实现。

  • 我们Java的开发者就通过给当前线程池设置上下文加载器的机制,就可以由设置的上下文加载器来实现对于接口实现类的加载。换句话说父类加载器可以使用当前线程上下文加载器加载父类加载器加载不了的一些接口的实现。完美了解决了由于SPI模型(接口定义在核心库中,而实现由各自的厂商以jar的形式依赖到我们项目中)的接口调用。

下面我提供了一张SPI的流程图。不知道什么是SPI的小伙伴儿,可以看一下这张图:

从上面的例子,我们可以看出,双亲委托模型的弊端。然后我们的jdk给我们提供了一种通过修改线程上下文类加载的方式来打破这种双亲委托的规则。关于修改线程上下文类加载的话题,我们下个章节再具体的讲解。接下来呢,我们再看看,获取类加载器的几个方法。并且奉上翻译好的java doc文档。方便我们后续学习线程类加载器。

获取类加载器的几个方法

  • Class.getClassLoader()

/**
* Returns the class loader for the class(返回加载该类的类加载器). Some implementations may use
* null to represent the bootstrap class loader(有一些jvm的实现可能用null来表示我们的启动类加载器比如 hotspot).
* This method will return null in such implementations if this class was loaded by the bootstrap class loader.
* 若这个方法返回null的话,那么这个类是由我们的启动类加载器加载
*
* If this object represents a primitive type or void, null is returned.
(原始类型 比如int,long等等的类或者 void类型 那么他们的类加载器是null)
*
*
*/
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
  • 1:返回代表加载该class的类加载器

  • 2:有一些虚拟机(比如hotspot) 的启动类加载器是null来表示

  • 3:原始类型 比如int ,long 或者是void类型 ,他们的类加载器是null

  • ClassLoader.getSystemClassLoader()方法解读

/**
* Returns the system class loader for delegation(该方法返回系统类加载器). This is the default
* delegation parent for new ClassLoader instances(也是我们自己定义的类加载器的委托父类), and is
* typically the class loader used to start the application(通常系统类加载器是用来启动我们的应用的)
*
* This method is first invoked early in the runtime's startup
* sequence(程序在运行早起就会调用该方法), at which point it creates the system class loader and sets it
* as the context class loader of the invoking <tt>Thread</tt>.(在那个时间,调用线程创建我们的系统类加载器同时把系统类加载器设置到我们线程上下文中)
*
* <p> The default system class loader is an implementation-dependent
* instance of this class.(这句话没有很好的理解)
*
* <p> If the system property "<tt>java.system.class.loader</tt>" is defined
* when this method is first invoked then the value of that property is
* taken to be the name of a class that will be returned as the system
* class loader. The class is loaded using the default system class loader
* and must define a public constructor that takes a single parameter of
* type <tt>ClassLoader</tt> which is used as the delegation parent. An
* instance is then created using this constructor with the default system
* class loader as the parameter. The resulting class loader is defined
* to be the system class loader.
我们可以通过java.system.class.loader 系统属性来指定一个自定义的类加载的二进制名称作为新的系统类加载器,
在我们自定的加载中我们需要定义个带参数的构造函数,参数为classLoader,那么我们这个自定义的类加载器就会看做系统类加载器

*
* @return The system <tt>ClassLoader</tt> for delegation, or
* <tt>null</tt> if none
*
* @throws SecurityException
* If a security manager exists and its <tt>checkPermission</tt>
* method doesn't allow access to the system class loader.
*
* @throws IllegalStateException
* If invoked recursively during the construction of the class
* loader specified by the "<tt>java.system.class.loader</tt>"
* property.
*
* @throws Error
* If the system property "<tt>java.system.class.loader</tt>"
* is defined but the named class could not be loaded, the
* provider class does not define the required constructor, or an
* exception is thrown by that constructor when it is invoked. The
* underlying cause of the error can be retrieved via the
* {@link Throwable#getCause()} method.
*
* @revised 1.4
*/
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
//初始化系统类加载器
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
  • 1:该方法的作用是返回系统类加载器

  • 2:也是我们自定义加载器的直接父类

  • 3:系统类加载器是用来启动我们的应用的

  • 4:在系统早期,调用线程会创建出我们的系统类加载器,并且把我们的系统类加载器设置到当前线程的上下文中.

  • 5:我们可以通过系统属性:java.system.class.loader来指定一个我们自定义类加载器来充当我们系统类加载器,不过我们的我们自定的加载器需要提供一个带参数(classloader)的构造器

这篇文章就写到这里,jvm的艺术会继续连载,请持续关注。。。

 

                                                                                                                                                    点击下方我“在看”,送我一颗心

本文分享自微信公众号 - 奇客时间(qiketime)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
2
3 收藏
分享
加载中
更多评论
打赏
0 评论
3 收藏
2
分享
返回顶部
顶部