类加载机制(二):类的初始化


title: 类加载机制(二):类的初始化
date: 2019-03-13 18:49:55
categories:

  • Java虚拟机
    tags:
  • 类加载机制
  • 类的初始化

类的初始化

引言

一般Java程序的class文件经过加载、连接后,就进入初始化阶段,顺序执行static语句,为静态变量赋予正确的值,执行static代码块,初始化类。

类的使用方式

Java程序对类的使用分为两种:

----主动使用

----被动使用

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

主动使用方式

主动使用分为七种:

----创建类的实例

----访问某个类或接口的静态变量,或者对该静态变量赋值

----调用类的静态方法

----反射(如Class.forName(com.test.Test)

----初始化一个子类

----Java虚拟机启动时被标明为启动类的类

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

除了以上七种情况,其他使用Java类的方法都被看作是对类的被动使用,都不会导致类的初始化。

类的初始化步骤

对于类来说:

假如这个类还没有被加载和连接,那就先进行加载和连接

假如类存在直接父类,并且这个父类还没有被初始化,那就先初始化父类

假如类中存在初始化语句,那就一次执行这些初始化语句

对于接口来说:

Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。

在初始化一个类时,并不会先初始化它所实现的接口

在初始化一个接口时,并不会先初始化它的父接口

因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。

示例

NO.1
public class MyTest1 {
    public static void main(String[] args) {
        System.out.println(Son.str);
    }

}
class Parent{
    public static String str = "parent str";
    static {
        System.out.println("parent static启动");
    }
}
class Son extends Parent{
    static {
        System.out.println("son static启动");
    }
}

输出结果

输出结果显示只有Parent类被加载了。对于静态字段来说,只有直接定义了该字段的类才会被初始化。虽然Son没有被主动使用,但它已经被加载了。类加载器并不需要等到某个类被首次主动使用时再加载它。

Parent类中的str变量注释掉,添加到Son类中

输出结果:

输出结果显示Parent类与Son类都被初始化了。通过使用Son的静态变量,导致Son的初始化,而当一个类在初始化时,首先要求其父类全部都已经初始化,即导致Parent初始化。

我们还可以从第一段打印类加载信息(通过添加虚拟机参数-XX:+TraceClassLoading)看出,虽然Son没有被主动使用,但它已经被加载了。类加载器并不需要等到某个类被“首次主动使用”时再加载它。

NO.2
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(Parent2.str);
        System.out.println(Parent2.bi);
        System.out.println(Parent2.si);
        System.out.println(Parent2.icons_1);
        System.out.println(Parent2.iconst_2);
    }
}
class Parent2{
    public static final String str = "Hello Jvm";
    public static final int bi = 127;
    public static final int si = 32767;
    public static final int icons_1 = 1;
    public static final int iconst_2 = 2;
    static {
        System.out.println("Parent2 init");
    }
}

输出结果

Hello Jvm
127
32767
1
2

Parentclass文件从classPath中删除掉,再运行程序,程序没报错,输出结果一样。

常量的本质含义:常量在编译阶段会存入调用这个常量的常量池中。本质上,调用这个常量并没有直接引用到定义常量的类,因此并不会触发定义常量的类的初始化。如:Paren2中定义的常量被存入到了MyTest2中,之后两个类就没有任何关系了。甚至将Paren2.class文件删除也没关系。

public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(Parent3.str);
    }
}
class Parent3{
    public static final String str = UUID.randomUUID().toString().replace("-","");
    static {
        System.out.println("Paren3 init");
    }
}

输出结果:

Paren3 init
2b00eb3dbd934bf7ab610407058d276f

输出结果显示Parent3被成功初始化了。而且,删除掉Parent3class文件,也会报java.lang.NoClassDefFoundError的错误。

在编译期间,对于并不能确定的常量来说,不会被存入到调用类的常量池中。而是在运行期间,主动使用常量的所属类,完成所属类的初始化。

NO.3
public class MyTest4 {
    public static void main(String[] args) {
//        Parent4 parent4 = new Parent4();
        Parent4[] parent4s = new Parent4[1];
        int[] ints = new int[1];
        System.out.println(parent4s.getClass());
        System.out.println(parent4s.getClass().getSuperclass());
        System.out.println("==============");
        System.out.println(ints.getClass());
        System.out.println(ints.getClass().getSuperclass());
    }
}
class Parent4{
    static {
        System.out.println("Paren4 init");
    }
}

输出结果

class [LclassLoader.Parent4;
class java.lang.Object
==============
class [I
class java.lang.Object

输出结果显示并没有触发Parent的初始化过程,但是却触发了class [LclassLoader.Parent4;的初始化阶段,打印出的这个名称,它直接继承class java.lang.Object,代表了数组的component,即数组的组成元素。

class文件反编译后,可以看出它的创建动作由助记符newarray触发。

anewarray:表示创建一个引用类型的数组(类、接口、数组),并将其引用值压入栈顶。
newarray:表示创建一个基本类型的数组(int、char),并将其引用值压入栈顶。

NO.4
public class MyTest6 {
    public static void main(String[] args) {
        Single instance = Single.getInstance();
        System.out.println("count1:" + Single.count1);
        System.out.println("count2:" + Single.count2);
    }
}
class Single{
    public static int count1;
    public static int count2 = 0;
    private static Single single = new Single();
    private Single(){
        count1++;
        count2++;
        System.out.println("构造方法count1:" + count1);
        System.out.println("构造方法count2:" + count2);
    }
    public static Single getInstance(){
        return single;
    }
}

输出结果:

构造方法count1:1
构造方法count2:1
count1:1
count2:1

MyTest6中调用Single的静态方法,触发Single的初始化阶段。

----连接阶段,将静态变量全置为默认值:

count1 = 0

count2 = 0

single = null

----初始化阶段,顺序执行静态语句:

执行到此句时 private static Single single = new Single();,执行Single的构造方法。

count1 = 1

count2 = 1

并将其打印,最后再在MyTest6main方法中调用时,直接从Single的常量池中取出。

修改下Single的代码

class Single{
    public static int count1;
    private static Single single = new Single();
    private Single(){
        count1++;
        count2++;
        System.out.println("构造方法count1:" + count1);
        System.out.println("构造方法count2:" + count2);
    }
    //调下顺序
    public static int count2 = 0;
    public static Single getInstance(){
        return single;
    }
}

输出结果:

构造方法count1:1
构造方法count2:1
count1:1
count2:0

再修改下Single的代码

class Single{
    //初值赋为1
    public static int count1 = 1;
    private static Single single = new Single();
    private Single(){
        count1++;
        count2++;
        System.out.println("构造方法count1:" + count1);
        System.out.println("构造方法count2:" + count2);
    }
    public static int count2 = 0;
    public static Single getInstance(){
        return single;
    }
}

输出结果:

构造方法count1:2
构造方法count2:1
count1:2
count2:0

经过上面的程序可以看出,静态变量的声明语句,以及静态代码块都被看做类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序来依次执行它们。

NO.5
public class MyTest7 {
    static {
        System.out.println("MyTest7 invoked");
    }
    public static void main(String[] args) {
        Parent7 parent7;
        System.out.println("---------------");
//        parent7 = new Parent7();
        Son7 son7 = new Son7();
        System.out.println("---------------");
        System.out.println(Son7.a);
    }
}
class Parent7{
    static int a = 5;
    static {
        System.out.println("Parent7 invoked");
    }
}
class Son7 extends Parent7{
    static int b = 6;
    static {
        System.out.println("Son7 invoked");
    }
}

输出结果:

MyTest7 invoked
---------------
Parent7 invoked
Son7 invoked
---------------
5

输出结果显示:首先使用MyTest7的启动类,导致了MyTest7的初始化,执行了静态代码块;然后声明了一个Parent7的变量,并不会导致Parent7的初始化;最后创建了一个Son7的实例,触发Son7的初始化,触发Parent7的初始化。

Son7 son7 = new Son7();替换为parent7 = new Parent7();

输出结果:

MyTest7 invoked
---------------
Parent7 invoked
---------------
5

输出结果显示:只有Parent7初始化,而Son7并没有初始化。

上述代码也印证了,在创建实例时以及启动类时,会导致类的初始化;当一个类初始化时,会先初始化它的父类。

NO.6
public class MyTest8 {
    public static void main(String[] args) {
        //System.out.println(Son8.a);
        Son8.doSomething();
    }
}
class Parent8{
    static int a = 1;
    static {
        System.out.println("Parent8 invoked");
    }
    static void doSomething(){
        System.out.println("Parent8'doSomething");
    }
}
class Son8 extends Parent8{
    static {
        System.out.println("Son8 invoked");
    }
}

输出结果:

Parent8 invoked
Parent8'doSomething

输出结果显示:Parent8被初始化了。

调用类的静态方法时,会导致类的初始化。

结论

​ 类的初始化是类加载过程的最后阶段,在前面的类加载过程中,都是有虚拟机来进行主导和控制(除了用户可以自定义类加载外,请看我后续博客),到了初始化阶段,才真正开始执行Java程序中的字节码。

​ 在连接中的准备阶段,静态变量被赋予了默认值,到了初始化阶段,这些变量才被赋予真正的值。在对类进行初始化时,Java虚拟机会按照初始化语句在类文件中的先后顺序来一次执行它们。

​ 一个类只有在被首次主动使用才会触发初始化阶段,也只有上文提到的七种方式才算主动使用,其他都是被动使用。

猜你喜欢

转载自blog.csdn.net/weixin_38950807/article/details/88545315