结合jvm源码学习java类加载器

类加载器的流程:

我自己记忆的一个快速方法:lvpriuu。含义是:load(加载),verify(验证),prepare(准备),resolve(解析),initialize(初始化),use(使用),unload(卸载)

加载

类加载完成的三件事情:

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

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

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

注意:

1.加载二进制流有多个来源:网络,java包以及class文件等。

2.对于数组类而言,是由虚拟机直接在内存当中动态构造出来。

 

触发类加载的六种情况:

在java虚拟机规范中没有明确规定一个类在什么时候会被加载,但是它严格规定了只有一下6中情况必须对类进行初始化操作,在初始化操作之前必定会触发类的加载和连接。

1.遇到new,getstatic,putstatic,invokestatic这四条字节码指令时。

1.1遇到new关键字;对应new字节码指令。

1.2读取或设置一个类的静态字段(被final修饰的、在编译器把结果放在常量池的静态变量除外)时;对应的getstaticputstatic字节码指令

1.3调用一个类的静态方法时;对应invokestatic字节码指令。

2.使用java.lang.reflect包的方法第一次对类进行反射调用时会触发类的初始化。

3.初始化类时,如果发现父类还没有初始化,则需要先触发父类的初始化

4.虚拟机启动时,用户需要制定一个主函数类(main()方法所在的类),虚拟机会先启动这个类。

5.使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHanlde实例最后的解析结果为REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄时,都需要先初始化该句柄对应的类

6.接口中定义了JDK 8新加入的默认方法(default修饰符),实现类在初始化之前需要先初始化其接口

 

验证

文件格式验证

主要目的是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个java类型信息的要求。

元数据验证

主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息。

字节码验证

主要目的是对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

 

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存(逻辑概念上的方法区,jdk7之前是永久代,jdk8以后是Java堆)。

进行内存分配的仅包含类变量,而不包含实例变量,初始值int类型为0,float为0.0等。

 

解析

类或接口的解析

假设当前代码所处的类为D,将一个从未解析过的符号引用N解析为一个类或接口C的直接引用,解析的过程包括以下3个步骤:

1.如果C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器区加载这个类C。在加载的过程中,由于元数据验证、字节码验证的需要,可能触发其他类的加载动作,例如加载这个类的父类或实现的接口。

2.如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符类似“[Ljava/lang/Integer”的形式,那将会哪找第一点的规则加载数组元素类型。接着由虚拟机生成一个代表数组维度和元素的数组对象。

3.如果上面两步没有出现任何异常,那么C已经在虚拟机中实际上已经成为了一个有效的类或借口了,但是解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

字段解析

如果第一次解析字段符号引用,先要检查字段所属的类或接口是否解析完成。如果解析类或接口成功后,按照下面步骤对C进行后续字段的搜索:

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

2.否则,如果在C中实现了接口,将会按照继承关系从下到上递归搜索各个接口和它的父类,如果查找成功,则结束。

3.否则,如果不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类。

4.如果都查不到,则派出java.lang.NoSuchFiledError。

查找成功后也要进行访问权限的验证。

总结一句话:现在当前类找,找不到再到父类和接口中去找。找到后需要进行权限验证。

注意:

字段的简单名称和描述符能唯一确定一个字段,但是编译器会采取一些更严格的规范。如一个同名字段同时出现在某个类的接口和父类当中,解析规则是可以通过的,但是javac编译器就可能拒绝将其编译为.class文件。

 

方法解析

解析流程与“字段解析”流程类似,不同的一点是如果从父接口或接口列表中查找到匹配的方法,说明类C是一个抽象类,抛出java.lang.AbstractMethodError异常。

 

接口方法解析

解析流程:

1.与类的方法解析相反,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那么就直接抛出java.lang.IncompatibleClassChangeError异常。

2.现在当前接口C中查找,如果没有找到再去父接口找。

3.否则,在接口C的父接口中递归查找,直到java.lang.Object类(接口方法的查找范围也会包括Object类的方法)位置,看是否有简单名称和描述符都与目标相匹配的方法,如果有返回直接引用。

注意:由于java的接口具有多重继承,如果C的不同父接口中存在多个都匹配的方法,那将会从这多个方法中返回一个并结束查找,《java虚拟机规范》中并没有进一步规范约束应该返回哪一个接口方法。但是javac编译器也会提出更严格的约束拒绝编译这种代码来避免不确定性。

 

初始化

在这一步 静态变量才会赋予正确的代码中写的值,如果有static变量,static代码块都会自动生成<clinit>方法。

初始化<clinit>在多线程环境下要进行正确的加锁同步。

 

jvm个人实践

参考《动手写java虚拟机》使用java写出的学习版本的jvm,实现了类加载,方法调用,异常抛出等功能。

https://github.com/laotang123/simple-jvm

 

posted @ 2021-02-28 22:24  老汤的猫  阅读(95)  评论(0编辑  收藏  举报