JVM调优之类的加载过程

类从被加载到虚拟机内存开始,到卸载出内存,他经历了加载、连接、初始化、使用和卸载五大部分,而在连接过程中,又包括验证、准备和解析三部分。下图就是虚拟机类加载的整个过程图。稍后,我们将每个阶段做逐一分析。

1.加载

“加载”是“类加载”过程的第一阶段,注意这两个词不能混淆了。他主要将类的二进制流加载到内存中的过程。这里的二进制流不仅限于.class文件,当然还包括但不限于以下几种情况。

1.1 二进制流的来源

1.1.1 非数组类的加载阶段

1)从Zip包中读取,这个很常见,最终成为常见的jar、EAR、WAR格式的基础。

2)从网络中获取,这种场景最典型的应用就是Applet。

3)运行时就算生成,这种场景使用的最多的就是动态代理技术。

4)有其他文件生成,最流行的就是JSP应用。

5)从数据库中读取,这种场景相对少见,例如有些中间件服务器。

1.1.2 数组类的加载阶段

1)如果数组的组件类型是引用类型,那就递归采用上面提到的非数组类的加载阶段,数组的组件类型将在加载该组件类型的类加载器的类名称空间被标识。

2)如果数组的组件类型不是引用类型(例如int[])Java虚拟机将会把数组组件类型标记为引导类加载器关联。

3)数组类的可见性与他的组件类型的可见性移植,如果组件类型不是引用类型,那数组类的可见性将默认为public。

扫描二维码关注公众号,回复: 9585019 查看本文章

1.2 类加载主要完成的任务

1)通过一个类的全限定名来湖区定义此类的二进制字节流。

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

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

2. 验证

验证作为连接阶段的第一步,目的是为了确保Class文件的字节路中包含的信息符合当期虚拟机的要求,放置危害虚拟机自身安全。验证阶段是非常重要的,他直接决定了Java虚拟机是否能承受恶意代码攻击。从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中占了相当大的一部分。

2.1 文件格式验证

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

1)是否以魔术0xCAFEBABE开头。

2)主次版本号是否在当前虚拟机处理范围之内。

3)常量池的常量中是否有不被支持的常量类型

4)指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量。

5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。

6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

2.2 元数据验证

这个阶段主要是进行语义分析,以保证去描述的信息符合Java语言规范的需求,主要包括以下几点:

1)这个类是否有父类(Object除外)

2)这个类的父类是否继承了不允许被集成的类(被final修饰的类)

3)如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法。

4)类中字段、方法是否与父类产生矛盾。

2.3 字节码验证

这一阶段主要通过数据流和控制流分析,确定程序语义是否合法、符合逻辑,保证被校验类的方法在运行时不会做出危害虚拟机的事情。

1)保证任意时刻操作栈的数据类型与指令代码序列都能配合工作,比如在操作站中放置了一个int类型数据,使用时按long类型啦加载到本地变量表。

2)保证跳转指令不会跳转到方法体外的字节码指令上。

3)保证方法体中的类型转换是有效的。例如子类对象可以强转父类,但是不允许父类强转子类是危险不合法的。

2.4 符号引用验证

1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。

2)指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。

3)符号引用中的类、字段、方法的访问性是否可被当前类访问。

3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里需要注意有三种定义变量的形式,我们逐个分析。

private int a = 123; #1

private static int b = 123; #2

private static final int c = 123; #3

#1.此变量属于实例变量,他不会在类加载的时候分配内存。

#2 此变量会分配内存。但是经过准备后,b的值为0。至于b=123的赋值是在类的饿初始化阶段<init>进行。

#3 对于有final的属性变量,在java文件编译成class文件的时候,会将c生成ConostantValue的属性,在准备阶段虚拟机就会更具ConstantValue设置c赋值为123。

4 解析

解析阶段是虚拟机常量池中的符号引用替换为直接引用的过程。符号引用也就是Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。这里简单介绍一下符号引用和直接引用。

♢符号引用:符号引用是以一组符号来描述所引用的目标,他要求使用时能无歧义的定位到目标即可。符号引用与虚拟机分布没有关系。

♢直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者能间接定位到的句柄。直接引用与虚拟机内存分布有关。

4.1 类或接口解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号N解析为一个类或接口C的直接引用,虚拟机会分为三种情况:

1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。C的加载符合前面正常类的加载过程。一旦这个加载过程出现任何异常,解析过程就宣布失败。

2)如果C是一个数组,并且数组的元素类型为对象,也就是N的描述符回事类似为"[Ljava/lang/Integer"的形式,那将按照第一条的规则加载数组元素类型。接着有虚拟机生成一个代表此数组维度和元素的数组对象。

3)如果上述都没有问题,那么C在虚拟机中实际上已经成为一个有效的类或者接口了。当然,在解析完成之前,还要验证D是否具备对C的访问权限。

4.2 字段解析

虚拟机要解析一个未被解析过的字段符号引用,首先会将字段表内class_index项中的CONSTANT_Class_info符号引用进行解析。如果出现异常,就会导致解析的失败。虚拟机将按照C进行后续字段的搜索。

1)如果C本身就包含了简单名称和名称描述符都与目标配置的字段,则返回这个字段的直接引用,查找结束。

2)否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和他的父接口,如果接口中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。

3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。

4)否则查找失败,也就是我们常见的java.lang.NoSuchFieldError的错误。

4.3 类方法解析

类方法解析的第一步骤与字段解析一样,也需要先解析出类方法表中的class_index项中索引的方法所属的类或接口,如果解析成功,我们依然用C表示这个类。虚拟机会按照如下步骤进行后续的类方法搜索。

1)类方法和接口方法符号引用的常量类型是分开的,如果在类方法中发现class_index中索引的C是个接口,就抛出异常。

2)如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则,在类C的父类总递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则,在类C实现的接口列表及他们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出AbstractMethodError异常。

5)否则宣告方法查找失败。抛出java.lang.NoSuchMethodError异常。

4.4 接口方法解析

接口方法解析与类解析基本相同,具体步骤如下:

1)与类方法解析不用,如果在接口方法中表现class_index中索引C是个类而不是接口,抛出异常IncompatibleClassChangeError异常。

2)否则在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

3)否则在接口C的父接口总递归查找,直到java.lang.Object,是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。

4)否则宣告方法查找失败。抛出java.lang.NoSuchMethodError异常。

5 初始化

初始化阶段是类加载的最后一个阶段,也是可以真正通过java代码控制的部分。前面提要private static Integer c = 123;中,在解析后c的值还是0,在初始化的过程中,也就是<clinit>方法中,将123赋值给c.

1)<clinit>方式是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中语句合并产生的,编译器收集顺序是有语句在源文件中出现的饿顺序决定的。静态语句块中只能访问到定义在静态语句块之前的变量。

2)<clinit>方法与类的的构造方法<init>不同,他不需要显示的调用父类构造器。虚拟机会保证在子类的方法执行之前,父类的<clinit>方法已经执行完毕。

3)父类的定义的静态语句块要先于子类的变量赋值操作。

4)<clinit>并不是必须的,如果类中没有静态静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>方法。

5)接口中不能使用静态语句块,但仍然又变了初始化的赋值操作,因此接口与类一样都会生成<clinit>方法。但接口与类不同的是,执行接口的<clinit>方法不需要先执行父接口的<clinit>方法。

6)虚拟机会保证一个类的<clinit>方法在多线程环境下呗正确的加锁、同步。

发布了72 篇原创文章 · 获赞 24 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/102888425
今日推荐