Java学习日志----类加载过程学习1

每个编写的".java"拓展名类文件都存储着需要执行的程序逻辑
这些".java"文件经过Java编译器编译成拓展名为".class"的文件
“.class"文件中保存着Java代码经转换后的虚拟机指令
当需要使用某个类时,虚拟机将会加载它的”.class"文件,并创建对应的class对象
将class文件加载到虚拟机的内存,这个过程称为类加载
类加载指的是将类的.class文件中的二进制数据读入到内存中
将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的java.lang.Class对象
用来封装类在方法区类的对象

1.加载

类加载过程的第一个阶段:通过一个类的完全限定查找此类字节码文件
并利用字节码文件创建一个Class对象
JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中
接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象
这个 Class 对象就是这个类各种数据的访问入口

2.验证

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证

2.1 文件格式验证

这里的文件格式是指Class的文件规范,验证主要保证加载的字节流符合Class文件的规范
在计算机中不可能是整个Class文件,只有0和1,也就是字节流
根据前面对Class类文件的描述,Class文件的每一个字节表示的含义都是确定的
以及保证这个字节流可以被虚拟机接受处理
在Hotspot的规范中,对文件格式的验证远不止这些(Hotspot是Java的虚拟机)
但是只有通过文件格式的验证才能进入方法区中进行存储
所以自然也就知道,后面阶段的验证工作都是在方法区中进行的

2.2 元数据验证

元数据可以理解为描述数据的数据,更通俗的说,元数据是描述类之间的依赖关系的数据
比如Java语言中的注解使用(使用@interface创建一个注解)
主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
具体的验证信息包括以下几个方面:
这个类是否有父类(除了java.lang.Object外其余的类都应该有父类)
这个类的父类是否继承了不允许被继承的类(比如被final修饰的类)
如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的方法
类中的字段、方法是否与父类产生矛盾(比如是否覆盖了父类的final字段)

2.3 字节码验证

这个阶段主要对类的方法体进行校验分析
通过了字节码的验证并不代表就是没有问题的,但是如果没有通过验证就一定是有问题的
整个字节码的验证过程比这个复杂的多,由于字节码验证的高度复杂性
在jdk1.6版本之后的虚拟机增加了一项优化
Class类文件结构这篇文章中说到过有一个属性:StackMapTable属性
可以简单理解这个属性是用于检查类型是否匹配

2.4 符号引用验证

这个验证是最后阶段的验证
符号引用是Class文件的逻辑符号,直接引用指向的方法区中某一个地址
在解析阶段,将符号引用转为直接引用,这里只进行转化前的匹配性校验
符号引用验证主要是对类自身以外的信息进行匹配性校验
比如符号引用是否通过字符串描述的全限定名是否能够找到对应点类
进行符号引用验证的目的在于确保解析动作能够正常执行
如果无法通过符号引用验证那么将会抛出java.lang.IncomingChangeError异常的子类

2.4.1 符号引用(Symbolic Reference)

符号引用以一组符号来描述所引用的目标
符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即
符号字面量,还没有涉及到内存
符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载在内存中
各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的
因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中

2.4.2 直接引用(Direct Reference)

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
可以理解为内存地址
直接引用是与虚拟机实现的内存布局相关的
同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同
如果有了直接引用,那引用的目标必定已经在内存中存在

3.准备

为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0
(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值)
这里不包含用final修饰的static,因为final在编译的时候就会分配了
注意这里不会为实例变量分配初始化,类变量会分配在方法区中
而实例变量是会随着对象一起分配到Java堆中
总而言之,对于三种不同类型的变量,JVM分配初始化有三种不同的策略
1.final static 修饰的变量,在文件进行编译时就完成初始化
2.static修饰的变量,类变量,类加载的准备阶段完成初始化
3.类成员变量,实例化对象时,随着对象加载到堆中

4.解析

主要将常量池中的符号引用替换为直接引用的过程
符号引用就是一组符号来描述目标,可以是任何字面量
而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
有类或接口的解析,字段解析,类方法解析,接口方法解析
这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》
在进行解析之前需要对符号引用进行解析
判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前)
还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)
如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外
虚拟机可以对第一次解析的结果进行缓存
在运行时常量池中记录引用,并把常量标识为一解析状态
这样就避免了一个符号引用的多次解析
主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用
这里主要说明前四种的解析过程

4.1 类或者接口解析

要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤:

4.1.1 如果该符号引用不是一个数组类型

那么虚拟机将会把该符号代表的全限定名称传递给调用这个符号引用的类
这个过程由于涉及验证过程所以可能会触发其他相关类的加载

4.1.2 如果该符号引用是一个数组类型,并且该数组的元素类型是对象

符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式
将会按照上面的规则进行加载,虚拟机将会生成一个代表此数组对象的直接引用

4.1.3 如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用

但是在解析完成之前需要对符号引用进行验证
主要是确认当前调用这个符号引用的类是否具有访问权限
如果没有访问权限将抛出java.lang.IllegalAccess异常

4.2 字段解析

对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的
只有在正确解析得到其类的正确的直接引用才能继续对字段的解析
对字段的解析主要包括以下几个步骤:

4.2.1 如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段

则返回这个字段的直接引用,解析结束

4.2.2 如果在该符号的类实现了接口

将会按照继承关系从下往上递归搜索各个接口和它的父接口
如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段
那么就直接返回这个字段的直接引用,解析结束

4.2.3 如果该符号所在的类不是Object类的话

将会按照继承关系从下往上递归搜索其父类
如果在父类中包含了简单名称和字段描述符都相匹配的字段
那么直接返回这个字段的直接引用,解析结束

否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证
如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常

4.3 类方法解析

进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
类方法和接口方法的符号引用是分开的

4.3.1 所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口

那么会抛出java.lang.IncompatibleClassChangeError的异常

4.3.2 如果class_index的索引确实是一个类

那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法
如果有的话就返回这个方法的直接引用,查找结束
否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段
如果有,则直接返回这个字段的直接引用,查找结束
否则,在这个类的接口以及它的父接口中递归查找
如果找到的话就说明这个方法是一个抽象类,查找结束
返回java.lang.AbstractMethodError异常(因为抽象类是没有实现的)
否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证
如果没有访问权限,就抛出java.lang.IllegalAccessError异常

4.4 接口方法解析

同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用
如果解析成功,就进行下面的解析工作:

4.4.1 如果在接口方法表中发现class_index的索引是一个类而不是一个接口

那么也会抛出java.lang.IncompatibleClassChangeError的异常
否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法
如果有的话就直接返回这个方法的直接引用。查找结束
否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
否则,查找失败
接口的所有方法都是public,所以不存在访问权限问题

5.初始化

类加载最后阶段,若该类具有超类,则对其进行初始化
执行静态初始化器和静态初始化成员变量
如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化
虚拟机这个阶段才开始真正执行Java程序代码,前文讲到对类变量的初始化,但那是仅仅赋初值
用户自定义的值还没有赋给该变量
只有到了初始化阶段,才开始真正执行这个自定义的过程
所以也可以说初始化阶段是执行类构造器方法clinit() 的过程

5.1 什么时候进行类的初始化

1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
5.2 类初始化步骤
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化
注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值