深入理解java虚拟机的相关知识(5)--虚拟机类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化、最终形成可以被虚拟机直接使用的java类型。

类的生命周期

== 加载 验证 准备 解析 初始化 使用 卸载==
其中验证 准备 解析统称为连接。

加载

在加载阶段,虚拟机需要完成以下3件事件:、

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种设计的访问入口。

对于数组类来说,本身不通过类加载器创建,而是由java虚拟机直接创建的。
如果数组的组件类型是引用类型,那就递归采用本节中定义的类加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识。
如果数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联。
加载阶段与连接阶段(验证 准备 解析)的部分内容交叉进行的,这两个阶段的开始时间任然保持着固定的先后顺序。

验证

为了确保Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上来看,验证阶段大致上会完成4个阶段的验证动作。
1 文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
例:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量是否有不被支持的常量类型
  • class文件中各个部分及文件本身是否又被删除的或附加的其他信息。

只有通过这个阶段的验证后,字节流才会进入内存的方法区中进行存储,后面3个验证是基于方法区的存储结构进行的,不会直接操作字节流。
2 元数据验证
对字节码描述的信息进行语义分析(对类的元数据信息语义校验),以保证其描述的信息符合Java语言规范的要求。
例:

  • 这个类是否有父类
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 如果该类不是抽象类,是否实现了其父类或接口的要求实现所有方法

3 字节码验证
通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。这个阶段是最浮渣的一个阶段,对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
例:

  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 保证方法体中的类型转换是有效的
  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作

4 符号引用验证
最后一个阶段校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段。符号引用验证可以看做是对类自身以外的信息进行匹配性校验。确保解析动作能正常执行
例:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的访问性是否可被当前类访问。

准备

正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,不包括实例变量在“通常情况”下初始值是零值,final修饰除外。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。
直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应常量池7种常量类型

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

初始化

执行类构造器()方法的过程。
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器手机的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但是不能访问。
@clinit方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证子类的clinit方法执行之前,父类的clinit方法已经执行完毕。
接口中不能使用静态语句块,但任然有边来那个初始化的赋值操作,因此接口与类一样都会生成clinit方法。但接口与类不同的是,执行接口的clinit方法不需要先执行父接口的clinit方法。只有当父接口中定义的变量使用时,父接口才会初始化。接口的实现类一样不会执行接口的clinit方法。

类“初始化”的情况(有且仅有5种)

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发初始化。(场景:new实例化对象/读取或设置类静态字段/调用类静态方法)
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先进行类初始化
  3. 当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先对父类进行初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机先初始化这个主类
  5. 当使用jdk1.7支持时,如果一个java.lang.invoke.MrethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先初始化。

类加载器

把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己来决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”

双亲委派模型

** 从java虚拟机的角度来讲**,只存在两种不同的类加载器:一种是启动类加载器,由c++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,由java语言实现,独立于虚拟机外部,并且全都继承自抽象类java,lang.ClassLoader。
从java开发人员角度来看,可分为启动类加载器 扩展类加载器 应用程序类加载器
在这里插入图片描述

类加载器的双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,通过组合关系来复用父类加载器的代码。
工作过程如果一个类记载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载器请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(没有找到所需的类),子加载器才会尝试自己去加载。
好处具有优先级的层次关系,保证java程序的稳定运作,实现非常简单。
双亲委派的代码都集中在java,lang,ClassLoader的loadClass()方法之中,

protected synchronized Class<?>loadClass(String name ,boolean resolve)throws ClassNotFoundException{
Class c=findLoadedClass(name)
//判断该类是否被加载过
if(c==null){
try{
if(parent!=null){
c=parent.loadClass(name,false);
}else{
c=findBootstrapClassOrNull(name);
//若父类加载器为空,则使用启动类加载器作为父加载器
}
}catch(ClassNotFoundException e){
//父类无法加载,抛出异常
}
if(c==null){
//在父类加载器无法加载的时候,调用本身的findclass方法来进行加载
c=findClass(name);
}
}
if(resolve){
resolveClass(c);
//解析该对象
}
return c;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值