类的生命周期
1、加载(Loading)
通过加载器加载类。在加载过程中,加载器会做以下三件事:
1)通过一个类的全限定名来获取其定义的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在Java堆中生生成一个代表这个类的java.lang.Class对象,作为给方法区数据的访问入口。
2、校验(Verification)
确保被加载的类的正确性。主要有如下四点需要校验(例子摘自《深入理解Java虚拟机》):
1)文件格式验证:
验证class文件是否符合规范,以保证字节流正确的解析并存入方法区,以方便后面在方法区中的进行的一系列验证。例如:
- 是否以魔数0xCAFEBABE开头
- 主、次版本号是否在当前虚拟机处理范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量。
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
- ……
2)元数据验证:
验证元数据信息的语义是否符合Java语言规范。例如:
- 这个类是否有父类(除java.lang.Object)
- 这个类是否继承了不允许被继承的类(如被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载)
- ……
3)字节码验证:
验证语义的合法性、逻辑性。例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来载入本地变量表中。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的。例如把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,是危险和不合法的。
- ……
4)符号引流验证:
验证对类以外的引用是否正确。例如:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
- ……
3、准备(Preparation)
为类的静态(static)变量分配内存,并将其初始化为默认值(具体值的赋值在初始化阶段进行)。
如果这个变量既是静态变量(static)又是常量(final),则该阶段会直接赋上具体值并放入该类的常量池。
4、解析(Resolution)
把类中的符号引用转换为直接引用。其中包括:
1)类或接口的解析
2)字段解析
3)类方法解析
4)接口方法解析
5、初始化(Initialization)
初始化类的字段、代码块、构造方法。顺序如下:
1)父类静态变量
2)父类静态代码块
3)子类静态变量
4)子类静态代码块
5)父类非静态变量
6)父类非静态代码块
7)父类构造方法
8)子类非静态变量
9)子类非静态代码块
10)子类构造方法
6、使用(Using)
7、卸载(Unloading)
加载器
用于实现类的加载动作。当两个类被不同的加载器加载,那么这两个类一定不相等。
加载器有以下几个类型:
1、启动类加载器(Bootstrap ClassLoader)
负责加载<JAVA_HOME>\lib目录下的、-Xbootclasspath参数指定的路径中的,并且是虚拟机识别(仅按照文件名识别)的类库加载到虚拟机内存中。
这个类加载器是C++实现的,是虚拟机的一部分。而其他的加载器是由Java实现的,继承自抽象类java.lang.ClassLoader,独立于虚拟机外部。
2、扩展类加载器(Extension ClassLoader)
由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录下的、java.ext.dirs系统变量指定的路径下的所有类库。
3、应用程序类加载器(Application ClassLoader)
由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径(ClassPath)所指定的类库。
4、自定义类加载器(User ClassLoader)
由开发者自定义的类加载器。
加载器特点:
1、双亲委托
在加载一个类的时候,会去查找其父类加载器是否加载过这个类。
2、负责依赖
在加载一个类时,如果其还依赖其他的类,那么加载器也会去加载这些类。
3、缓存加载
一个类在加载后会放在缓存里,以避免重复加载。
类的加载时机
1、虚拟机启动时,初始化主类。
2、new一个类时。
3、调用静态方法、访问静态字段时,初始化其所在类。
4、子类初始化时,父类也会初始化。
5、如果一个接口定义额default方法,那么当直接或者间接实现该接口的类初始化时,会触发该接口的初始化。
6、使用反射API对某个类进行反射调用时,初始化该类。
7、初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
不会初始化类(可能会加载)的情况
1、通过子类引用父类的静态字段,父类会初始化,但是子类不会。
2、定义对象数组时,不会触发该类初始化。
3、调用常量时。因为常量在编译期间就已经存入了调用类的常量池中,本质上并没有直接引用类,所以不会触发初始化。
4、通过类名获取Class对象时。例如:Hello.Class时,不会初始化Hello类。
5、通过Class.forName加载指定类时,如果指定参数initialize为false时,不会触发类的初始化。不设置时默认是true。
6、通过ClassLoader默认的loadClass方法,不会触发初始化(加载了,但是不初始化)。
参考资料
1、极客时间的秦老师的Java进阶训练营
2、《深入理解Java虚拟机》