Java 基础 —— 类加载

类初始化基本知识java

JVM 和类

当使用 java 命令运行 Java 程序时,会启动一个 Java 虚拟机进程。同一个 JVM 的全部线程、全部变量都处于同一个进程里,他们都使用该 JVM 进程的内存区。当系统出现以下状况时,JVM 进程将被终止。面试

  • 程序运行到最后正常结束
  • 程序使用了 System.exit()Runtime.getRuntime().exit()
  • 程序遇到未捕获的异常或错误
  • 程序所在平台强制结束了 JVM 进程

两个运行的 Java 程序处于两个不一样的 JVM 进程中,两个 JVM 之间并不会共享数据。缓存

类的加载

当程序主动使用某个类时,若是该类还未被加载到内存中,则系统会经过加载、链接、初始化三个步骤来进行该类的初始化。这三个步骤统称为「类加载」或「类初始化」。网络

「类加载」指的是将类的 class 文件读入内存,并为之建立一个 java.lang.Class 对象。换言之,程序中使用任何类时,系统都会为之创建一个 java.lang.Class 对象。jvm

系统中全部的类实际上也是实例,它们都是 java.lang.Class 的实例。测试

类的加载由类加载器完成,类加载器由 JVM 提供。除此以外,开发者能够经过集成 ClassLoader 基类来自定义类加载。**类加载器一般无须等到”首次使用“该类时才加载它,Java 虚拟机规范容许系统预先加载某些类。this

类的链接

类被加载后,系统会为之生成对应的 Class 对象,接着就会进入链接阶段。链接阶段负责把类的二进制数据合并到 JRE 中。类接连分为以下三个阶段:线程

  1. 验证:验证阶段用于检验被加载的类是否有正确的内部结构
  2. 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值
  3. 解析:将类的二进制数据中的符号引用替换成直接引用

类的初始化

类初始化阶段主要就是虚拟机堆类变量进行初始化。在 Java 类中堆类变量指定初始值有两种方法:code

  1. 声明类变量时指定初始值
  2. 使用静态初始化块为类变量指定初始值

若是类变量没有指定初始值,则采用默认初始值对象

{% note success no-icon %}

  • 静态初始化块只会被执行一次(第一次加载该类时),静态初始化块先于构造器执行。
  • 类初始化块和类变量所指定的初始值都是该类的初始化代码,它们的执行顺序与源程序中的排列顺序相同。

{% endnote%}

JVM 初始化一个类包含以下几个步骤:

  1. 加入该类未被加载和链接,则程序先加载并链接该类
  2. 假如该类的直接父类尚未被初始化,则先初始化其直接父类
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句

第 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 种方式使用某个类或接口时,系统就会初始化该类或接口:

  1. 建立类的实例。包括使用 new 操做符来建立实例、经过反射建立实例、经过反序列化建立实例
  2. 调用某个类的类方法(静态方法)
  3. 调用某个类的类变量,或为该类变量赋值
  4. 使用反射方式来强制建立某个类或接口对应的 java.lang.Class 对象。例如 Class.forName("Person")
  5. 初始化某个类的子类。(就是前面介绍过的,该子类的全部父类都会被初始化)
  6. 直接使用 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 !🐂🍺

参考

  • 《疯狂 Java 讲义》第四版,18 章

公众号-二维码-截图

相关文章
相关标签/搜索