JVM类加载机制

JVM类加载机制

   前不久实习面试被问到了JVM类加载机制,回答的比较差,近几天又看了些相关的内容,所以打算写个博客记录下来。本文的主要内容源自于[深入理解Java虚拟机][1]、[IBM类加载器的文档][2]以及一些优秀的博客。

概述

   在Java语言中,类加载链接过程都是在程序运行的时候完成的,换言之就是动态加载和动态连接,这使Java可以动态扩展,同样也带来了一定的性能开销。

类加载过程

   类的生命周期主要分为七个阶段:加载、连接(验证、准备、解析)、初始化、使用以及卸载。其中,只有加载、验证、准备、初始化和卸载这五个过程顺序是确定的,必须按照这个顺序开始,但是这些阶段总是相互交叉地混合式进行。
类的生命周期

加载

   在加载过程中,JVM主要完成三项工作:
  1. 通过类的全限定名获取类的二进制流

  2. 将静态存储结构转化为方法区的运行时数据结构

  3. 在堆中生成代表这个类的Class对象,作为访问入口

    需要注意的是,这里并没有规定如何去获取二进制流,我认为这也是类加载器可以重载的主要原因。

验证

   这一过程主要就是保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这一过程主要包括四个阶段:**文件格式验证**、**元数据验证**、**字节码验证**和**符号引用验证**。

文件格式验证

   第一部分主要就是**验证字节流是否符合Class文件的规范并且能够被当前版本的虚拟机处理**。比如是否已0xCAFEBABE开头、主次版本是否符合虚拟机要求、指向常量值的索引是否合法等。

元数据验证

   第二部分主要是**对字节码描述的信息进行语义分析,保证其符合Java语言规范的要求**。比如是否有类、父类是否可以被继承、若不是抽象类,是否实现了所有方法等。

字节码验证

   第三部分主要是**进行数据流和控制流的分析**。其实这一部分跟编译原理上学到的内容比较相似,也是真个过程中最复杂的一部分,在JDK 1.6之后,增加了一个名为“StackMapTable”的属性来减少这一过程所使用的时间。比如保证类型转换是有效的、保证跳转指令不会跳转到方法体以外的字节码指令上等。

符号引用验证

   最后一个阶段发生在**虚拟机将符号引用转化为直接引用的时候**,这个转化动作将在解析过程中发生。比如通过符号引用中的描述是否可以找到对应的类、指定类中是否含有符合方法的字段描述符及简单名称所表述的方法和字段。

   整个验证过程,我还是认为是非常复杂,尤其是字节码验证过程是很难实现的。

准备

   准备阶段则是正式为**类变量**在方法区分配内存并设置**类变量**初始值的阶段。这里需要注意到主要有:
  • 非常量字段会被初始化“零值”
  • 常量字段会被初始化常量值

解析

   解析阶段是**虚拟机将常量池内符号引用替换为直接引用的过程**。虚拟机规范并未规定解析阶段发生的具体时间,只要求在执行13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用:直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄

解析动作主要针对类或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)四类。

初始化

   除了用户可以采用自定义类加载器参与之外,前面所有的过程都是由虚拟机主导和控制的。到了初始化阶段,才真正意义上执行类中定义的Java程序代码,也就是<clinit>类创建函数中的内容。<clinit>函数则是由编译器自动收集类中的所有的类变量的赋值动作和静态语句块中的语句合并产生的。
   我认为在这一过程中,有以下几点是需要注意的:
  • 静态语句块中可以访问、赋值定义在静态语句块之前的变量,定义在它之后的变量只可以赋值。

  • 虚拟机保证在子类<clinit>()方法执行之前,父类的<clinit>()一定已经执行完毕,这也就意味着第一个执行初始化的类一定是Object类

  • 如果一个类中并不存在静态语句块,也不存在对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法

  • 只有当父接口中定义的变量被使用时,父接口才会被初始化(对于接口的实现类也是一样)。

  • <clinit>是线程安全操作,不要在<clinit>中使用耗时很久的操作

    在Java虚拟机规范中强制性的规定了如果一个类未初始化时必须初始化的四种情况:
    
  • 遇到new、getstatic、putstatic或invokestatic四条字节码(实例化对象、静态字段以及静态方法)

  • 使用反射时

  • 初始化子类时,要先初始化父类

  • 包含main函数的类

    需要注意三种情况是不会引起类初始化:
    
  • 通过子类引用父类的静态字段,不会导致子类初始化

  • 数组引用不会导致类初始化

  • 引用常量不会引起类初始化

    对于第一种情况,从字节码上看确实是调用了getsatic字节码,不过从输出结果上看,的确没有子类信息的初始化,这一部分我在我在网络上也没有找到解释,等以后有时间再填坑。
    
public class testParentStatic {
    public static void main(String[] args) {
        System.out.print(SubClass.i);
    }
}

class SuperClass {
    static {
        System.out.println("SuperClass <clinit>");
    }

    public static int i = 50;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass <clinit>");
    }
}
   输出结果为:

SuperClass <clinit>
50

...
   #3 = Fieldref           #23.#24        // SubClass.i:I
...
  #23 = Class              #32            // SubClass
  #24 = NameAndType        #33:#34        // i:I
...
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: getstatic     #3                  // Field SubClass.i:I
         6: invokevirtual #4                  // Method java/io/PrintStream.print:(I)V
...
   对于第二部分就很好解释了,在创建数组时,字节码为 **newarray**  ,如 **new int[20]** ,这是初始化的类为 **[I**。其实Java的数组类是动态创建了特殊的类,其中并没有**length**等字段,都是通过 **arraylength**  等字节码由JVM实现的。

   对于第三种情况,常量字段是存储在常量池中,并不会使用符号引用作为入口,当然也不会使类初始化了。

   与类不同,接口只存在于一种情况:
  • 一个接口初始化时,并不要求其父接口全部完成初始化,只有使用了父接口的成员时才会初始化

卸载

   对于使用过程,是一个比较熟悉的部分了,在此就不再赘述了,再谈一谈类生命周期的卸载过程。类卸载,我认为本质上讲就是GC对方法区(也就是所谓的永生代)的类数据进行垃圾回收。根据Java虚拟机规范,只有**无用的类**才可以被回收,这需要满足三个条件:
  1. 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;

  2. 加载该类的CLassLoader(实例)已经被回收;

  3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    引导类加载器实例永远为reachable状态,有引导类加载器加载的对象理论上说应该永远不会被卸载。其实,我认JVM默认提供的三种类加载器加载的类应该都是不会被回收的,只有用户自定义的类加载器才会被回收。
    当然满足上述三个条件的无用的类也只是可以被回收,至于会不会回收,什么时候回收都还不一定的(关于GC,深入理解Java虚拟机已经比较详细了,这里就不说了)。


类加载器

   类加载器是Java中的一个核心功能,通过类加载器实现类加载阶段的“通过一个类的全限定名来获取表述此类的二进制字节流”。在Java中有三种主要的预定义类型类加载器,当JVM启动时,Java默认使用这三种类加载器(这一部分名称以[IBM文档][2]为准):
  • 引导加载器:负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(文件名识别)Java核心库加载到虚拟机内存中。采用原生代码实现,并不继承自ClassLoader。由于引导类加载器涉及到虚拟机的本地实现细节,因此开发者无法直接获取到启动类加载器的引用,不允许直接通过引用进行操作。

  • 扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所制定的路径下的所有类库。

  • 系统类加载器:负责加载用户类路径(CLASSPATH)上指定的类库,一般情况下这个就是程序中默认的加载器。可以通过ClassLoader.getSystemClassLoader()获取其引用。

    其实还有线程上下文加载器,这个将在后面单独介绍。
    

双亲委派模型

   以上三个类加载器实际上都是满足一定的层次关系的,这种关系称为双亲委派模型。双亲委派模型要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系一般不会以继承关系来实现的,而是使用组合关系来复用父加载器的代码。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
双亲委托模型
   在**ClassLoader**类中有四个方法尤为重要,下面看下这四个方法的简要介绍:
    // 加载指定全限定名的二进制类型,这是供用户使用的接口
    public Class<?> loadClass(String name) throws ClassNotFoundException{...} 
    // resolve表示是否解析,主要供继承使用
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{...}
    // loadClass中使用的类载入方法,供继承用
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    // 定义类型,在findClass方法中读取到对应字节码后调用,JVM已经实现了对应的功能,解析相应的字节码,产生相应的内部数据结构放置到方法区,不可继承    
    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError{...}
   在扩展加载器器和系统加载器中**loadClass**方法使用的都是与父类**ClassLoader**相同的代码代码。
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;
        }
    }

    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

    // return null if not found
    private native Class<?> findBootstrapClass(String name);
   这部分代码逻辑十分清晰,首先检查类是否已经被加载,若没有加载则调用父加载器的**loadClass()**方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器加载失败,则在抛出异常后,调用自己的**findClass()**方法进行加载。需要提及的是**ClassLoader**的**loadClass()**方法如果不被子类复写是线程安全方法。
   这就带来了一种优先级关系。这也就是双亲委托机制带来的好处所在了,真正完成类的加载工作的类加载器和启动这个加载过程的类加载器是可以不是同一个。真正完成类的加载工作是通过调用**defineClass**来实现的;而启动类的加载过程是通过调用**loadClass**来实现的。前者称为一个类的定义加载器,后者称为初始加载器。在JVM判断两个类是否相同的时候,使用的是类的定义加载器(对于任意一个类,都需要由加载它的类和这个类本身一同确定其在JVM中的唯一性,不同加载器加载的类被置于不同的命名空间之中)。比如Object类,它存放在**rt.jar**之中,无论哪一个类加载器要加载这个类,最终都是委派给引导加载器进行加载,它们总是同一个类。
public class testParentClassLoader {
    public static void main(String[] args) {
        try {
            System.out.println(ClassLoader.getSystemClassLoader());
            System.out.println(ClassLoader.getSystemClassLoader().getParent());
            System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果为:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null

   在这里,我们可以判定系统加载器的父加载器是扩展加载器,但是扩展加载器的父加载器为null,但是我们注意到当调用其父类时,采用的native本地方法,这便是调用了引导加载器方法,同时也未在Java文件中获取相应的引用。

   上文中提到了采用反射的方式也可以是类初始化,所以采用反射的方式创建类的实例一定会有类的载入这一过程,我们观察下代码:
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }


        public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }
   很显然我们可以看出来,在一个类中采用**Class.forName(String name)**的方式创建一个类的实例默认是采用调用类的加载器来进行加载,当然也可以采用具有类加载器参数的方法进行创建。

破坏双亲委派模型

   上文中提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的类加载器的实现方式。到目前为止,主要主要出现过三次较大规模的“破坏”情况。

双亲委派模型之前

   第一次破坏发生在双亲委派模型之前,为了兼容以前的代码在这之后的**ClassLoader**增加了一个新方法**findClass()**,在此之前用户只通过**loadClass()**实现自定义类加载器。在JDK 1.2之后,已经不再提倡采用覆盖**loadClass()**,而应当把自己的类加载逻辑写到**findClass()**方法完成加载,这样可以保证新写出来的类加载器是符合双亲委派模型的。

线程上下文加载器

   双亲委派模型本身是存在着缺陷的,无法解决基础类调用回用户代码的情况。很典型的例子就是JNDI服务,它的代码由引导类加载器去加载,但JNDI的目的就是对资源进行管理和查找,它需要调用由独立厂商实现并部署在应用程序**CLASSPATH**下的JNDI接口提供者(SPI)的代码。
   在Java中采用线程上下文加载器解决这一问题,如果不进行额外的设置,那么线程上下文加载器就是系统上下文加载器。在SPI接口是使用线程上下文加载器,就可以成功加载到SPI实现的类。
   当然,使用线程上下文加载类,也需要注意保证多个需要通信的线程间类加载器应该是同一个,防止因为类加载器示例不同而导致类型不同。
   在JDK中,**URLClassLoader**配合**findClass**方法使用**defineClass**(这里的**defineClass**方法与上文提到有所不同)实现从网络或者硬盘上加载class文件。先简单看下,**URLClassLoader**的继承关系:
public class URLClassLoader extends SecureClassLoader {...}
public class SecureClassLoader extends ClassLoader {...}

现在我们再仔细看下URLClassLoader SecureClassLoader中的各种defineClass方法:

    //SecureClassLoader:
    protected final Class<?> defineClass(String name,
                                         byte[] b, int off, int len,
                                         CodeSource cs)
    {
        return defineClass(name, b, off, len, getProtectionDomain(cs));
    }
    
    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         CodeSource cs)
    {
        return defineClass(name, b, getProtectionDomain(cs));
    }




    //URLClassLoader:
    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
   实际上,每一层都对**defineClass**进行了一次封装,通过每一层的解析最终转换成了最终的模式。

如何选择类加载器?
如果代码是限于某些特定框架,这些框架有着特定的加载规则,则不需要做任何改动,让框架开发者来保证其工作。再其他情况,我们可以自己选择最合适的类加载器,可以使用策略模式来设计选择机制。其思想将“总是使用上下文加载器”或者“总是使用当前类加载器”的决策同具体逻辑分离开。以下是参考博客使用的策略方式,应该可以适应大部分的工作场景:

/** 
 * 类加载上下文,持有要加载的类 
 */  
public class ClassLoadContext {  
  
    private final Class m_caller;  
  
    public final Class getCallerClass() {  
        return m_caller;  
    }  
  
    ClassLoadContext(final Class caller) {  
        m_caller = caller;  
    }  
}  

/** 
 * 类加载策略接口 
 */  
public interface IClassLoadStrategy {  
  
    ClassLoader getClassLoader(ClassLoadContext ctx);  
}  

/** 
 * 缺省的类加载策略,可以适应大部分工作场景 
 */  
public class DefaultClassLoadStrategy implements IClassLoadStrategy {  
  
    /** 
     * 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器 
     * 和当前线程上下文类加载中选择一个最底层的加载器 
     * @param ctx 
     * @return  
     */  
    @Override  
    public ClassLoader getClassLoader(final ClassLoadContext ctx) {  
        final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();  
        final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();  
        ClassLoader result;  
  
        // If 'callerLoader' and 'contextLoader' are in a parent-child  
        // relationship, always choose the child:  
        if (isChild(contextLoader, callerLoader)) {  
            result = callerLoader;  
        } else if (isChild(callerLoader, contextLoader)) {  
            result = contextLoader;  
        } else {  
            // This else branch could be merged into the previous one,  
            // but I show it here to emphasize the ambiguous case:  
            result = contextLoader;  
        }  
        final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();  
        // Precaution for when deployed as a bootstrap or extension class:  
        if (isChild(result, systemLoader)) {  
            result = systemLoader;  
        }  
          
        return result;  
    }  
      
    // 判断anotherLoader是否是oneLoader的child  
    private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){  
        //...  
    }  
  
    // ... more methods   
}  
   决定应该使用何种类加载器的接口是**ClassLoaderStrategy**,为了帮助**IClassLoaderStrategy**做决定,给它传递了个**ClassLoadContext**对象作为参数,**ClassLoadContext**持有要加载的类。
   上面的代码逻辑十分清晰:如果调用类的当前类加载器和上下文类加载器是父子关系,则总选择自类加载器。对子类加载器可见的资源通常是对父类可见资源的超集,因此如果每个开发者都遵循代理规则,这样做大多数情况下是合适的。
   如果当前类加载器和上下文类加载器是兄弟关系时,决定使用哪一个是比较困难的。理想情况下,Java运行时不应产生这种模糊。但一旦发生,上面代码选择上下文类加载器(参考博主的实际经验)。**一般来说,上下文类加载器要比当前类加载器更适合于框架编程,而当前类加载器则更适合于业务逻辑编程。**最后需要检查一下,以便保证所选类加载器不是系统类加载器的父亲,在开发标准扩展类库时这通常是个好习惯。

代码热替换、热部署

   实际上就是希望应用程序能够像我们的电脑外设那样,插上鼠标或U盘,不用重启就能够立即使用,鼠标有问题或者升级就换个鼠标,不同停机也不用重启。对于个人电脑来说,重启一次没什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况热部署对于软件开发者,尤其是企业级软件开发者具有很大的吸引力。
   OSGi是当前业界Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
   在OSGi环境中,类加载器不再是双亲委托模型的树状结构,而是进一步发展为网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
  1. 将以java.*开头的类,委托给父类加载器加载

  2. 否则,将委派列表名单内的类,委派给父类加载器加载

  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载

  4. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载

  5. 否则,类查找失败

    上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。
    其实,对于OGSi我并没有怎么使用过,也不是很了解,所以在这里就不详细的介绍了,等我什么时候有时间了解了以后可能会水篇博客。

代码热替代的简单实现

   所谓热替代,通俗的说就是指一个类已经被一个加载器加载以后,在不卸载它的情况下重新加载它一次。实际上,为了实现这一功能必须在加载的时候进行新的处理,先判断是否已经加载,若是则重新加载一次,否则直接首次加载它。首先介绍下**ClassLoader**类和热替换有关的一些方法:
  1. findLoadedClass:每个类加载器都会维护有自己的一份已加载类名字空间,其中不能出现两个同名类。凡是通过该类加载器加载的类,无论是直接还是间接,都是保存在自己的名字空间中,该方法就是在该名字空间中,该方法就是在改名字空间中寻找指定的类是否已存在,如果存在就返回类的引用,否则返回null。

  2. getSystemClassLoader:该方法返回系统使用的CLassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。

  3. defineClass:该方法是ClassLoader中的非常重要的方法,它接收以字节数组表示的类字节码,并把它转换成Class实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。

  4. loadClass:加载类的入口方法,调用该方法完成类的显示加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。

  5. resolveClass:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法。

    在实现热替换时需要有两点进行特别的说明:

  6. 要想实现同一个类的不同版本互存,那么这些不同版本必须由不同的类加载器进行加载,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统加载器。

  7. 为了做到这一点,就不能采用系统默认的类加载委托规则,换言之,我们定制的类加载器的父加载器必须设置为null。

    下面是一个很简单的官方demo:

package com.dongxi.hotswaptest;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashSet;


public class HotSwapClassLoader extends ClassLoader {

    private String basedir; // 需要该类加载器直接加载的类文件的基目录
    private HashSet dynaclazns; // 需要由该类加载器直接加载的类名

    public HotSwapClassLoader(String basedir, String[] clazns) throws Exception {
        super(null); // 指定父类加载器为 null
        this.basedir = basedir;
        dynaclazns = new HashSet();
        loadClassByMe(clazns);
    }

    private void loadClassByMe(String[] clazns) throws Exception {
        for (int i = 0; i < clazns.length; i++) {
            loadDirectly(clazns[i]);
            dynaclazns.add(clazns[i]);
        }
    }

    private Class loadDirectly(String name) throws Exception {
        Class cls = null;
        StringBuffer sb = new StringBuffer(basedir);
        String classname = name.replace('.', File.separatorChar) + ".class";
        sb.append(File.separator + classname);
        File classF = new File(sb.toString());
        cls = instantiateClass(name, new FileInputStream(classF),
                classF.length());
        return cls;
    }

    private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
        byte[] raw = new byte[(int) len];
        fin.read(raw);
        fin.close();
        return defineClass(name, raw, 0, raw.length);
    }

    protected Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Class cls = null;
        cls = findLoadedClass(name);
        if (!this.dynaclazns.contains(name) && cls == null)
            cls = getSystemClassLoader().loadClass(name);
        if (cls == null)
            throw new ClassNotFoundException(name);
        if (resolve)
            resolveClass(cls);
        return cls;
    }
}
package com.dongxi.hotswaptest;

public class Holder {
    public void sayHello() {
        System.out.println("hello world! (version one)");
    }
}
package com.dongxi.hotswaptest;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;


public class TestSwap {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                while (true) {
                    try {

                        HotSwapClassLoader classLoader =
                                new HotSwapClassLoader("C:\\Users\\22541\\IdeaProjects\\testclassloading\\target\\classes\\",
                                        new String[]{"com.dongxi.hotswaptest.Holder"});
                        Class clazz = classLoader.loadClass("com.dongxi.hotswaptest.Holder");
                        Object holder = clazz.newInstance();
                        Method m = holder.getClass().getMethod("sayHello", new Class[]{});
                        m.invoke(holder, new Object[]{});
                        TimeUnit.SECONDS.sleep(5);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }

        }).start();
    }
}


   编译、运行我们的程序,会输出:

hello world! (version one)
hello world! (version one)
hello world! (version one)

热替换之前的输出
   现在对**Holder**进行修改,将其中的**version one**更改为**version two**:
package com.dongxi.hotswaptest;


public class Holder {
    public void sayHello() {
        System.out.println("hello world! (version two)");
    }
}

   重新编译运行,我们发现输出已经发生了变化:

hello world! (version two)
hello world! (version two)
hello world! (version two)
hello world! (version two)

代码热替换之后的输出
   这里需要提及的是我们并未在测试类中使用了类型转换(***Holder holder = (Holder)clazz.newInstance();***),这里就涉及到了我们在之前提到的JVM对类型的判定(由加载它的类和这个类本身一同确定其在JVM中的唯一性),如果进行类型转换那么会抛出*ClassCastException*异常。这是由于*clazz*是由我们自定义的类加载器的,而*holder*变量类型和转型的*Holder*是由run方法所属的类加载器(系统加载器)进行加载的,所以会抛出异常。如果采用增加接口的方式进行转换,那么也是不可以的,原因也大致相同。

扩展

在运行时判断系统类加载器加载路径

   一是可以直接调用*ClassLoader.getSystemClassLoader()*或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自*URLClassLoader*),调用*URLClassLoader*中的*getURLs()*方法可以获取到。
   二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :*System.getProperty("java.class.path")*。

在运行时判断标准扩展类加载器加载路径

import java.net.URL;
import java.net.URLClassLoader;

/**
 * Created by 22541 on 2017/5/9.
 */
public class TestClassLoaderPathHas {
    public static void main(String[] args) {
        try {
            URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
            for (URL url : extURLs) {
                System.out.println(url);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_131/jre/lib/ext/zipfs.jar

通过类加载器加载非类资源

   ClassLoader除了用于加载类外,还可以用于加载图片、视频等非类资源。同样可以采用双亲委派模型将加载资源的请求传递到顶层的引导类加载器,若失败再逐层返回。
URL getResource(String name)
InputStream getResourceAsStream(String name)
Enumeration<URL> getResources(String name)

源码上的一些小东西

   对于*ClassLoader*的源码也简单看了下,不过比较悲伤的是很多东西都看不懂,就把我能看懂的拿出来简单说下,等以后能看懂了再来填这个坑。
   前文中提到了**loadClass**方法是线程安全的,该方法是通过对**getClassLoadingLock**方法返回的Object完成的,我们就先来看看这个方法:
    protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
    }
   我们可以看到这里有一个变量名为**parallelLockMap**,如果这个变量为空,那么就锁定当前实例,如果不为空,那么则通过**putIfAbsent(className, newLock);**方法来获得一个Object实例,这个方法的功能也跟名字一样,在key不存在的时候加入一个值,如果key存在就不放入,它的实现代码为:
V v = map.get(key);
if (v == null)
   v = map.put(key, value);
 
return v;
   我们在转到**ClassLoader**的构造函数,这里有**parallelLockMap**变量初始化的过程:
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
   
   我们可以看到构造函数根据**ParallelLoaders.isRegistered()**来给**parallelLockMap**赋值,**ParallelLoaders**是**ClassLoader**中的一个静态内部类,该类封装了并行的可装载类型的集合:
 private static class ParallelLoaders {
        private ParallelLoaders() {}

        // the set of parallel capable loader types
        private static final Set<Class<? extends ClassLoader>> loaderTypes =
            Collections.newSetFromMap(
                new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
        static {
            synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
        }

        /**
         * Registers the given class loader type as parallel capabale.
         * Returns {@code true} is successfully registered; {@code false} if
         * loader's super class is not registered.
         */
        static boolean register(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                if (loaderTypes.contains(c.getSuperclass())) {
                    // register the class loader as parallel capable
                    // if and only if all of its super classes are.
                    // Note: given current classloading sequence, if
                    // the immediate super class is parallel capable,
                    // all the super classes higher up must be too.
                    loaderTypes.add(c);
                    return true;
                } else {
                    return false;
                }
            }
        }

        /**
         * Returns {@code true} if the given class loader type is
         * registered as parallel capable.
         */
        static boolean isRegistered(Class<? extends ClassLoader> c) {
            synchronized (loaderTypes) {
                return loaderTypes.contains(c);
            }
        }
    }
   在ClassLoader中通过这个类来指定并行能力,如果当前的加载器具有并行能力,那么在根据类的名称返回一个Object作为锁,如果不具有并行能力,那就不用去创建这些东西了,直接把该实例锁了就可以了,就酱。

结语

   这篇文章由于我本身对类加载机制也不是分的了解,肯定还有很多的不足,也留了一些坑等着以后填,感觉要学的东西好多的说。

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

推荐阅读更多精彩内容