Java虚拟机类加载机制

概述

    我们都知道,计算机并不能直接读懂我们人所使用的语言,所以我们编写的程序必须转换成计算机能读懂的语言的才能执行。在JAVA的Class文件中描述的各种信息也不例外,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?这些都是接下来将要说的内容。

    虚拟机把描述类的数据从Class文件加载(Loading)到内存,并对数据进行连接(Linking)初始化(Initialization),其中连接分为 验证(Verification)准备(Preparation)解析(Resolution) 三步,最终形成可以被虚拟机直接使用的JAVA类型,这就是虚拟机的类加载机制


类加载的过程

    接下来我们详细说一下JAVA虚拟机中类加载的全过程,也就是加载验证准备解析初始化这5个阶段所执行的具体动作。

加载

    加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下3件事情:

    1、通过一个类的全限定名来获取定义此类的二进制字节流。

    2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    在这里,第一条中的二进制字节流并不只是单纯的从一个Class文件中获取,他还可以从ZIP包中读取,从网络中获取(最典型的应用:Applet),还可以运算时生成(动态代理技术)等方式。
    相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确的说是加载阶段中获取二进制字节流的的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。
    加载阶段与连接阶段的部分内容(一部分字节码文件格式验证动作)是交叉进行的,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

    验证是连接阶段的第一步,主要目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击。从整体上来看,验证阶段大致上分为4个阶段的检验动作:文件格式验证元数据验证字节码验证符号引用验证

    1.文件格式验证:该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。只有通过了这个阶段的验证后,字节流才会进入内存的方法区中存储,后面3个验证阶段全部是基于方法区的存储结构进行,不会再直接操作字节流。

    2.元数据验证:这个阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息,此阶段是对元数据信息的数据类型进行校验。

    3.字节码验证:本阶段是整个验证过程中最复杂的一个阶段,主要是通过 数据流控制流 分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析。

    4.符号引用验证:本阶段是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候,这个转化将在连接的第三阶段——解析阶段发生。符号引用验证的目的是确保解析动作能正常执行。

准备

    准备阶段是正式为类变量分配内存设置类变量初始值的阶段。这里需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量会在对象实例化时伴随对象一起分配在Java堆中。其次,这里的初始值“通常情况”下为数据的零值,看下方的代码:

public static int value = 123;

这里value在准备阶段过后的初始值是0而不是123,把value赋值为123的动作将在初始化阶段才会执行。既然有通常情况,也就会有“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,代码如下:

public static final int value = 123;

这时,在准备阶段虚拟机会根据ConstantValue的设置将value赋值为123.

解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)的过程。

初始化

    在准备阶段,变量已经赋值过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量其他资源,举个例子:

public static int value1 = 2;
public static int value2 = 6;
static{
    value2 = 8;
}

在准备阶段,value1和value2都等于0,在初始化阶段value1和value2分别等于2和8。类的初始化阶段就是执行类构造器“< clinit>()”方法的过程,该方法只能在类加载的过程中由JVM调用,下面介绍一下< clinit>()的特点和细节:

    1、编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量;

    2、如果一个类的父类还没有初始化,那么会优先初始化父类,但是< clinit>()方法不会显示地调用父类< clinit>()方法,JVM负责保证一个类的< clinit>()方法执行之前,他父类的< clinit>()方法已经被执行;因此在虚拟机中第一个被执行< clinit>()方法的类一定是java.lang.Object。

    3、JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

    4、如果一个类中没有静态语句块,也没有对类变量的进行赋值操作,那么该类可以没有< clinit>()方法。另外,在接口中不需要先执行父接口的< clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会初始化;接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。

触发初始化的时间:

    1、遇到newgetstaticputstaticinvokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

    2、使用java.lang.reflect包的方法对类进行反射调用的时候。

    3、放初始化一个类的时候,发现其父类还没有进行初始化过,则先初始化父类。

    4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。

    5、当使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStstic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则先触发初始化。


其他

    Java虚拟机规范中并没有对 什么时候开始类加载过程的加载阶段 进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有上文提到的5种情况。

    类的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段;其中加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始(不代表进行或完成),但解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。


转载请注明出处:小Hang同学的博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值