1 类加载时机
1.1 生命周期
类从被虚拟机加载到内存,到卸载出内存为止,类的整个生命周期:加载、连接(验证、准备、解析)、初始化、使用和卸载;
1.2 加载时机
虚拟机规范没有强制约束类加载的时机,但严格规定了有且只有 5 种情况必须立即对类进行初始化
- 遇到 new、getstatic、putstatic 和 invokestatic 指令
- 对类进行反射调用时如果类没有进行过初始化
- 初始化时发现父类还没有进行初始化;
- 虚拟机启动指定的主类
- 动态语言中 MethodHandle 实例最后解析结果 REF_getStatic 等的方法句柄对应的类没有初始化时
2 类加载过程
加载、连接(验证、准备、解析)、初始化
2.1 加载
- 通过一个类的全限定名来获取定义此类的二进制流
- 将这个字节流转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
2.2 验证
确保 Class 文件的字节流信息符合虚拟机的要求,并且不会危害虚拟机安全;
- 决定了 Java 虚拟机是否能承受恶意代码的攻击;
- 校验动作
- 文件格式验证(基于二进制字节流)
- 元数据验证(对类的元数据语义分析)
- 字节码验证(对方法体语义分析)
- 符号引用验证(对类自身以外的信息进行匹配性校验)
2.3 准备
为类变量(static)分配内存并设置零值,如 static final 则是直接赋值;
2.4 解析
将常量池内的符号引用替换为直接引用,可对解析结果进行缓存
2.5 初始化
执行类构造器方法
- 类初始化时要求父类全部初始化过;接口初始化,只有真正用到父接口时才初始化
- 方法初始化是加锁阻塞等待的,应当避免在方法中有耗时很长的操作;
- 在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行
初始化顺序
先初始化静态成员,然后从父类到子类中,依次初始化非静态对象和构造器。
- 父类静态初始化块
- 子类静态初始化块
- 父类初始化块
- 父类构造器
- 子类初始化块
- 子类构造器
3 类加载器
- 虚拟机设计团队把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,实现这个动作的代码模块称为类加载器;
- 这是 Java 语言的一项创新,也是 Java 语言流行的重要原因,在类层次划分、OSGI、热部署、代码加密等领域大放异彩;
3.1 类与类加载器
- 类在虚拟机的唯一性,需要由类加载器和类本身一同确立。每个类加载器都有独立的类名称空间;
- 比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义;
3.2 双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。
三种系统提供的类加载器:启动类加载器、扩展类加载器、应用程序类加载器;
1)工作过程
如果一个类加载器收到了类加载的请求,首先把这个请求委派给父类加载器去完成,只有父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
2)好处
Java 类随着它的类加载器具备了一种带有优先级的层次关系,对保证 Java 程序的稳定运作很重要;
3)实现
实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass 方法中,逻辑清晰易懂
3.3 破坏双亲委派模型
双亲委派模型是 Java 设计者推荐给开发者的类加载器实现方法,但不是一个强制性的约束模型;
典型的两种情况:
- 为了解决 JNI 接口提供者(SPI)引入的线程上下文类加载器;
- 为了程序动态性加强的 OSGI 的 Bundle 类加载器;
3.4 自定义类加载器
除了 BootstrapClassLoader
其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
。
自定义自己的类加载器,需要继承 ClassLoader
,然后重写 loadClass()
即可。