浅谈Java类加载的初始化阶段

类加载的初始化阶段对类变量赋予正确的值。主要有两种初始化方式,一种是通过类变量初始化语句;一种是静态初始化语句。如下述代码所示,前者是类变量初始化语句,后者是静态初始化语句。

public class Example1 {
    static int width;
    static int height = (int) (Math.random() * 2.0);

    static {
        width = (int) (3 * Math.random() * 5.0);
    }
}

所有的类变量初始化语句和静态初始化语句都被Java编译器收集在一起,放在一个特殊方法里。对于类而言,该方法称为类初始化方法,对于接口而言,该方法称为接口初始化方法。在Java class文件里,类和接口的初始化方法统一被称作为<clinit>() 方法。并且这种方法只能被Java虚拟机调用,Java程序是无法调用的。
初始化一个类包含两个步骤:
如果类存在超类,先初始化超类
如果类存在类初始化方法,就执行此方法
初始化一个接口只有一个步骤:
如果该接口存在接口初始化方法,就执行此方法,接口不初始化父接口。

注意:初始化类的过程必须保持同步,如果有多个线程初始化一个类,仅仅允许一个线程执行初始化,其他的线程都需要等待。

  1. <clinit>()方法
    Java方法将类变量初始化语句和静态初始化语句的代码都放在Java class文件的<clinit>()中,比如查看Example1.java的class源文件,我们可以发现<clinit>()如下:
  0: invokestatic  #2                  // Method java/lang/Math.random:()D
  3: ldc2_w        #3                  // double 2.0d
  6: dmul
  7: d2i
  8: putstatic     #5                  // Field height:I
 11: ldc2_w        #6                  // double 3.0d
 14: invokestatic  #2                  // Method java/lang/Math.random:()D
 17: dmul
 18: ldc2_w        #8                  // double 5.0d
 21: dmul
 22: d2i
 23: putstatic     #10                 // Field width:I
 26: return

<clinit>()方法先执行类变量初始化语句,初始化height,接着执行了静态初始化语句,初始化了width。
并非每个类都拥有<clinit>()方法,以下三种情况就没有
类没有申明类变量,也没有任何静态初始化语句;
类申明了类变量,但是没有任何的类变量初始化语句,页没有静态初始化语句进行初始化;
类近包含静态final变量的类变量初始化语句,而且是编译时候的常量;
Example2 .java

public class Example2 {
    static final int angle = 35;
    static final int length = angle * 2;
}

没有<clinit>()方法
Example3 .java

public class Example3 {
    static final int angle = (int) (35 * Math.random());
    static final int length = angle * 2;
}

<clinit>()方法,因为Math.random()不是编译器常量,因此angle和length都要进行初始化

 0: ldc2_w        #2                  // double 35.0d
 3: invokestatic  #4                  // Method java/lang/Math.random:()D
 6: dmul
 7: d2i
 8: putstatic     #5                  // Field angle:I
11: getstatic     #5                  // Field angle:I
14: iconst_2
15: imul
16: putstatic     #6                  // Field length:I
19: return

对于接口Example4.java

public interface Example4 {
    int angle = (int) (35 * Math.random());
    int length = 2;
}

接口中的变量默认是public static final,可以查看class文件看到

public static final int angle;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public static final int length;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2

而且把编译期常量2之前赋值好,不产生<clinit>()方法。但是会为变量angle产生<clinit>()方法,因为他的值是运行期决定的。

 0: ldc2_w        #1                  // double 35.0d
 3: invokestatic  #3                  // Method java/lang/Math.random:()D
 6: dmul
 7: d2i
 8: putstatic     #4                  // Field angle:I
11: return

接下来我们看Java虚拟机何时初始化类变量
2. 主动使用和被动使用
那么Java虚拟机何时初始化类变量呢?主动使用的时候初始化。主动使用一共有6种情况:

1)创建某个类的新实例(new,不明确的创建,反射,克隆或反序列化);
2)调用类的静态方法(即执行字节码invokestatic指令);
3)使用某个类的或接口的静态字段,或者对该字段赋值(即执行字节码getstatic,putstatic指令),用final修饰的静态字段除外,因为被初始化为一个编译时的常量表达式;
4)调用Java API中的反射方法
5)初始化某个类的子类(某个类被初始化,超类必须已经被初始化了)
6)虚拟机启动某个被标明启动类的类(包含main方法的那个类),和条款3类似,静态方法

被动使用,子类或者子接口使用了父类或父接口的非常量静态变量;此时该子类或子接口不发生初始化。
也就是说主动使用中的字段必须是该类或该接口明确申明的,而不是父类或父接口中的。看下面这个例子:

public class NewParent {
    static int hoursOfSleep = (int) (Math.random()*3.0);
    static {
        System.out.println("NewParent was initialized");
    }
}

public class NewbornBaby extends NewParent {

    static int hoursOfCrying = (int) (6 + (int)( Math.random() * 2.0));

    static {
        System.out.println("NewbornBaby was initialized");
    }
}

测试代码1:

public class Example5 {

    public static void main(String[] args) {
        int hours = NewbornBaby.hoursOfSleep;
    }

    static {
        System.out.println("Example5 was initialized");
    }
}

输出结果:

Example5 was initialized
NewParent was initialized

NewbornBaby调用的字段不是本身的,是父类的,因此发生了被动引用,NewbornBaby不进行初始化,也不需要被加载。只会导致Example5和NewParent发生初始化。那么NewParent初始化是因为是NewbornBaby父类初始化还是被调用了字段而初始化呢?看下面例子
测试代码2:

public class Temp {
    static int tempValue = (int) (Math.random()*3.0);
    static {
        System.out.println("Temp was initialized");
    }
}
public class Example5 {

    public static void main(String[] args) {
        int hours = Temp.tempValue;
    }

    static {
        System.out.println("Example5 was initialized");
    }
}

测试结果:

Example5 was initialized
Temp was initialized

可见是因为字段被调用了而发生了初始化。

接下来看一个主动引用的例子
测试代码3:

public class Example5 {

    public static void main(String[] args) {
        int hours = NewbornBaby.hoursOfCrying;
    }

    static {
        System.out.println("Example5 was initialized");
    }
}

输出结果:

Example5 was initialized
NewParent was initialized
NewbornBaby was initialized

NewbornBaby调用自身的字段,因此发生了主动引用,初始化NewbornBaby,但是初始化子类之前,父类必须被初始化,因此NewParent也被初始化。

如果一个自动既是静态的又是final的,并且使用一个编译时常量表达式初始化,使用这样的字段就不是对该字段所在类的主动使用。Java编译器会把这样的字段解析成常量的本地拷贝。
测试代码4:

public class Angry {
    static final String greeting = "hello world!";
    static {
        System.out.println("Angry is initialized");
    }
}
public class Example6 {
    public static void main(String[] args) {
        String str = Angry.greeting;
    }

    static {
        System.out.println("Example6 was initialized");
    }
}

测试结果:

Example6 was initialized

类Angry未执行初始化过程

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值