概述
不知道大家有没有这种感觉,对jvm来说,原理好像都能理解,但是很容易忘记也不好实践。有一句话很适合jvm,听说过很多道理,缺依然过不好这一生。
本文的目的主要为了实践验证类初始化。主要通过以下以下几点来描述:
- jvm类加载概述
- 类被初始化的几种情况
- 类在初始化过程中做了什么
- 验证类被初始化
- 容易被混淆并不会初始化的几种情况
jvm类加载概述
有过java开发经验的可能都了解,jvm虚拟机加载过程为:
- 加载阶段(通过本地classpath,网络等方式获取class字节码文件内容加载)。
- 链接阶段(细分为验证、准备、解析三个子阶段。)
- 验证阶段负责字节码文件内容是否符合jvm规范等校验(如魔数、版本号等检测)
- 准备阶段给静态属性分配内存
- 解析阶段负责将符合引用解析为直接引用(解析阶段并不是按照顺序进行的,只要在字节码执行前完成即可)
- 初始化阶段
类被初始化的几种情况
jvm规范并没有约定类什么时候会被加载,规范约定了以下几种情况会触发类初始化阶段:
- 启动类
- new 指令目标类
- 子类初始化导致父类初始化
- 调用目标类静态方法或静态字段
- 子类导致其实现的接口初始化
- 反射Class.forClassName
- MethodHandler
类在初始化过程中做了什么
知道了类在上述描述的几种情况下会被初始化,那初始化过程中具体做了什么呢?
- 声明为final static的基本类型属性,虚拟机会标记成常量值,并为其初始化指定的值。
- 其它声明为static的属性赋值操作以及静态代码块会被java编译器置于clinit()方法中。
验证类被初始化
下面我们来做几个测试,来验证上面的情况会被初始化。
- 测试new 指令目标类
public class TestClassInit1 {
private static class Person {
static {
System.out.println("person static code");
}
}
public static void main(String[] args) {
new Person();
}
}
输出:
person static code
- 子类初始化导致父类初始化
public class TestClassInit2 {
private static class Person {
static {
System.out.println("Person static code");
}
}
private static class Student extends Person {
static {
System.out.println("Student static code");
}
}
public static void main(String[] args) {
new Student();
}
}
输出:
Person static code
Student static code
- 调用目标类静态方法或静态字段
public class TestClassInit3 {
private static class Person {
static int num = 1024;
static {
System.out.println("Person static code");
}
static void print() {
System.out.println("Person static method");
}
}
public static void main(String[] args) {
// System.out.println(Person.num);
Person.print();
}
}
输出:
Person static code
Person static method
- 调用目标类静态方法或静态字段
public class TestClassInit4 {
private static class PersonHolder {
static {
System.out.println("holder static code");
}
}
private interface Person {
PersonHolder OBJ = new PersonHolder();
/**
* 方法有默认实现,子类初始化将导致接口初始化(注释此方法后将不会导致接口初始化)
*/
default void print() {
System.out.println("person print");
}
/**
* 方法没有默认实现,子类初始化并不会导致接口初始化
*/
void print2();
}
private static class Student implements Person {
static {
System.out.println("Student static code");
}
@Override
public void print2() {
System.out.println("student print");
}
}
public static void main(String[] args) {
new Student();
}
}
输出:
holder static code
Student static code
- Class.forName方法导致初始化
public class TestClassInit5 {
private static class Student {
static {
System.out.println("Student static code");
}
}
public static void main(String[] args) throws Exception {
// 导致初始化
Class.forName("TestClassInit5$Student");
}
}
输出:
Student static code
容易被混淆并不会初始化的几种情况
- 声明数组类型属性不会被初始化
public class TestClassNotInit1 {
private static class Student {
static {
System.out.println("Student static code");
}
}
public static void main(String[] args) throws Exception {
Student[] s = new Student[1];
}
}
不会输出
- 直接获取.class不会初始化
public class TestClassNotInit2 {
private static class Student {
static {
System.out.println("Student static code");
}
}
public static void main(String[] args) throws Exception {
Class clz = Student.class;
System.out.println(clz);
// 以下代码也不会导致类初始化
System.out.println(clz.getSimpleName());
System.out.println(clz.getSuperclass());
System.out.println(clz.getDeclaredFields());
System.out.println(clz.getDeclaredMethods());
}
}
输出:
class TestClassNotInit2$Student
Student
class java.lang.Object
[Ljava.lang.reflect.Field;@7229724f
[Ljava.lang.reflect.Method;@4c873330
总结
对于初始化的几种情况不管是平时看书还是网上看博客都或多或少能了解, 其实实践一下还是会收获一些细节。 如类初始化导致接口初始化这句话并不严谨。