java虚拟机-类加载机制

类加载时机

从类被加载到虚拟机中开始,到卸载出内存为止,整个生命周期包括 加载,验证,准备,解析,初始化,使用,卸载七个阶段。验证,准备,解析这三个阶段统称为连接。

加载,验证,准备,初始化这个五个阶段顺序是确定的,类加载过程必须按这种顺序开始(只是开始,不是进行或者完成,开始之后,可以交叉混合进行,通常会在一个阶段执行过程中,激活或者调用另外一个阶段),但解析阶段不一定,解析在某些情况下是在初始化之后才开始的(为了支持java的动态绑定)。

什么时候开始类加载过程的第一个阶段,虚拟机规范中没有进行强制约束,由虚拟机自由把握。

有四种情况规定,如果类没有初始化,必须进行类初始化:

  • 遇到 new,getstatic,putstatic,或者invokestatic时。java代码场景:new 实例化对象,读取或者设置静态字段(没有被final 修饰),调用一个类的静态方法。
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当初始化一个类的时候,父类没有初始化,则先触发父类初始化
  • 虚拟机启动时,用户需要指定一个执行的主类(含有main 方法的类),先初始化这个主类。

有且只有这四种场景被称为对一个类进行主动引用。除此之外,所有的引用类的方式,都不会触发初始化,称为被动引用
三个被动引用的例子:

  • 通过子类引用父类的静态变量,不会导致子类初始化
  • 通过数组定义引用类,不会触发此类初始化
  • 引用常量,不会触发定义常量的类的初始化

类加载过程

加载

加载阶段,完成三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将字节流代表的静态储存结构转化为方法区运行的数据结构
  • 在java堆中生成一个代表这个类的 java.lang.Class对象,作为方法区这些数据的入口

对于规范的三点要求并不具体,例如获取二进制字节流的来源就可以多种多样。加载阶段是开发期可控性最强的阶段,因为可以使用系统提供的类加载器,也可以使用用户自定义的类加载器(从而用户可以控制字节流的获取方式)

验证

验证是连接的第一步,这一阶段目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全

java语言本身是相对安全的语言,使用java代码不能访问数组边界以外的数据,将对象转型为没有实现的类型,跳转到不存在的代码行之类,以外编译期间会报错。但是,字节码的来源不一定是java代码编译而来,可以通过任何途径 产生class文件

虚拟机规范对这个阶段的验证限制和规范显得十分笼统,但一般会完成四个阶段的检验:

  • 文件格式验证,可能验证魔数,主次版本号是否在该虚拟机处理访问,检查常量编码等等,内容很多这里只是列举几条
  • 元数据验证:对接字节码描述的信息进行语义分析,确保符合java规范要求,例如是否有父类,是否继承不该继承的类,抽象类的检测,是否覆盖final等等
  • 字节码验证:进行数据流和控制流分析,确保被校验类的方法在运行时不会出现危害虚拟机安全的行为。主要是验证方法体内的类型转换,跳转,操作栈中数据类型与指令代码序列能配合工作(防止 int 类型数据,使用时按 Long类型加载)。
  • 符号引用验证:这一阶段,发生在将符号引用转为直接引用的时候(不是发生这个动作,而是这个动作发生前,进行校验),这个转换在第三阶段-解析中发生。校验内容包括符号引用的字符串描述的全限定名是否能找到相应的类;指定类中,所描述的方法和字段是否存在,符号引用类的在当前类的访问权限(包括字段,方法)等等。符号引用的目的是确保解析动作能正常执行

准备

准备阶段正式为类变量分配内存并设置变量初始值,这些内存都将在方法区内分配
类变量:被static修饰的变量,不包括实例变量,实例变量是对象实例化时被分配到java堆中。
变量初始值:是指通常情况下变量的 “零值”,例如 Public static int a=123,准备阶段,这个变量值为 0,因为此时没有执行任何java方法,给类变量赋值的 putstatic指令是在程序被编译后,存放于类构造器< client >方法中的,这个赋值操作只有在初始化阶段才进行。

上面说的是通常情况,那么特殊情况就是 类字段字段属性表中存在 ConstantValue属性,准备阶段就会赋予指定的值。 例如 public static final int a=124,这样编译时,就会为a变量生成 ConstantValue属性。

解析

解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中了。

直接引用:直接指向目标的指针,相对偏移量或者能间接定位到目标的句柄。直接引用与内存相关,有了直接引用,说明引用目标已经在内存中实现了。符号引用相同的,在不同虚拟机实例上,翻译出来的直接引用一般不同。

虚拟机规范中并未规定解析阶段发生的时机。只是要求对一些操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析,例如 checkcast,new,getstatic,putstatic等等。所以这个会根据需要来判断,是类加载时就解析常量池的符号引用,还是符号引用将要被使用时被解析,这个都不确定。

对于同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可能会对第一次解析结果进行缓存(运行时常量池中记录直接引用,把常量表示为已解析状态),避免解析动作重复进行。虚拟机需要保证是在同一个实体中,所以一个符号引用已经成功解析过了,后续引用解析就一直成功。如果第一次解析失败,后续其他指令对这个符号的解析,也应该收到同样的异常
解析动作主要针对 类或接口字段类方法接口方法四类符号引用进行。具体解析过程自行查阅。

类加载器

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

全限定类名:就是类名全称,带包路径的用点隔开,例如: java.lang.String。
即全限定名 = 包名+类名

非全限定名:类名

类与类加载器

类加载器的作用不止存在于类加载阶段,比如任意一个类,都需要加载它的类加载器类本身来确定其在java虚拟机中的 唯一性。例如,同一个class文件,加载它的类加载器不同,它就不相等。这里说的相等,包括类的 equals方法,isAssignableFrom方法,isInstance方法返回的结果。如果我们用自定义的类加载器去加载类,然后实例化这个类,调用 instanceof 方法,去比较系统默认加载器加载的类(直接写类名或者全限定类名,就是使用系统默认加载器),那么返回就是false。

双亲委派模型

从java虚拟机的角度讲,有两种加载器:启动类加载器(c++实现,是虚拟机的一部分)和其他类加载器(java实现,继承于抽象类 java.lang.ClassLoader)。
从java开发人员的角度,类加载器的分类更细致一点。

  • 启动类加载器:负责将 < JAVA_HOME> \lib目录中,或者被参数 -Xbooyclasspath 所指定的路径中,被虚拟机识别的类库加载到虚拟机中。这个加载器不能被java代码直接引用
  • 扩展类加载器:由 sun.misc.Launcher$ExtClassLoader实现,负责加载< JAVA_HOME>\lib\ext目录中的,或者java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用这个加载器
  • 应用程序类加载器:这个类由 sun.misc.Launcher$AppClassLoader实现,是ClassLoader中的 getSystemClasssLoader()方法的返回值,是系统类加载器,负责加载用户路径上所指定的类库,是程序默认加载器

我们的应用程序是由这三种类加载器相互配合进行加载的。如果有必要,可以加入自定义的加载器。

在这里插入图片描述
这类加载器中的层次关系,称为类加载器的双亲委派模型。处理顶层的启动类加载器外,其余类都有自己的“父类加载器”,这种父子关系不是通过继承实现的,而是通过组合实现

该模型由JDK1.2期间被引入,不是强制性约束模型,可以被破坏。
工作过程:如果一个类加载器收到类加载请求,首先不会尝试自己加载这个类,而是把这个请求委派个父类加载器去完成,每一层次都是如此,因此所以加载请求都会传送到启动类加载器,只有父类反馈无法加载这个类(抛出 ClassNotFoundException),子类加载器才会尝试自己去加载(调用findClass()方法)。

优点: java类随着类加载器一起,具备了优先级层次关系。例如Obkect类,无论是哪一个加载器加载它,包括自定义加载器,最终都是由启动类加载器加载的,在虚拟机中属于同一个类。这样就避免了虚拟机中出现多个Object类(不是实例,是类),使得java类型体系中,一些最基础的行为无法得到保证

注意:实现双亲委派的代码都集中在 ClassLoader的locadClass()方法中,先检查类是否已经被加载,没有则调用父类加载器的loadClass()方法。父类加载器为空,则默认启动类为父类加载器。父类加载失败,抛出异常,则调用自己的findClass()方法进行加载

破坏双亲委派模型

java世界大部分加载器都遵循双亲委派模型,但是也有例外。下面是该模型三次大规模 被破坏的情况。
(1)JDK1.2之前,没有双亲委派模型,但是类加载器和抽象类 java.lang.ClassLoader在JDK1.0就存在,那时用户自定义类加载器就是复写 loadClass()方法。JDK1.2为了向前兼容,添加了一个 findClass()方法,这里可以写自己的类加载逻辑。
(2)双亲委派模型解决了基础类统一的问题,越上层的类由上层加载器加载,但是一些场景需要基础类调用用户代码(上层类加载器不认识这些用户代码),所以有了线程上下文类加载器。
(3)用户程序对动态性的追求,例如:热替换,模块热部署。这样导致一些加载规则不在符合双亲委派模型,具体内容自行查阅。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值