ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JVM类的加载机制

2022-01-21 17:03:35  阅读:191  来源: 互联网

标签:委派 Java 虚拟机 双亲 JVM 机制 模型 加载


类的加载机制

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接。
在这里插入图片描述

一:类加载的过程

1.加载

在加载阶段,Java虚拟机需要完成以下三件事情:

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

2.验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证阶段大致上会完成下面四个阶段的检验动作

  1. 文件格式验证:第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。例如:是否以魔数0xCAFEBABE开头,主、次版本号是否在当前Java虚拟机接受范围之内。
  2. 元数据验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。例如:这个类是否有父类,这个类的父类是否继承了不允许被继承的类。
  3. 字节码验证:第三阶段主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。例如:保证不会出现在操作栈放置了int类型的数据,使用时却按long类型载入本地变量表中这样的情况。
  4. 符号引用验证:第四阶段可以看作是对类自身以外的各类信息进行匹配性校验。例如:该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

3.准备

准备阶段是正式为类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。

关于准备阶段,还有两个容易产生混淆的概念需要强调。首先是这时候进行内存分配的仅包括类变量,而不包括实例变量。其次是这里所说的初始值“通常情况”下是数据类型的零值

假设一个类定义了如下的类变量,那么value在准备阶段过后的初始值为0,因为这时尚未开始执行任何方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器 < clinit >()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
在这里插入图片描述
下表列出了Java中所有基本数据类型的零值:
在这里插入图片描述
上面提到在“通常情况”下初始值是零值,那言外之意是相对的会有某些“特殊情况”,即如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的
初始值,假设将value的定义做出如下修改,那么编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
在这里插入图片描述

4.解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,这两种引用的区别如下:

  1. 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
  2. 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

5.初始化

类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。本质上,初始化阶段就是执行类构造器< clinit >()的过程。

二:类加载器

类和类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

也就是说,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

类加载器的分类:

站在Java虚拟机的角度来看,只存在两种不同的类加载器:
①一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;
②另外一种就是其他所有的类加载器,这些类加载器都由Java语言
实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
在这里插入图片描述

ClassLoader源码

ClassLoader 与现有类加载器的关系:
在这里插入图片描述

1.loadClass()

例:加载我们自定义的一个用户类:com.zzuli.User
在这里插入图片描述

2.findClass()

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

在这里插入图片描述

3.defineClass()

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

在这里插入图片描述

双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

对于双亲委派模型就是由 java.lang.ClassLoader 中的loadClass()方法来实现的。

优点:
① 避免类的重复加载,确保一个类的全局唯一性。
② 保护程序安全,防止核心API被随意篡改。

缺点:
在这里插入图片描述

双亲委派模型被破坏的场景

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,双亲委派模型主要出现过3次较大规模的“被破坏”的情况。

1.双亲委派模型的第一次“被破坏”发生在双亲委派模型出现之前

双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了兼容这些已有代码,只能在之后的ClassLoader中添加一个protected方法findClass()并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。双亲委派的具体逻辑就实现在这里面,按照loadClass()的逻辑,如果父类加载失败,会自动调用自己的findClass()来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器符合双亲委派规则。

2.双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的

双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题,基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

一个典型的例子便是JNDI服务

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。

3.双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的

这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换、模块热部署等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。

IBM公司主导的JSR-291(即OSGi R4.2)提案——》OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

标签:委派,Java,虚拟机,双亲,JVM,机制,模型,加载
来源: https://blog.csdn.net/A12115419/article/details/122609545

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有