类加载
类加载
双亲委托模型
Java语言是一种具有动态性的解释型语言,类只有被加载到JVM中才能运行。JVM会将编译生成的.class文件按需求和一定的规则加载到内存,并组织成一个完整的Java应用程序,这个过程由加载器来完成(ClassLoader及其子类),类加载器也是一个类,其实质是把类文件从硬盘读取到内存中。
类加载分为隐式装载(new的方式)和显式装载(class.forName);
程序启动时只把需要的类加载到JVM,其他的在被使用的时候才会被加载,采用这种方式一方面可以加快加载速度,另一方面可以节约程序运行过程中对内存的开销。Java中每个类和接口都对应一个.class文件,这些文件可被看作可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要编译所有的文件;
Java类的加载是动态的,类可分为三类:系统类、扩展类和自定义类。Java针对这三种提供了三种类型的加载器:
- BootstrapLoader 启动类加载器:负责加载系统类;jre/lib/rt.jar、resources.jar、charsets.jar和class等
- sun.misc.Launcher.ExtClassLoader 扩展类加载器:负责加载扩展类;jre/lib/ext/*.jar的类
- sun.misc.Launcher.AppClassLoader 应用程序加载器:负责加载应用类;classpath指定的目录或jar中的类
用户还可以自定义类加载器,除了顶层的启动类加载器外,其他的类加载器由自己的父类加载,这种父子关系是通过组合关系来实现而非继承。
当有类需要被加载时,类加载器会请求父类来完成加载工作,父类会根据自己的搜索路径来搜索需要被载入的类,如果搜索不到那么子类就按照自己的搜索路径来搜索待加载的类。
类加载的主要3个步骤:
- 装载:根据查找路径找到相应的class文件,然后导入;
- 链接:分为3步
- 检查:检查加载的class文件的正确性;
- 准备:给类中的静态变量分配空间;
- 解析:将符号引用转换为直接引用(可选的)。
- 初始化:对静态变量和静态代码块执行初始化工作。
类加载的方式:
-
命令行启动应用时候由 JVM 初始化加载
-
通过 Class.forName() 方法动态加载
-
通过 ClassLoader.loadClass() 方法动态加载
其中Class.forName()与ClassLoader.loadClass()的区别:
Class.forName():将类的 .class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块;ClassLoader.loadClass():只干一件事情,就是将 .class 文件加载到jvm中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。Class.forName(name,initialize,loader)带参函数也可控制是否加载 static 块。并且只有调用了 newInstance() 方法采用调用构造函数,创建类的对象 。
JVM类加载机制:
- 「全盘负责」,当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 「父类委托」,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 「缓存机制」,缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓存区。这就是为什么修改了 Class 后,必须重启 JVM,程序的修改才会生效。
两个类相等必须满足的条件:
- 这两个类由同一个类加载器加载
- 这两个类的.class路径和名字是相同的
双亲委派模型的系统实现原理
- 加载类是通过调用java.lang.ClassLoader类的loadClass方法实现的,这个方法会首先检查类是否已被加载,如果没有它会把类加载请求委派给父类加载器去完成;
- 同理父类加载器也把类加载请求委派给它的父类加载器,直到所有的类加载请求都传递给顶层的启动类加载器;
- 如果父类加载器加载失败,子类加载器会尝试加载,以此类推直到最初被请求使用的加载器,如果成功那么类已经被成功加载,否则加载失败抛出ClassNotFoundException异常,同时不会再调用其子类加载器去进行类加载。
线程上下文类加载器
Java默认使用的类加载模型就是双亲委派模型,但是对于Java提供的很多服务提供者接口(Service Provider Interface,SPI)在Java类库中只定义了接口,允许第三方来实现这些接口(JDBC,JCE,JNDI,JAXP,JBI等)。以JDBC为例,它是Java语言提供的一组来执行SQL语句的接口,由两部分组成:Java类库提供的接口和数据库厂商提供的具体实现类(通常称为driver)。接口和具体实现应该由同一个类加载器进行加载,否则就会造成接口找不到具体的实现。
这种情况如果使用双亲委派模式进行加载,那么SPI接口是Java核心库的一部分,是由启动类加载器Bootstrap Loader来加载,但SPI的具体实现类只能由应用程序加载器AppClassLoader来加载。显然启动类加载器无法找到SPI的实现类,因为它只负责加载核心库,同时也不能代理给应用程序类加载器,因为它是应用程序类加载器的父类,在这种情况下双亲委派模型不能很好的工作了。
在线程上下文类加载器中可以自定义类加载器,并且指定这种类加载器不使用双亲委托机制,从而可以实现接口和实现类使用相同的类加载器。
Thread类中提供了getContextClassLoader()和setContextClassLoader(ClassLoader cl)两个方法用来获取和设置上下文类加载器,如果没有set那么线程将继承父线程的上下文类加载器,如果整个环境都没有设置,就会使用默认的应用程序类加载器(Application ClassLoader),对于SPI接口和实现类可以通过这种方式而不使用双亲委派模式。
类加载常见错误
ClassNotFoundException
表示类找不到异常,是一种 Exception,通常发生在载入阶段,当开发者主动调用 Class.forName()、ClassLoader.loadClass()或 ClassLoader.findSystemClass()动态加载指定类时候,类加载器就会去 classpath 下寻找类,如果找不到就会抛出此错误。
还有另外一种情况是当一个类已经被某个类加载器加载到内存中,另外一个类加载器试图去加载时也会发生错误。
发生在主动执行动态加载时
NoClassDefFoundError
NoClassDefFoundError 是一种和 ClassNotFoundException 很像的错误,只不过它是更严重的 error 类型。它发生在链接阶段,表示 jvm 在编译阶段可以找到相应的类,但在执行过程中却找不到相应的类。
一种原因是由于在编译后运行前类被更改或者删除了。另外一种则是 classpath 本身被修改过了,这可以通过System.getProperty("java.classpath")来找到程序实际运行的 classpath,或者通过-classpath 命令来指定正确的 classpath。
NoSuchMethodError
NoSuchMethodError 错误,它表示找不到方法,但找不到方法归根结底是找到了不正确的类。
通常情况下是因为 jar 包冲突问题,即加载了不匹配版本的类导致的。例如应用中有 A、B 两个二方包,A 依赖 C-v1 包,而 B 依赖 C-v2 包,如果 maven 仲裁最后使用的是 C-v1 包,那么当 B 加载到 C-v2 中有而 C-v1 中没有的方法时就会报 NoSuchMethodError。
LinkageError
LinkageError 相比较之前几种错误不那么常见,只有多个类加载器同时作用交互时才会出现。
jvm 中一个类由全限定类名与类加载器确定类实例,那么不同类加载器加载的同一个类是属于不同类实例的,然后在内存中如果两者发生交互,就会出现 LinkageError 异常。
- tomcat 之类的 javaEE 环境中,常常出这种状况,这是由于 tomcat 上的 web 应用类加载机制稍有不同,每个资源模块(比如一个 war 包)都优先使用自身的资源,突破了双亲委派模型:当 appClassLoader 加载类时候,会首先在自己的本地资源库中查找类,其次才会走双亲委派模型。那么如果一个类 A 由 AppClassLoaderx 加载,但其超类在 AppClassLoader 中没有,只有委托 XXXClassLoader 才能找到,当类 A 与其超类进行交互时就会报错了。
- 进行自定义类加载器开发时遇到。比如开发类隔离容器时,期望将某些中间件都由与应用不同的独立类加载器加载,但这时候如果中间件依赖 spring context,而应用本身也依赖 spring context,那么 作为 spring bean 交互时候就会妥妥报 LinkageError 了。
解决这个问题的办法包括 2 种,即控制不同类加载器加载的类不进行交互,或者都交于一个共同的父加载器进行加载。

浙公网安备 33010602011771号