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

1,979 阅读5分钟

对类的使用方式

  • 主动使用
  • 被动使用

所有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");
    }
}

结果总结

  • 对于静态字段来说,只有直接定义了该字段的类才会被初始化
  • 虽然调用了Child.str.但是由于对Child没有进行主动使用,所以不会被初始化,所以不会输出静态代码块中的内容
  • 因为初始化了子类,所以也会初始化父类,并且父类先初始化完毕
  • 每个类只会初始化一次

添加JVM参数-XX:+TraceClassLoading

image-20200311155635549

可以观察到即使调用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,所以就会导致这种输出结果. 这个例子在实际工作中基本不会这样写,但是能很好的帮助理解类的准备阶段和初始化阶段做分别做的事情