Java 类加载机制
类的生命周期
总的来说,有:加载 => 连接 => 初始化 => 使用 => 卸载
需要注意的是,这几个阶段是按顺序开始的,并不是按顺序进行或者完成的,通常在一个阶段的执行过程中调用或者激活另一个阶段。
类的加载
该阶段主要完成三件事:
- 根据类的全限定名找到读取对应的字节码文件
- 解析字节码文件,静态数据结构转化为方法区的运行时数据结构
- 在 Java 堆中,生成 Class 对象,提供方法区数据结构的访问入口
需要注意的点:
- jvm 规范并没有要类在使用的时候加载,虚拟机可以进行预加载
- 加载过程中,发现字节码文件缺失或者其他错误,不会立即抛出异常,而是等到使用该类时,才会抛出异常
连接
仔细看图,连接其实也是类加载阶段的一部分,上文只是阐述了结果,这里详细叙述过程:
1、验证
就是验证字节码文件是否符合规范,是否有缺损,指令是否正确等。如果能确保字节码文件无误,可以采用la-Xverifynone
来关闭大部分的类验证措施,缩短虚拟机的类加载时间。
2、准备
该阶段主要的工作就是:为类的静态变量分配内存,并将其初始化为默认值。
在哪里分配内存?
静态结构都是在方法区中,静态常量也是。不过需要注意的是,如果是对象的话,还是在堆内存中分配的,方法区中只是存有引用而已。
默认值都有哪些?
0、0L、false、null
需要注意的几点:
同时被
static final
修饰的变量,在准备阶段就已经被赋予初值了,而不是默认值。被
static final
修饰的变量,编译过后,字节码文件中会出现一个ConstantValue
属性,在准备阶段虚拟机识别到后,就会显式的进行赋值操作:
只要标量(java中的基本数据类型)才会这样,而聚合量(java中的对象)不会:
它还是会在静态初始化块函数中。
3、解析
主要工作:将字节码文件中的符号引用转换为直接引用。
所谓的符号引用就是类中字面量的替代,而直接引用可以是内存地址、偏移量或者一个目标句柄。
两者的共同点都是用于指向特定的目标的。
符号引用就是一个目标标识,类被加载入内存后,我们总不能拿着这个标识去询问整个 java 进程空间或者某块没存区域吧,这样的效率太慢了。将这些标识转换为一个目标的内存引用,效率就要高很多。
注意:
该阶段不一定在下一个阶段(初始化阶段)执行,这是为了支持 Java 的运行时动态绑定机制
(没有截取完全)
invokeinterface
指令,表示调用一个接口方法。很明显,接口不能定义方法,调用的肯定是实现类的方法。这里的话 jvm 会根据示例的对象类型,决定调用哪个方法。(详情请看下一篇文章)
初始化
该阶段的初始化只针对类的成员变量,也就是静态变量。(如果是对象的成员变量的话,那就是对象的生命周期了,不是类的了)
无论是类变量直接赋值,还是在静态代码块中赋值,编译过后都是会调用一个静态代码块的初始化方法,在里面进行赋值。(顺序从上到下)
类只有在真的使用时才会进行初始化操作:
- 创建类对象 => new、反射
- 调用静态方法或者访问静态变量
- 初始化该类的子类
使用
平时写代码不都是使用嘛?
这里还是要提高一点认知的,所谓的对象,其实就是数据集合。
对象的行为,即方法,在方法区中存放着。
对象调用方法的时候,实际上都是去访问 Class 对象,它是对应类方法区的入口,然后调用方法区中的对应方法。
(详细过程下一篇文章有写)
卸载
最容易达到的场景:java 进程退出,即 jvm 进程退出。
还有就是需要达到这三个条件:
- 堆中不存在任何该类或者派生类的对象
- 加载该类的类加载器被回收了
- Class 对象无引用,即没有任何地方通过反射访问该类
就算达到了这三点,JVM 也不承诺一定会回收类。
类加载器和类加载机制(类加载阶段的扩充)
BootstrapClassLoader
是 jvm 自己的实现,称作启动类加载器
,负责加载lib
目录下类库,不能被 java 程序直接引用ExtensionClassLoader
称作扩展类加载器
,负责加载lib\ext
目录下的字节码文件,我们可以自行扩展ApplicationClassLoader
称作应用程序类加载器
,负载加载用户类路径下的字节码文件,也就是我们自己写的代码,我们也可以自行扩展。
类加载机制
总结以下几点:全盘负责
,父类委托
,缓存机制
:
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入
- 父类委托:就是所谓的
双亲委派机制
,类加载器收到了类的加载请求,会将这个请求一层一层的委托给上级的父类加载器进行加载,父类加载器找不到,再由自己来完成,如果自己都找不到,那就抛出ClassNotFoundException
异常。 - 缓存机制:类被加载过后会放入对应的缓存区,类加载器在加载一个类时,会优先从缓存中拿去,获取不到才执行对应的加载流程。(这就是为什么修改了 Class 文件之后,必须重启 JVM 才能生效的原因)
自定义类加载器
通过继承ClassLoader
类即可,具体自己百度吧。
(java 默认的类加载路径是本地,自己可实现从数据库或者网络I/O的形式加载一个类)
总结
本章内容相对轻松和愉悦!
2025/01/23
writeBy kaiven