一、类加载器

概念:

  通过一个类的全限定类名来获取描述该类的二进制字节流,将这个动作放到JAVA虚拟机外部去实现,以便应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为**“类加载器”**。

1.1 类与类加载器

  对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JAVA虚拟机的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话的意思是,比较两个类是否相等,只有在这两个类都是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同个class文件,被同一个JAVA虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。这里的相等指的是Class对象的equals()方法、instanceof 关键字做对象所属关系判定等各种情况。

public class DemoClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader() {
		//这个方法如果被覆盖掉了,说明双亲委派机制失效,双亲委派机制,是通过loadClass来连接父加载器的
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException();
                }
            }
        };
        Object o = classLoader.loadClass("com.example.demo.classLoader.DemoClassLoader").newInstance();
        System.out.println(o.getClass());
        //自定义的类加载器实例化的对象
        System.out.println(o instanceof DemoClassLoader);
        DemoClassLoader a = new DemoClassLoader();
        DemoClassLoader b = new DemoClassLoader();
        System.out.println(o.getClass()==a.getClass());//false
        System.out.println(a.getClass()==b.getClass());//true
    }
}

以上的输出结果为:

class com.example.demo.classLoader.DemoClassLoader
false
false
true

从结果不难得知,我们自定义了一个类加载去加载这个类,并实例化出了对象,但是从上面个的输出结果可以看出,这个类确实是com.example.demo.classLoader.DemoClassLoader,但是在给对象做所属类型检验是返回了false,说明JAVA虚拟机中同时存在两个DemoClassLoader类,一个是由虚拟机的应用程序加载,一个是由我们自定义的加载器加载,虽然他们都是来自于同个class文件,但是在虚拟机中仍然是连个相互独立的类。

1.3 三层类加载器

  对于JAVA虚拟机来说,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
  站在开发者角度,则分为三层类加载器:启动类加载器、扩展类加载器、应用程序类加载器。

  • 启动类加载器:
      这个类加载器负责加载<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是JAVA虚拟机能够识别的类库加载到虚拟机的内存中。启动类加载器无法被JAVA程序直接引用,所以当获取启动类加载器的时候,会返回一个null值。

  • 扩展类加载器:
      这个类加载器由ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

  • 应用程序类加载器:
      这个类加载器由AppClassLoader实现,负责加载用户类路径(classPath)上所有的类库,一般情况下,如果程序中没有自定义类加载器,AppClassLoader为应用程序的默认的类加载器。

1.4 双亲委派机制

  双亲委派模型的工作过程是:如果一个类加载器收到了类的加载请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有类的加载请求,最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候(在搜索范围内搜索不到对应的类),子加载器才会尝试自己去完成加载。
  双亲委派机制的一个好处就是,它设置了类加载器的优先级,例如类java.lang.Object,它存在rt.jar包中,无论哪一个类加载器要加载这个类,最终都委派给处于模型最顶端的启动类加载器,这样就能确保程序里面的Object类在各种类加载器中都是同一个。以下代码为双亲委派机制的核心代码在loadClass方法里面。

  双亲委派机制的好处:

  • 可以避免重复加载,当父类加载器已经加载到该类时,就没必要子加载器再加载一次。

  • 考虑到安全因素,JAVA核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委派机制模型传递启动类加载器,而启动类加载器在核心java api中发现这个名称的类已经被加载过了,并不会重新加载网络传递过来的java.lang.Object,而直接返回已经加载过的Object,从而保证相同的类名的字节码文件,在虚拟机内存只保存一份。这样就可以避免核心api没恶意篡改,不过这个机制的前提是,这个传递过来的Object类加载请求必须是得到启动类加载器才可以,因为同一个类在同一个加载器里,只会被加载一次,但是如果是不同的类加载器的话,还是可以被加载的。

二、自定义类加载器

  在讲解热部署之前,需要先连接ClassLoader这个类里面几个比较核心的方法:

  • findClass: 在自定义类加载时,一般是覆盖这个方法,且ClassLoader中给出了一个抛错的实现。
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
  • loadClass: 这个方法是双亲委派机制的代码实现,只有父类加载器加载不到类的时候,才会调用自身findClass方法进行类的查找,所以在定义自己的类加载器时,不要覆盖掉该方法,而是应该覆盖掉findClass方法。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查类是否已经被加载过
            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异常说明父加载器,加载不到
                }

                if (c == null) {
                    // 父加载器无法加载时,调用本身的findClass方法来进行加载
                    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;
        }
    }
  • defineClass: 用来将class字节解析成虚拟机能够识别的对象,defineClass方法一般和findClass一起结合使用,在自定义类加载器的时候,一般会覆盖ClassLoader的findClass方法,然后再调用defineClass方法生成Class对象。
    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }
  • resolveClass: 连接指定的类。

三、通过类加载器来实现热部署

绕开双亲委派机制核心方法loadClass,直接调用自身重写的findClass方法

四、JAVA模块化系统(JDK9)

//TODO