深入分析ClassLoader工作机制

ClassLoader顾名思义就是类加载器,负责将Class加载到JVM中,它就好比开会时 门口的接待员,负责给进入会场的嘉宾发放入会证明,入会的嘉宾分为VIP会员、黄金会 员、白金会员和普通会员等。对应的接待室也会分为VIP会员接待室、黄金会员接待室、 白金会员接待室和普通接待室,不同等级的会员会被分到不同的接待室接待。所有的会员 要想进入会场得有入会证明才行,一旦会员进入会场就会根据接待室的等级标识他们,也 就是会员的身份由接待室决定。如果你是一位大佬但是你不是VIP接待室接待的,那么对 不起,你仍然不是VIP会员。当然对你是不是VIP会员会有严格的审查规定,如果你是也 不会冤枉你,但是如果你想混进来那就另当别论了。

事实上,ClassLoader除了能将Clas加载到JVM之外,还有一个重要的作用就是 审查每个类该由谁加载,它是一种父优先的等级加载机制,为何是这种加载机制我们将 在后面详细分,ClassLoader除了上述两个作用外还有一个任务就是将Class字节码重新 JVM统一要求的对象格式。

本章主要分析ClassLoader的前两个作用,也就是ClassLoader的加栽机制和加栽类的过程,另外还将着重介紹在Java Web中常用的ClassLoader是如何实现的,理解它们将帮助我们在日常的开发过程中更好在理解程序是如何工作的。

ClassLoader类结构分析

我们经常会用到或扩展ClassLoader,主要会用到如图6-1所示的几个方法,以及它们的重载方法。

image.png

其中defineClass方法用来将byte字节流解析成JVM能够识别的Class对象,有了这个方法意味着我们不仅仅可以通过class文件实例化对象,还可以通过其他方式实例化对象,如我们通过网络接收到一个类的字节码,拿这个字节码流直接创建类的Class对象形式实例化对象。注意,如果直接调用这个方法生成类的Class对象,这个类的Class对象还没有resolve,这个resolve将会在这个对象真正实例化时才进行.

defineClass通常是和findClass方法一起使用的,我们通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则,从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象,如果你想在类被加载到JVM中时就被链接(Link),那么可以接着调用另办一个 resolveClass方法,当然你也可以选择让JVM来解决什么时候才链接这个类。

如果你不想重新定义加载类的规则,也没有复杂的处理逻辑,只想在运行时能够加载自己指定的一个类,那么你可以用 this.getClass().getClassLoader().loadClass("class")调用ClassLoader的loadClass方法以获取这个类的Class对象,这个loadClass还有重载方法,你同样可以决定在什么时候解析这个类。

ClassLoader是个抽象类,它还有很多子类,我们如果要实现自己的ClassLoader,一般都会继承URLClassLoader这个字类,因为这个类已经帮我们实现了大部分工作,我们只需要在适当的地方做些修改就好了,就像我们要实现Servlet时通常会直接继HttpServlet —样 。

前面介绍的这几个方法都是我们在扩展ClassLoader时需要用到的,ClassLoader 还提供了 另外一些辅助方法, 如获取 class 文件的法 getResource ,getResourceAsStream 等,还有就是获取SystemClassLoader的方法等。对这些方法我们在后面再详细介绍。

ClassLoader的等级加载机制

在前面的会员进入会场的规则中,如何保证不同等级的会员通过不同的会员接待室进入会场? 因为可能有些会员自己并不能正确地找到接待自己的接待室,也有可能有些会员会冒充更髙级的会员身份混进去,所以必须要有机制能够保证所有会员都被正确的接待室接待进入会场,而且一个会员只能被一个接待室接待,不能出现被两个接待室重复接待的情况,也就是不能同时拿到两个入场征明;从而保证接待的一致性。如何设计这个接待规则呢?

ClassLoader就设计了这样—种接待机制,这个机制就是上级委托接待机制。它是这样的:任何—个会员到达任何一个会员接待室时,这个接待室会检查这个会员是否已经被自己接待过,如果已经接待过,则拒绝本次接待,也就是不再发入场证明了,如果没有接待过,那么会向上询问这个会员是否应该在上一级的更髙级别的接待室接待,上级接待室会根据它们的接待规则,检查这个会员是否已经被接待过,如果已经接待过,同样的处理方法,将已经接待的结果反馈给下一级,如果也没有接待过,则向更高一级(如果有更高一级的话)接待室转发接待请求,更髙一级也是同样的处理方法,直到有—级接待室接待或者诗诉它下一级这个会员不是自己接待的这个结果,如果这个会员来到的这个接待室得到它上一级的接待室反馈认为这个会员没有被接待,并且也不应该由它们接待,这个接待室将会正式接待这个会员,并发给它入会证明,这个会员就被定义为这个接待室等级的会员。

这种接待规则看上去有点麻零,佴是它®能够%揉所耷都被正确的筆待室接待,会员的身份也不会错,也不存在冒充身份的会员。

整个JVM平台提供三层ClassLoader,这三层ClassLoader可分为两种类型,可以理解为接待室服务的接待室和为会员服务的接待室两种。

(1 )Bootstrap ClassLoader,这个ClassLoader就是接待室服务自身的,它主要加载JVM自身需要的类,这个ClassLoader完全是由 JVM自己控制的,需要加载哪个类,怎么加载由JVM自己控制,别人也访问不到这个类,所以这个ClassLoader是不遵守前面介绍的加载规则的,它仅仅是一个类的加载工具而已,既没有更高一级的父加载器,也没有子加载器。

(2) ExtClassLoader,这个类加载器有点特殊,它是JVM自身的 一部分,但是它的血统也不是很纯正,它并不是JVM亲自实现的,我们可以理解为这个类加载器是那些与这个大会合作单位的员工会员,这些会员既不是JVM内部的,也和普通的外部会员不同,

所以就由这个类加载器来加载。它服务的特定目标在System.getProperty("java.ext.dirs”)目录下。

(3) AppClassLoader,这个类加载器就是专门为接待会员服务的,它的父类是ExtClassLoader。它服务的目标是广大普通会员,所有在 System.getProperty("java.class.path”)目录下的类都可以被这个类加载器加载,这个目录就是我们经常用到的classpath。

如果我们要实现自己的类加载器,不管你是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或者其他子类,它的父加载器都是AppClassLoader,因为不管调用哪个父类构造器,创建的对象都必须最终调用getSystemClassLoader()作为父加载器。而getSystemClassLoader()方法获取到的正是 AppClassLoader。

通常一个应用中的类加载器的等级结构如图6-2所示。

image.png

很多文章在介绍ClassLoader的等级结构时把Bootstrap ClassLoader也列在ExtClassLoader的上一级中,其实Bootstrap ClassLoader并不属于JVM的类等级层次,因为Bootstrap ClassLoader并没有遵守ClassLoader的加载规则。另外Bootstrap ClassLoader并没有子类,ExtClassLoader 的父类也不是 Bootstrap ClassLoader,ExtClassLoader 并没有父类,我们在应用中能提取到的顶层父类是ExtClassLoader。

ExtClassLoader 和AppClassLoader都位于sun.misc.Launcher 类中,它们是Launcher类的内部类,如图6-3所示。

image.png

ExtClassLoader 和 AppClassLoader 都继承了 URLClassLoader 类,而 URLClassLoader又实现了抽象类ClassLoader,在创建Launcher对象时首先会创建ExtClassLoader,然后将ExtClassLoader作为父加载器创建 AppClassLoader 对象,而通过 Launcher.

getClassLoader()方法获取的ClassLoadiet就是AppClassLoader对象。所以如果在Java应用中没有定义其他ClassLoader,那么除了 System,getPropeirty("java.ext..dirs")目录下的类是由ExtClassLoader加载外,其他类都由AppClassLoader来加载。

JVM加载class文件到内存有两种方式。

◎隐式加载:所谓隐式加载就是不通过在代码里调用ClassLoader来加载需要的类,而是通过JVM来自动加载需要的类到内存的方式。例如,当我们在类中继承或者引用某个类时,JVM在解析这个类时发现引用的类不在内存中,那么就会自动将这些类加载到内存中。

◎显式加载:相反的显式加载就是我们在代码中通过调用ClassLoader类来加载个类一个类的方式,例如,调用this.getClass.getClassloader().loadClass()或者 Class.forName(),或者我们自己实现ClassLoader的findClass()方法等。

其实这两种方式是混合使用的,例如,我们通过自定义的ClassLoader显示加载一个类时,这个类中又引用了其他类,那么这些类就是隐式加载的。

如何加载class文件

前面分析ClassLoader的结构信息时也分析了 ClassLoader的加载机制,下面我们看看它是如何将class文件加载在JVM中的。

如图6-4所示是ClassLoader加载一个class文件到JVM时需要经过的步骤。


image.png
  • 第一个阶段是找到.class文件并把这个文件包含的字节码加载到内存中。

  • 第二个阶段又可以分为3个步骤,分别是字节码验征、Class类数据结构分析及相应的内存分配和最后的符号表的链接。

  • 第三个阶段是类中静态属性和初始化赋值,以及静态块的执行等。

加载字节码到内存

其实在抽象类ClassLoader中并没有定义如何去加载,如何去找到指定类并且把它的字节码加载到内存需要的子类中去实现,也就是要实现findClass()方法。我们看一下子类URLClassLoader 是如何实现 flndClass()的,在 URLClassLoader 中通过一个 URLClassPath 类帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的字节流.丨通过调用defineClass()方法来创建类对象。

这个实现机制如同在第2章中介绍的InputStream和OutputStream 一样,只是定义了读取文件的机制和形式,并没有定义从哪里和如何读取它。

我们再看看URLClassLoader类的构造函数,如图6-5所示,我们可以发现必须 要指定—个URL数据才能够创建URLClassLoader对象,也就是必须要指定这个ClassLoader默认到哪个目录下去杳找class 文件。


image.png

这个URL数组也是创建URLClassPath对象的必要条件。从URLClassPath的名字中就可以发现它是通过URL的形式来表示ClassPath路径的。

在创建URLClassPath对象时会根据传过来的URL数组中的路径来判断是文件还是jar包,根据路径的不同分别创建FileLoader或者JarLoader,或者使用默认的加载器。当JVM调用findClass时由这几个加载器来将class文件的字节码加载到内存中。

如何设置每个ClassLoader的搜索路径呢?如表6-1所示是Bootstrap ClassLoader、ExtClassLoader 和 AppClassLoader 的参数形式。

表6-1 三个ClassLoader类型的参数形式

ClassLoader 类型 参数选项 说 明
-Xbootclasspath: 设置Bootstrap ClassLoader的搜索路径
Bootstrap ClassLoader -Xbootclasspath/a: 把路径添加到己存在Bootstrap ClassLoader搜索路径的后面
-Xbootclasspath/p: 把路径添加到已存在Bootstrap ClassLoader搜索路径的前面
ExtClassLoader -Djava.ext.dirs 设置ExtClassLoader的搜索路径
ClassLoader 类型 参数选项 说 明
-Xbootclasspath: 设置Bootstrap ClassLoader的搜索路径
Bootstrap ClassLoader -Xbootclasspath/a: 把路径添加到己存在Bootstrap ClassLoader搜索路径的后面
-Xbootclasspath/p: 把路径添加到已存在Bootstrap ClassLoader搜索路径的前面
ExtClassLoader -Djava.ext.dirs 设置ExtClassLoader的搜索路径

在上面的参数设置中,最常用到的就是设置classpath的环境变量,因为通常都是让Java运行指定的程序。如果在通过命令行执行一个类时出现NoClassDefFoundError错误,那么很可能是没有指定classpath所致,或者指定了 classpath但是没有指明包名,关于

ClassLoader的出错分析在后面会详细介绍。

验证与解析

◎字节码验证,类装入器对于类的字节码要做许多检测,以确保格式正确、行为正确。

◎类准备,在这个阶段准备代表每个类中定义的字段、方法和实现接口所必需的数据结构。

◎解析,在这个阶段类装入器装类所用的其他所有类。可以用许多方式引用类,如超类、接口、字段、方法签名、方法、方法中使用的本地变量。

初始化Class对象

在类中包含的静态初始化器都被执合,在在这一阶段末尾静态字段被初始化为默认值。

常见加载器类错误分柝

在执行Java程序时经常会碰到 ClassNotFoundException和 NoClassDefFoundError 两个异常,它们都和类加载有关.下面详细分析一下出现这两个异常的原因。

ClassNotFoundException

ClassNotFoundException异常恐怕是Java程序员经常碰到的异常,尤其是对初学者来说,简直让人崩溃.明明那个类就在那里,为啥就是找不到呢?无数个Java程序员都这样问过自己。

这个异常通常发生在显式加载类的时候,例如,用如下方式调用加载一个类时就报这个错了 :

public class notfountexception {
    public static void main(String[] args) {
        try {
            Class.forName("notFountClass”);
        }catch (ClassNotFoundException e) {
           e.printStackTrace();
        }
    }
}

显式加载一个类通常有如下方式:

◎通过类Class中的forName()方法。

◎ 通过类 ClassLoader 中的 loadClass()方法。

◎通过类 ClassLoader 中的 findSystemClass()方法。

出现这类错误也很好理解,就是当JVM要加载指定文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在。解决的办法就是检査在当前的classpath目录下有没有指定的文件存在。如果不知道当前的classpath路径,就可以通过如

下命令来获取:

this .getClass().getClassLoader().getResource ("").toString()


NoClassDefFoundError

NoClassDefFoundError是另外一个经常遇到的异常,这个异常在第一次使用命令行执行Java类时很可能会碰到,如下面这种情况:

java -cp example.jar Example

在这个jar包里面只有一个类,这个类是net.xulingbo.Example,可能让你感到郁闷的是,明明在这个jar包里有这个类为啥会报如下错误呢?

Exception in thread "main" java.lang.NoClassDefFoundError:example/jar
Caused by: java.lang.ClassNotFoundException: example.jar
               at java. net. URLClassLoader$l. run (URLClassLoader .java: 200)
               at java.security.AccessController.doPrivileged (Native Method)
               at java.net .URLClassLoader. findClass (URLClassLoader. java: 188)
      at java. lang. Class Loader. loadClass (ClassLoader. java : 306)
      at sun.misc. Launcher$AppClassLoader . loadClass (Launcher .java: 276)
      at java.lang.ClassLoader.loadClass(ClassLoader.java ; 251)
      at java. lang. ClassLoader. loadClass Internal (ClassLoader. java : 319)



这是因为你在命令行中没有加类的包名,正确的写法是这样的:

java -cp example.jar net.xulingbo.Example


这里同时报了NoClassDefFoundError和 ClassNotFoundException异常,原因是 Java虚拟机隐式加载了exanple.jar后显式加载Example时没有找到这个类,所以是ClassNotFoundException 引发了 NoClassDefFoundError 异常。

在JVM的规范中描述了出现NoClassDefFoundError可能的情况就是使用new关键字、属性引用某个类、继承个接口或类,以及方法的某个参数中引用了某个类,这时会触发JVM隐式加载这些类时发现这些类不存在的异常。

解决这个错误的办法就是确保每个类引用的类都在当前的classpath路径下面。

UnsatisfiedLinkError

这个异常倒不是很常见,但是出错的话,通常是在JVM启动的时候,如果一不小心将在JVM中的某个lib删除了,可能就会报这个错误了,代码如下:

public class NoLibException {
    public native void nativeMethod();
        static {
            System.loadLibrary("NoLib");
        }
        public static void main(String[] args) {
            new NoLibException().nativeMethod();
        }
}

这个错误通常是在解析native标识的方法时JVM找不到对应的本机库文件时出现,代码如下:


image.png

ClassCastException

这个错误也很常见,通常在程序出现强制类型转换时出现这个错误,如下面这段代码所示:

public class CastExceptlon {
    public static Map m = new HashMap(){{
        put ("a","2");
    } };
    public static void main(String[] args) {
        Integer islnt =(Integer)m.get("a”);
        System.out.print(islnt);
    }
}


当强制将本来不是Integer类型的字符串转成Integer类型时会报如下错误:

Exception in thread "main” java .lang .ClassCastException: j ava. lang. String  csnnot be cast to java.lang.Integer
    at EmptyProject.classloader.CastException.main((Cast Exception.java: 17)              
    at sun.reflect.NativeMethodAccessorlmpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorlmpl.invoke (NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorlmpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:613)
    at com.intellij.rt.execution.application.AppMain .main (AppMain .java: 110)



JVM在做类型转换时会按照如下规侧进行检査:

  • 对于普通对象,对象必须是貝标类的实例或目标类的子类的实例。如果目标类是接口,那么会把它当作实现了该接口的一个子类。

  • 对于数组类型,目标类必须是数组类型或java.lang.Object 、java.lang.Cloneable或java.io.Serializable。

如果不满足上面的规则,JVM就会报这个错误。要避免这个错误有两种方式:

  • 在容器类型中显式地指明这个容器所包含的对象类型,如在上面的Map中可以写为 Map<String,Integer> m = new HashMap<String,Integer>(),.这祥上面的代码在编译阶段就会检査通过。

  • 先通过instanceof检查是不是目标类型,然后再进强制类型转换。

ExceptionInInitializerError

这个错误在JVM规范中是这样定义的:

  • 如果Java虚拟机试图创建类ExceptionlnInitializerError的新实例,但是因为出现Out-Of-Memory-Error而无法创建新实倒.,那么就抛出OutOfMemoryError对象作为替代。

  • 如果初始化器抛出一些Exception,而县Exception类不是Error或者它的某个子类,那么就会创建ExceptionlnlnitializeiError类的一个新实例,并用Exception作为参数,用这个实例代替Exception。

将上面的代码例子稍微改了一下:

public class CastExceptlon {
    public static Map m = new HashMap(){{
        m.put ("a","2");
    } };
    public static void main(String[] args) {
        Integer islnt =(Integer)m.get("a”);
        System.out.print(islnt);
    }
}

这段代码在执行时就会报如下错误:


image.png

在初始化这个类时,给静态属性m赋值时出现了异常导致抛出错误ExceptionInInitializerError.

常用的ClassLoader分析

前面分析了 ClassLoader的工作机制,我们再来看看一些开源的框架是如何根据这种工作机制来设置自己的ClassLoader的,它们为何要设置这种ClassLoader呢?

创建一个简单的Web应用,里面有一个HelloWorldServlet,然后在这个Servlet中打印加载它的ClassLoader,代码如下:


image.png

将这个应用通过方式配置在server.xml中,代码如下:


image.png

注意,不要将这个应用直接放在Tomcat的webapps目录下,因为直接放在webapps目录下,Tomcat使用的ClassLoader会不一样,这个在后面会介绍。

上面这段代码打印出来的结果如下:


image.png

可见这个Servlet的ClassLoader等级结构如图6-6所示。


image.png

下面看看Tomcat是在什么时候创建这些ClassLoader的,首先看看StandardClassLoader的创建过程。

StandardClassLoader 是在 Bootstrap 类的 initClassLoaders 方法中创建的,Bootstrap 调用 ClassLoaderFactory 的 createClassLoader()方法创建 StandardClassLoader 对象。如果没有指定 StandardClassLoader 类的父 ClassLoader,就默认设置 getSystemClassLoader()方法返回的ClassLoader 作为其父类,getSystemClassLoader()返回..的 ClassLoader 通常就是 AppClassLoader。

如果StandardClassLoader创建成功,将设置到Bootstrap的catalinaLoader属性作为整个 Tomcat 的根 ClassLoader。接下来 Tomcat 将以 StandardClassLoader 来加载 org.apache.catalina.startup.Catalina 类并创建对象,最终也将 StandardClassLoader 设置到 Catalina 的parentClassLoader属性中。后面整个Tomcat容器的加载 ClassLoader 都将是StandardClassLoader 。

这里有一点一定要说明下,我们前面说Tomcat容器的加载ClassLoader是StandardClassLoader,但是如果你调用Tomcat中任何一个类拿,如StandardContext类,通过 getClass().getClassLoader()方法返回的 ClassLoader 并不是 StandardClassLoader,而是AppClassLoader,为什么呢?原因是StandardClassLoader虽然是加载 StandardContext的类,但是可以看StandardClassLoader的实现方法可以发现StandardClassLoader只是一个代理类,并没有覆盖ClassLoader的loadClass()方法,StandardClassLoader仍然沿用委托加载器,它首先会父加载器来加载,所以真正加载类仍然是是通过其父类AppclassLoader来完成的,加载Tomcat容器本身仍然是AppclassLoader。

但是如果Tomcat的ClassPath没有被设置,那么AppClassLoader就将加载不到Tomcat容器的类,这时就要通过StandardClassLoader来加载了。其实不管是StandardClassLosder还是AppClassLoader加载,都没有任何影响;因为它们的加载规则一模—样,唯一不同的就是加载的路径不同。

其实我们真正关心的不是Tomcat容器本身是谁加载的,而是我们的应用是怎么加载的,也就是一个Web应用需要Tomcat执行时,这应用中的类是通过什么规则加载起来的?

我们知道,一个应用在Tomcat中由一个StandardContext表示.由 StandardContext来解释Web应用的web.xml配置文件实例化所有的Servlet。Servlet的calss是由来指定的,所以可想而知,每个Servlet类的加载肯定是通过显式加载方法加载到Tomcat容器中的。

那么Servlet是如何被加载的呢?先看看StandardContext类的startInternal ()方法,在StandardContext初始化时將会检査loader属性是否存在,不存在就将创建它。看如下代码:


if (getLoader() == null)[
    WebappLaader webappLoader = new WebappLaader (getParentClassLoader());   
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}

这段代码清楚地表示将创建WebappLoader对象,而WebappLoader对象将创建WebappClassLoader 作为其 ClassLoader。再翻阅 StandardWrapper 类的 loadServlet()方法可以发现,所有的Servlet都是InstanceManager实例化的,那么InstanceManager类使用的ClassLoader 是不是 WebappClassLoader 呢?

再看一下InstanceManager构造函数,代码如下:


image.png

InstanceManager 对象的 ClassLoader也获取 StandardContext 的 Loader 中的ClassLoader,也就是前面设置的 WebappClassLoader,所以 Servlet 的 ClassLoader 是WebappClassLoader .WebappClassLoader 不像 StandardClassLoader 那么简单,它覆盖了父类的 loadClass 方法,使用自己的加载机制,这个加载机制有点复杂,大体分为以下几个步骤:

(1) 首先检查在WebappClassLoader中是否已经加载过了,如果请求的类以前是被WebappClassLoader 加载的,那么肯定在 WebappClassLoader的缓存容器 resourceEntries 中。


(2) 如果不在WebappClassLoader的resourceEntries中,则继续检查在JVM虚拟机中是否已经加载过,也就是调用ClassLoader的findLoadedClass方法。


(3) 如果在前两个缓存中都没有,则先调用SystemClassLoader加栽请求的类,SystemClassLoader在这里是AppClassLoader,也就是在当前的JVM的ClassPath路径下査找请求的类。


(4) 检查请求的类是否在packageTriggers定义的包名下,如果在这个设置的包目录下,则将通过StandardClassLoader类来加载。

​(5 )如果仍然没有找到,将由WebappClassLoader来加栽,WebappClassLoader将会在这个应用的WEB-INF/classes目录下查找请求的类文件的字节码。找到后将创建一个ResourceEntry对象保存这个类的元信息,并把它保存在WebappClassLoader的resourceEntries容器中便于下次查找。接着将调用defineClass方法生成请求类的Class对象并返回给InstanceManager来创建实例。Servlet的创建过程将在后面的早节中详细介绍。


从上面的分析来看,Tomcat仍然沿用了 JVM的类加载规范,也就是委托式加载,保证核心类通过AppCIassLoader来加载。但是Tomcat会优先检查WebappClassLoader己经加载的缓存,而不是JVM的findLoadedClass缓存,这一点需要注意。

这也说明了如果你将一个Web应用直接放到webapp目录下,那么Tomcat就通过StandardClassLoader 直接加载,而不是通过 WebappClassLoader 来加载。

如何实现自己的ClassLoader

通过前面的分析,ClassLoader能够完成的事情无非有以下几种情况。

◎在自定义路径下查找自定义的class类文件,也许我们需要的class文件并不总是在己经设置好的ClassPath下面,那么我们必须想办法来找到这个类,在这种情况下我们需要自己实现一个ClassLoader。


◎对我们自己的要加载的类做特殊处理,如保证通过网络传输的类的安全性,可以将类经过加密后再传输,在加载到JVM之前需要对类的字节码再解密,这个过程就可以在自定义的ClassLoader中实现。


◎可以定义类的实现机制,如果我们可以检查已经加载的class文件是否被修改,如果修改了可以重新加载这个类,从而实现类的热部署。

下面就这几种情况来创建自己的ClassLoader。

加载自定义路径下的class文件

我们自己成现一个ClassLoader,并指定这个ClassLoader的加载路径可以通过如式来实现,如下面的代码所示:


image.png

在上面这段代码中到classPath目录下去加载指定包名的class文件,如果不是“net.xulingbo. classloader ”,仍然使用父类加载器去加载。

还有一种方式是继承URLClassLoader类,然后设置自定义路径的URL来加载URL下的类,这种方式更加常见,如下面代码所示:


image.png

我们将指定的目录转化成URL路径,然后作为参数创建URLPathClassLoader对象,那么这个ClassLoader在加载时就在URL指定的目录下查找指定的类文件。

加载自定义格式的class文件

假设我们通过网络从远处主程上下载一个class文件的字节码,但是为了安全性,在传输之前对这个字节码进行了简单的加密处理,然后再通过网络传输。当客户端接收到这个类的字节码后需要经过解密才能还原成原始的类格式,然后再通过ClassLoader的 defineClassO方法创建这个类的实例,最后完成类的加载工作,如下代码所示:


image.png

image.png

image.png

在方法deCode()中可以对从网络传输过来的字节码进行某种解密处理,然后返回正确的class字节码,调用defineClass()来创建类对象。

实现类的热部署

我们知道,JVM在加载类之前会检査请求的类是否已经被加载过来,也就是要调用flndLoadedClass()方法查看是否能够返回类实例。如果类已经加载过来,再调用loadClass()将会导致类冲突。但是JVM表忝一个类是否是同一个类会有两个条件。一是看这个类的 完整类名是否一样,这个类名包括类所在的包名。 二是看加载这个类的ClassLoader是否是同一个,这里所说的同一个是指ClassLoader的实例是杏是同一个实例。即使是同一个ClassLoader类的两个实例,加同一个类也会不一样。所以要实现类的热部署可以创建不同的ClassLoader的实例对象,然后通过这个不同的实例对象来加载同名的类,如下面的代码所示:


image.png

image.png

运行上面代码打印出来的是两个不同的类实例对象,如果不是创建了两个不同的ClassReloader对象,如将上面的main方法改成下面的代码:

image.png

image.png

那么重复加载一个类会抛出java.lang.LinkageError,出错信息如下:

image.png

使用不同的Classloader实例加载同一个类,会不会导致JVM的PermGen区无限增大?

答案是否定的,因为我们的Classloader对象也和其他对象一样,当没有对象再引用它以后,也会被JVM回收,但是需要注意的一点是,被这个Classloader加载的类的字节码会保存在JVM的PermGen区,这个数据一般只是在执行Full GC时才会被回收的,所以如果在你的应用中都是大量的动态类加载,FullGC又不是太频繁,也要注意PermGen区的大小,防止内存溢出。

java应不应该动态加载类

我想大家都知道用Java有一个痛处,就是修改一个类,必须要重肩一遍,很费时,于是就想能不能来个动态类的加载而不需要重启JVM,如果你了解JVM的工作机制,就应该放弃这样的念头。

Java的优势正是基于共享对象的机制,达到信息的髙度共享,也就是通过保存并持有对象的状态而省去类信息的重复创建和回收。我们知道对象一旦被创建,途个对象就可以被人持有和利用。


假如,我只是说假如,如我们能够动态加载一个对象进入JVM,但是如何做到JVM中对象的平滑过渡?几乎不可能!虽然在JVM中对象只有一份,在理论上可以直接替换这个对象,然后更新Java栈中所有对原对象的引用关系。看起来好像对象可以被替换了,但是这仍然不可行,因为它违反了 JVM的设计原则,对象的引用关系只有对象的创建者持有和使用,JVM不可以干预对象的引用关系,因为JVM并不知道对象是怎么被使用的,这就涉及JVM并不知道对象的运行时类型而只知道编译时类型。


假如一个对象的属性结构被修改,但是在运行时其他对象可能仍然引用该属性。虽然完全的无障碍的替换是不现实的,但是如果你非要那样做,也还是有一些“旁门左道”的。前面的分析造成不能动态提供类对象的关键是,对象的状态被保存了,并且被其他对象引用了,一个简单的解决办法就是不保存对象的状态,对象被创建使用后就被释放掉,下次修改后,对象也就是新的了。


这种方式是不是就很好呢?这就是JSP,它难道不是可以动态加载类吗?也许你已经想到了,所有其他解释型语言都是如此。

总结

本章介绍了 ClassLoader的基本工作机制,同时也介绍了 Tomcat等框架的ClassLoader的实现原理,最后介绍了如何创建自己的ClassLoader。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容