[关闭]
@dongxi 2017-08-03T22:29:21.000000Z 字数 20162 阅读 757

JVM类加载机制

JAVA JVM

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


概述

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

类加载过程

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


加载

       在加载过程中,JVM主要完成三项工作:

  1. 通过类的全限定名获取类的二进制流
  2. 将静态存储结构转化为方法区的运行时数据结构
  3. 在堆中生成代表这个类的Class对象,作为访问入口

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

验证

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

文件格式验证

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

元数据验证

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

字节码验证

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

符号引用验证

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

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

准备

       准备阶段则是正式为类变量在方法区分配内存并设置类变量初始值的阶段。这里需要注意到主要有:

解析

       解析阶段是虚拟机将常量池内符号引用替换为直接引用的过程。虚拟机规范并未规定解析阶段发生的具体时间,只要求在执行13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析(这一部分涉及到Java执行引擎的内容,具体可以参见我之后的文章)。

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

初始化

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

       在Java虚拟机规范中强制性的规定了如果一个类未初始化时必须初始化的四种情况:

       需要注意三种情况是不会引起类初始化:

       对于第一种情况,从字节码上看确实是调用了getsatic字节码,不过从输出结果上看,的确没有子类信息的初始化,这一部分我在我在网络上也没有找到解释,等以后有时间再填坑。

  1. public class testParentStatic {
  2. public static void main(String[] args) {
  3. System.out.print(SubClass.i);
  4. }
  5. }
  6. class SuperClass {
  7. static {
  8. System.out.println("SuperClass <clinit>");
  9. }
  10. public static int i = 50;
  11. }
  12. class SubClass extends SuperClass {
  13. static {
  14. System.out.println("SubClass <clinit>");
  15. }
  16. }

       输出结果为:

SuperClass <clinit>
50

  1. ...
  2. #3 = Fieldref #23.#24 // SubClass.i:I
  3. ...
  4. #23 = Class #32 // SubClass
  5. #24 = NameAndType #33:#34 // i:I
  6. ...
  7. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  8. 3: getstatic #3 // Field SubClass.i:I
  9. 6: invokevirtual #4 // Method java/io/PrintStream.print:(I)V
  10. ...

       对于第二部分就很好解释了,在创建数组时,字节码为 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文档为准):

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


双亲委派模型

       以上三个类加载器实际上都是满足一定的层次关系的,这种关系称为双亲委派模型。双亲委派模型要求除了顶层启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父子关系一般不会以继承关系来实现的,而是使用组合关系来复用父加载器的代码。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

双亲委托模型

       在ClassLoader类中有四个方法尤为重要,下面看下这四个方法的简要介绍:

  1. // 加载指定全限定名的二进制类型,这是供用户使用的接口
  2. public Class<?> loadClass(String name) throws ClassNotFoundException{...}
  3. // resolve表示是否解析,主要供继承使用
  4. protected Class<?> loadClass(String name, boolean resolve)
  5. throws ClassNotFoundException{...}
  6. // loadClass中使用的类载入方法,供继承用
  7. protected Class<?> findClass(String name) throws ClassNotFoundException {
  8. throw new ClassNotFoundException(name);
  9. }
  10. // 定义类型,在findClass方法中读取到对应字节码后调用,JVM已经实现了对应的功能,解析相应的字节码,产生相应的内部数据结构放置到方法区,不可继承
  11. protected final Class<?> defineClass(String name, byte[] b, int off, int len,
  12. ProtectionDomain protectionDomain)
  13. throws ClassFormatError{...}

       在扩展加载器器和系统加载器中loadClass方法使用的都是与父类ClassLoader相同的代码代码。

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class.
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. // this is the defining class loader; record the stats
  25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  27. sun.misc.PerfCounter.getFindClasses().increment();
  28. }
  29. }
  30. if (resolve) {
  31. resolveClass(c);
  32. }
  33. return c;
  34. }
  35. }
  36. private Class<?> findBootstrapClassOrNull(String name)
  37. {
  38. if (!checkName(name)) return null;
  39. return findBootstrapClass(name);
  40. }
  41. // return null if not found
  42. private native Class<?> findBootstrapClass(String name);

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

  1. public class testParentClassLoader {
  2. public static void main(String[] args) {
  3. try {
  4. System.out.println(ClassLoader.getSystemClassLoader());
  5. System.out.println(ClassLoader.getSystemClassLoader().getParent());
  6. System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. }

输出结果为:

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

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

       上文中提到了采用反射的方式也可以是类初始化,所以采用反射的方式创建类的实例一定会有类的载入这一过程,我们观察下代码:

  1. public static Class<?> forName(String className)
  2. throws ClassNotFoundException {
  3. Class<?> caller = Reflection.getCallerClass();
  4. return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
  5. }
  6. public static Class<?> forName(String name, boolean initialize,
  7. ClassLoader loader)
  8. throws ClassNotFoundException
  9. {
  10. Class<?> caller = null;
  11. SecurityManager sm = System.getSecurityManager();
  12. if (sm != null) {
  13. // Reflective call to get caller class is only needed if a security manager
  14. // is present. Avoid the overhead of making this call otherwise.
  15. caller = Reflection.getCallerClass();
  16. if (sun.misc.VM.isSystemDomainLoader(loader)) {
  17. ClassLoader ccl = ClassLoader.getClassLoader(caller);
  18. if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
  19. sm.checkPermission(
  20. SecurityConstants.GET_CLASSLOADER_PERMISSION);
  21. }
  22. }
  23. }
  24. return forName0(name, initialize, loader, caller);
  25. }

       很显然我们可以看出来,在一个类中采用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的继承关系:

  1. public class URLClassLoader extends SecureClassLoader {...}
  2. public class SecureClassLoader extends ClassLoader {...}

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

  1. //SecureClassLoader:
  2. protected final Class<?> defineClass(String name,
  3. byte[] b, int off, int len,
  4. CodeSource cs)
  5. {
  6. return defineClass(name, b, off, len, getProtectionDomain(cs));
  7. }
  8. protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
  9. CodeSource cs)
  10. {
  11. return defineClass(name, b, getProtectionDomain(cs));
  12. }
  13. //URLClassLoader:
  14. private Class<?> defineClass(String name, Resource res) throws IOException {
  15. long t0 = System.nanoTime();
  16. int i = name.lastIndexOf('.');
  17. URL url = res.getCodeSourceURL();
  18. if (i != -1) {
  19. String pkgname = name.substring(0, i);
  20. // Check if package already loaded.
  21. Manifest man = res.getManifest();
  22. definePackageInternal(pkgname, man, url);
  23. }
  24. // Now read the class bytes and define the class
  25. java.nio.ByteBuffer bb = res.getByteBuffer();
  26. if (bb != null) {
  27. // Use (direct) ByteBuffer:
  28. CodeSigner[] signers = res.getCodeSigners();
  29. CodeSource cs = new CodeSource(url, signers);
  30. sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
  31. return defineClass(name, bb, cs);
  32. } else {
  33. byte[] b = res.getBytes();
  34. // must read certificates AFTER reading bytes.
  35. CodeSigner[] signers = res.getCodeSigners();
  36. CodeSource cs = new CodeSource(url, signers);
  37. sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
  38. return defineClass(name, b, 0, b.length, cs);
  39. }
  40. }
  41. protected Class<?> findClass(final String name)
  42. throws ClassNotFoundException
  43. {
  44. final Class<?> result;
  45. try {
  46. result = AccessController.doPrivileged(
  47. new PrivilegedExceptionAction<Class<?>>() {
  48. public Class<?> run() throws ClassNotFoundException {
  49. String path = name.replace('.', '/').concat(".class");
  50. Resource res = ucp.getResource(path, false);
  51. if (res != null) {
  52. try {
  53. return defineClass(name, res);
  54. } catch (IOException e) {
  55. throw new ClassNotFoundException(name, e);
  56. }
  57. } else {
  58. return null;
  59. }
  60. }
  61. }, acc);
  62. } catch (java.security.PrivilegedActionException pae) {
  63. throw (ClassNotFoundException) pae.getException();
  64. }
  65. if (result == null) {
  66. throw new ClassNotFoundException(name);
  67. }
  68. return result;
  69. }

       实际上,每一层都对defineClass进行了一次封装,通过每一层的解析最终转换成了最终的模式。

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

  1. /**
  2. * 类加载上下文,持有要加载的类
  3. */
  4. public class ClassLoadContext {
  5. private final Class m_caller;
  6. public final Class getCallerClass() {
  7. return m_caller;
  8. }
  9. ClassLoadContext(final Class caller) {
  10. m_caller = caller;
  11. }
  12. }
  13. /**
  14. * 类加载策略接口
  15. */
  16. public interface IClassLoadStrategy {
  17. ClassLoader getClassLoader(ClassLoadContext ctx);
  18. }
  19. /**
  20. * 缺省的类加载策略,可以适应大部分工作场景
  21. */
  22. public class DefaultClassLoadStrategy implements IClassLoadStrategy {
  23. /**
  24. * 为ctx返回最合适的类加载器,从系统类加载器、当前类加载器
  25. * 和当前线程上下文类加载中选择一个最底层的加载器
  26. * @param ctx
  27. * @return
  28. */
  29. @Override
  30. public ClassLoader getClassLoader(final ClassLoadContext ctx) {
  31. final ClassLoader callerLoader = ctx.getCallerClass().getClassLoader();
  32. final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
  33. ClassLoader result;
  34. // If 'callerLoader' and 'contextLoader' are in a parent-child
  35. // relationship, always choose the child:
  36. if (isChild(contextLoader, callerLoader)) {
  37. result = callerLoader;
  38. } else if (isChild(callerLoader, contextLoader)) {
  39. result = contextLoader;
  40. } else {
  41. // This else branch could be merged into the previous one,
  42. // but I show it here to emphasize the ambiguous case:
  43. result = contextLoader;
  44. }
  45. final ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
  46. // Precaution for when deployed as a bootstrap or extension class:
  47. if (isChild(result, systemLoader)) {
  48. result = systemLoader;
  49. }
  50. return result;
  51. }
  52. // 判断anotherLoader是否是oneLoader的child
  53. private boolean isChild(ClassLoader oneLoader, ClassLoader anotherLoader){
  54. //...
  55. }
  56. // ... more methods
  57. }

       决定应该使用何种类加载器的接口是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:链接一个指定的类。这是一个在某些情况下确保类可用的必要方法。

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

  1. 要想实现同一个类的不同版本互存,那么这些不同版本必须由不同的类加载器进行加载,那么这些不同版本必须由不同的类加载器进行加载,因此就不能把这些类的加载工作委托给系统加载器。
  2. 为了做到这一点,就不能采用系统默认的类加载委托规则,换言之,我们定制的类加载器的父加载器必须设置为null。

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

  1. package com.dongxi.hotswaptest;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.InputStream;
  5. import java.util.HashSet;
  6. public class HotSwapClassLoader extends ClassLoader {
  7. private String basedir; // 需要该类加载器直接加载的类文件的基目录
  8. private HashSet dynaclazns; // 需要由该类加载器直接加载的类名
  9. public HotSwapClassLoader(String basedir, String[] clazns) throws Exception {
  10. super(null); // 指定父类加载器为 null
  11. this.basedir = basedir;
  12. dynaclazns = new HashSet();
  13. loadClassByMe(clazns);
  14. }
  15. private void loadClassByMe(String[] clazns) throws Exception {
  16. for (int i = 0; i < clazns.length; i++) {
  17. loadDirectly(clazns[i]);
  18. dynaclazns.add(clazns[i]);
  19. }
  20. }
  21. private Class loadDirectly(String name) throws Exception {
  22. Class cls = null;
  23. StringBuffer sb = new StringBuffer(basedir);
  24. String classname = name.replace('.', File.separatorChar) + ".class";
  25. sb.append(File.separator + classname);
  26. File classF = new File(sb.toString());
  27. cls = instantiateClass(name, new FileInputStream(classF),
  28. classF.length());
  29. return cls;
  30. }
  31. private Class instantiateClass(String name, InputStream fin, long len) throws Exception {
  32. byte[] raw = new byte[(int) len];
  33. fin.read(raw);
  34. fin.close();
  35. return defineClass(name, raw, 0, raw.length);
  36. }
  37. protected Class loadClass(String name, boolean resolve)
  38. throws ClassNotFoundException {
  39. Class cls = null;
  40. cls = findLoadedClass(name);
  41. if (!this.dynaclazns.contains(name) && cls == null)
  42. cls = getSystemClassLoader().loadClass(name);
  43. if (cls == null)
  44. throw new ClassNotFoundException(name);
  45. if (resolve)
  46. resolveClass(cls);
  47. return cls;
  48. }
  49. }
  1. package com.dongxi.hotswaptest;
  2. public class Holder {
  3. public void sayHello() {
  4. System.out.println("hello world! (version one)");
  5. }
  6. }
  1. package com.dongxi.hotswaptest;
  2. import java.lang.reflect.Method;
  3. import java.util.concurrent.TimeUnit;
  4. public class TestSwap {
  5. public static void main(String[] args) {
  6. new Thread(new Runnable() {
  7. public void run() {
  8. while (true) {
  9. try {
  10. HotSwapClassLoader classLoader =
  11. new HotSwapClassLoader("C:\\Users\\22541\\IdeaProjects\\testclassloading\\target\\classes\\",
  12. new String[]{"com.dongxi.hotswaptest.Holder"});
  13. Class clazz = classLoader.loadClass("com.dongxi.hotswaptest.Holder");
  14. Object holder = clazz.newInstance();
  15. Method m = holder.getClass().getMethod("sayHello", new Class[]{});
  16. m.invoke(holder, new Object[]{});
  17. TimeUnit.SECONDS.sleep(5);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. }
  23. }).start();
  24. }
  25. }

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

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

热替换之前的输出

       现在对Holder进行修改,将其中的version one更改为version two

  1. package com.dongxi.hotswaptest;
  2. public class Holder {
  3. public void sayHello() {
  4. System.out.println("hello world! (version two)");
  5. }
  6. }

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

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")

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

  1. import java.net.URL;
  2. import java.net.URLClassLoader;
  3. /**
  4. * Created by 22541 on 2017/5/9.
  5. */
  6. public class TestClassLoaderPathHas {
  7. public static void main(String[] args) {
  8. try {
  9. URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
  10. for (URL url : extURLs) {
  11. System.out.println(url);
  12. }
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }

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除了用于加载类外,还可以用于加载图片、视频等非类资源。同样可以采用双亲委派模型将加载资源的请求传递到顶层的引导类加载器,若失败再逐层返回。

  1. URL getResource(String name)
  2. InputStream getResourceAsStream(String name)
  3. Enumeration<URL> getResources(String name)

源码上的一些小东西

       对于ClassLoader的源码也简单看了下,不过比较悲伤的是很多东西都看不懂,就把我能看懂的拿出来简单说下,等以后能看懂了再来填这个坑。
       前文中提到了loadClass方法是线程安全的,该方法是通过对getClassLoadingLock方法返回的Object完成的,我们就先来看看这个方法:

  1. protected Object getClassLoadingLock(String className) {
  2. Object lock = this;
  3. if (parallelLockMap != null) {
  4. Object newLock = new Object();
  5. lock = parallelLockMap.putIfAbsent(className, newLock);
  6. if (lock == null) {
  7. lock = newLock;
  8. }
  9. }
  10. return lock;
  11. }

       我们可以看到这里有一个变量名为parallelLockMap,如果这个变量为空,那么就锁定当前实例,如果不为空,那么则通过putIfAbsent(className, newLock);方法来获得一个Object实例,这个方法的功能也跟名字一样,在key不存在的时候加入一个值,如果key存在就不放入,它的实现代码为:

  1. V v = map.get(key);
  2. if (v == null)
  3. v = map.put(key, value);
  4. return v;

       我们在转到ClassLoader的构造函数,这里有parallelLockMap变量初始化的过程:

  1. private ClassLoader(Void unused, ClassLoader parent) {
  2. this.parent = parent;
  3. if (ParallelLoaders.isRegistered(this.getClass())) {
  4. parallelLockMap = new ConcurrentHashMap<>();
  5. package2certs = new ConcurrentHashMap<>();
  6. domains =
  7. Collections.synchronizedSet(new HashSet<ProtectionDomain>());
  8. assertionLock = new Object();
  9. } else {
  10. // no finer-grained lock; lock on the classloader instance
  11. parallelLockMap = null;
  12. package2certs = new Hashtable<>();
  13. domains = new HashSet<>();
  14. assertionLock = this;
  15. }

       我们可以看到构造函数根据ParallelLoaders.isRegistered()来给parallelLockMap赋值,ParallelLoadersClassLoader中的一个静态内部类,该类封装了并行的可装载类型的集合:

  1. private static class ParallelLoaders {
  2. private ParallelLoaders() {}
  3. // the set of parallel capable loader types
  4. private static final Set<Class<? extends ClassLoader>> loaderTypes =
  5. Collections.newSetFromMap(
  6. new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
  7. static {
  8. synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
  9. }
  10. /**
  11. * Registers the given class loader type as parallel capabale.
  12. * Returns {@code true} is successfully registered; {@code false} if
  13. * loader's super class is not registered.
  14. */
  15. static boolean register(Class<? extends ClassLoader> c) {
  16. synchronized (loaderTypes) {
  17. if (loaderTypes.contains(c.getSuperclass())) {
  18. // register the class loader as parallel capable
  19. // if and only if all of its super classes are.
  20. // Note: given current classloading sequence, if
  21. // the immediate super class is parallel capable,
  22. // all the super classes higher up must be too.
  23. loaderTypes.add(c);
  24. return true;
  25. } else {
  26. return false;
  27. }
  28. }
  29. }
  30. /**
  31. * Returns {@code true} if the given class loader type is
  32. * registered as parallel capable.
  33. */
  34. static boolean isRegistered(Class<? extends ClassLoader> c) {
  35. synchronized (loaderTypes) {
  36. return loaderTypes.contains(c);
  37. }
  38. }
  39. }

       在ClassLoader中通过这个类来指定并行能力,如果当前的加载器具有并行能力,那么在根据类的名称返回一个Object作为锁,如果不具有并行能力,那就不用去创建这些东西了,直接把该实例锁了就可以了,就酱。

结语

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


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注