目录

一、概述

二、初始化阶段赋值和准备阶段赋值的对比

三、类初始化方法()的线程安全性

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


一、概述

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

1、具体描述

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

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

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及static语句块合并产生的
public class InitializationTest {public static int id = 1;public static int num;static {num = 2;System.out.println("static()...");}
}

通过jclasslib查看clinit()类初始化方法:

 2、说明

2.1 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类之前被调用。也就是说,父类的static块优先级高于子类。例:

public class InitializationTest {public static int id = 1;public static int num;static {num = 2;System.out.println("father static()...");}
}//子类
class SubInitialization extends InitializationTest {static {//num属性必须提前已经加载:一定会先加载父类。num = 4;System.out.println("son static{}");}public static void main(String[] args) {System.out.println(num);}
}

输出结果:

father static()...
son static{}
4

2.2 Java编译器并不会为所有的类都产生<clinit>()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含<clinit>()方法?

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

如下几种情况:

public class InitializationTest {//场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法public int num = 1;//场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法public static int num1;//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法public static final int num2 = 1;//不加final的变量会生成clinit,加final变为常量就不会生成clinitpublic static int num3 = 1;
}

二、初始化阶段赋值和准备阶段赋值的对比

static不加final的变量都在初始化环节赋值;

static加final的常量 -> static final

  • 加static:

    • 基本型,在链接阶段的准备阶段赋值;
    • 引用类型:
    • 字面量声明在链接的准备阶段赋值(直接赋值常量,而非调用方法);
    • new 等方式在初始化阶段clinit()中赋值;

结论:使用static + final修饰,且显式赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。

例:通过代码理解static和final的搭配使用情况

/*** <p>* 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?* 情况1:在链接阶段的准备环节赋值* 情况2:在初始化阶段<clinit>()中赋值* <p>* 结论:* 在链接阶段的准备环节赋值的情况:* 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行* 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行* <p>* 在初始化阶段<clinit>()中赋值的情况:* 排除上述的在准备环节赋值的情况之外的情况。* <p>* 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。*/
public class InitializationTest {public static int a = 1;   //在初始化阶段<clinit>()中赋值public static final int INT_CONSTANT = 10;  //在链接阶段的准备环节赋值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000); //在初始化阶段<clinit>()中赋值public static final String s0 = "helloworld0";    //在链接阶段的准备环节赋值public static final String s1 = new String("helloworld1"); //在初始化阶段<clinit>()中赋值public static String s2 = "helloworld2";   //在初始化阶段<clinit>()中赋值public static final int NUM = 2;   //字面量,在链接阶段的准备环节赋值public static final int NUM1 = new Random().nextInt(10); //在初始化阶段<clinit>()中赋值,编译阶段确定不了具体值
}

通过jclasslib查看其字节码,如果是在准备阶段赋值的话,编译之后的字节码中字段会多出一个属性ConstantValue,如下图:

三、类初始化方法<clinit>()的线程安全性

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

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

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

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

例: 如下代码:

  • loadA线程加载了staticA,staticA加载了staticB;
  • loadB线程加载了staticB,staticB加载了staticA;
  • staticA和staticB都有1s的阻塞,所以当staticA想要加载staticB的时候,staticB已经被loadB线程先加载;
  • 所以loadA线程需要等待staticB,而loadB线程也要等待staticA;
  • 由此出现了类的交叉加载行为,继而出现了类的死锁行为;
  • 程序呈现出僵持状态,输出语句不会打印,相当于程序进入阻塞状态,出现了事实上的死锁;
/*** 死锁举例*/
class StaticA {static {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}try {Class.forName("com.wsh.jvm.test.StaticB");} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println("StaticA init OK");}
}class StaticB {static {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}try {Class.forName("com.wsh.jvm.test.StaticA");} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println("StaticB init OK");}
}public class StaticDeadLockMain extends Thread {private char flag;public StaticDeadLockMain(char flag) {this.flag = flag;this.setName("Thread" + flag);}@Overridepublic void run() {try {Class.forName("com.wsh.jvm.test.Static" + flag);} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println(getName() + " over");}public static void main(String[] args) throws InterruptedException {StaticDeadLockMain loadA = new StaticDeadLockMain('A');loadA.start();StaticDeadLockMain loadB = new StaticDeadLockMain('B');loadB.start();}}

运行程序,我们发现,程序进入阻塞状态,出现了事实上的死锁现象。

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

Java程序对类的使用分为两种:主动使用和被动使用

(一)、主动使用

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

  • 1.当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化;
  • 2.当调用类的静态方法时,即当使用了字节码invokestatic指令;
  • 3.当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。(对应访问变量、赋值变量操作);
  • 4.当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName("com.atguigu.java.Test");
  • 5.当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;

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

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

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

  • 6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化;
  • 7.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;

JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。

  • 8.当初次调用 MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类);

例:

【a】1、2中的情况

/*** 测试类的主动使用:意味着会调用类的<clinit>(),即执行了类的初始化阶段* <p>* 1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。* 2. 当调用类的静态方法时,即当使用了字节码invokestatic指令。*/
public class ActiveUse1 {public static void main(String[] args) {Order order = new Order();}//序列化的过程:public void test1() {ObjectOutputStream oos = null;try {oos = new ObjectOutputStream(new FileOutputStream("order.dat"));oos.writeObject(new Order());} catch (IOException e) {e.printStackTrace();} finally {try {if (oos != null) {oos.close();}} catch (IOException e) {e.printStackTrace();}}}//反序列化的过程:(验证)public void test2() {ObjectInputStream ois = null;try {ois = new ObjectInputStream(new FileInputStream("order.dat"));Order order = (Order) ois.readObject();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} finally {try {if (ois != null)ois.close();} catch (IOException e) {e.printStackTrace();}}}public void test3() {Order.method();}}class Order implements Serializable {static {System.out.println("Order类的初始化过程");}public static void method() {System.out.println("Order method()....");}
}

【b】3中的情况

import java.util.Random;/*** 3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。* (对应访问变量、赋值变量操作)*/
public class ActiveUse2 {public static void main(String[] args) {ActiveUse2 activeUse2 = new ActiveUse2();activeUse2.test1();activeUse2.test2();}public void test1() {System.out.println(User.num);//类初始化了   num是变量System.out.println(User.num1);//类没有初始化 num1是静态常量System.out.println(User.num2); //类初始化了 num2不是字面量方式声明的,需要方法调用,编译期间无法确定}public void test2() {System.out.println(CompareA.NUM1);  //接口不需要初始化System.out.println(CompareA.NUM2);  //接口需要初始化}
}class User {static {System.out.println("User类的初始化过程");}public static int num = 1;public static final int num1 = 1;public static final int num2 = new Random().nextInt(10);}interface CompareA {public static final Thread t = new Thread() {{System.out.println("CompareA的初始化");}};public static final int NUM1 = 1;public static final int NUM2 = new Random().nextInt(10);}

【c】其他情况

/*** 类的主动使用其他情况* 8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。* (涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)*/
public class ActiveUse3 {static {System.out.println("ActiveUse3的初始化过程");}//4、 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName("com.xxxx.xxx.Test")public void test1() {try {Class clazz = Class.forName("com.wsh.jvm.test.Order");//输出语句//ActiveUse3的初始化过程//Order类的初始化过程} catch (ClassNotFoundException e) {e.printStackTrace();}}//5、 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。// 注意:// 当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。// >在初始化一个类时,并不会先初始化它所实现的接口// >在初始化一个接口时,并不会先初始化它的父接口// 因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,// 才会导致该接口的初始化。public void test2() {System.out.println(Son.num);  //接口CompareD不会初始化//依次输出//ActiveUse3的初始化过程//Father类的初始化过程//Son类的初始化过程//1}public void test3() {System.out.println(CompareC.NUM1); //CompareC父接口CompareB并不会初始化//依次输出//ActiveUse3的初始化过程//CompareC的初始化  -> 子接口初始化了,但是父接口并没有初始化//2094196067}//6、 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。// 如果Son2还有子类,根据5,那么其子类的父类,父类的父类也都要被初始化public void test4() {System.out.println(Son2.num);//依次输出//ActiveUse3的初始化过程//Father类的初始化过程//CompareB的初始化 -> CompareB接口定义了default方法,所以接口要初始化,注意区分与CompareD的区别//Son2类的初始化过程//1}//7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。public static void main(String[] args) {System.out.println("hello");//依次输出//ActiveUse3的初始化过程//hello}
}class Father {static {System.out.println("Father类的初始化过程");}
}class Son extends Father implements CompareD {static {System.out.println("Son类的初始化过程");}public static int num = 1;
}class Son2 extends Father implements CompareB {static {System.out.println("Son2类的初始化过程");}public static int num = 1;
}interface CompareB {public static final Thread t = new Thread() {{System.out.println("CompareB的初始化");}};public default void method1() {System.out.println("你好!");}
}interface CompareC extends CompareB {public static final Thread t = new Thread() {{System.out.println("CompareC的初始化");}};public static final int NUM1 = new Random().nextInt();
}interface CompareD {public static final Thread t = new Thread() {{System.out.println("CompareD的初始化");}};
}

(二)、被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化。

也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

  • 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化;

当通过子类引用父类的静态变量,不会导致子类初始化;

  • 2. 通过数组定义类引用,不会触发此类的初始化;
  • 3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了;
  • 4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化;

例:

【a】1、2中的情况

/*** 关于类的被动使用,即不会进行类的初始化操作,即不会调用<clinit>()* 说明:没有初始化的类,不意味着没有加载!*/
public class PassiveUse1 {public static void main(String[] args) {PassiveUse1 passiveUse1 = new PassiveUse1();passiveUse1.test1();passiveUse1.test2();}/*** 1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。* > 当通过子类引用父类的静态变量,不会导致子类初始化*/public void test1() {System.out.println(Child.num);//输出情况//Parent的初始化过程  -> 子类不需要初始化//1}/*** 2. 通过数组定义类引用,不会触发此类的初始化*/public void test2() {Parent[] parents = new Parent[10];   //不打印 —> 没有初始化,运行时才会加载进来System.out.println(parents.getClass()); //class [Lcom.wsh.jvm.test.Parent;System.out.println(parents.getClass().getSuperclass());  //class java.lang.Objectparents[0] = new Parent();    //Parent的初始化过程    -> 类初始化parents[1] = new Parent();    //不打印 -> 类已经初始化过了}
}class Parent {static {System.out.println("Parent的初始化过程");}public static int num = 1;
}class Child extends Parent {static {System.out.println("Child的初始化过程");}
}

【b】3、4中的情况

import java.util.Random;public class PassiveUse2 {public static void main(String[] args) {PassiveUse2 passiveUse2 = new PassiveUse2();passiveUse2.test1();passiveUse2.test2();passiveUse2.test3();}/*** 3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。*/public void test1() {System.out.println(Person.NUM);   //不会触发Person类的初始化System.out.println(Person.NUM1);  //触发了Person类的初始化}public void test2() {System.out.println(SerialA.ID);   //接口同理 使用SerialA.ID不会触发SerialA的初始化System.out.println(SerialA.ID1);  //接口同理 ID1会触发SerialA的初始化}/*** 4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。*/public void test3() {try {//不会触发Person类的初始化,被动使用Class clazz = ClassLoader.getSystemClassLoader().loadClass("com.wsh.jvm.test.Person");} catch (ClassNotFoundException e) {e.printStackTrace();}}}class Person {static {System.out.println("Person类的初始化");}public static final int NUM = 1;//在链接过程的准备环节就被赋值为1了。public static final int NUM1 = new Random().nextInt(10);//此时的赋值操作需要在<clinit>()中执行
}interface SerialA {public static final Thread t = new Thread() {{System.out.println("SerialA的初始化");}};int ID = 1;int ID1 = new Random().nextInt(10);//此时的赋值操作需要在<clinit>()中执行
}

类的加载过程详解之过程三:Initialization(初始化)阶段相关推荐

  1. 尚硅谷2020最新版宋红康JVM教程-中篇-第3章类的加载过程(类的生命周期)详解-4-过程三:Initialization(初始化)阶段

    static与final的搭配问题 初始化阶段,简言之,为类的静态变量赋予正确的初始值. 具体描述 类的初始化是类装载的最后一个阶段.如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中.此时,类 ...

  2. php自动加载类与路由,PHP实现路由与类自动加载步骤详解

    这次给大家带来PHP实现路由与类自动加载步骤详解,PHP实现路由与类自动加载步骤详解的注意事项有哪些,下面就是实战案例,一起来看一下. 项目目录如下 入口文件index.php<?php def ...

  3. JVM--类加载器详解

    42. JVM--类加载器详解 ● 类加载器子系统作用: 1. 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识. 2. ClassLoader只负责 ...

  4. C 编译器、链接器、加载器详解

    原文请见 C 编译器.链接器.加载器详解 0. 预编译 在编译 C++ 程序的预处理阶段,源程序中的所有常量表达式都需要首先计算并替换为对应的具体数值. C语言编译器在对源代码编译之前,还需要进一步的 ...

  5. 新手必看:访问url到加载全过程详解(看完不会我吃shi)

    新手必看:访问url到加载全过程详解(看完不会我吃shi) 1.放在前面:新手必须知道的那些概念 1.1 什么是IP.域名.主机名.url.服务器 1.2 http & https 1.3 O ...

  6. JVM-类加载器 详解(手画多图)面试常问 绝对值得阅读!!!

    受多种情况的影响,又开始看JVM 方面的知识. 1.Java 实在过于内卷,没法不往深了学. 2.面试题问的多,被迫学习. 3.纯粹的好奇. 很喜欢一句话:"八小时内谋生活,八小时外谋发展. ...

  7. JVM------类加载器详解

    JVM------类加载器详解 1.图解类加载器工作流程 2.类加载器种类 3.类加载器的加载顺序 4.一些需要了解的机制 1.图解类加载器工作流程 2.类加载器种类 启动类加载器(Bootstrap ...

  8. android 图片横竖判断_Android横竖屏切换及其对应布局加载问题详解

    本文为大家分享了Android横竖屏切换及其对应布局加载问题,供大家参考,具体内容如下 第一,横竖屏切换连带横竖屏布局问题: 如果要让软件在横竖屏之间切换,由于横竖屏的高宽会发生转换,有可能会要求不同 ...

  9. hql懒加载后判断对象是否存在_hibernate延迟加载(懒加载)详解

    延迟加载 一.什么是懒加载?他的作用? 延迟加载,也叫懒加载,它是Hibernate为提高程序执行效率而提供的一种机制,即只有真正使用该对象的数据时才会创建. Hibernate中主要是通过代理(pr ...

  10. JDBC驱动加载机制详解以及spi机制

    首先有两个问题: 1.java连接数据库时是否真的需要加载驱动? 2.JDBC如何区分多个驱动? 以下摘自:https://blog.csdn.net/buqutianya/article/detai ...

最新文章

  1. php单例模式的核心语句,PHP单例模式的核心思想
  2. hdoj 2544 最短路
  3. 微软:明明修复了Bug,你们还把我骂上热搜?
  4. Eclipse配置Tomcat服务器,通用方法
  5. Django-Json 数据返回
  6. 我的MVVM框架 v0.1发布
  7. 【算法】插值查找算法
  8. 会议论文有影响因子吗_论文投稿——会议论文能否被SCI或EI收录
  9. 智能创新引导工具软件——工作量分配和里程碑安排
  10. dB dBm概念及计算
  11. 仙剑四小说【第一章:结伴入世(下)】
  12. 大专一年级计算机考试题,大专一年级语文期中考试试卷.doc
  13. Rocketmq 消息的同步发送,异步发送,oneway方式
  14. Stimulsoft.Report的代码实现功能自学整理(二)
  15. 1688-seller_info - 获得店铺详情
  16. 怎样在VS2005中添加Flash控件
  17. 我所经历的一次Dubbo服务雪崩,这是一个漫长的故事
  18. 千亿赛道群雄逐鹿,口腔医疗服务商们如何做长远品牌?
  19. t111111111111111111111
  20. 7.试定义RECT类(长方形)及其派生类CUB(长方体)

热门文章

  1. 自动驾驶的Pipline -- 如何打造自动驾驶的数据闭环?(中)
  2. 详解iOS打包、发布与证书体系,深入解析证书非对称加密原理 知其所以然
  3. Linux tcp数据分节接收,TCP的建立和终止 图解
  4. 2021-09-08321. 拼接最大数 单调栈
  5. 简述delete与delete[]的区别
  6. linux容器安装cmake
  7. python安装graphviz和pydotplus
  8. linux mysql 开发_Linux64下mysql安装和开发
  9. 凸优化第四章凸优化问题 4.4二次优化问题
  10. python数据模型和各种实用小技巧,保证让你更PYTHONIC