Java类加载机制

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

类的生命周期:一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。
在这里插入图片描述其中解析也可以在初始化之后再开始,位置并不是唯一的。

加载
在加载阶段,虚拟机要完成三件事情

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

这个阶段需要用到加载器,“双亲委派模型”与此有关。数组类对象是虚拟机直接在动态内存中构造出来,但这个数组的组件类型由加载器加载。比如int[],char[]组件类型不是引用类型,由引导类加载器加载,而类似Integer[]这种组件为引用类型的,就按照本文说的步骤加载。

而关于第一点,获取类的二进制字节流。并没有规定必须从哪里获取,从ZIP压缩包读取,从网络中获取(例如Web Applet),运算时计算生成(如动态代理),其他文件生成(如JSP),从加密文件中获取等等。

验证
Class文件可以由Java源代码编译而来,但它也可以由其他途径产生。毕竟Class文件只是一种格式,最底层的还是0/1,它可以使用包括靠键盘0和1直接在二进制编辑器中敲出 Class文件在内的任何途径产。Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为 载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,

文件格式验证:检查是否符合Class文件的格式,如Class文件开头的魔数,主次版本号是否可以被虚拟机接受、常量池是否正确等等

元数据验证:对类的元数据信息进行语义校验。比如是否有父类,是否继承了不被允许的类、是否实现了接口或者抽象类中的方法等等

字节码验证:通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。就是对将要执行的程序字节码验证(即Class中的Code属性),是否出现了逻辑性的错误。字节码验证有问题,则程序一定不对,但字节码验证没问题,程序不一定能正确执行。通过程序去校验程序逻辑是无法做到绝对准 确的,不可能用程序来准确判定一段程序是否存在Bug。这一阶段很费时间,所以在编译器生成Class文件时就会生成StackMapTable,这样虚拟机直接检查StackMapTable记录是否合法就行。

符号引用验证:验证将符号引用转化为直接引用时候合法,判断该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源。这个动作实在解析阶段进行的,所以这7个步骤并不是严格的顺序,很多时候是交替进行的。

验证阶段有点类似查杀病毒,确保虚拟机执行的Class文件都是正确的。它很重要但并不是必须的,如果使用的Class类都是经过反复使用和验证的,那就可以关闭类验证措施,缩小类加载的时间。就像不开启防火墙,不下载一些恶意软件也能正常使用计算机。

准备
准备阶段是为类中静态变量分配内存并设置类变量初 始值的阶段。此处仅包括静态变量(static修饰),而不包括实例变量,实例变量实在对象实例化的时候随着对象一起分配在堆中。而且此处的初始值指的数据类型默认的零指。比如 public static int a = 123, 准备阶段会给a赋值为0,而不是123。a变成123是类构造器完成的,所以是在初始化阶段才会执行。也有例外,如果此处类字段属性存在ConstantValue属性,那么在准备阶段就会赋值。例如public static final int value = 123, 添加final关键字后类会有ConstantValue属性,此时在准备阶段a就会被赋值为123.

解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用是以一组符号来描述所引用的目标,它与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机 的内存中存在。

初始化
类加载过程最后一步。进行准备阶段时,静态变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通 过程序编码制定的主观计划去初始化类变量和其他资源。通俗的说初始化就是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的 语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问 到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

public class Test {
static { i = 0; // 给变量复制可以正常编译通过 可以给后面定义都赋值
System.out.print(i); // 这句编译器会提示“非法向前引用” static int i=1在此之后才定义 无法访问}
static int i = 1; //此处才定义
}

父类中的()方法在子类之前执行,保证在子类执行前父类的()已经执行完毕。()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的 赋值操作,那么编译器可以不为这个类生成()方法。

类加载器

在类加载流程第一步“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流,实现这一功能的代码叫做类加载器。Java中的任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每 一个类加载器,都拥有一个独立的类名称空间。

判断两个类是否相等,必须在这两个类是被同一个加载器加载的前提下才有意义。否则即使Class文件相同,被同一个虚拟机加载,但是使用不同的类加载器,那么两个类也是不同的。

双亲委派模型

如果一个类被不同的加载器加载,那虚拟机会认定是不同的类,这样Java体系最基础的行为也无从保证,应用程序会变得混乱。所以有了双亲委派模型,双亲委派一共分了四层的加载器。求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,子类使用“组合”关系来复用父类加载器的代码,而不是继承。
在这里插入图片描述
双亲委派模型一共分四层:
启动类加载器:负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的类
扩展类加载器:它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库
应用类加载器:负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
自定义加载器:用户自行扩展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类 加载器实现类的隔离、重载等功能。

果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

破坏双亲委派

亲委派很好地解决了各个类 加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),但如果有基础类型又要调用回用户的代码就不得不破坏双亲委派模型,也就是父类加载器需要去请求子类加载器去完成需要的类。典型例子如JNDI,JNDI的代码由启动类加载器完成加载,但是这个类需要应用程序ClassPath下的JNDI服务提供者接口,启动类加载器无法加载这些代码。为解决这个问题使用线程上下文类加载器,JNDI服务使用这个线程上下文类 加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行 为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器。

OSGi实现模块化热部署:在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更 加复杂的网状结构。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值