JVM 的类加载原理

一、什么是类加载器(ClassLoader)

类加载器是指在系统运行过程中动态的将字节码文件加载到 JVM 中的工具,是一个类。基于这个工具的整套类加载流程,称作类加载机制。在 IDE 中编写的都是源代码文件,以后缀名为.java的文件形式存在于磁盘上,经过编译后生成后缀名为.class的字节码文件,类加载器加载的就是这些字节码文件。
JVM类加载

首先 Java 源代码(.java)文件会被 Java 编译器编译为字节码(.class)文件,然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行。在整个程序执行过程中,JVM 会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作Runtime Data Area (运行时数据区),也就是常说的 JVM 内存。Java 中常说的内存管理就是针对这段空间进行的管理(如何分配和回收内存空间)。

二、类加载器的种类

Java 默认提供了三个类加载器,分别是启动类加载器(BootStrap ClassLoader)扩展类加载器(Extension ClassLoader)应用类加载器(Application ClassLoader),依次前者分别是后者的【父加载器】。父加载器不是「父类」,三者之间没有继承关系,只是因为类加载的流程使三者之间形成了父子关系。还有一种是用户自定义类加载器(java.lang.ClassLoader的子类)。
Java2开始,类加载过程采取了双亲委派模型(【PDM】Parents Delegation Model),PDM 更好的保证了 Java 平台的安全性。在该机制中,JVM 自带的 BootStrapClassLoader 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 BootStrap ClassLoader 的引用。

三、双亲委派模型工作过程

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

说明:
PDM 只是 Java 推荐的机制,并不是强制的。可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持 PDM,就重写 findClass(name);如果想破坏 PDM,就重写 loadClass(name)。JDBC使用线程上下文加载器打破了 PDM,原因是 JDBC 只提供了接口,并没有提供实现。

四、为什么需要双亲委派模型

1️⃣防止内存中出现多份同样的字节码。
反向思考,如果没有 PDM 而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的同名类并放在 ClassPath 中,多个类加载器都能加载该类到内存中,系统中将会出现多个不同的 Object 类,那么类之间的比较结果及类的唯一性将无法保证。而且如果不使用 PDM 将会给虚拟机的安全带来隐患。所以,要让类对象进行比较有意义,前提是它们要被同一个类加载器加载。

2️⃣试想一个场景:
黑客自定义一个具有系统 String 类相同功能的java.lang.String类,在 equals 方法加入了一些“病毒代码”,并且通过自定义类加载器加入到 JVM 中。此时,如果没有 PDM,JVM 就可能误以为黑客自定义的java.lang.String类是系统的 String 类,导致“病毒代码”被执行。而有了 PDM,该自定义类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

3️⃣在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?
确实可行。但是,JVM 判断一个对象是否为某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,会返回 false。任意一个类,都需要由加载它的类加载器和该类本身一同确立其在 JVM 中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说,判断两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类必定不相等。例如:ClassLoader1、ClassLoader2 都加载java.lang.String类,对应 Class1、Class2 对象。那么 Class1 对象不属于 ClassLoader2 对象加载的java.lang.String类型。

五、线程上下文类加载器

并非所有的类加载机制都遵循这个模型,这个模型是被破坏过的。PDM 很好的解决了各个类加载器的基础类的同一问题,基础类是总被用户代码所调用的 API,但是基础类要调用用户的代码的时候,PDM 就出现了缺陷。比如 JNDI 服务,属于 rt.jar,它需要调用应用程序的代码来实现资源管理,但是启动类加载器并不能识别应用程序代码,因此出现了线程上下文类加载器。该类加载器由Thread类的 setContextClassLoaser() 进行设置,如果线程未创建,它将会从主线程中继承一个。JNDI 可以使用线程上下文加载器来加载所需要的 SPI 代码,也就是父类加载器去请求子类加载器加载 Class。Java 中所有涉及 SPI 加载的基本上都采用这个方法。

六、关于几个类加载器的说明

1️⃣【BootStrap ClassLoader 启动类加载器】C/C++ 实现
使用 Extension ClassLoader 的实例调用 getParent(),获取其父加载器时会得到一个 null 值,比如调用String.class.getClassLoader()。该加载器会默认加载系统变量 sun.boot.class.path 指定的类库(jar 文件和 .class 文件),默认是 $JRE_HOME/lib 下的类库,如 rt.jar、resources.jar 等,具体可以输出该环境变量的值来查看。除了加载这些默认的类库外,也可以使用 JVM 参数 -Xbootclasspath/a 来追加额外需要让根加载器加载的类库。

总之,对于 BootStrap ClassLoader 需要知道三点:

  1. 使用 C/C++ 编写,无法在 Java 中获得其实例。
  2. 默认加载系统变量 sun.boot.class.path 指定的类库。
  3. 可以使用 -Xbootclasspath/a 参数追加默认加载类库。

2️⃣【Extension ClassLoader 扩展类加载器】Java 实现
用于加载系统所需要的扩展类库。默认加载系统变量 java.ext.dirs 指定位置下的类库,通常是 $JRE_HOME/lib/ext 目录下的类库。可以在启动时修改 java.ext.dirs 变量的值来修改扩展类加载器的默认类库加载目录,但通常并不建议这样做。如果真的有需要扩展类加载器在启动时加载的类库,可以将其放置在默认的加载目录下。

总之,对于 Extension ClassLoader 需要知道两点:

  1. 它是 Java 实现的,可以在程序中获得它的实例并使用。
  2. 通常不建议修改java.ext.dirs参数的值来修改默认加载目录,如有需要,可以将要加载的类库放到这个默认目录下。

3️⃣【Application ClassLoader 应用类加载器】Java 实现
它的作用是加载应用程序 classpath 下所有的类库。它是应用最广泛的类加载器,是最常用的类加载器,在程序中调用的很多 getClassLoader() 返回的都是它的实例。在自定义类加载器时如果没有特别指定,那么自定义的类加载器的默认父加载器也是这个应用类加载器。

总之,对于 Application ClassLoader 需要知道两点:

  1. 它是 Java 实现的,负责加载应用程序 classpath 下的类库。
  2. 应用类加载器是最常用的类加载器。

4️⃣【 自定义类加载器】
除了上述三种 Java 默认提供的类加载器外,还可以通过继承java.lang.ClassLoader自定义类加载器。如果创建自定义类加载器时未指定父加载器,那么默认使用 Application ClassLoader 作为父加载器。

七、类加载器的启动顺序

BootStrap ClassLoader 是一个使用 C/C++ 编写的类加载器,它已经嵌入到了 JVM 的内核之中。当 JVM 启动时,BootStrap ClassLoader 也会随之启动并加载核心类库。当核心类库加载完成后,BootStrap ClassLoader 会创建 Extension ClassLoader 和 Application ClassLoader 的实例,两个 Java 实现的类加载器将会加载自己负责路径下的类库,这个过程可以在 sun.misc.Launcher 中窥见。

原理:
JVM 中类的加载是由类加载器和它的子类来实现的。类加载器是 Java 重要的运行时系统组件,它负责在运行时查找和装入类文件中的类。由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载连接(验证、准备和解析)初始化

1️⃣加载:将类的 .class 文件加载到内存中,通常是创建一个字节数组读入 .class 文件。并将静态数据转化为方法区中的运行时数据结构,在堆中生成一个代表该类的java.lang.class对象,作为方法区类数据的访问入口。加载完成后 Class 对象还不完整,所以此时的类还不可用。

2️⃣连接:当类被加载后就进入连接阶段。将 Java 类的二进制代码合并到 JVM 的运行状态中:

  1. 验证:确保加载的类信息符合 JVM 规范。
  2. 准备:正式为类变量分配内存并设置初始值(初始化 static 变量),这些内存都在方法区中分配。
  3. 解析:JVM 常量池的符号引用替换为直接引用。

3️⃣初始化
执行类构造方法 clinit()。该方法由编译器自动收集类中所有变量的赋值动作和静态语句块(static块)中的语句合并产生。当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化。虚拟机会保证一个类的 clinit() 在多线程的环境下被正确的加锁和同步。当访问一个 Java 类的静态域时,只有真正声明这个域的类才会被初始化。

类的正常加载过程

八、Java 静态代码块的执行时机

加载与类加载是两个截然不同的过程。

Java的类加载是指类从被加载到虚拟机内存中开始,到卸载出虚拟机内存为止的整个生命周期中的整个过程,包括加载、验证、准备、解析和初始化五个阶段。加载指的是类加载的第一个阶段。加载阶段,虚拟机需要完成以下3件事情:

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

类中的静态块会在整个类加载过程中的初始化阶段执行,而不是在类加载过程中的加载阶段执行。初始化阶段是类加载过程中的最后一个阶段,该阶段就是执行类构造器<clinit>方法的过程,<clinit>方法由编译器自动收集类中所有类变量(静态变量)的赋值动作和静态语句块中的语句合并生成,一个类一旦进入初始化阶段,必然会执行静态语句块。所以说,静态块一定会在类加载过程中被执行,但不会在加载阶段被执行。

九、类初始化的条件

Java 虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:

  1. 使用 new 字节码指令创建类的实例,或者使用 getstatic、putstatic 读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
  2. 通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
  3. 当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
  4. 当虚拟机启动时,用户需要指定一个主类(包含main()的类),虚拟机会首先初始化这个类。
  5. 使用 jdk1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果 REF_getStatic、REF_putStatic、RE_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

除了以上这五种情况,其他任何情况都不会触发类的初始化。比如下面这几种情况就不会触发类初始化:

  1. 通过子类调用父类的静态字段。此时父类符合情况一,而子类不符合任何情况。所以只有父类被初始化。
  2. 通过数组来引用类,不会触发类的初始化。因为 new 的是数组,而不是类。
  3. 调用类的静态常量不会触发类的初始化,因为静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类。

十、静态方法为何不能调用非静态方法和变量?

静态方法的内存分配时间与实例方法不同。

  1. 静态方法属于类,在类加载的时候就会分配内存,有了入口地址,可以通过“类名.方法名”直接调用。
  2. 非静态成员(变量和方法)属于类的对象,只有该对象初始化之后才会分配内存,然后通过类的对象去访问。
  3. 也就是说在静态方法中调用非静态成员变量,该变量可能还未初始化。因此编译器会报错。
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
JVM类加载是由类加载器及其子类实现的。类加载器Java运行时系统的重要组成部分,负责在运行时查找和加载文件中的类。在JVM中,类加载器按照一定的层次结构进行组织,每个类加载器负责加载特定位置的类。其中,启动类加载器(Bootstrap ClassLoader)是负责加载存放在<JAVA_HOME>/lib目录中的核心类库,如rt.jar、resources.jar等,同时也可以加载通过-Xbootclasspath参数指定的路径中的类库。启动类加载器是用C语言编写的,随着JVM启动而加载。当JVM需要使用某个类时,它会通过类加载器查找并加载这个类。加载过程会经历连接阶段,包括验证、准备和解析。在验证阶段,JVM会确保加载的类信息符合JVM规范。在准备阶段,JVM会为类变量分配内存并设置初始值,在方法区中分配这些内存。在解析阶段,JVM会根据符号引用替换为直接引用,以便后续的使用。通过类加载器的协同工作,JVM能够在运行时动态加载类,从而实现Java的灵活性和跨平台性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [JVM类加载原理](https://blog.csdn.net/ChineseSoftware/article/details/119212339)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [JVM类加载器](https://blog.csdn.net/rockvine/article/details/124825354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值