资讯专栏INFORMATION COLUMN

java类的加载过程

ormsf / 1270人阅读

摘要:一类加载的过程虚拟机加载类主要有五个过程加载验证准备解析和初始化。初始化在虚拟机中严格规定需要对类进行初始化的,有下面五种情况遇到,,或者这条字节码指令时。

在<深入理解Java虚拟机-周志明>这本书里面,在讲到类初始化的五种情况时,提及了一个比较有趣的事情。先来看看下面的代码

public class SubClass {
    static{
        System.err.println("I m your son");
    }
    public static final int name = 111;
}

这个时候如果调用SubClass.name,是根本不会触发SubClass初始化的(这里是因为name是一个常量,和下面的例子不一样,如果这里把final去掉,是会触发Subclass的初始化的,因为对于静态字段而言,如果静态字段被引用,就会调用getstatic指令和putstatic指令,那么自然就会引发类的初始化,详情看下面关于触发类初始化的五种情况)。再来看看另一种情况;

public class SuperClass {

    static{
        System.err.println("I am your father");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass{
    static{
        System.err.println("I m your son");
    }
}

这个时候如果调用SubClass.value(静态字段和静态方法是可以继承但是无法被覆盖,所以这里调用value,只会导致直接定义这个静态变量的类被初始化),同样也是不会使得SubClass这个类进行初始化。那么问题来了,到底类在什么时候会进行初始化,类的初始化顺序到底是怎样的?让我们接着往下看。

一. 类加载的过程
虚拟机加载类主要有五个过程:加载、验证、准备、解析和初始化。

加载:加载是“类加载”的一个过程,希望读者没有混淆这两个概念。

在这个过程虚拟机主要完成三件事,
 通过一个类的全限定名___[解释全限定名]___来获取此类的二进制字节流,这点上,虚拟机并没有指明要从哪里获取类的二进制字节流,因此发展出了很多不一样的加载方式。比如jar,zip等压缩包中加载,从网络获取[如Applet],或者由其他文件生成[如从JSP生成]。
 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
 在Java堆[这个没有强制规定,比如HotSpot则选择在方法区中生成这个对象]中生成一个代表这个类的java.lang.Class对象,作为程序访问方法区中的各种数据的外部入口[也就是说当常量池表中的数据被转换成运行时数据结构的时候,实际上[堆/方法区]有一个Class对象的实例可以访问到方法区的各类数据,包括常量池表,代码等]。
如果加载对象是普通的类或者接口(统称为C),则是通过类加载器(L)去加载C的二进制表示来创建。但是如果加载的是数组类,那情况就有所不同了,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但是数组类内部的元素类型最终还是要靠类加载器去加载。[后续可以添加类加载器的详细解释]

验证

验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机本身的安全。验证大致上有以下4个过程:
1) 文件格式验证:
a) 检查魔数,主、次版本号是否在当前虚拟机处理范围。
b) 常量池的常量是否不被支持[通过检查tag],指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
c) CONSTANT_Utf8_info类型的常量中是否有不符合UTF8编码的数据。
d) Class文件中各个部分及文件本身是否有被删除或者附加其他信息等等。
这个节点的主要目的是保证输入的字节流能被正确的解析并存储于方法区内,格式上符合描述一个java类型信息的要求。这个阶段是基于二进制流,只要通过了这个阶段的验证,字节流才会进入内存的方法区中存储。所以后续的三个阶段基于方法区的存储结构进行的,不会再直接操作字节流。
2) 元数据验证:这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。主要验证包括以下几点:
a) 这个类是否有父类(除了java.lang.Object外,所有的类都应该有父类)。
b) 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
c) 如果这个类不是抽象类,那么应该实现其父类或接口中要求实现的方法。
d) 类中的字段,方法是否与父类相矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载)。
这个阶段主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java规范的元数据信息。
3) 字节码验证:
4) 符号引用验证:

准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,这些变量所使用的内存都将在方法区中分配。这里有几个值得注意的点:
1) 这里初始化的仅仅是类变量(被static修饰的变量)的初始化,并不包括实例变量。实例变量将会在对象实例化的时候随着对象一起分配在java堆中。
2) 这里所说的初始值,通常是数据类型的零值,举个例子:
public static int value = 123;
这句代码中,value在准备阶段的初始值为0,而不是123,因为这个时候还没开始执行任何的java方法。而把value的值置为123的putstatic指令是程序被编译后,存放在类构造器()方法中的。所以value置为123是在初始化[第五阶段]阶段才会执行。[还有一些其他类型的零值,可以参考虚拟机规范]
当然,上述情况也有例外的地方,如果类字段的字段属性表(参考class文件中的属性数据结构)中存在ConstatntValue[即同时被final和static修饰]属性,那么在准备阶段,变量value就会被初始化为ConstantValue属性所指定的值,例如上述变量中,编译时javac将会为value生成的ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue属性而将value赋值为123。

解析

解析阶段就是虚拟机将常量池内的符号引用[使用一组描述符来描述所引用的目标,符可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中]替换为直接引用[直接引用可以是直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄。直接引用与内存的布局有关,如果有了直接引用,则目标一定存在]的过程,符号引用在Class文件内的常量池中以CONSTANT_Fieldref_info,CONSTANT_Class_info,CONSTANT_Methodref_info等类型出现。那么,解析阶段中的直接引用于符号引用又有什么关联呢?
对同一个符号引用进行多次解析请求是很常见的,比如你在代码里面多次new同一个类。这里要分成两种情况:
1) invokeddynamic指令:这个指令的特殊之处在于,它是为了支持动态语言而存在的,也就是说,必须等到程序实际运行这条指令的时候,解析动作才能进行[目前仅使用java语言并不会生成这条指令]。相对的,其余触发的解析指定都是“静态”的,可以在刚刚完成加载阶段,还没开始执行代码时就进行解析。
2) 除了上述的指令外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)。从而避免了多次解析。
解析动作主要针对“类或接口”,“字段”,“类方法”,“接口方法”,“方法类型”,“方法句柄”和“调用点限定符”7类符号引用进行[分别对应7种常量池表的CONSTATN_Class_info,CONSTATN_Fieldref_info,CONSTATN_Methodref_info,CONSTATN_InterfaceMethodref_info,CONSTATN_MethodType_info,CONSTATN_MethodHandle_info,CONSTATN_InvokeDynamic_info,后续三种和动态类型有关,目前java还是静态类型语言]。

初始化

在虚拟机中严格规定需要对类进行初始化的,有下面五种情况:
1) 遇到new,getstatic,putstatic或者invokestatic这4条字节码指令时。
2) 使用java.lang.reflect包的方法对类进行反射调用的时候。
3) 当初始化一个类,发现其父类并没有初始化时,需要先初始化父类。
4) 虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有初始化,则需要先触发其初始化。
对于以上五种初始化场景,虚拟机规范中使用了“只有”,除此之外,所有的引用类的方式都不会触发初始化。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66901.html

相关文章

  • JVM 知识点 01

    摘要:新生代又被划分为三个区域和两个幸存区。这样划分的目的是为了使能够更好地管理堆内存中的对象,包括内存的分配及回收。新生代主要存储新创建的对象和尚未进入老年代的对象。 在Java中主要有以下三种类加载器: 引导类加载器(bootstrap class loader) --用来加载java的核心库(Strin...

    不知名网友 评论0 收藏0
  • JVM实战---类加载过程

    任何程序都需要加载到内存才能与CPU进行交流 同理, 字节码.class文件同样需要加载到内存中,才可以实例化类 ClassLoader的使命就是提前加载.class 类文件到内存中 在加载类时,使用的是Parents Delegation Model(溯源委派加载模型) Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化 showImg(https://s...

    bladefury 评论0 收藏0
  • jvm类加载机制

    摘要:前面提到,对于数组类来说,它并没有对应的字节流,而是由虚拟机直接生成的。对于其他的类来说,虚拟机则需要借助类加载器来完成查找字节流的过程。验证阶段的目的,在于确保被加载类能够满足虚拟机的约束条件。 Java 虚拟机将字节流转化为 Java 类的过程。这个过程可分为加载、链接以及初始化 三大步骤。 加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类...

    lastSeries 评论0 收藏0
  • Java虚拟机如何加载类的

    摘要:虚拟机有个一加载机制,叫做双亲委派模型。扩展类加载器扩展类加载器的父类的加载器是启动类加载器。验证验证的目的就是需要符合虚拟机的规范。虚拟机会通过加锁的方式确保方法只执行一次。 引言 上一篇文章谈到Java运行的流程,其中有一环是类加载。今天就继续深入探讨JVM如何加载虚拟机。首先JVM加载类的一般流程分三步:·加载·链接·初始化那么是否全部Java类都是这样三步走的方式加载呢?我们可...

    TANKING 评论0 收藏0
  • 类的加载机制,双亲委派模型,搞定大厂高频面试题

    摘要:验证验证是连接阶段的第一步,这一阶段的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。字节码验证通过数据流和控制流分析,确定程序语义是合法的符合逻辑的。 看过这篇文章,大厂面试你「双亲委派模型」,硬气的说一句,你怕啥? 读该文章姿势 打开手头的 IDE,按照文章内容及思路进行代码跟踪与思考 手头没有 IDE,先收藏,回头看 (万一哪次面试问...

    Object 评论0 收藏0
  • Java 虚拟机总结给面试的你(中)

    摘要:验证过程验证过程的目的是为了确保文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。二虚拟机字节码执行引擎虚拟机的执行引擎自行实现,可以自行制定指令集与执行引擎的结构体系。 本篇博客主要针对Java虚拟机的类加载机制,虚拟机字节码执行引擎,早期编译优化进行总结,其余部分总结请点击Java虚拟总结上篇 。 一.虚拟机类加载机制 概述 虚拟机把描述类的数据从Clas...

    MRZYD 评论0 收藏0

发表评论

0条评论

ormsf

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<