6、类加载与类加载器

类生命周期 7 个阶段
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
加载(Loading)-> 验证(Verification)-> 准备(Preparation)-> 解析(Resolution)-> 初始化(Initialization)-> 使用(Using)-> 卸载(Unloading)7 个阶段。
其中验证、准备、解析 3 个部分统称为连接(Linking)
在这里插入图片描述
在这里插入图片描述

加载、校验、准备、初始化和卸载这五个阶段的顺序是确定的,但是对于“解析”阶段则不一定,它在某些情况下可以在初始化之后再开始,这样做是 为了支持 java 的运行时绑定特征(也称为动态绑定或晚期绑定)。

1、加载阶段:JVM 虚拟机的实现都是使用的懒加载
加载阶段需完成3件事情:
(1)通过一个类的全限命名来获取定义此类的二进制字节流
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2、验证阶段:为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
(1)文件格式验证:基于二进制流数据来验证的。
魔数,版本,常量池,是否符合UTF-8编码数据
(2)元数据验证:基于方法区的内存来验证的。
方法,字段,内的一些验证
(3)字节码验证:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做 出危害虚拟机安全的行为
(4)符号引用验证:主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,将会抛出异常。
3、准备阶段主要是为静态变量初始化
基本数据类型的零值表
在这里插入图片描述

4、解析阶段是JVM将常量池内的符号引用替换为直接引用的过程
 类或接口的解析  字段解析  类方法解析  接口方法解析
常遇到的异常:

  • java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
  • java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
  • java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)
    5、初始化阶段:初始化主要是对一个 class 中的 static{}语句进行操作(对应字节码就是 clinit 方法)。
    (1)遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
    (2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其父类的初始化。
    (3)实始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
    (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类
    (5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法 句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
    (6)当一个接口中定义了 JDK1.8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前 被初始化。
    6、使用
    7、卸载
    JDK提供的三层类加载器:
    (1)启动类加载器 Bootstrap Class Loader
    (2)拓展类加载器 Extension Class Loader
    (3)应用程序类加载器 Application Class Loader

类加载器
类加载器做的就是上面 5 个步骤的事(加载、验证、准备、解析、初始化)。

JDK 提供的三层类加载器:
(1)启动类加载器:Bootstrap ClassLoader
任何类的加载行为,都要经它过问。它的作用是加载核心类库,也就是 rt.jar、resources.jar、charsets.jar 等。当然这些 jar 包 的路径是可以指定的,-Xbootclasspath 参数可以完成指定操作。 这个加载器是 C++ 编写的,随着 JVM 启动。
(2)拓展类加载器:Extention ClassLoader
扩展类加载器,主要用于加载 lib/ext 目录下的 jar 包和 .class 文件。同样的,通过系统变量 java.ext.dirs 可以指定这个目录。 这个加载器是个 Java 类,继承自 URLClassLoader。
(3)应用程序类加载器 :Application ClassLoader
这是我们写的 Java 类的默认加载器,有时候也叫作 System ClassLoader。一般用来加载 classpath 下的其他所有 jar 包和 .class 文件,我们写的代码, 会首先尝试使用这个类加载器进行加载。
(4)自定义加载器:Custom ClassLoader
支持一些个性化的扩展功能。

类加载器的问题
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这 句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一 个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关 键字做对象所属关系判定等情况

双亲委派模型:稳定
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)
的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在 程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将 会变得一片混乱。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Tomcat 类加载机制
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。
tomcat 类加载器的层次结构:
在这里插入图片描述
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。 这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。 那么 tomcat 是怎么打破双亲委派机制的呢?可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。
在这里插入图片描述
在这里插入图片描述
但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。
但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。

SPI
Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
例:常用的数据库驱动加载
在使用 JDBC 写程序之前,通常会调用Class.forName(“com.mysql.jdbc.Driver”) 用于加载所需要的驱动类。但即使删除了 Class.forName 这一行代码,也能加载到正确的驱动类。MySQL 的驱动代码,就是在这里实现的。
路径:mysql-connector-java-8.0.11.jar!/META-INF/services/java.sql.Driver 里面的内容是:com.mysql.cj.jdbc.Driver
在这里插入图片描述
通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。 SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,主要使用 java.util.ServiceLoader 类进行动态装载。
在这里插入图片描述
这种方式,同样打破了双亲委派的机制。 DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的。它们的类加载器是 Bootstrap ClassLoader,也就是最上层的那个。 而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
它把当前的类加载器,设置成了线程的上下文类加载器。
继续跟踪代码。找到 Launcher 类,就是 jre 中用于启动入口函数 main 的类。我们在 Launcher 中找到以下代码。
在这里插入图片描述
当前线程上下文的类加载器,是应用程序类加载器。使用它来加载第三方驱动。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值