转载

JVM(二)类的主动使用与被动使用

  • 主动使用
  • 被动使用

所有Java虚拟机实现必须在每个类或接口被Java程序 首次主动使用 时才初始化

  • 主动使用才进行初始化
  • 第一次主动使用才进行初始化,之后就不再初始化

只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可认为是对类或接口的主动使用

主动使用

  • 创建类的实例
    • new Object()
  • 访问某个类或接口的 静态变量 ,或者对该 静态变量 赋值
    • 助记符,通过javap 反编译后可以得到 getstatic , putstatic 这两个指令
  • 调用类的静态方法
    • invokestatic
  • 反射
    • Class.forName("com.xxx.xxx")
  • 初始化一个类的子类
  • Java虚拟机启动时被标明为启动类的类
    • 包含main方法的类
  • JDK7开始提供的动态语言支持,java.lang.invoke.MethodHandle

被动使用

除了主动使用的7种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的 初始化 ,但是依然会对类进行 加载连接

代码模拟类的主动和被动使用

public class Test02 {

    public static void main(String[] args) {
        //情况1.调用子类的str,
        //输出    parent static block
        //       hello jvm
//        System.out.println(Child.str);
        //情况2.调用子类的str2
        //输出   parent static block
        //      child static block
        //      hello jvm2
        //
        System.out.println(Child.str2);
      
    }
}

class Parent{

    public static String str = "hello jvm";

    static {
        System.out.println("parent static block");
    }

}

class Child extends Parent{

    public static String str2 = "hello jvm2";

    static {
        System.out.println("child static block");
    }
}

复制代码

结果总结

主动使用

添加JVM参数 -XX:+TraceClassLoading

JVM(二)类的主动使用与被动使用

可以观察到即使调用Child.str,Child没有被初始化,但是依然会被jvm加载到内存中

JVM参数规律

-XX: 是开头

  • -XX:+option,开启option选项
  • -XX:-option,关闭option选项
  • -XX:option=value,标识将option的值设置为value

final修饰的变量

代码举例

public class Test03 {
    public static void main(String[] args) {
        //情况1.没有使用final
//        System.out.println(Parent2.str);
        //情况2,使用了final
        System.out.println(Parent2.str2);
    }
}

class Parent2 {
    /**
     * 注意是final
     */
    public static String str = "hello jvm";
    public static final String str2 = "hello jvm";

    static {
        System.out.println("Parent2 static block");
    }
}

复制代码

如图中,打印的输出结果是

hello jvm
复制代码

final修饰的常量,在编译阶段,就会被放在调用这个常量的方法的所在的类的常量池,本质上,调用类并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

.通过javap -c 反编译Test03的class文件

javap -c Test03
复制代码
Compiled from "Test03.java"
public class com.r09er.jvm.classloader.Test03 {
  public com.r09er.jvm.classloader.Test03();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       // 已经将final的静态变量直接定义为常量
       3: ldc           #4                  // String hello jvm
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

复制代码

ldc

助记符:

ldc,表示将int,float或者是String类型的常量值从常量池中推送至栈顶

非编译期常量

public class Test04 {
    public static void main(String[] args) {
        System.out.println(Parent4.str);
    }
}

class Parent4 {
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("parent4 static block");
    }
}
复制代码

这个例子中,虽然 str 也是静态常量,但是在编译期str的值并不能确定,这个值就不会被放到常量池中,所以在程序运行时,会导致主动使用这个常量所在的类,导致这个类的初始化

数组实例

对于数组实例来说,其类型是JVM在运行期动态生成的,表示为 [Lcom.xxx.xxx

这种形式,动态生成的类型,其父类型就是Object

对于数组来说,JavaDoc经常将构成数组的元素称为Component,实际上就是将数组降低一个维度之后的类型

代码示例

public class Test05 {

    public static void main(String[] args) {
        Parent5[] parent5Arr = new Parent5[1];
        System.out.println(parent5Arr.getClass());

        Parent5[][] parent5Arr2 = new Parent5[1][1];
        System.out.println(parent5Arr2.getClass());

        System.out.println(parent5Arr.getClass().getSuperclass());
        System.out.println(parent5Arr2.getClass().getSuperclass());

        System.out.println("===");
        int[] intArr = new int[1];
        System.out.println(intArr.getClass());
        System.out.println(intArr.getClass().getSuperclass());

    }
}
class Parent5{
    static {
        System.out.println("Parent5 static block");
    }
}
复制代码

输出

class [Lcom.r09er.jvm.classloader.Parent5;
class [[Lcom.r09er.jvm.classloader.Parent5;
class java.lang.Object
class java.lang.Object
===
class [I
class java.lang.Object
复制代码

接口

当一个接口在初始化时,并不要求其父接口完成了初始化,只有在真正使用到父类接口的时候(如引用接口中定义的常量时),父类才会被初始化

初始化阶段的顺序

在类的初始化阶段,会从从上至下初始化类的静态属性,所以会有一个顺序性问题.这个问题可能会导致意外结果 如下有一个例子

public class Test07 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        System.out.println("counter1=="+Singleton.counter1);
        System.out.println("counter2=="+Singleton.counter2);
    }
}

class Singleton {
    public static int counter1=1;


    private static Singleton singleton = new Singleton();

    private Singleton(){
        //已经被初始化了
        counter1++;
        //由于counter还在后面,还未进行初始化,所以用的是默认值0
        counter2++;
        System.out.println("construct counter1=="+counter1);
        System.out.println("construct counter2=="+counter2);
    }

    public static int counter2 = 0;


    public static Singleton getInstance(){
        return singleton;
    }
}
复制代码

输出

construct counter1==2
construct counter2==1
counter1==2
counter2==0
复制代码

在这个例子中,在初始化阶段,执行到 private static Singleton singleton = new Singleton(); 时候,会执行私有构造,在私有构造中,由于 counter1 已经完成了初始化,即已经被赋值为1,所以count++后输出结果为2,然而 counter2 还未执行初始化,所以使用的还是在准备阶段的默认值 0 ,所以就会导致这种输出结果. 这个例子在实际工作中基本不会这样写,但是能很好的帮助理解类的准备阶段和初始化阶段做分别做的事情

原文  https://juejin.im/post/5e872d70f265da47d4056471
正文到此结束
Loading...