一、初始化阶段

初始化阶段,简言之,为类的静态变量赋予正确的初始值。

1.1、具体描述

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的Java程序代码。)

初始化阶段的重要工作是执行类的初始化方法:()方法。

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及static语句块合并产生的。

image.png

1.2、说明

1.2.1 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类cclinit>之前被调用。也就是说,父类的static块优先级高于子类。
口诀:由父及子,静态先行。
image.png
输出结果:
image.png
1.2.2 Java编译器并不会为所有的类都产生()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含()方法?

  • 一个类中并没有声明任何的类变量,也没有静态代码块时。
  • 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。
  • 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式。

image.png

二、static与final的搭配问题

2.1、难度剖析

  1. /**
  2. 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
  3. 情况1:在链接阶段的准备环节赋值
  4. 情况2:在初始化阶段<clinit>()赋值
  5. 结论:
  6. 在链接阶段的准备环节赋值的情况:
  7. 1.对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
  8. 2,对于String来说,如果使用字面量的方式赋值,使用static finaL修饰的话,则显式赋值通常是在链接阶段的准备环节进行
  9. 在初始化阶段<clinit>()中赋值的情况:
  10. 排除上述的在准备环节赋值的情况之外的情况。
  11. 最终结论:使用static t finaL修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,
  12. 是在链接阶段的准备环节进行。
  13. */
  14. public class InitilizationTest1 {
  15. public static int a = 1;//在初始化阶段<clinit>()赋值
  16. public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值
  17. public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()赋值
  18. public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()赋值
  19. public static final string s0 = "helloworld01";//在链接阶段的准备环节赋值
  20. public static final String s1 = new String("helloworld02");//在初始化阶段<clinit>()赋值
  21. }
  22. #############################
  23. <clinit>方法下的字节码指令
  24. 0 iconst_1
  25. 1 putstatic #2 <studies/classloader/LoadingTest.a>
  26. 4 bipush 100
  27. 6 invokestatic #3 <java/lang/Integer.valueOf>
  28. 9 putstatic #4 <studies/classloader/LoadingTest.INTEGER_CONSTANT1>
  29. 12 sipush 1000
  30. 15 invokestatic #3 <java/lang/Integer.valueOf>
  31. 18 putstatic #5 <studies/classloader/LoadingTest.INTEGER_CONSTANT2>
  32. 21 new #6 <java/lang/String>
  33. 24 dup
  34. 25 ldc #7 <helloworld02>
  35. 27 invokespecial #8 <java/lang/String.<init>>
  36. 30 putstatic #9 <studies/classloader/LoadingTest.s1>
  37. 33 return

从字节码层面看,只有INTEGER_CONSTANT1和s0是在链接阶段的准备环节赋值
image.png
总体来说在static加final的的基本数据类型并且没有创建对象的情况下的赋值和字符串字面量形式赋值的都是在链接阶段的准备环节赋值,其它的static修饰的都是在中进行赋值

三、()的线程安全性

3.1、说明

对于()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。

image.pngclinit是隐式的锁,所以没有将synchronized体现出来

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。

如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行()方法了。那么,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。

3.2、举例

  1. /**
  2. 以下代码中当线程去初始化StaticA的时候,v又在初始化StaticB因为clinit是线程安全的,
  3. 在初始化StaticA正在初始化clinit,这个时候StaticB也在初始化clinit,
  4. clinit处于阻塞状态,这样就会导致死锁。
  5. */
  6. package studies.statics;
  7. class StaticA{
  8. static {
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. try {
  15. Class.forName("studies.statics.StaticB");
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. System.out.println("StaticA init ok");
  20. }
  21. }
  22. class StaticB{
  23. static {
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. try {
  30. Class.forName("studies.statics.StaticA");
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. }
  34. System.out.println("StaticB init ok");
  35. }
  36. }
  37. public class StaticDeadLockMain extends Thread{
  38. private char flag;
  39. public StaticDeadLockMain(char flag) {
  40. this.flag = flag;
  41. }
  42. @Override
  43. public void run() {
  44. try {
  45. Class.forName("studies.statics.Static"+flag);
  46. } catch (ClassNotFoundException e) {
  47. e.printStackTrace();
  48. }
  49. System.out.println(getName()+" over");
  50. }
  51. public static void main(String[] args) {
  52. StaticDeadLockMain loadA = new StaticDeadLockMain('A');
  53. loadA.start();
  54. StaticDeadLockMain loadB = new StaticDeadLockMain('B');
  55. loadB.start();
  56. }
  57. }

四、类的初始化情况:主动使用vs被动使用

Java程序对类的使用分为两种:主动使用和被动使用。
如果针对代码,设置参数-XX:+TraceClassLoading,可以追踪类的加载信息并打印出来。

4.1、主动使用

4.1.1、说明

Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的“使用”,是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化操作。而初始化操作之前的加载、验证、准备己经完成。)

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
  2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。
  3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)。
  4. 当使用java.lang.reflect包中的方法反射类的方法时。比如: Class.forName(“ com.atguigu.java.Test”)。
  5. 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  6. 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
  7. 当虚拟机启动时,用户需要指定一个要执行的主类〈包含main()方法的那个类),虚拟机会先初始化这个主类。
  8. 当初次调用MethodHandle实例时,初始化该MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)。

针对5,补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
>在初始化一个类时,并不会先初始化它所实现的接口
>在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

4.1.2、举例

测试类的主动使用:意味着就会调用()方法,即执行了类的初始化

  1. package studies.statics;
  2. /**
  3. * 1、当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
  4. */
  5. public class ActiveUser1 {
  6. public static void main(String[] args) {
  7. Order order = new Order();
  8. }
  9. //序列化的过程
  10. @Test
  11. public void test1(){
  12. ObjectOutputStream oos = null;
  13. try {
  14. oos = new ObjectOutputStream(new FileOutputStream("order.dat"));
  15. oos.writeObject(new Order2());
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }finally {
  19. try {
  20. if (oos != null){
  21. oos.close();
  22. }
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. //反序序列化的过程:(验证)
  29. @Test
  30. public void test2(){
  31. ObjectInputStream ois = null;
  32. try {
  33. ois = new ObjectInputStream(new FileInputStream("order.dat"));
  34. Order2 order = (Order2)ois.readObject();
  35. } catch (IOException | ClassNotFoundException e) {
  36. e.printStackTrace();
  37. }finally {
  38. if (ois != null){
  39. try {
  40. ois.close();
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. }
  46. }
  47. }
  48. class Order{
  49. static {
  50. System.out.println("Order类的初始化过程");
  51. }
  52. }
  53. ######################
  54. 控制台输出结果:
  55. Order类的初始化过程
  56. class Order2 implements Serializable {
  57. static {
  58. System.out.println("Order2类通过序列化进行初始化过程");
  59. }
  60. }

/**
2、当调用类的静态方法时,即当使用了字节码invokestatic指令
static修饰的方法不会出现在<clinit>方法,静态方法只有在主动调的时候才会执行
*/
class Order{
    static {
        System.out.println("Order类的初始化过程");
    }
    public static void method(){
        System.out.println("Order method()");
    }
}
public class ActiveUser1 {
    @Test
    public void test2(){
        Order.method();
    }
}
########################
控制台输出:
Order类的初始化过程
Order method()
package studies.statics;
import org.junit.Test;

/**
 *3、当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作)。
 */
//例子1
public class ActiveUser2 {

    @Test
    public void test1(){
        System.out.println(User.num);
    }

}
class User{
    public static int num = 1;
    static {
        System.out.println("User类的初始化过程");
    }
}
########################
控制台输出:
User类的初始化过程
1
//-------------------------------------------------------
//例子2,如果只是调取static + final的直接字面量,是不会触发类(<clinit>)初始化
public class ActiveUser2 {

    @Test
    public void test1(){
        System.out.println(User.NUM2);//是不会触发类(<clinit>)初始化
        //System.out.println(User.NUM3);会触发类(<clinit>)初始化,因为调取了方式
    }

}
class User{
    public static int num = 1;
    static {
        System.out.println("User类的初始化过程");
    }
    public static final int NUM2 = 5;
    //public static final int NUM3 = new Random().nextInt(5);
}
########################
控制台输出:
5


//---------------------------------------------------------------
//例子3
public class ActiveUser2 {

    @Test
    public void test2(){
        System.out.println(CompareA.NUM1);//不会输出"CompareA的初始化"
        System.out.println(CompareA.NUM2);//会输出"CompareA的初始化"
    }

}
interface CompareA{

    public static final Thread thred = new Thread(){
        {
            System.out.println("CompareA的初始化");
        }
    };
    public static final int NUM1 = 1;
    public static final int NUM2 = new Random().nextInt(5);
}
/**
4、当使用java.lang.reflect包中的方法反射类的方法时。比如: Class.forName("studies.statics.Order3")。
*/
public class ActiveUser2 {
    @Test
    public void test1(){

        try {
            Class clazz = Class.forName("studies.statics.Order3");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}
class Order3{
    static {
        System.out.println("Order类的初始化过程");
    }
    public static void method(){
        System.out.println("Order method()");
    }
}
########################
控制台输出:
Order类的初始化过程
package studies.statics;
import org.junit.Test;
/**
 * 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
 */
public class ActiveUser2 {
    @Test
    public void test1(){
        System.out.println(Son.num);
    }

}
class Father{
    static {
        System.out.println("Father的初始化");
    }
}
class Son extends Father{
    static {
        System.out.println("Son的初始化");
    }
    public static  int num = 1;
}
########################
控制台输出:
Father的初始化
Son的初始化
1

如果针对代码,设置参数-XX:+TraceClassLoading,可以追踪类的加载信息并打印出来。
image.png
或者
image.png
image.png
可以看到父类Father的加载
image.png

package studies.statics;
import org.junit.Test;
/**
针对5,补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
>在初始化一个类时,并不会先初始化它所实现的接口
>在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。
 */
public class ActiveUser2 {
    @Test
    public void test1(){
        System.out.println(Son.num);
    }
}
class Father{
    static {
        System.out.println("Father的初始化");
    }
}
class Son extends Father implements CompareB{
    static {
        System.out.println("Son的初始化");
    }
    public static  int num = 1;
}
interface CompareB{
    public static final Thread thred = new Thread(){
        {
            System.out.println("CompareA的初始化");
        }
    };
}
########################
控制台输出:
Father的初始化
Son的初始化
1

可以看看出CompareB加载了,但没有被初始化
image.png

package studies.statics;
import org.junit.Test;
/**
针对5,补充说明:
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。
>在初始化一个类时,并不会先初始化它所实现的接口
>在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。
 */
public class ActiveUser2 {
    @Test
    public void test1(){
        System.out.println(CompareC.NUM1);
    }
}
interface CompareB{
    public static final Thread thred = new Thread(){
        {
            System.out.println("CompareA的初始化");
        }
    };
}
interface CompareC extends CompareB{
    public static final Thread thred = new Thread(){
        {
            System.out.println("CompareC的初始化");
        }
    };
    public static final int NUM1 = new Random().nextInt();
}
//父接口不会被初始化
########################
控制台输出:
CompareC的初始化
package studies.statics;
import org.junit.Test;
/**
 * 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
 */
public class ActiveUser2 {
    @Test
    public void test1(){
        System.out.println(Son.num);
    }
}
class Father{
    static {
        System.out.println("Father的初始化");
    }
}
class Son extends Father implements CompareB{
    static {
        System.out.println("Son的初始化");
    }
    public static  int num = 1;
}
interface CompareB{
    public static final Thread thred = new Thread(){
        {
            System.out.println("接口CompareB的初始化");
        }
    };
    //带default标识的方法会导致接口初始化
    public default void method1(){
        System.out.println("method1!你好");
    }
}
########################
控制台输出:
Father的初始化
接口CompareB的初始化
Son的初始化
1

4.2、被动使用

4.2.1 说明

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
    • 当通过子类引用父类的静态变量,不会导致子类初始化。(注意,虽然子类没有执行初始化,但子类已经完成加载阶段)
  2. 通过数组定义类引用,不会触发此类的初始化。
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
  4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

    4.2.2 举例

    关于类的被动使用,即不会进行类的初始化操作,即不会调用()

    package studies.statics;
    import org.junit.Test;
    /**
    * 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
    *    • 当通过子类引用父类的静态变量,不会导致子类初始化。(注意,虽然子类没有执行初始化,但子类已经完成加载阶段)
    * 2. 通过数组定义类引用,不会触发此类的初始化。
    */
    public class PassiveUser1 {
     @Test
     public void test1(){
         System.out.println(Child.num);//测试子类不会被初始化
     }
    
     @Test
     public void test2(){
         Parent[] parents = new Parent[10];//数组不会触发初始化,所以Parent类不会被初始化
     }
    }
    class Parent{
     static {
         System.out.println("Parent的初始化过程");
     }
     public static int num = 1;
    }
    class Child extends Parent{
     static {
         System.out.println("Child的初始化过程");
     }
    }
    ########################
    控制台输出:
    Parent的初始化过程
    1
    说明子类不会被初始化,但子类已经被加载了
    
    package studies.statics;
    import org.junit.Test;
    /**
    * 3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
    * 4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
    */
    public class PassiveUser1 {
     @Test
     public void test1(){
         System.out.println(Person.NUM);
     }
     @Test
     public void test2(){
         try {
             //这种方式调取一个类也不会执行<clinit>,这属于被动使用,
             //而Class.forName("studies.statics.Person")属于主动使用
             Class clazz = ClassLoader.getSystemClassLoader().loadClass("studies.statics.Person");
         } catch (ClassNotFoundException e) {
             e.printStackTrace();
         }
     }
    }
    class Person{
     static {
         System.out.println("Person的初始化过程");
     }
     public static final int NUM = 1;//在链接过程的准备环节就被赋值为1了。所以不需要初始化
    }
    ########################
    控制台输出:
    1
    
    public class T { //T:使用系统类加载器加载。
     public static int k = 0;
     public static T t1 = new T("t1");
     public static T t2 = new T("t2");
     public static int i = print("i");
     public static int n = 99;
    
     static {
         print("静态块");
     }
    
     public int j = print("j");
    
     {
         print("构造块");
     }
    
     public T(String str) {
         System.out.println((++k) + ":" + str + "  i=" + i + "  n=" + n);
         ++n;
         ++i;
     }
    
     public static int print(String str) {
         System.out.println((++k) + ":" + str + "  i=" + i + "  n=" + n);
         ++n;
         return ++i;
     }
    
     public static void main(String[] args) {
         String str = "info";  //String:使用引导类加载器加载
     }
    }