类加载机制
类加载过程 加载
、验证
、准备
、解析
、初始化
。
JVM 加载类
JVM
按需将 .class
文件中的字节码读入内存,将其放在运行时数据区的方法区(method code)内,最终
在 堆区(heap) 中创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。
何时进行类加载
一般来说,只有在第一次 主动调用某个类时才会去进行类加载。如果一个类有父类,会先去加载其父类,然后再加载其自身。
- 第一次意味着:每个类有且仅有一次初始化的机会。
- 主动调用
- 一个类的实例被创建
- new
- 反射
- cloning
- 反序列化
- 调用类的 static 方法
- 使用或对类/接口的 static 属性进行赋值时 (不包括 final 的与在编译期确定的常量表达式)
- 当调用 API 中的某些反射方式
- 子类被初始化
- 被设定为 JVM 启动时的启动类 (具有 main 方法的类)
- 一个类的实例被创建
类加载的生命周期
JVM 将字节码转为运行时对象分为三个阶段, loading、linking、initializing。
Loading
Loading
过程主要由 ClassLoader
完成
- 根据
类的全限定名称
获取其定义的二进制字节流
。
- 从本地系统中直接加载
- 网络
- zip、jar
- 专有数据库中提取
- 将 Java 源文件动态编译
- 将这个
字节流
所代表的静态存储结构
转化为方法区
的运行时数据结构
。 - 在
堆
中生成Class 对象
,作为对方法区中这些数据的访问入口。
ClassLoader 双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上, 因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类 时,即无法完成该加载,子加载器才会尝试自己去加载该类。
ClassLoader 继承关系,不同 CL 负责加载不同类
Linking
Verification
确保加载的类符合 JVM 规范,并进行安全检查。
Preparation
在这个阶段,JVM 会为类成员变量(不包括实例变量)分配内存空间并且赋予默认初始值,需要注意的是这个阶段 不会执行任何代码,而只是根据变量类型决定初始值。如果不进行默认初始化,分配的空间的值是随机的,有点类型c语言中的 野指针问题。
Resolution
Resolution 阶段主要工作是确认类、接口、属性和方法在类run-time constant pool的位置,并且把这些符号引用 (symbolic references)替换为直接引用(direct references)。
Initialization
这个阶段会去真正执行代码,具体包括:代码块、构造函数、变量显式赋值。
这些代码执行的顺序遵循以下两个原则:
- 有static先初始化static,然后是非static的
- 显式初始化,构造块初始化,最后调用构造函数进行初始化
class Singleton {
// private static Singleton mInstance = new Singleton();// 位置1 counter1: 1, counter2: 0
public static int counter1;
public static int counter2 = 0;
private static Singleton mInstance = new Singleton();// 位置2 counter1: 1, counter2: 1
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getInstantce() {
return mInstance;
}
}
public class InitDemo {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstantce();
System.out.println("counter1: " + singleton.counter1);
System.out.println("counter2: " + singleton.counter2);
}
}