【Java 类加载过程分析】简单理解

类加载


在了解 Java 类加载器之前我们先谈谈什么是类加载机制:

  • Java 虚拟机一般使用 Java 类的流程:首先将开发者编写的 Java 源代码(.java 文件)编译生成 Java 字节码文件 (.class 文件),然后类加载器会读取字节码文件,并转换成 java.lang.Class 对象。有了该 Class 对象后,Java 虚拟机 可以利用反射方法创建其真正的对象了。Java 提供的类加载器绝大多数都继承自 ClassLoader 类,它们被用来加载不 同来源的字节码文件

  • 当类加载器要加载一个字节码文件时,它是如何判别此目标文件是一个字节码文件呢?可以通过后缀名对目标 文件进行识别吗?Of Course Not!!(试将一个.mp3 音乐文件的后缀名进行修改之后,你会发现,音乐依然可以正常 播放)。那么类加载器又是如何辨别一个文件是字节码文件呢?
    我们来简单了解一下字节码的文件格式,用 javap -verbose 文件名 可以查看字节码文件格式内容在这里插入图片描述

  • 上图存在 MD5 checksum 这个标识。MD5 checksum 的作用主要是用来进行文件的校验。MD5 checksum 后的长 串数字是经过 MD5 加密之后呈现的。那么 MD5 又是对哪些内容进行加密后得到长串数字的呢?字节码文件的前 4 个字节我们将其称为魔数(0xCAFEBABE),魔数的作用就是用来标记字节码文件的类型,MD5 就是对专门用来标记 字节码文件类型的魔数进行加密操作的。
    类加载器在识别字节码文件之后,紧接着就进行字节码文件的加载工作。JDK 提供了多种类加载器,那么究竟 由哪一种类加载器对此字节码文件进行加载呢?
    在这里插入图片描述
    思考:为什么加载 String 类和 ClassLoaderTest 类的类加载器不是同一种类加载器呢?

  • 下面我们就来了解一下 JDK 提供的类加载器,同时解释一下运行结果
    主要介绍一下 JDK 提供的三种类加载器,分别是 Bootstrap ClassLoader(启动类加载器),ExtClassLoader(扩展类加 载器),AppClassLoader(应用类加载器)。每一种类加载器都有其指定的类加载路径
    (1) Bootstrap ClassLoader(启动类加载器又称为根加载器/引导类加载器)主要加载 JAVA_HOME/jre/lib 里的 jar 包,该目录下的所有 jar 包都是运行 JVM 时所必需的 jar 包。注意:类加载器其实自身也是一个 Java 类,因此,自身类加载器需要被其他类加载器进行加载后方可使用,显然必须有一个类加载器的顶级父类(也就是 Bootstrap ClassLoader,该类加载器是由 C 语言代码进行开发的)是其他类加载器的父类。关键点在于,如果一个类的类加载器 是 Bootstrap ClassLoader,那么该类的 getClassLoader()方法返回 null
    (2) ExtClassLoader 主要加载 Java 核心扩展类,即 JAVA_HOME/jre/ext 目录下的 jar 文件
    (3) AppClassLoader 主要加载的是开发者在应用程序中编写的类,即 CLASSPATH 路径下所有的 jar 文件

那么这三种类加载器之间的关系又是怎样的呢?
在这里插入图片描述
既然我们对类的加载范围有了初步的了解,那么如果出现下述情况,你能准确分析其产生的原因吗?

  • 问题:将当前用户自定义的 People 类所在的源文件以 jar 包的形式存放到 ExtClassLoader 加载类路径下,我们 调用 People 类的 getClassLoader()方法,返回的是 AppClassLoader 类加载器还是 ExtClassLoader 类加载器呢?还是说 两者都返回呢?

这里我就直接说结果了:返回的是ExtClassLoader 类加载器

这是因为类加载器之间采用的是双亲委派模型的加载方式进行类加载的。
在这里插入图片描述
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器。

双亲委派模型的工作过程如下:

  • (1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则返回原来已经加载的类
    (2)如果没有找到,就去委托父类加载器去加载。父类加载器也会采用同样的策略,查看自己已经加载过的 类中是否包含这个类,有就返回,没有就委托其父类去加载,直到委托到启动类加载器为止。因为如果父类加载器 为空了,就代表使用启动类加载器作为父加载器去加载该类。(也就是看到的 String 类加载器为 null)
    (3)如果启动类加载器加载失败,就会使用扩展类加载器来尝试加载,继续失败则会使用 AppClassLoader 来 加载,继续失败就会抛出一个异常 ClassNotFoundException

使用双亲委派模型的好处:

  • (1)安全性,避免用户自己编写的类动态替换 Java 的一些核心类。如果不采用双亲委派模型的加载方式进行的加载工作,那我们就可以随时使用自定义的类来动态替代 Java 核心 API 中定义的类。例如:如果黑客将“病毒 代码”植入到自定义的 String 类当中,随后类加载器将自定义的 String 类加载到 JVM 上,那么此时就会对 JVM 产生 意想不到“病毒攻击”。而双亲委派的这种加载方式就可以避免这种情况,因为 String 类已经在启动时就被引导类 加载器进行了加载。
    (2)避免类的重复加载,因为 JVM 判定两个类是否是同一个类,不仅仅根据类名是否相同进行判定,还需要 判断加载该类的类加载器是否是同一个类加载器,相同的 class 文件被不同的类加载器加载得到的结果就是两个不 同的类

Java类加载机制的七个阶段
在这里插入图片描述
加载

  • 加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口

验证: 确保被加载的类的正确性

  • 当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。这个校验过程大致可以分为下面几个类型:
    1.JVM规范校验 JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等
    2.代码逻辑校验 JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。一个方法要求返回 String 类型的结果,但是最后却没有返回结果。代码中引用了一个名为 Apple 的类,但是你实际上却没有定义 Apple 类

  • 当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。这个过程对于我们解答问题也没有直接的关系,但是了解类加载机制必须要知道有这个过程

准备(重点): 为类的静态变量分配内存,并将其初始化为默认值

  • 当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型
    内存分配的对象: Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始

如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值

解析: 把类中的符号引用转换为直接引用

  • 当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用

初始化(重点):
JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值