类加载就是 .class 文件, 从文件(硬盘) 被加载到内存(元数据区)中的过程
类加载的过程
加载: 找 .class 文件的过程, 打开文件, 读文件, 把文件读到内存中
验证: 检查 .class 文件的格式是否正确
- .class 是一个二进制文件, 其格式有严格的说明
准备: 给类对象分配内存空间 (先在元数据区占个位置), 也会使静态成员被设置成 0 值
解析: 初始化字符串常量, 把符号引用转换为直接引用
- 字符串常量,有一块内存空间存放其实际内容, 还有一个引用,来保存这块内存空间的起始地址
- 在类加载之前,字符串常量处在 .class 文件中, 此时这个 引用 记录的并非是字符串常量的真正地址, 而是它在文件中的 “偏移量” (或者说是占位符)
- 类加载之后, 才真正把这个字符串常量给放到内存中, 此时才有 “内存地址”,该引用才能被真正赋值成指定的内存地址
初始化: 调用构造方法, 进行成员初始化, 执行代码块, 静态代码块, 加载父类 …
顺便看一下类的生命周期
一个类加载的时机(一个类何时会被加载)
JVM 采用的是懒汉模式
1.构造类的实例
2.调用该类的静态方法 / 使用静态属性
3.加载子类的时候, 会先加载其父类
即用到了, 才加载, 加载过后, 后续使用不必重复加载
双亲委派模型
上述类加载过程中有个环节 ---- 加载 : 找到 .class 文件, 并读取到内存中
双亲委派模型,描述的就是该过程
JVM 提供了三个类加载器
BootstapClassLoader : 负责加载 标准库 中的类, Java 规范要求提供的类(即所有 JVM 都会提供的)
ExtensionClassLoader : 负责加载 JVM 拓展类 中的类, 规范之外, JVM 厂商 / 组织提供的额外的功能
ApplicationClassLoader : 负责加载 用户提供的第三方库 / 用户项目代码 中的类
上述三个类加载器存在 “父子关系”, 即每个类加载器中有一个 parent 属性指向自己的 父 类加载器
上述类加载器如何配合工作?(双亲委派模型的流程)
简单版本:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父
类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启
动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)
时,子加载器才会尝试自己去完成加载。
详细版本:
当加载一个类的时候, 先从 ApplicationClassLoader 开始
但是 ApplicationClassLoader 不会直接加载, 而是会把任务交给自己的父亲,让父亲去完成
于是 ExtensionClassLoader 就去加载了, 但是 ExtensionClassLoader 也不是直接加载, 而是再委托给自己的父亲去加载
现在 BootstrapClassLoader 就要去加载了, 在此之前, BootstrapClassLoader 也会先委派给自己的父亲, 此时会发现, 自己的父亲是 null
即 BootstrapClassLoader 并没有父亲
- 没有父亲或者父亲加载完了,没有找到类, 这两种情况下才会由自己加载
此时 BootstrapClassLoader 会加载该类, 即搜索自己负责的标准库目录里的相关类, 如果找到, 就加载, 找不到,则由子类加载器进行加载
ExtensionClassLoader 搜索拓展库相关目录, 如果找到就加载, 找不到就由子类加载器进行加载
ApplicationClassLoader 搜索用户项目相关目录, 如果找到就加载, 找不到就由子类加载器进行加载 (由于 ApplicationClassLoader 没有子加载器, 会抛出异常 ---- 类找不到)
双亲委派模型的目的
保证 Bootstrap 能够先加载类, Appcation 能够后加载类
放在当用户自定义的类和标准库中类同名时, 能够优先使用标准库中的类(防止不必要的 bug)
双亲委派模型的优点(其实就是上面目的的另一种说法)
- 避免重复加载类
- 安全性: 使用双亲委派模型保证 Java 的核心 API 不被篡改
破坏双亲委派模型
很好的一个示例是 JDBC
其中 Driver 接口的实现类是由 子类加载器加载的 (我自己包里的类肯定是要用的, 如果遵循双亲委派模型, 那我不是白写了 [用不到] )
综上, 是否遵循双亲委派模型可以根据需求具体修改