ClassLoader原理与实践(分享讲稿整理,不放图)

来源:互联网 发布:淘宝 卓诗尼 编辑:程序博客网 时间:2024/05/17 23:40


java.lang.ClassLoader(类加载器)是JRE中一个重要成员,因为大家一直都在主动或被动地在使用。



首先来讲一讲classloader到底是什么。大家知道平时写的java文件编译完之后会得到class文件,然后要发挥作用需要被写入到jvm的内存里。而类文件的二进制字节码写入内存的关键过程就是由类加载器来做的。而且载入这个动作对用户类是动态的,即只有当要用到这个类的时候,只要能找到class文件就可以加载。


类的加载分为显式和隐式。显式就是在代码中直接调用类加载器的loadClass或forName方法。平时大家对classloader感知少是因为隐式加载占多数。隐式加载在以下情形会发生:new对象,读写static变量,调用static方法,反射,加载子类时父类还未加载,jvm启动加载main所在类。


广义的加载除类载入外还包括连接和初始化。连接包括验证类合法性,创建静态域和解析依赖类(不是import而是代码块中的引用)。初始化则是类在真正被用到初始化static域和执行static代码块。此处不做展开。根据OOP的思想,类本身也是一个类,即Java.lang.Class。当然,类也是会被卸载的,自定义类的数据一直存放在JVM的持久代,其回收时机是当其本身不被引用、其类加载器被回收、无实例对象时其会被回收。这个下文还会提到。






然后我们来看一看有哪些classloader和他们的分工合作机制。
(1)BootStrap[启动类加载器] 是最顶层的类加载器,它是由C++编写而成,并且已经内嵌到JVM中了,主要用来读取Java的核心类库  (rt.jar等核心包,java.等命名的包)
(2)Extension ClassLoader[扩展类加载器] 是用来读取Java的扩展类库,读取JRE/lib/ext/*.jar (举个例子 jce.jar加密算法包就属于这类)
(3)App ClassLoader[应用类加载器] 是用来读取当前应用下CLASSPATH指定的所有jar包或目录的类文件【是用户自定义类装载器的缺省父装载器】
(4)Custom ClassLoader[用户自定义类加载器] 是用户自定义编写的,它用来读取指定类文件


Java默认和推荐的classloader协作方式是双亲委派机制。每个classloader都限定了其潜在加载类的搜索范围,且都持有一个父加载器的引用(不是继承关系)。加载时,先检查类是否存在,递归向上查看是否已加载,递归向下尝试加载,找到最终实际载入这个类的类加载器,建立起清晰的类层级关系。这样做的意义在于防止类重复加载,既是节约资源,更是从安全上防止对jvm核心类的恶意重写类加载。(思考,在用户目录放一个java.lang.Bug能否加载成功?答案是不能的,会抛出包名被禁用的安全异常,针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载)


但也有一些场景不应使用双亲委派。如以JDBC为代表的JavaSPI接口在核心库,实现类在应用库,使用线程上下文类加载器获得默认类加载器AppClassLoader才能加载这些实现类,因为当前类加载器ExtClassLoader不能加载,参见ServiceLoader.load方法。




一个classloader实例只能加载一个类一次,但同一个虚拟机上允许多个classloader实例分别加载同一个类,这里的同一个类是由包名+类名定位的SystemDictionary,在JVM中对同一个类或包的判断也是以类加载器+包名/类名来唯一确定的(实际由defineClass方法完成)。尝试在委托链上加载类的加载器称为初始类加载器,最终实际加载了类的加载器称为定义类加载器。jvm为每个类加载器维护的一个表,这个表记录了所有以此类加载器为初始类加载器(而不是定义类加载器,即一个类同时存在于委托链上每个类加载器的表中),对应了一个命名空间。除非用反射,否则不同命名空间的类不会交互。




讲完了概念,我们来看一下应用。由于classloader的API开放给了开发者,我们可以在想要的时候自定义动态加载所需要的类,而且不用一开始就把类文件准备好,这就是我们所讲的热加载了。我们常见的应用场景是这样的三级构造:JVM-tomcat服务器-用户自定义容器。应用入口是tomact的Bootstrap类main函数,首先加载了JVM中的基础类和扩展类,然后是Tomcat用到的类。Tomcat也有一个类加载器树,通过自定义类加载器实现了tomcat容器和各个web应用的类隔离。那么,同样道理,我们也可以在自己的web应用中,也实现自定义容器和各个子模块的类隔离。Tomcat可以做到分钟级别的类更替,而我们可以做到秒级别。


classloader特性也使我们可以对代码加密,在我们使用自定义的classloader的时候,可以装载加密过的class文件,然后解密再执行类加载,如此则不能反编译,此处不做展开。


下面介绍一下一个在自定义主动类加载器的使用场景,Bean的热加载机制。完整的流程是,在独立代码流开发模块业务代码,并按需deploy二方包到MVN仓库,触发容器去仓库拉取新jar,载入包中所有类,在类层面完成新老更替。随后生成新类的实例,配合ABtest机制完成迭代。


在模块代码中,我们将类文件区分为两类,一类是Bean,一类是Bean的支持类。前者需遵循接口、命名、路径的规范,因为他们将以统一的形式对外暴露提供服务。后者则没有什么特殊的要求。这两者在加载方式上也有区别。


我们自行编写了CustomerClassLoader来做Bean的主动加载,该类继承了URLClassLoader,用于从指定路径,也就是最新jar的路径,加载资源,且在加载前了路径检查。这个类也遵循了双亲委托机制,其parent是Tomcat的WebAppClassLoader。webappClassLoader只能加载/WEB-INF下的类,这个路径一般是放web层的类及其依赖的jar,显然出于区别考虑我们不会把拉取过来的jar放在这个路径。因此在主动加载的时候,其实每个Bean最终都是CustomerClassLoader来加载的,因为只有这个类知道正确的路径。


那么剩下的就是Bean的支持类了。这些类不会被主动加载,一般情况下,他们的加载时机是Bean的实例生成完后,运行时碰到了new这些支持类对象。根据使用当前类加载器的原则,也是由CustomerClassLoader加载得到。


不管是Bean还是其支持类,都不可避免地会需要引用外部类,比如guava。模块中引用的外部类,可能容器也引用了,也可能没引用。
根据类的命名空间规则,WebAppClassLoader加载的类可以被CustomerClassLoader加载的类看到,反之则不能。那么此时应该由CustomerClassLoader来加载还是WebAppClassLoader来加载比较合适呢?两者各有好处。我们的选择是统一交由WebAppClassLoader来加载,原因有以下几点,如果不依赖平台提供外部类,模块中引用类需要添加到jar中,久之难以管理。如果容器和模块的外部类版本不一致,不利于排查问题。我们的做法是,在容器中统一配置一个pom提供模块所需的全部外部依赖,如有任何新依赖的外部包,不管容器本身是否依赖,统一添加到该pom。这样所有的外部类都会由WebAppClassLoader加载一个统一的版本,而且模块jar不用集成任何外部依赖,当然缺点是不能热引入外部依赖。




实际生产环境中,二方包是会在容器不重启的情况下被反复加载的。而上文提到过,同一个类不能加载两次。那么,如果我们修改了一个上一个jar中已经存在的同名类,应该怎样让他安全快速地生效?我们的做法是做jar版本统一更新,每一次重新加载,使用一个新的CustomerClassLoader实例,负责重新加载jar中的类,实际上他对之前有没有这个类是无感知的。我们在对象层面统一封装了一个BeanMap,维护每个bean唯一的引用,用单例模式对外提供服务。当新的加载器实例装载完bean的实例后,将BeanMap清空老的bean实例并装入新的。随后随着调用,bean辅助类也被加载,完成了所有类的更新。旧版本jar的类,待其对象被gc后,理论上具备了回收条件,但是由于gc的机制,类和对象一样没有办法强制卸载。当然我们线上机器permSize是256M,实际使用率都在15%,几个类文件带来的影响可以忽略。
















原创粉丝点击