类加载概念
类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类加载过程
类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期包括以下7个阶段:
- 加载: 通过类的全限定名(包名+类名),获取该类的.class文件的二进制字节流。将二进制字节流所代表静态存储结构,转化为方法区运行时的数据结构。在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(这个阶段需要用到加载器,双亲委派模型与此有关。)
- 连接: 验证、准备、解析3个阶段统称为连接。
- 验证: 确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证被加载的class类的正确性。(毕竟class文件的产生途径不清楚,如果完全信任的话,很可能载入了有错误的字节码流导致系统崩溃)验证阶段大致会完成四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证。
- 准备: 为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了。
- 解析: 这个阶段是虚拟机将常量池的符号引用直接替换为直接引用的过程。(符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经存在在内存中了。)
- 初始化: 是类加载过程的最后一阶段。准备阶段时,静态变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据代码编写的实际情况去初始化类变量和其它资源。简单来说就是执行类的构造器方法。
- 使用: 使用类或者创建对象。
- 卸载: 如果有下面的情况,类就会被卸载:1. 该类所有实例都已经被回收,也就是java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。3. 类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类。
类加载器
在类加载流程的第一步“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流,实现这一功能的代码叫做类加载器。
Java中的任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有独立的类名称空间。
判断两个类是否相等,必须在这两个类是被同一加载器加载的前提下才有意义。否则即使Class文件相同,被同一个虚拟机加载,但是使用不同的类加载器,那么两个类也是不同的。
双亲委派模型
如果一个类被不同的加载器加载,那么虚拟机会认定是不同的类,这样Java体系最基础的行为也无从保证,应用程序会变成混乱。
所以有了双亲委派模型,双亲委派一共分了四层的加载器。除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,子类使用“组合”关系来复用父类加载器的代码,而不是继承。
- 启动类加载器: 加载Java核心类库,无法被java程序直接引用。负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的类。
- 扩展类加载器: 用来加载Java的扩展库。负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
- 应用类加载器: 根据Java的类路径来加载类,负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
- 自定义加载器: 用户自行扩展。可以让用户加载任何来源的class文件。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。 每一个层次的类加载器都是如此,因此所有的类加载请求都会传送到最顶层的启动类加载器中,只有当父加载器无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
为什么需要双亲委派模型?
前面已经提到:每个类加载器都有属于自己的命名空间。 一个类如果被不同的类加载器加载,将被认为是不同的类,这将给程序带来混乱。
所以需要在默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中它就是唯一的,不会产生歧义。所以需要双亲委派模型来是实现这种需求。
- 避免类的重复加载。
- 保证Java程序安全稳定运行,Java核心API定义类型不会被随意替换。
为了防止内存中出现多个相同的字节码,因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。
不同的类加载器,除了读取二进制流的动作和范围不一样,后续的加载逻辑是否也不一样?
我们认为除了Bootstrap ClassLoader,所有的非Bootstrap ClassLoader都继承了java.lang.ClassLoader,都由这个类的defineClass进行后续处理。
遇到限定名一样的类,这么多类加载器会不会产生混乱?
越核心的类库被越上层的类加载器加载,而某限定名的类一旦被加载过了,被动情况下,就不会再加载相同限定名的类。这样,就能能够有效避免混乱。
打破双亲委派模型的例子
SPI机制:各产商的服务通过SPI机制注册到JDK提供的接口上,SPI机制的ServiceLoader扫描配置文件得到类的全限定名,ServiceLoader使用当前线程context中的类加载器(默认为Application Classloader),所以第三方类能被正常加载。
对JDK代码包中的加载使用的是Bootstrap ClassLoader,但调用JDK中的接口时,接口所在的类会引起第三方类库的加载,会调用Application Classloader,这就不符合自下而上的委派加载顺序,而是上层类加载器委派下层类加载器去加载类的情况,这就产生了对双亲委派模型的破坏。