java 类的主动引用和被动引用

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。本文主要围绕类的初始化展开了解,其他的建议直接阅读《深入了解java虚拟机》或后续再做相关更新记录。

类的主动引用是指类进行了初始化,类的被动引用是指类没有进行初始化

(题外:类的初始化是类加载过程的最后一步,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,只有到了初始化阶段,在java代码执行的角度来看,在真正开始执行代码逻辑。)

下面我结合代码对主动和被动引用的每种情况一一说明。

主动引用(进行了类的初始化)

1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化;生成这4条指令的最常见的java代码场景是:

(1).使用new关键字实例化对象的时候(开发中最长见的一种);

public class Solution {
    public static void main(String[] args) {
        Test test = new Test(); // 输出 "Test init"
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
}

(2).读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候;

public class Solution {
    public static void main(String[] args) {
        int a = Test.a; // 输出 "Test init"
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
    public static int a = 1;
}

(3).调用一个类的静态方法的时候。

public class Solution {
    public static void main(String[] args) {
        Test.methodA(); // 输出 "Test init"
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
    public static void methodA() {}
}

2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;

public class Solution {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = Class.forName("com.cyx.jvm.Test"); // 输出 "Test init"
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
}

3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;

(依次输出"Test init"和"SubTest init")

public class Solution {
    public static void main(String[] args) {
        SubTest subTest = new SubTest();
        // 依次输出 "Test init" "SubTest init"
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
}
class SubTest extends Test {
    static {
        System.out.println("SubTest init");
    }
}

4.当虚拟机启动时,用户需要执行一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;

public class Solution {
    static {
        System.out.println("Solution init");
    }
    public static void main(String[] args) {
        // 输出 "Solution init"
    }
}

5.当使用jdk 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

public class Solution {
    public static void main(String[] args) {
        MethodHandle methodHandle;
        try {
            methodHandle = MethodHandles.lookup().findVirtual(Test.class, "sayHello", MethodType.methodType(void.class));
            methodHandle.invoke(new Test()); // 使用句柄调用sayHello方法
            // 最终输出 "Test init" 和 "Hello"
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
    public void sayHello() {
        System.out.println("Hello");
    }
}

对于这5种会触发类进行初始化的场景,虚拟机规范种使用了一个很强烈的限定语:"有且只有",这5种场景中的行为称为对一个类进行主动引用。除此之外的所有引用类的方式都不会触发类的初始化,称为被动引用

被动引用(没有进行类的初始化)

1.通过子类引用父类的静态字段,不会导致子类初始化;

public class Solution {
    public static void main(String[] args) {
        int a = SubTest.a;
        // 只输出 "Test init",子类没有初始化
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
    public static int a = 1;
}
class SubTest extends Test {
    static {
        System.out.println("SubTest init");
    }
}

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

2.通过数组定义来引用类,不会触发此类的初始化;

public class Solution {
    public static void main(String[] args) {
        Test[] arr = new Test[10];
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
}

3.常量在编译阶段会存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

public class Solution {
    public static void main(String[] args) {
        int a = Test.A;
    }
}
class Test {
    static {
        System.out.println("Test init");
    }
    public static final int A = 1;
}

上述三种被动引用的情况都不会有对应类的初始化输出。

猜你喜欢

转载自blog.csdn.net/qq_38058241/article/details/120472187
今日推荐