类加载是一个复杂的过程, 那么我们平时说的类加载到底是干啥的呢?
一. 类加载是干啥的
我们都知道Java程序在运行之前, 需要进行编译, 由 .java => .class文件(二进制字节码文件) , 而在运行的时候呢, Java进程(JVM), 就会读取对应的 .class文件, 并且解析他的内容, 在内存中构造出类对象并进行初始化.
总的来说就是: 类 从 文件 加载到 内存里.
二. 类加载过程
对于一个类来说, 他的生命周期是这样的:
其中前五步是固定的顺序, 并且也是类加载的过程, 其中之间的三步属于连接, 所以对于类加载来说主要分以下几个步骤:
1. 加载.
2. 连接
- 验证
- 准备
- 解析
3. 初始化
下面来看集具体的执行内容.
1. 加载
找到 .class文件, 读取文件内容, 并且按照 .class文件的格式来解析.
在这个阶段, Java虚拟机需要完成一下三个事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流.
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.
- 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口.
2. 验证
检查当前的 .class文件里的内容格式是否符合要求, 保证这些信息被当作代码后运行不会危害虚拟机自身的安全.
.class文件长啥样, 官方文档有明确描述. 如下图:
Chapter 4. The class File Format
3. 准备
给类里的静态变量分配内存空间.
假设这样一段代码:
static int a = 10;
准备阶段就是给 a 分配内训空间(四个字节), 同时他初始化的值是0, 而不是10.
4. 解析
初始化字符串常量, 把符号引用替换成直接引用. 也就是初始化常量的过程.
.class文件里就会包含字符串常量. (代码中也会有很多地方用到字符串常量)
比如代码里有一行:
String str = "hello world";
但是, 在类加载之前, "hello world" 这个字符串常量是没有分配内存空间 (得类加载完了之后, 才有内存空间), 没有内存空间, str 里也就无法保存字符串常量的真实地址, 只能先使用一个占位符, 标记一下, 这块是 "hello world" 这个常量的地址, 等到真正给他分配过内存之后, 然后就用这个真正的地址代替之前的占位符.
5. 初始化
针对类进行初始化, 初始化静态成员, 执行静态代码块, 并且加载父类.
三. 合适触发类加载呢?
使用一个类的时候就触发了.
- 创建了这个类的实例.
- 使用了类的静态方法/静态属性.
- 使用了类的子类, 加载子类会触发加载父类.
四. 双亲委派模型
类加载器
JVM加载类, 是由 类加载器(class loader) 这样的模块来负责的.
JVM自带了多个类加载器.
- Bootstrap Classloader 负责加载标准库中的类
- Extension Classloader 负责加载JVM扩展的库的类
- Application Classloader 负责加载自己项目里的自定义的类
这三个类加载器各自负责各自的那一部分.
什么是双亲委派模型?
描述上述类加载器相互配合的工作过程就是双亲委派模型.
如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求 (它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载.
如下图:
那么按照这个顺序加载的好处在哪呢?
好处在于: 如果一个程序猿正好写了一个类, 他的全限定类名和标准库中的类冲突了. (比如你自己写一个类叫做 java.lang.Thread) , 此时就可以保证类加载可以加载到标准库中的类, 防止代码加载出现问题.
- 上述三个类加载器存在父子关系.
- 进行类加载的时候, 输入的内容 全限定类名, 例如 java.lang.Thread.
- 加载的时候, 从 Application Classloader 开始加载.
- 如果到最后回到 Application Classloader 之后也没有找到类, 那么就会抛出一个 "类未找到" 的异常, 类加载就失败了.
然后我也很奇怪, 双亲委派模型, 怎么没有双亲啊, 这不是儿子,父亲,爷爷吗, 也没有父母怎么叫双亲啊, 这就是机翻害死人了.