类初始化基本知识java
当使用 java 命令运行 Java 程序时,会启动一个 Java 虚拟机进程。同一个 JVM 的全部线程、全部变量都处于同一个进程里,他们都使用该 JVM 进程的内存区。当系统出现以下状况时,JVM 进程将被终止。面试
System.exit()
或 Runtime.getRuntime().exit()
两个运行的 Java 程序处于两个不一样的 JVM 进程中,两个 JVM 之间并不会共享数据。缓存
当程序主动使用某个类时,若是该类还未被加载到内存中,则系统会经过加载、链接、初始化三个步骤来进行该类的初始化。这三个步骤统称为「类加载」或「类初始化」。网络
「类加载」指的是将类的 class 文件读入内存,并为之建立一个 java.lang.Class
对象。换言之,程序中使用任何类时,系统都会为之创建一个 java.lang.Class
对象。jvm
系统中全部的类实际上也是实例,它们都是
java.lang.Class
的实例。测试
类的加载由类加载器完成,类加载器由 JVM 提供。除此以外,开发者能够经过集成 ClassLoader 基类来自定义类加载。**类加载器一般无须等到”首次使用“该类时才加载它,Java 虚拟机规范容许系统预先加载某些类。this
类被加载后,系统会为之生成对应的 Class 对象,接着就会进入链接阶段。链接阶段负责把类的二进制数据合并到 JRE 中。类接连分为以下三个阶段:线程
类初始化阶段主要就是虚拟机堆类变量进行初始化。在 Java 类中堆类变量指定初始值有两种方法:code
若是类变量没有指定初始值,则采用默认初始值对象
{% note success no-icon %}
{% endnote%}
JVM 初始化一个类包含以下几个步骤:
第 2 个步骤中,若是直接父类又有父类,会再次重复这三个步骤
实例初始化块负责对象执行初始化,而类初始化块是类相关的,系统在类初始化阶段执行,而不是在建立对象时才执行。所以,类初始化块老是比实例初始化块先执行。只有当类初始化完成以后,才能够在系统中使用这个类,包括访问类的类方法、类变量或者用这个类来建立实例。
栗子
Root.java:
public class Root { static { int root = 1; System.out.println("Root 的类初始化块"); } { System.out.println("Root 的实例初始化块"); } public Root() { System.out.println("Root 的无参构造器"); } }
Mid.java:
public class Mid extends Root { static { int mid = 2; System.out.println("Mid 的类初始化块"); } { System.out.println("Mid 的实例初始化块"); } public Mid() { System.out.println("Mid 的无参构造器"); } public Mid(String msg) { this(); System.out.println("Mid 的有参构造器,其参数值:" + msg); } }
Leaf.java:
public class Leaf extends Mid { static { int leaf = 3; System.out.println("Leaf 的类初始化块"); } { System.out.println("Leaf 的实例初始化块"); } public Leaf() { super("初始化测试"); System.out.println("执行Leaf的构造器"); } }
Test.java:
public class Test { public static void main(String[] args) { new Leaf(); new Leaf(); } }
运行结果会是怎样的呢?停下来想想。
输出结果:
Root 的类初始化块 Mid 的类初始化块 Leaf 的类初始化块 Root 的实例初始化块 Root 的无参构造器 Mid 的实例初始化块 Mid 的有参构造器,其参数值:初始化测试 Leaf 的实例初始化块 执行Leaf的构造器 Root 的实例初始化块 Root 的无参构造器 Mid 的实例初始化块 Mid 的有参构造器,其参数值:初始化测试 Leaf 的实例初始化块 执行Leaf的构造器
说明:
实例初始化块就是指没有 static 修饰的初始化块。当建立该类的 Java 对象时,系统老是先调用该类定义的实例初始化块(固然,类初始化要已经先完成)。实例初始化是在建立 Java 对象时隐式执行的,并且,在构造器执行以前自动执行。
实例初始化栗子
public class InstanceTest { { a = 1; } int a = 2; public static void main(String[] args) { // 输出 2 System.out.println(new InstanceTest().a); } }
若是上面例子,将实例初始化块和实例变量声明顺序调换,输出就会变为 1。
建立 Java 对象时,系统先为该对象的全部实例变量分配内存(前提是该类已被加载过),接着程序对这些实例变量进行初始化:先执行实例初始化块或声明实例变量时指定的初始值(按照它们在源码中的前后顺序赋值),而后再执行构造器里指定的初始值。
{% note success no-icon %}
实际上实例初始化块是一个假象,使用 javac
命令编译 Java 类后,该 Java 类中的实例初始化块会消失—实例初始化块中代码会被“还原”到每一个构造器中,且位于构造器全部代码的前面。
{% endnote%}
Java 程序首次经过下面 6 种方式使用某个类或接口时,系统就会初始化该类或接口:
java.lang.Class
对象。例如 Class.forName("Person")
java.exe
命令运行某个主类{% note warning no-icon %}
对于 final
型的类变量,若是该类变量的值在编译时就肯定了,那么,这个类变量至关于「宏变量」。Java 编译器会在编译时直接将该类变量出现的地方替换为它实际的值。所以,程序使用这种静态变量不会致使该类的初始化。
{% endnote %}
栗子:
class MyTest { static { System.out.println("静态初始化块"); } static final String compileConstant = "类初始化 demo"; } public class ComileConstantTest { public static void main(String[] args) { System.out.println(MyTest.compileConstant); } }
输出:
类初始化 demo
因而可知,的确没有初始化 MyTest 类。
当类变量使用了
final
修饰,而且,它的值在编译时就能肯定,那么它的值在编译时就肯定了,程序中使用它的地方至关于使用了常量。
若是上面栗子中代码改成以下:
static final String compileConstant = System.currentTimeMillis() + "";
这时候输出就是:
静态初始化块 1596804413248
由于上面 compileConstant
修改以后,它的值必须在运行时才能肯定,所以,触发了 MyTestg 类的初始化。
此外,ClassLoader
类的 loadClass()
方法来加载某个类时,该方法只是加载类,并不会执行类的初始化。使用 Class.forName()
静态方法再回强制初始化类。
栗子:
package class_load; /** * description: * * @author Michael * @date 2020/8/7 * @time 8:54 下午 */ class Tester { static { System.out.println("Tester 类的静态初始化块"); } } public class ClassLoadTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader cl = ClassLoader.getSystemClassLoader(); cl.loadClass("class_load.Tester"); System.out.println("系统加载 Tester 类"); Class.forName("class_load.Tester"); } }
输出:
系统加载 Tester 类 Tester 类的静态初始化块
经测试能够发现,loadClass
方法确实没有触发类的初始化,而 Class.forName
则会初始化 Tester
类。
类加载器负责将 .class
文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成 java.lang.Class
对象。
类加载器负责加载全部的类,系统为全部被载入内存中的类生成一个 java.lang.Class
对象/实例。一旦一个类被载入 JVM 中,同一个类就不会再次被载入。正是由于有这样的缓存机制存在,因此 Class 修改以后,必须重启 JVM 修改才会生效。
类加载器加载 Class
大体通过以下步骤:
开发者也能够经过继承
ClassLoader
来自定义类加载器。由于暂时未涉及这块,本文暂且略过。
本文重点是了解了类初始化的流程,同时,也结合栗子比较了与实例初始化的区别。类初始化块、实例初始化块、构造器的执行顺序也是面试题常考的内容。最后补充了类加载机制的内容,暂时仅是了解。
绘图采用的 ProcessOn 在线绘制,安利~
生命不息,折腾不止!关注 「Coder 魔法院」,祝你 Niubilitiy !🐂🍺