JVM什么情况下初始化一个类(代码示例)

在前面的日志当中,已经介绍过类的生命周期

类加载

  将已经存在的class文件从磁盘当中加载到内存中,查找类的二进制数据,如果不存在直接抛出异常

连接

  验证:确保被加载类的正确性,确保字节码没有被恶意修改

  准备:为类的静态变量分配内存,并将其初始化为默认值,整型的默认值是0,引用类型是null, 程序当中赋值是1,但是这里会把int默认值设置为0,

  解析:将类当中的符号引用转化为直接引用,符号引用是间接引用的表达方式,直接引用通过指针的方式指向目标方法的内存地址。

初始化

  对类当中静态变量进行赋值,为类的静态变量赋予初始值

使用

  在程序当中使用

卸载

  在内存当中销毁,如果卸载掉,不能通过类创建对象

  在什么情况下,类会进行初始化

  所有的java虚拟机实现必须在每个类或者接口被java程序首次主动使用时才会初始化他们。

主动使用有以下七种情况

1、创建类的实例

2、访问某个类或者接口的静态变量,或者是对静态变量的赋值

3、调用类的静态方法(本质上和第二种情况一样的)

4、使用反射(如:Class.forName("java.lang.String"))

5、初始化一个类的子类(同时也会初始化这个子类的父类)

6、java虚拟机启动时被标记为启动类的类,包含了main方法的类,程序的入口

7、JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandler实例的解析结果REF_getStatic, REF_putStatic,REF_invokeStatic 句柄对应的类,如果没有初始化,则进行初始化。

  除了以上七种方式,其他方式情况下调用类,都不会触发类的初始化,有可能会进行类的加载,进行连接,但是并不会导致初始化。

接下来,使用具体的例子,来看下以上几种情况

public class Test1 {
    public static void main(String[] args) {
        
        System.out.println(MyChild1.str);
       // System.out.println(MyChild1.str2);
    }
}
class MyParent1{
    public static String str = "hello world";
    static {
        System.out.println("my parent static block");
    }
}
class MyChild1 extends MyParent1{
    public static String str2 = "str2";
    static {
        System.out.println("my child static block");
    }
}

在这两个类当中,分别都创建的有静态代码块,如果类被初始化的时候,就会打印出其对应的静态代码块

当我们打印MyChild1.str, 即父类的静态变量时,这个时候的代码执行结果如下:

my parent static block
hello world

虽然我们是通过MyChild的方式去访问父类当中的静态变量的,但是并没有初始化MyChild,只初始化了MyParent类,但是有没有加载MyChild1我们可以通过设置JVM参数为-XX:+TraceClassLoading查看是否加载了MyChild1

如果我们调用MyChild.str2,即MyChild2当中的静态变量,我们看下执行结果如下

my parent static block
my child static block
str2

可以看到这个时候,不仅初始化了MyParent还初始化了MyChild类,

关于我们上面定义的7种主动使用场景,上面这个例子就满足了两种,调用类的静态变量,和初始化子类时,初始化父类。

还需要注意一点:对于静态代码字段来说,只有直接定义了该字段的类才会被初始化。

Example2:

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

    public static final String str = "hello";
    static {
        System.out.println("my parent2 static block");
    }
}

执行结果:

hello

可以看到,MyParent2当中,变量str是一个常量值,并且没有初始化MyParent2这个类,原因在于,final类型的变量,是个常量值,在编译阶段,这个常量就会被保存到调用这个常量的方法所在的类的常量池当中,我们当前这个类中,str的值“hello”会被放在Test2类的常量池中,本质上,调用类并没有直接引用到定义常量的类,不会触发定义常量的类初始化。这个时候,如果我们手动删除掉MyParent2编译的class文件,再次执行程序,是不会有影响的,因为字符串str已经被放入到Test2的常量池中了

Example3:

public class Test3 {
    public static void main(String[] args) {
        System.out.println(MyParent3.str);
    }
}
class MyParent3{
    public static final String str = UUID.randomUUID().toString();
    static {
        System.out.println("My Parent3 static code");
    }
}

执行结果:

My Parent3 static code
9a33d583-41a3-4f28-bf02-27b4b5bcea40

这个例子当中我们访问的变量也是一个静态常量,但是却初始了MyParent3这个类,两者不同的地方在于,一个是编译期常量,一个是运行期常量,如果一个常量的值不是在编译期可以确定的话,那么它的值就不会被放置在调用方法所在的类的常量池当中,在程序运行时,会导致主动使用这个常量所在的类,就会导致这个类的初始化。

Example4:

public class Test4 {
    public static void main(String[] args) {
       MyParent4 p = new MyParent4();
     

    }
}
class MyParent4{
    static {
        System.out.println("my parent4 static code");
    }
}

上面这个例子很明显会打印出来一条语句,复合我们前面说的,实例化一个类的时候,就需要初始化这个类。

Example5:

public class Test4 {
    public static void main(String[] args) {
        MyParent4[] arr = new MyParent4[1];
    }
}
class MyParent4{
    static {
        System.out.println("my parent4 static code");
    }
}

上述例子执行之后,并不会初始化MyParent4,原因在于new一个数组时,确实创建了一个实例,但是这个实例的类型并不是我们的MyParent4类型,可以打印出来,看到数组类型是 [Lcom.jvm.classloader.MyParent4 ,在原来类型的基础上添加了[L, 可以看到我们代码当中并没有声明这个类型。对于数组实例,其实在运行时,虚拟机动态生成的。

Example6:

public class Test5 {
    public static void main(String[] args) {
        System.out.println(MyChild5.a);
    }
}
interface MyParent5{
    public static final int a = 5 ;
    public static Thread thread1 = new Thread(){
        {
            System.out.println("my parent invoked!");
        }
    };
}
interface MyChild5 extends MyParent5 {
    public static int b = new Random().nextInt(4);
    public static Thread thread = new Thread(){
        {
            System.out.println("mychild invoked");
        }
    };
}

1、如果程序当中打印的MyChild5.a 执行结果是 只有5

2、如果程序当中打印的是MyChild5.b 执行结果是 mychild invoked 和数字3

interface当中的变量都是static final 类型的,如果变量是编辑期变量,那么变量的值会在编译期拷贝至调用方法所在类的常量池当中,如果调用的变量是运行期常量,那么需要初始化变量所在的接口,或者类,这里我们定义了个一个Thread的静态变量,如果接口进行了初始化操作,那么需要给静态变量进行赋值,就会打印出来信息。

总结:在一个接口进行初始化时,并不要求其父接口也完成初始化,同样,如果子类在初始化时,并不要求其父接口进行初始化

Example7

public class MyTest10 {
    static {
        System.out.println("myTest10 block");
    }

    public static void main(String[] args) {
        Parent10 parent10;
        System.out.println("-----");
        parent10 = new Parent10();
        System.out.println("-------");
        System.out.println(parent10.a);
        System.out.println("-------");
        System.out.println(Child10.b);
    }
}
class Parent10{
    static int a = 3;
    static{
        System.out.println("parent 2");
    }
}
class Child10 extends Parent10{
    static int b = 4;
    static{
        System.out.println("child 2");
    }
}

执行结果:

myTest10 block
-----
parent 2
-------
3
-------
child 2
4

结合我们上面所说的主动使用的七种情况,MyTest10当中包含main方法,因此会初始化,如果只是声明了一个类引用,但是并没有指向具体的对象,并不会初始化,只有在实例化时,才会初始化这个对象

Example8

class Parent11{
    static int a = 3;
    static {
        System.out.println("parent11 static block");
    }
    static void doSomeThing() {
        System.out.println("do something");
    }
}
class Child11 extends Parent11{
    static{
        System.out.println("child11 static block");
    }
}

public class MyTest11 {
    public static void main(String[] args) {
      //  System.out.println(Child11.a);
        Child11.doSomeThing();
    }
}

执行结果
parent11 static block
do something

调用静态方法也会导致类的初始化

Example9

class CL{
    static {
        System.out.println("CL static");
    }
}
public class MyTest12 {
    public static void main(String[] args)throws Exception {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = classLoader.loadClass("com.shengsiyuan.jvm.classloader.CL");
        System.out.println(clazz);
        System.out.println("------");
        clazz = Class.forName("com.shengsiyuan.jvm.classloader.CL");
        System.out.println(clazz);
    }
}

执行结果
class com.shengsiyuan.jvm.classloader.CL
------
CL static
class com.shengsiyuan.jvm.classloader.CL

调用loadClass并不会初始化类,只有使用反射时,才会初始化类

以上就是所有主动使用的情况下,初始化类的操作。如有错误欢迎指正~

参考:圣思园-jvm课程

猜你喜欢

转载自blog.csdn.net/summerzbh123/article/details/81091848