类的加载过程

x33g5p2x  于2021-12-18 转载在 其他  
字(5.0k)|赞(0)|评价(0)|浏览(511)

类的加载过程

类的加载指的是将类的.class文件中的二进制数据读入到内存中,然后在堆区创建一个这个类的java.lang.Class对象。

类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

一个java文件从被加载到被卸载这个生命过程,总共要经历7个阶段,JVM将类的生命周期过程分为:

加载

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中。

加载类的方式有以下几种:

  • 从本地系统直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件(服务器)

验证

验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如jdk版本是否兼容,高版本编译的class无法在低版本运行。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,父类是否存在。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是为类变量在方法区分配内存并设置默认值的阶段。对于该阶段有以下几点需要注意:

  1. 此时内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 此处设置的初始值是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
  3. 静态常量直接被赋予默认值。

先看下面一个例子:

package com.morris.jvm.load;

public class PrepareTest {

    public static int a = 10;

    public static final int b = 10;

}

在准备阶段a=0,而b=10。因为final修饰的常量(可直接计算出结果)不会导致类的初始化,是一种被动引用。更严谨的说法是常量在编译阶段会将其value生成一个ConstandValue,直接赋予10。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用就是一组符号来描述目标,可以是任何字面量。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

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

  1. 声明类变量是指定初始值
  2. 使用静态代码块为类变量指定初始值

初始化阶段最重要的一件事就是执行<cinit>方法(class initialize,类构造器),此方法由jvm编译后生成,使用javap命令查看class字节码可看到此方法。

  • 类构造器<cinit>:按源代码顺序收集类中所有静态代码块和类变量赋值语句组成。
  • 对象构造器<cinit>:按源代码顺序收集成员变量赋值和普通代码块,最后收集对象的构造方法,最终组成对象构造器。

注意:静态代码块中只能对后面的变量进行赋值,不能进行访问。

static {
        x = 20;
        System.out.println(x); // 此行编译不通过
    }
    
    public static int x = 10;

再来看一个例子,深刻理解类的初始化:

package com.morris.jvm.load;

public class BookTest {
    static BookTest book = new BookTest();
    static int amount = 112;

    static {
        System.out.println("书的静态代码块");
    }

    int price = 110;

    {
        System.out.println("书的普通代码块");
    }

    BookTest() {
        System.out.println("书的构造方法");
        System.out.println("price=" + price + ",amount=" + amount);
    }

    public static void main(String[] args) {
        staticFunction();
    }

    public static void staticFunction() {
        System.out.println("书的静态方法");
    }
}

运行结果如下:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法

对于BookTest类,其类构造器可以简单表示如下:

static BookTest book = new BookTest();

    static {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;

对于BookTest类,其对象构造器可以简单表示如下:

{
        System.out.println("书的普通代码块");
    }
    
    int price = 110;

    BookTest() {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }
}

类的初始化顺序如下:

  1. 如果这个类还没有被加载和连接,那先进行加载和连接
  2. 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
  3. 假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
  4. 总的来说,初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类构造方法- -> 子类构造方法

类的主动使用与被动使用

每个类只有在首次主动使用才会被初始化。

以下是类的6种主动使用场景。

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(Class.forName)
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. 调用main方法
    除了以上6种情况外,其余的都称为被动使用,不会导致类的初始化。

关于类的主动使用与被动使用,下面是几个容易混淆的例子:

构造某个类的数组时不会导致类的初始化

package com.morris.jvm.load;

public class ArrayLoadTest {

    public static void main(String[] args) {
        A[] arrays = new A[10]; // // 只不过是在堆内存开辟了4byte*10的空间
    }

    public static class A {
        static {
            System.out.println("class A init.");
        }
    }
}

引用类的静态常量不会导致类的初始化

package com.morris.jvm.load;

import java.util.Random;

public class ConstantTest {

    public static void main(String[] args) {
        System.out.println(A.MAX);
        System.out.println(A.RANDOM);
    }

    public static class A {
        public static final int MAX = 10;  // 引用类的静态常量不会导致类的初始化

        public static final int RANDOM = new Random().nextInt(); // 只有在初始化后才能得到具体值,会导致了类的初始化

        static {
            System.out.println("class A init.");
        }
    }
}

运行结果如下:

10
class A init.
-859057135

从下面的字节码可以看出常量A.MAX在编译阶段会直接赋予10,而常量A.RANDOM需要初始化ConstantTest类后才能得出结果。

// access flags 0x19
  public final static I MAX = 10

  // access flags 0x19
  public final static I RANDOM

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 15 L0
    NEW java/util/Random
    DUP
    INVOKESPECIAL java/util/Random.<init> ()V
    INVOKEVIRTUAL java/util/Random.nextInt ()I
    PUTSTATIC com/morris/jvm/classloader/ConstantTest$A.RANDOM : I
   L1
    LINENUMBER 18 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "class A init."
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 19 L2
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}

使用

当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。

卸载

JVM规定了一个Class只有在满足一下三个条件的时候才会被GC回收,也就是类的卸载。

  1. 该类所有的实例已经把GC。
  2. 该类的Class对象不再被引用,即不可触及。
  3. 加载该类的ClassLoader已经被GC。

面试题

最后以一个面试题来结束本文。

package com.morris.jvm.load;

public class StaticTest {

    public static int k = 0;

    public static StaticTest t1 = new StaticTest("t1"); 

    public static StaticTest t2 = new StaticTest("t2"); 

    public static int i = print("i");

    public static int n = 99;

    public int j = print("j");
     
    {
        print("构造块");
    }

    static{
        print("静态块");
    }

    public StaticTest(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);
        ++i;
        return ++n;
    }
    
    public static void main(String[] args) {
        new StaticTest("init");
    }
 
}

运行结果如下:

1:j i=0 n=0
2:构造块 i=1 n=1
3:t1 i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:j i=8 n=100
10:构造块 i=9 n=101
11:init i=10 n=102
上一篇:JVM内存模型
下一篇:jvm之类加载器

相关文章