JVM探索之初始化

2023年 10月 8日 22.9k 0

前言

Java的生命周期依次经历了加载、连接、初始化、使用、卸载5个阶段,这篇文章就来探索初始化这一阶段

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量时指定初始值
  • 使用静态代码块为类静态变量指定初始值
  • JVM初始化步骤

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句
  • 下面我们从类的初始化、接口初始化、实例初始化三个进行对比

    类初始化

    类初始化的目的:为类中的静态变量进行赋值。

    实际上,类初始化的过程时在调用一个()方法,而这个方法是编译器自动生成的。编译器会将如下两部分的所有代码,按顺序合并到类初始化()方法体中。

  • 静态类成员变量的显式赋值语句
  • 静态代码块中的语句
  • 特点:整个类初始化只会进行一次,如果子类初始化时,发现父类没有初始化,那么会先初始化父类

    初始化时机

    对于加载过程JVM规范中并没有强行约束,可以交给虚拟机的具体实现自由把握,但是对于初始化阶段JVM规范是严格规定了下面几种情况会对类进行初始化

  • 创建类的实例(new)
  • 访问类的静态变量(除常量【被final修饰的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译
  • 访问类的静态方法
  • 通过反射执行上面三种行为,如(Class.forName("my.xyz.Test"))
  • 当初始化一个类时,发现其父类还未初始化,则先触发父类的初始化
  • 虚拟机启动时,定义了main()方法的那个类先初始化
  • 以上情况称为对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用” 。

    接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

    被动引用例子

  • 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化
  • 通过数组定义来引用类,不会触发类的初始化
  • 访问类的常量,不会初始化类
  • 举例说明

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitChild.childName);
            System.out.println("----------------------");
            System.out.println(InitChild.childName);
        }
    }
    
    class InitBase {
        public static String baseName = "base";
    
        static {
            System.out.println("base static block");
        }
    }
    
    
    class InitChild extends InitBase {
        public static String childName = "child";
    
        static {
            System.out.println("child static block");
        }
    }
    

    运行结果

    base static block
    child static block
    child
    ----------------------
    child
    

    这个例子是通过引用类的静态变量来初始化Child类,但是在初始化时发现父类没有初始化于是先初始化父类,执行了父类和子类的静态代码块,但是第二次没有打印验证了类初始化只会初始化一次

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitChild.baseName);
        }
    }
    
    class InitBase {
        public static String baseName = "base";
    
        static {
            System.out.println("base static block");
        }
    }
    
    
    class InitChild extends InitBase {
        public static String childName = "child";
    
        static {
            System.out.println("child static block");
        }
    }
    

    运行结果

    base static block
    base
    

    这里直接通过子类访问父类的静态变量发现只有父类进行了加载,子类并没有加载。符合被动引用的特性:只有定义这个字段的类才会被初始化

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitChild.name);
            System.out.println("----------------------");
            System.out.println(InitChild.name);
        }
    }
    
    class InitBase {
        public static String baseName = "base";
    
        static {
            System.out.println("base static block");
        }
    }
    
    
    class InitChild extends InitBase {
        public static final String childName = "child";
    
        static {
            System.out.println("child static block");
        }
    }
    

    运行结果

    child
    ----------------------
    child
    

    这个例子和之前相比唯一的不同就是子类的变量是final修饰是一个常量,访问类的静态常量不会触发初始化流程所以不会打印静态代码块的内容

    常量在编译阶段会存入到调用这个常量的方法所在类的常量池中。本质上,调用类并没有直接引用到定义常量的类,因此不会触发定义常量类的初始化,也不会加载这个类

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitChild.name);
            System.out.println("----------------------");
            System.out.println(InitChild.name);
        }
    }
    
    class InitBase {
        public static String baseName = "base";
    
        static {
            System.out.println("base static block");
        }
    }
    
    
    class InitChild extends InitBase {
        public static final String childName = UUID.randomUUID().toString();
    
        static {
            System.out.println("child static block");
        }
    }
    

    运行结果

    base static block
    child static block
    2acea4dc-36b7-439b-a463-d6a850638abf
    ----------------------
    2acea4dc-36b7-439b-a463-d6a850638abf
    

    这个例子跟上面不同的点是这个子类中的name仍然使用了final进行修饰,理论上执行结果应该和上一个例子一样,但是我们这边打印的结果中发生了类加载。这是因为在编译期间无法确定这个常量的值,需要到运行阶段才能确认,就不会将这个值放到常量池中。因此当我们执行代码的时候发生类加载的行为

    接口初始化

    接口中不能存在static静态代码块,但是接口中可以定义变量(默认是public static final修饰的),因此接口也存在初始化的场景。但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitChildInterface.childName);
        }
    }
    
    interface InitBaseInterface {
        String baseName = "base";
    
        /**
         * 接口中无法定义static代码块,只能创建一个线程,如果接口实例化了那么这个线程就会实例化执行构造代码
         */
        Thread thread = new Thread() {
            {
                System.out.println("base interface");
            }
        };
    }
    
    interface InitChildInterface extends InitBaseInterface {
        String childName = "child";
    
        Thread thread = new Thread() {
            {
                System.out.println("child interface");
            }
        };
    }
    

    运行结果

    child
    

    这里看到由于定义的childName在编译期间已经确定了值,所以和类初始化一样这个值直接放入到常量池中,没有直接引用这个接口,所以不会发生任何的初始化

    public class InitDemo {
       public static void main(String[] args) {
            System.out.println(InitChildInterface.childName);
            System.out.println("----------------------------");
            System.out.println(InitChildInterface.baseName);
        }
    }
    
    interface InitBaseInterface {
        String baseName = "base";
    
        /**
         * 接口中无法定义static代码块,只能创建一个线程,如果接口实例化了那么这个线程就会实例化执行构造代码
         */
        Thread thread = new Thread() {
            {
                System.out.println("base interface");
            }
        };
    }
    
    interface InitChildInterface extends InitBaseInterface {
        String childName = UUID.randomUUID().toString();
    
        Thread thread = new Thread() {
            {
                System.out.println("child interface");
            }
        };
    }
    

    运行结果

    child interface
    895c7788-3a72-4165-bf36-0861c1877da2
    ----------------------------
    base interface
    e08b4e73-27ba-4d89-b9cd-3f71a5b08810
    

    和之前的类初始化相同,这里的常量在编译期无法确定值的情况下,当使用这个常量时就会发生初始化,这里看到只是子接口发生了初始化但是父接口并没有发生初始化,当真正引用了父接口的变量时就会初始化父接口

    public class InitDemo {
        public static void main(String[] args) {
            System.out.println(InitBaseClass.baseClassName);
            System.out.println("-----------------------");
            System.out.println(InitBaseClass.baseName);
        }
    }
    
    interface InitBaseInterface {
        String baseName = UUID.randomUUID().toString();
    
        /**
         * 接口中无法定义static代码块,只能创建一个线程,如果接口实例化了那么这个线程就会实例化执行构造代码
         */
        Thread thread = new Thread() {
            {
                System.out.println("base interface");
            }
        };
    }
    
    class InitBaseClass implements InitBaseInterface{
        public static String baseClassName = "base class name";
    
        static {
            System.out.println("base class");
        }
    }    
    

    运行结果

    base class
    base class name
    -----------------------
    base interface
    2badc18b-5a86-4545-9721-3aa949d6e8d5 
    

    这个例子和上面一样说明子类初始化时如果没有用到父接口的静态变量不会发生父接口的初始化,只有当父接口的静态变量被直接引用的时候才会去初始化父接口

    对象初始化

  • 实例初始化的目的:为类中非静态成员变量赋值
  • 实际上我们编写的代码在编译时,会自动处理代码,整理出一个()的类初始化方法,还会整理出一个或多个的(...)实例初始化方法。一个类有几个实例初始化方法,由这个类就有几个构造器决定
  • 实例初始化方法的方法体,由四部分构成:

  • super()或super(实参列表) 这里选择哪个,看原来构造器首行是哪句,没写,默认就是super()
  • 非静态实例变量的显示赋值语句
  • 非静态代码块
  • 对应构造器中的代码
  • 特别说明:其中2和3是按顺序合并的,1一定在最前面4一定在最后面

    执行特点:

  • 创建对象时,才会执行
  • 调用哪个构造器,就是指定它对应的实例初始化方法
  • 创建子类对象时,父类对应的实例初始化会被先执行,执行父类哪个实例初始化方法,看用super()还是super(实参列表)
  • 举例说明

    public class InitDemo {
        public static void main(String[] args) {
            InitChild initChild1 = new InitChild();
            System.out.println("===================================");
            InitChild initChild2 = new InitChild();
        }
    }
    
    class InitBase {
        public Thread base = new Thread() {
            {
                System.out.println("base thread");
            }
        };
    
        {
            System.out.println("base block");
        }
    
        public InitBase() {
            System.out.println("base constructor");
        }
    
        public InitBase(String baseName) {
            System.out.println("base constructor with param");
        }
    }
    
    
    class InitChild extends InitBase {
        {
            System.out.println("child block");
        }
    
        public Thread child = new Thread() {
            {
                System.out.println("child thread");
            }
        };
    
        public InitChild() {
            System.out.println("child constructor");
        }
    
        public InitChild(String childName) {
            System.out.println("child constructor with param");
        }
    } 
    

    运行结果

    base thread
    base block
    base constructor
    child block
    child thread
    child constructor
    ===================================
    base thread
    base block
    base constructor
    child block
    child thread
    child constructor 
    

    这个例子中,当我们尝试创建child对象时,首先初始化父类(默认使用super()),同时在父类中按照定义变量和代码块的顺序进行了顺序的初始化,最后调用了构造方法。并且我们看到两次new对象打印的完全相同说明每次new对象都会进行一次对象的初始化

    clinit和init

    init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法

    init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化

    class X {
       static Log log = LogFactory.getLog(); // 
    
       private int x = 1;   // 
    
       X(){
          // 
       }
    
       static {
          // 
       }
    } 
    
  • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。静态代码块中只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面的静态代码块可以赋值,但是不能访问
  • public class Test{
        static {
            i = 0;                          //给变量赋值可以正常编译通过
            System.out.print(i);            //这句话编译器会提示“非法向前引用”
        }
        public static int i = 1;
    }
    
  • ()方法与实例构造器()不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法已经执行完毕,因此在虚拟机中第一个执行的类构造器方法的类一定是java.lang.Object。

  • 由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

  • ()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个()方法。

  • 接口中不能使用静态代码块,但接口中可能会有变量赋值操作,因此接口也会生成()方法。但是接口与类不同,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的()方法。

  • 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的类构造器方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的类构造器方法中有耗时很长的操作,那么就可能造成多个进程阻塞。需要注意的是:其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入方法。同一个类加载器下,一个类型只会初始化一次

  • 总结

    初始化在Java的生命周期中主要担任着为变量赋正确值的功能,clinit主要为类变量进行赋值与之对应的是init主要是为实例对象进行赋值。clinit会优先于init

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论