java虚拟机(JVM) 一 类加载过程
Posted rurya
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java虚拟机(JVM) 一 类加载过程相关的知识,希望对你有一定的参考价值。
java虚拟机学习一
挖一个大坑有时间就去填,先来一张图来自公众号@芋道源码,大概会根据这张图去搜索相关知识并学习(图比较大,建议放大或者下载查看)
加载过程
可分为三个主要过程:加载(Loading)→链接(Linking)→初始化(Initialization)。此过程中的连接又分为三个过程:验证(Verification)→准备(Preparation)→监听(Resolution)
之后的过程为使用(Using)和卸载(Unloading)下面来分别分析各个过程中所完成的事情。
1.加载
在此阶段中,虚拟机会完成三件事情:(1)通过一个类的全限定名来获取此类的二进制字节流;
(2)将这个类的字节流的静态数据结构转化为运行时方法区的数据结构;
(3)在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种访问入口;
在加载时,数组本身不会被类加载器创建,它是由java虚拟机直接创建的,但是数组的元素类型是由类加载器创建的。如果数组的元素类型不是引用类型,JVM将会把数组标记为与引导类加载器相关联。
2.验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要去,并且不会危害虚拟机自身的安全。大致会分为4个过程:
(1)对于文件格式的校验,验证字节流是否符合文件格式的规范,而且能被当前版本的虚拟机处理。这里可能包括以下几点:
是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的数据类型、指向常量池的各种索引值中是否有指向不存在的常量或者不符合类型的常量、Class文件的各个部分已经自身是否有被删除的或者附加的其他信息。
这里的校验是基于字节流,之后的校验由于已经将类加载进JVM,所以会基于方法区。
(2)数据元校验,对字节码描述的信息进行语义分析,以确保描述的信息符合java语言规范的要求,这个阶段可能包含以下几点:
这个类是否有除了java.lang.Object类的其他父类、这个类的父类是否继承了不被允许继承的类(如被final修饰)、如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的类、类中的方法字段是否与父类产生矛盾。
(3)字节码验证,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,在第二阶段对元数据的数据类型进行校验后,这个阶段将对类的方法体进行校验,以确保在运行过程中不会产生危害虚拟机安全的事件:
确保任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上、确保方法体重的类型转换是有效的。
(4)符号引用验证,最后一个阶段发生在解析阶段,其对类自身以外(常量池中各种符号的引用)信息进行匹配性校验,通常需要校验以下内容:
符号引用中通过字符的全限定名是否能够找到对应的类、在指定类中是否存在符合方法的字段描述以及简单的方法和字段、符号引用中的类、字段、方法的访问性是否可被当前类访问。
符号引用验证的作用是确保解析动作能够正常执行,如果无法通过符号引用验证会跑出异常,该阶段是一个非常重要但不是必要的阶段。
3.准备
准备阶段主要为静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中分配。
这里需要注意:这里初始化变量仅仅为static修饰变量,而实例变量会随着实例化对象一起初始化于java堆中。其次这里设置的默认值通常是数据类型的初始值,而不是java中被显式赋予的值。
对于基本数据类型来说,static变量和全局变量如果不显式地赋值而直接使用,则系统会对其赋予默认的零值,而对局部变量来说,如果不赋值编译不会通过;
对于同时被static和final修饰的常量,必须在声明的时候就显式的赋值,否则编译不通过,而只被final修饰的常量既可以在声明时对其赋值也可以在初始化时赋值但是必须在使用前赋值,系统不会为其初始化零值;
对于引用数据类型来说,如数组引用,对象引用等,若没有在使用之前对其进行显式赋值,则系统会为其赋予零值(null)。
对于数组,如果在初始化的时候没有赋值,那么系统会自动为其元素类型赋予零值;
4.解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。
符号引用就是一组符号用来描述目标,可以是任何字面量;直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。
解析动作作用域:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类
类或接口的解析:
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机需要以下3个步骤:
如果C不是一个数组类型,JVM将会把代表N的全限定名传给D的类加载器去加载类C;
如果C是一个数组类型,并且数组元素都为对象,将会以上面流程加载元素类型;
如果上面步骤没有出现任何异常,那么C在虚拟机中实际已经成为了一个有效的接口或者类了。但在解析完成之前还要进行符号引用认证,确认D具备对C的访问权限。
字段解析:
在对一个从未用过的字段符号引用解析时,首先会先将字段所属的类或接口的符号引用进行解析,在以上类或者接口C解析成功后JVM会对C进行后续字段搜索:
如果C本身就包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束;
否则,如果C中实现了接口,就会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述都与目标字段匹配的字段,则返回这个字段的直接引用,查找结束;
否则,如果C不是java.lang.Object的话,就会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述都与目标字段匹配的字段,则返回这个字段的直接引用,查找结束;
否则,查找失败,抛出java.lang.NoSuchFieldErro异常;
最后如果查找成功返回了直接引用,就会对这个字段进行权限校验,如果发现不具备对字段访问的权限会抛出java.lang.IllegalAccessError异常。
类方法解析:
与字段相似,首次解析时需要将方法的所属类或接口的符号引用进行解析,如果解析C成功会进行如下类方法搜索:
类方法和接口方法的符号引用常量类型是分开的,如果在类方法表中发现C诗歌接口,直接抛出异常;
否则,如果C本身就包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束;
否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束;
否则,在类C实现的接口列表及它们的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则说明C是一个抽象类,查找结束,抛出异常。
最后,如果查找过程成功返回了直接引用,就会对这个方法进行权限校验,如果发现不具备对这个方法的访问权限,将抛出java.lang.IllegalAccessError异常
接口方法解析:
接口方法解析也需要先解析出接口方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索:
如果在接口方法中发现C是个类而不是接口,直接抛出异常;
否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束;
否则,在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束;
否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题。
5. 初始化
初始化是指为类的静态变量赋予初始值,JVM负责对类进行初始化,主要对类变量进行初始化,在java设定对变量的初始化有两种方式:
声明类变量时指定初始值;
使用静态代码块为类变量指定初始值。
JVM初始化步骤:
如果这个类还没有被加载和连接,则程序会先加载和连接该类;
如果该类的直接父类没有初始化,则会先初始化其直接父类;
如果类中有初始化语句,类会依次执行这些初始化语句;
只有当对类主动使用的时候才会导致类的初始化:
创建类的实例,也就是new一个对象;
访问类或者接口的静态变量,或者对该静态变量赋值;
调用静态方法;
反射;
初始化某个类的子类,则其父类也会被初始化;
JVM启动时被标记为启动类的类,直接使用java.exe命令来运行某个主类。
以上是关于java虚拟机(JVM) 一 类加载过程的主要内容,如果未能解决你的问题,请参考以下文章