java类加载模式与web容器的类加载模式

一.概述

在初学java的时候,很多书和资料都会说java是一个跨平台的语言,是一个动态语言,可以在运行期间加载类。首先说,java是一个跨平台的语言,是因为它的两个关键点:

  1. java虚拟机 jvm
  2. .class 字节码文件

任何一个java文件都是先编译成 .class文件,然后再经过jvm解释成机器码。只要拥有.class文件和jvm,那么任何一个平台都可以运行。然后再看,java是一个动态语言,这是这篇的重点。
当java文件被编译成 .class文件之后,它是怎么被使用的呢?
在java.lang包中有一个抽象类,名为 ClassLoader ,这个类是所有类加载器的超类。这个类中有这几个重要方法:

  • getParent() 返回该类加载器的父类加载器。
  • loadClass(String name) 加载名称为name的类,返回的结果是 java.lang.Class类的实例。
  • findClass(String name) 查找名称为name的类,返回的结果是 java.lang.Class类的实例。
  • findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回的结果是
    java.lang.Class类的实例。
  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
  • resolveClass(Class c) 链接指定的 Java 类。

从上面的说明可以看出来ClassLoader通过loadClass()方法来加载字节码文件,然后返回一个字节码对象。来看一下源码:

代码1protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
        /*首先通过findLoadedClass方法检查这个类是否被加载,过程是去元空间中查看是否有当前类的信息,是否被加载需要检查 包名+类名+类加载器的id*/
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                /*双亲委派模型,后面细讲*/
                    if (parent != null) {
                    /*如果没有被加载,就调用它的父加载器去加载*/
                        c = parent.loadClass(name, false);
                    } else {
                    /*一直向上委派到bootStrap类加载器*/
                        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
/*查找这个类是否能被这个类加载器加载,不能的话就返回null,回到它的子加载器,递归回来,查看子加载器是否能被加载*/
                    long t1 = System.nanoTime();
                    c = findClass(name);
。。。。
                }
            }
            if (resolve) {
            /*如果这个类加载了,那么就链接它*/
                resolveClass(c);
            }
            return c;
        }
    }

一些重要的点,都在代码中注释出来了。看完这段,是不是有一点还不清楚呢?这里好像只是讲怎么发现字节码文件,并没有加载类啊。这个后面讲,先来讲一下java最基础的类加载机制模型。

二.java的类加载流程和双亲委派模型

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader
    最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变BootstrapClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
  • ExtentionClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
  • AppclassLoader也称为SystemAppClass 加载当前应用的classpath的所有类。
    这三个加载器都是jvm通过一个Launcher类来初始化创造的,来看一下jvm是怎么加载它们的:
    这里写图片描述
    有兴趣的可以查看Launcher的源码,三个类加载器的创建分别在这相应的三个内部类中。这里简单说一下,并介绍类加载器的继承体系和父子体系。
怎么创建AppclassLoader
/*静态内部类继承了URLClassLoader*/
 static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        /*得到classpath下的所有类的路径*/
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
    /*这里创建AppClassLoader,注意这里并没有去加载路径下的类*/
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

ExtentionClassLoader 类似于这种方式去创建,那BootStrapClassLoader是怎么创建的呢?
其实java中并没有这个类,只是一个概念,它嵌入到jvm中了,由c++编写的。也就是jvm一启动就会有这个类,但是在Launcher类中规定了它加载类所在的路径

 private static String bootClassPath = System.getProperty("sun.boot.class.path");

从上面的代码也看到了 AppclassLoader 继承的是URLClassLoader,但它的父加载器却是ExtentionClassLoader ,采用这种方式是有两个方面:

  • 继承URLClassLoader,是为了继承这个类中的findClass()方法为了找到规定路径下的类。继续看URLClassLoader的源码:
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        。。。。
                    public Class<?> run() throws ClassNotFoundException {
                    /*解析类的路径*/
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
           /*通过字节码文件的路径获得相应的Class类*/
                                return defineClass(name, res);
                            } 
        。。。。。
        return result;
    }

前面有个问题是只是找到字节码文件,并没有加载它,答案就在这里,通过defineClass(name, res)来加载它。这里又引出一个问题,为什么不在loadClass()中加载它,而是通过findClass()来加载?继续留问题,后面讲。

  • 所有类加载器中的基类ClassLoader中有一个属性
 private final ClassLoader parent;

是通过类与类组合的方式来定义一个父加载器的,为了构成双亲委派模型。

那什么是双亲委派模型呢?先来看张图:
这里写图片描述
在对这个图回头看代码1,结合下面的例子来理解.
这里有一个Hello.java

public Class Hello {
    public void test() {
        String str="Hello";
    }
}

当编译完成后,有了一个Hello.class的文件。现在要加载这个字节码文件:

  • 通过AppclassLoader来加载,因为它是当前应用下的classpath下的类的的加载器(默认没有使用自定义加载器),在运行到AppclassLoader的loadClass()方法时,根据代码1的分析,它要去它的父加载器ExtentionClassLoader中的loadClass()方法中去,同样在执行过程中,也要去父加载器中走一圈,它的父加载器是bootStrapClassLoader(它没有父加载器了)
  • 通过findLoadedClass方法看是否加载,如果没有加载,试着用findClass去加载,如果不能加载,返回null,到子加载器中加载,一直进行到AppclassLoader,发现可以加载,于是开始运行。
  • 运行到了String str=”Hello”;要加载String类了,(事实上,我并不清楚它在启动时候是否由bootStrap加载器加载过吗-。-,我推测这种系统自定义的类,应该是加载过了。这里假设没有),于是又进行了一轮向上委派,委派到BootStrapClassLoader的时候,正好它可以加载。

这里回答一下,关于String类是否加载引起的思考,到底类加载是在什么时候呢? 资料上的回答是:什么情况下需要开始类加载过程的第一个阶段:”加载”。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握。 没有答案就自己找答案吧

public class StringDemo {
    //空的
}

public class IntDemo {
    private Integer a=3;
    private Integer a1=3;
    public static void main(String[] args){
        IntDemo intDemo=new IntDemo();
    //  StringDemo stringDemo=new StringDemo();
    }
}

在idea中加入jvm参数 -XX:+TraceClassLoading 追踪类加载的顺序。
第一次运行,StringDemo相关被注释掉:
[Loaded sun.launcher.LauncherHelper from D:\jdk\jre\lib\rt.jar]
[Loaded sun.misc.URLClassPath FileLoader 1 from D:\jdk\jre\lib\rt.jar]
[Loaded java.net.Inet6Address from D:\jdk\jre\lib\rt.jar]
[Loaded IntDemo from file:/F:/code/JVM/out/production/JavaString/]
[Loaded java.net.URI from D:\jdk\jre\lib\rt.jar]
[Loaded sun.launcher.LauncherHelper$FXHelper from D:\jdk\jre\lib\rt.jar]
[Loaded java.lang.Void from D:\jdk\jre\lib\rt.jar]

第二次运行,去掉注释:

[Loaded sun.misc.URLClassPath FileLoader 1 from D:\jdk\jre\lib\rt.jar]
[Loaded IntDemo from file:/F:/code/JVM/out/production/JavaString/]
[Loaded sun.launcher.LauncherHelper$FXHelper from D:\jdk\jre\lib\rt.jar]
[Loaded java.lang.Void from D:\jdk\jre\lib\rt.jar]
[Loaded StringDemo from file:/F:/code/JVM/out/production/JavaString/]

两段信息对比可以看出两点:

  1. bootStrap和Extclassloader 两个加载器在启动后会加载所有规定路径下的类。即:启动后会加载java核心包。
  2. appclassloader 加载器对应的应用类,只有在被用到的时候才会被加载。比如上面的StringDemo类。

那么为什么要设计双亲委派模型呢?
假设,整个系统中没有这个设计,还是上面的程序,没有双亲委派模型,那么String这个类就由AppclassLoader加载了,然后可能在有另一个自定义的加载器加载String这个类了,那么程序就乱套了!!到处都是真假难辨的String。而有了双亲委派模型,不管是哪个加载器加载的String,这个类都是由BootStrapClassLoader加载。双亲委派模型最主要的作用是保证java核心类的安全性。

注意:一个类可以被多次加载,但是一个加载器只能加载一次,而且判断一个类是不是相同的,是比较 包名+类名+类加载器id
当某个classloader加载的所有类实例化的对象都被GC回收了,那么这个加载器就会被回收掉。

三.双亲委派模型是不可改变的吗?

首先,明确双亲委派模型是一种规范,在自定义类加载器的时候完全可以重写loadClass()方法中的逻辑。这里回答一下前面的问题,为什么不用loadClass()实现类加载的功能,而是用findClass()。这是为了把委派模型的逻辑和类加载器要实现的逻辑分离开了。所以一般自定义类加载器loadClass()一般不动,而是重写findClass()。
什么时候要破坏委派模型呢?

  • 第一种情况

来看一个例子java需要连接数据库,但是数据库的品种这么多,每家厂商写的程序都不同,为了让每个厂商统一,java制定了一系列规范的接口,放在BootStrapClassLoader可以加载的路径中。所有厂商只要按照这些接口规范写就好了,并且统一有一个管理的类在DriverManager。这样java制定者和厂商都开开心心,这时候出现了一个问题。
根据委派模型,A类中引用了B类

public class A{
    private B = new B();
}

那么默认的B的加载器也是由A的加载器加载的!!
所以通过DriverManager统一来得到Driver的话,那么BootStrapClassLoader默认是加载 java.sql 包下的Driver接口。但实际上必须要加载它的实现类。
可是,根据委派模型,父类加载器去调用子类加载器是不可能完成的
必须由BootStrapClassLoader来加载,这就难办了,BootStrapClassLoader说我的工作就是加载 %JRE_HOME%\lib 下的,要我加工作量,不干!!况且我也找不到在哪啊,我只知道它的接口啊,并不知道它的实现类

于是,出现了一个设计,在线程Tread类中内置一个

/* The context ClassLoader for this thread 默认是appclassloader ,可以自己设置*/
    private ClassLoader contextClassLoader;

因此在父类加载器需要调用子类加载器的时候,就可以通过

Thread.currentThread().getContextClassLoader();

来获取想要的类加载器。
下面来看JDBC的源码:
在java.sql.DriverManager中

//从Thread.currentThread中取出appclassloader
 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

 是怎么使用这个classLoader的呢?

  private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver != null) {
            Class<?> aClass = null;
            try {
       //是通过Class.forName()并指定类加载器。
                aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
            } 
            。。。。
        return result;
    }
  • 第二种情况

java web中的热部署,指的是热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。在编写java程序的时候,每一次改动源代码,就必须重启一次应用程序,那java的这个缺陷是由什么导致的呢?

  1. java类只能由类加载器加载一次,这样在改动代码后,如果不重启就不能被重新加载。
  2. java类一般被系统自带的appclassloader 来加载。

然而热部署对于开发来说实在太重要了,想想当你调试js代码的时候,一个屏幕源代码,一个屏幕浏览器,随时随地的观察代码带来的变化。而java对于这一点,由于它语言的特性导致很难做到这一点。
如果想要实现热部署,需要做哪些工作呢?

  • 销毁ClassLoader
  • 创建新的ClassLoader去加载更新后的class类文件。

第一步销毁ClassLoader,如果要做到这一步,那么这个类文件一定不能是appclassloader加载的,因此要自定义ClassLoader。
第二步,要做到的话,首先必须有一个监听器去监听类发生变化,然后才能相应的创建ClassLoader去加载。
这也是Tomcat服务器在处理类加载的时候进行的做法,每一个web应用都有一个相应的类加载器。原理图如下:
这里写图片描述
并且Tomcat自定义的类加载器并不遵循双亲委派模型,而是先检查自身可不可以加载这个类,而不是先委派。前面也提到了,BootStrap,System和Common 这三个类加载器遵守委派模型同时加载java以及tomcat本身的核心类库。这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。(当然可以配置使得不是所有的app都要被加载,默认是全部加载)
Tomcat中当类发生改变时,监听器监听到触发StandardContext.reload(),然后销毁以前的类加载器,重新创造一个类加载器。
在使用idea开发的时候,可以在debug模式下,配置下图两个地方:
这里写图片描述
就可以实现方法层面的修改,即你修改了方法中的代码,不需要tomcat重新发布,也不需要重启服务器。就可以实现热部署了。

这是因为jdk1.6增加了agentmain方式,实现了运行时动态性(通过The Attach API 绑定到具体VM)。其基本实现是通过JVMTI的retransformClass/redefineClass进行method body级的字节码更新,ASM、CGLib之类基本都是围绕这些在做动态性。

猜你喜欢

转载自blog.csdn.net/mooneal/article/details/78397751