Java虚拟机-类加载器以及类的加载过程

在这里插入图片描述

类加载器子系统的作用

  • 类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的标识。
  • ClassLoader只负责class文件的加载,至于它是否可以运行,由Execution Engine决定
  • 加载类信息存放在方法区的内存空间中。除了类信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(Class文件中常量池部分的内存映射)。

类加载器 ClassLoader

在这里插入图片描述

  • class file 存储于本地硬盘上,最终这个文件执行的时候需要加载到jvm中,根据这个文件来实例化出n个一摸一样的对象。
  • class file 加载到jvm中,被称为DNA元数据模版,放在方法区。
  • 在 .class 文件 -> JVM -> 最终成为元数据模版,此过程就要一个运输工具(类装载器 ClassLoader),扮演一个快递员的工作。(以二进制流的方式)

类加载的过程

概述

在这里插入图片描述

  • 一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分称为链接(Linking)。
    在这里插入图片描述
    上图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种过程按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(动态绑定)。这些阶段通常相互交叉进行,会在一个阶段的执行过程中激活另外一个阶段。

  • 关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制性约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段《Java虚拟机规范》则是严格规定了有且只有这六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前完成)

  • 遇到 new、getstatic、putstatic 或 invokestatic 这四条指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令典型Java代码场景有:
    1:使用new关键字实例化对象的时候。
    2:读取或者设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
  • 使用 java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先对其进行初始化。
  • 当初始化类的时候,如果发现其父类还没有进行初始化、则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含Main方法的那个类),虚拟机会优先初始化这个主类。
  • 当使用 JDK7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandler 实例最后的解析结果为 REF_getStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型具柄,并且这个方法具柄对应类没有进行过初始化,则需要先触发其初始化。
  • 当一个接口中定义了 JDK8 新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的视线类发生了初始化,那该接口要在其之前初始化。

对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》使用了非常强烈的限定语“有且只有”,这六种场景中的行为,称为对一个类型进行主动引用。除此之外,所有引用类型的方法一都不会触发初始化,称为被动引用。

  • 被动引用的例子1
public class SuperClass {
    static {
        System.out.println("SuperClass init ...");
    }

    public static int value = 1;
}

public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init ...");
    }
}

public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() {
        System.out.println(SubClass.value);
    }
}

打印如下:

SuperClass init ...
1

上述代码运行以后只会输出 SuperClass init ,而不会输出SubClass init 。对于静态字段,只会直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会出发子类的初始化。至于是否需要触发子类的加载和验证阶段,取决于虚拟机具体的实现。对于 HotSpot 虚拟机,可以通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类加载的。

  • 引用例子2
public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() {
        SuperClass[] ss = new SuperClass[10];
    }
}

通过定义数组来引用类,不会出发此类的初始化。但是会触发其对应的一维数组对应的class。

  • 例子3
public class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String HELLO = "Hello World";

}
public class Test {

    public static void main(String[] args) {

        System.out.println(ConstClass.HELLO);

    }
}

上述代码也不会打印 ConstClass init ,因为虽然在Java源码中确实引用了 ConstClass类的常量。但其实在编译阶段通过常量传播优化,已经将此常量的值 hello world 直接存储在 Test 类的常量池中,以后Test对Hello的引用,实际都被转化为Test类对自身常量池的引用了。也就是说 Test已经不存在对ConstClass的引用入口,两个类已经没有什么联系了。

加载
  • 加载(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:
  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种访问入口。

Java虚拟机规范并没有指定二进制字节流从哪里获取。下面是获取 .class 文件的方式

  • 从本地系统中直接加载
  • 通过网络获取。如:Web Applet
  • 从zip压缩包中获取,成为日后 jar ,war 包的基础。
  • 运行时计算生成。如:动态代理技术
  • 由其他文件生成,如JSP应用
  • 从专有数据库中读取.class 文件
  • 从加密文件中获取,典型防class文件反编译的保护措施
  • 对于数组而言,情况有所不同,数组本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态创建而来。但是数组类与类加载器仍有很多密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载,数组加载遵循以下原则:
  • 如果数组组件类型(Component Type)是引用类型,那就递归采用上面提到的加载过程去加载这个组件类型。数组C将会被标识在加载这个类型的类加载器类名称空间上
  • 如果数组的类型不是引用类型(如 int[]数组的组件类型为int),Java将会把数组C标记为与引导类加载器关联。
  • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类可访问性默认为public,可被所有类和接口访问。
验证(Verify)
  • 验证的目的在于确保Class文件的字节流包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全。

  • 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从代码量和耗费的执行性能的角度上讲,验证阶段工作量在虚拟机的类加载过程中占用了相当大的比重。

  • 1.文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这一阶段可以包括以下验证点:

  • 是否以0xCAFEBABE开头。
  • 主、次版本号是否在当前Java虚拟机接受范围内。
  • 常量池中的常量是否有不被支持的常量类型(检查常量tag标记)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型常量中是否有不符合 UTF-8编码的数据
  • Class 文件中各个部分以及文件本身是否有被删除或者附加的其他信息
  • 其他…
  • 验证阶段是基于二进制字节流进行的,只有通过了验证阶段,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储。所以后面的三个验证阶段全都是基于方法区的存储结构上进行的,不会在操作读取字节流了。

  • 2.元数据验证
    第二阶段是对字节码描述的信息进行语义分析,保证其描述的信息符合《Java虚拟机规范》的要求,这个阶段可能包括验证以下几点

  • 这个类是否有父类(除了java.lang.Object 以外,都应该有父类)
  • 这个类的父类是否继承了不允许被继承的类(final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段,方法,是否与父类的产生矛盾(例如覆盖了final字段,或者出现不符合规则的重载等)
  • 其他…

这个阶段主要目的是对类的元数据信息进行语义校验,保证不存在与《Java虚拟机规范》定义的元数据信息相悖。

  • 3.字节码验证
    字节码验证是整个验证阶段最复杂的阶段,主要目的是通过数据流分析和控制流分析,确定语义是否合法、符合逻辑。在第二阶段对元数据信息中的数据类型校验完毕后,这个阶段就要对类的方法体进行校验分析,保证被校验类的方法在运行时不会出现危害虚拟机的行为。

  • 4.符号引用验证
    最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段-解析阶段发生。符号应用验证可以看作是对类自身以外的各类信息进行匹配校验。

准备(Prepare)
  • 为类变量分配内存并且设置该类(static)变量的默认初始值(如:int = 0 boolen = false)
  • 这里不包含final修饰的static,因为final在编译时就已经分配了,准备阶段会显示初始化(即直接赋具体值)。
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析(Resolve)
  • 解析阶段是将常量池内的符号引用转换为直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者能直接访问到对象的句柄。

  • 事实上,解析操作往往会伴随着JVM在执行完成初始化执行之后再执行
  • 解析动作主要针对接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT——Methodref_info等
初始化
  • 初始化阶段就是执行类构造器方法< clinit >() 的过程;(其实就是将 显示初始化的逻辑和static 静态代码块的逻辑组成一个 clinit 方法来执行),此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

  • 构造器方法中执行按语句在源文件的顺序执行

  • ()不同于类的构造器。(关联:构造器是虚拟机视角下的())

  • 若该类具有父类,JVM会保证子类的<clinit()>执行前,父类的()已经执行完毕。

  • 虚拟机必须保证一个类的()方法在多线程下被同步加载。(多线程的情况只需要加载一次)

  • 看下面代码

public class Test {

    public static int a = 1;

    static {
        a = 2;
        b = 20;
    }
    public static int b = 10;

    public static void main(String[] args) {
        System.out.println(a);
        System.out.println(b);
    }
}

打印结果如下:

2
10

看执行的方法如下:
在这里插入图片描述

输出10 的原因如下:b 最后赋值成了 10 (如果将b的定义放在static之前,那么输出就是20)

 0 iconst_1
 1 putstatic #13 <tes/Test.a>
 4 iconst_2
 5 putstatic #13 <tes/Test.a>
 8 bipush 20
10 putstatic #25 <tes/Test.b>
13 bipush 10
15 putstatic #25 <tes/Test.b>
18 return

因为b变量定义在了 static 之后,所以输出 为10 ;在static中如果使用b变量会报错(非法的前向引用)
在这里插入图片描述

注意:如果类中没有类变量(static变量)和静态代码块,那么将不会有clinit()方法,接下来把静态去掉看一下

public class Test {

    public int a = 1;


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

在这里插入图片描述

  • 上述截图中的 init 方法 其实就是构造器方法,任何一个类声明都会有一个构造器,可以是显示声明,也可以是默认生成的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值