关于Java代码热加载的思考

2017/12/03

关于Java代码热加载的一些个人总结

JAVA虚拟机的内存模型

JAVA虚拟机的内存模型

如图看到的是java虚拟机的内存模型,类加载关系到的内存区域是在静态方法区,所以类一单加载进来,就一定要full gc的时候才能被回收了

类加载器

java主要的三个内建类加载器

  • 启动类加载器(BootstrapClassLoader) 启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类).

  • 扩展类加载器(ExtClassLoader) 扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器.

  • 系统类加载器(AppClassLoader) 也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader.它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器.

自定义类加载器

实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。那么编写自定义类加载器的意义何在呢?

  • 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。

  • 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。

  • 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑

类加载器的代理模式

双亲委派模型

  • 类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推.

  • 比较两个类是否”相等”,只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

  • 代理模式是为了保证 Java 核心库的类型安全,通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的.

线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的.类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器.如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器.Java 应用运行的初始线程的上下文类加载器是系统类加载器.在线程中运行的代码可以通过此类加载器来加载类和资源.

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现.常见的 SPI 有 JDBC,JCE,JNDI,JAXP 和 JBI 等.这些 SPI 的接口由 Java 核心库来提供.如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中.这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包.SPI 接口中的代码经常需要加载具体的实现类.如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例.这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的.引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库.它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器.也就是说,类加载器的代理模式无法解决这个问题.

线程上下文类加载器正好解决了这个问题.如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器.在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类.线程上下文类加载器在很多 SPI 的实现中都会用到

类的卸载条件

该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例. 加载该类的ClassLoader已经被回收. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法.

风险

  1. 如果热加载进来的类包的class对象或者实例对象被非热加载的类包一直引用就会导致类卸载失败,然后程序长时间运行下去就会有可能导致静态区的内存溢出

  2. 如果classloader一直被持有,被垃圾回收,也会出现1的情况,所以最好别让它活过一个方法的区域,就是new出来了,创建完了需要的类就立即抛弃它

可行办法

  1. 热加载的内容,最后只会有一个引用被非热加载的实例持有,classloader在加载完立刻抛弃,这样,在卸载类的时候,只要置换被持有的引用实例就可以了,毫无风险

  2. 只让热加载的jar包的类调用非热加载的内容,而且不要让热加载的jar包所产生的实例在非热加载的jar包的实例中持续存活,但热加载的jar的实例持续持有非热加载的jar的实例是允许的,因为一旦热加载的类被卸载了,被持有的非热加载的类实例也会被卸载,但是前提是加载和卸载都是以一个整体来进行,意思就是,例如:一加载就加载一整个jar一卸载就卸载一整个jar

  3. 热加载的包,只含有无状态的类,这也是可行的. 所谓无状态,就一个类是只做计算,中间没有任何属性,(存在属性就意味着存在状态).简单来说,就是所有的方法都只是函数

  4. 无论是1和2,方法的返回值都是非热加载的类型

总结

最后,其实热加载的实例存活在非热加载的实例中也是可以的,只是这样做,程序会变得很乱,很不好管理.

设计只要遵循一个原则:干净,低耦合

要不然,自己搞的垃圾,自己都收拾不干净