JVM类加载器与双亲委派模型(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012668018/article/details/79050959

(1)动态加载

      类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。
这段话是从IBM网站上摘抄的一段,核心意思是说,Java语言可以动态的把需要的代码(类)从其他地方(硬盘或网络)加载到内存,然后使用。与之相对的,是传统的C语言程序,在生成可执行文件的时候,会把所有相关的库文件写进目标代码代码中,这会导致可执行文件异常的大,运行时占用的内存很多。
那么Java程序呢?也许静态文件(class文件)很大,但是开始运行时只是只是其中一部分代码,其他代码没有用到的时候不会加载进内存;而加载进内存的代码,不再使用的时候也可以被回收(垃圾回收机制)。总体下来,Java程序在运行时,似乎总是没有使用全部的程序代码。这么做的好处主要有两个:节省内存;可以动态扩展。


(2)类加载器体系
      类加载器的主要任务就是从磁盘(其实也可以是其他地方,比如网络)中根据类的全限定名称定位到class文件,以二进制流的方式将这个文件读入到内存,并将其转化为JVM规定的数据结构存储在方法区。但是方法区的类数据无法直接访问,所以还需要将其包装成一个Class对象,作为外界访问的入口。在HotSpot的实现里,Class对象由于其特殊性并没有存放在堆中,而是放在方法区。
    
      本质上,类加载器应该只有两种,那就是JVM本身的由C++语言实现的类加载器,也称为启动类加载器;以及由Java语言本身实现的类加载器。关于后者,其实有个“自举”的问题,也就是“先有鸡还是先有蛋”:Java的类加载器也是一个类,它想要加载别的类,首先得把自己加载到内存中,问题是谁来加载呢?所以启动类加载器是必须的,也就是借助外力把自己加载到内存。从这里可以看出来,java语言是无法自举的。
但是从编程的角度将,可以细分为四类:首先是前面的启动类加载器,由C++实现;然后是扩展类加载器,由Java实现,位于${JAVA_HOME}/rt.jar/sun/misc的Launcher类中,它是一个内部类,叫ExtClassLoader;接着是应用程序类加载器,也称为系统类加载器,同ExtClassLoader,位于Launcher类中,叫AppClassLoader;剩下的就是自定义的类加载器了。
    (4)问题
      为什么JVM要设计这么多类加载器呢?所有的类都由启动类加载器来加载有啥不好?
      考虑这么一个场景:要运行两个java程序,它们都使用了某个类,然而是不同的版本。这时候,jvm能把依赖的这个类的两个版本都加载到内存吗?
做个实验就好,现在我自己写一个java.lang.Objcet类,如下:
package java.lang;

public class Object {
    static {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        new Object();
    }
}
然后运行一下:
java java java.lang.Object
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
      出现这种情况,是因为jvm执行的Object类是rt.jar中的那个,而不是自己写的。类加载器的工作原理就是,加载一个类时,先根据其全名去自己的缓存中查找,找到之后直接使用;没有找到才去加载,然后缓存。而Object类做为java的核心基础类,在jvm启动的时候就已经被加载到内存了,所以后来所有名为java.lang.Object的类都不会被加载到内存。可以使用-XX:+TraceClassLoading参数验证一下:
java -XX:+TraceClassLoading java.lang.Object     
[Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
…
错误: 在类 java.lang.Object 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
[Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar]
      从日志中可以看到,jvm首先打开了rt.jar,然后第一个加载的就是java.lang.Object。所以我们可以得出结论:对于同名的类,只有先加载的那个才会生效;后来的根本没机会加载进内存。
      JVM虽然是所谓“按需加载”,但是对于一些核心的类,在启动之初就必须加载进内存。因为只有这样才能保证内存中的java核心类是真的,而不是用户自己编写的。

      然而,刚才的需求是让内存中存在两个同名的类,只不过二者的版本或者实现不同。当原始设计和后续需求出现矛盾的时候,只能引入多个类加载器了。也就是说,虽然类名一样,但是如果是被不同的类加载器加载到内存的,那么JVM将会视其二者为两个不同的类。也就是说,JVM通过扩展类加载器体系的方式解决了这个矛盾,同时又没有破坏之前的加载机制——同一个类加载器只会加载一个同名类。

(5)Java类的相等性    
      根据前面的论述,决定两个类(class对象)是否相同的因素,包括该类的全限定名以及类加载器。同一个class文件被不同的类加载器加载到内存中,那么equals方法的返回值仍然是false。
      所以,任意一个类,都是由class文件本身和类加载器共同确定其在jvm中的唯一性的。每个类加载器都有自己独立的类名称空间。

(6)java.lang.ClassLoader
      Java通过扩展“类加载器”体系解决了内存中无法存在同名类的问题。在这个体系中,首先是C++实现的启动类加载器,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包放在<JAVA_HOME>/lib目录下也是没有作用的。至于虚拟机能识别的文件名有哪些,我也不知道。另外,即使是rt.jar,也不是里面全部的类会被加载,从上面的类记载日志中可以看到,比如包名开头为“javax”的那些类就没有加载。到底会加载哪些哪些类,只有去看官方文档了,甚至不同的jvm实现加载的类也不一样。
      其余的类加载器都是使用java语言实现的,它们有一个共同的父类就是java.lang.ClassLoader。父类中实现了大多数核心方法:
public abstract class ClassLoader {...}
这是一个抽象类,无法直接创建对象,继承该类的子类才可以创建对象。

首先是最核心的loadClass(String name)方法,从名称上就可以看出来,这个方法就是用来加载类的,类加载动作也确实是从调用这个方法开始。代码如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}
它调用了重载的同名方法,并且第二个参数设为false,重载的方法代码如下:
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
真正干活的是这个方法,但是它的访问修饰符是protected,也就是只有同包的类和子类才可以访问。方法内部的逻辑和注释已经写的非常清楚了,流程就是先调用findLoadedClass()方法去自己的加载缓存中查找;若没有找到则委托给父“类加载器”的 loadClass()方法去加载,父“类加载器”执行同样的流程;若仍然没有找到,则继续调用爷爷“类加载器”的这个方法;直到parent的值为null,也就是没有父“类加载器”了。
 java实现的类加载器都是ClassLoader类的子类,从而都有一个parent字段代表父“类加载器”;当这个字段被设为null时,表明它的父“类加载器”是启动类加载器,因为启动类加载器是C++实现的,java代码无法直接引用,所以用null代替。于是loadClass方法的代码中有了下面的分支:
if (parent != null) {
    c = parent.loadClass(name, false);
} else {
    c = findBootstrapClassOrNull(name);
}
 当parent == null时,不再递归调用parent.loadClass方法,而是直接调用findBootstrapClassOrNull,后者最终调用了如下方法:
private native Class findBootstrapClass(String name);
 可以看出这是一个本地方法,也就是启动类加载器中的C++代码。我们无法查看这个方法的代码,但是可以猜测,应该也是先去缓存中查找;没有的话再去自己的路径中查找并加载,若还是没有,那就只好返回null了。注意,启动类加载器查找类的时候并不是满世界乱找,而是限定了一个范围或路径,只要在这个范围内没有找到,就返回null;而这个范围,前面已经说过了。
 我突然明白,这个范围限定机制也是类加载器体系的一部分。试想一下,假如没有这个范围限制,一是效率很定会下降;二是和子类划分清楚势力范围,各司其职,不然的话父“类加载器”可以加载所有的类,还走这么多层递归调用干嘛,还要子“类加载器”干嘛?
 然后,如果缓存中没有找到,父“类加载器”也没有找到,那就只好自己动手了,也就是调用findClass方法,其代码如下:
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
what?居然什么都不干直接抛异常?其实,这正是子类可以介入的地方,只需要覆盖了这个方法,那么子类的对象最后调用的将是自己的findClass方法,而不是这个。JDK1.2之前,常用的套路是子类直接覆盖loadClass方法;之后,Java已经不建议直接覆盖loadClass方法,而是覆盖findClass方法,将自己的查找逻辑封装在这个方法中。

 总结一下,所谓双亲委派模型就是,当一个类加载器加载某个类的时候,如果自己缓存中没有,那么它不会立即从自己的加载路径中加载这个类;相反,它会先委派给父“类加载器”去加载这个类,父“类加载器”遵循同样的逻辑加载该类;当所有的父“类加载器”都没有找到这个类时,最底层的类加载器才去尝试自己加载。
 保证这个机制正常运行的关键就是子“类加载器”继承java.lang.ClassLoader类,并且没有覆盖其中作为类加载动作入口的loadClass方法。 


猜你喜欢

转载自blog.csdn.net/u012668018/article/details/79050959