JVM类加载机制与类加载器

本文详细介绍了JVM的类加载机制,包括加载、连接(验证、准备、解析)和初始化等步骤,强调了类加载器的作用和双亲委派模型的重要性。解析阶段的后期绑定实现多态,而双亲委派模型则确保了类的唯一性,防止类的重复加载。此外,文章还探讨了如何破坏双亲委派模型并分析了类加载器的层级结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【JVM类加载机制】学习视频来自B站寒食君

类加载机制

类加载流程的目的:把一份被javac编译过的class文本文件,通过加载生成某种形式的Class数据结构进入内存,程序可以调用这个数据结构来构造出Object。

这个过程是在运行时进行的,所以这也是Java动态拓展性的根基。

来看《深入理解JVM》中的一张图:

  1. 这张图其实展示了一个类的生命周期,在最开始加上javac编译阶段显得更完整。而 【类加载】只包括加载、连接、初始化这3个过程

  2. 区分“类加载”与“加载”:“加载”只是“类加载”的第一个环节

  3. “解析”部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”,其它环节的顺序不可改变。

加载Loading

Java是一种具有动态性的解释型语言,类Class只有被加载到JVM后才能运行。

加载是类型的加载,就是读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在中生成一个便于用户调用的Class类型的对象的过程。

  • Class文件泛指各种来源的二进制流,比如说来自于网络、数据库、甚至是即时生成的Class文件。动态代理技术就是使用了即时计算出来的CLass,然后实例化代理对象。
  • 最终在堆中生成Class对象,注意不是目标类对象,该对象封装了类在方法区中的数据结构,并且想用户提供了访问方法区数据结构的接口,即Java反射的接口

通过什么来进行加载?类加载器

连接Linking

连接阶段负责把类的二进制数据合并到JRE中,即把Java类的二进制代码合并到JVM的运行状态之中。

验证Verification

验证主要是为了确保类符合JVM规范,没有安全方面的问题。

验证动作有很多个步骤,分散在不同的阶段内:

首先是对文件格式的验证:发生在加载阶段,如果通过,那么才能顺利加载;

顺利加载之后,此时方法区内虽然已经存在了该class的静态结构,堆中也存在了该class类型的对象,但是这并不代表JVM已经完全认可了这个类。

程序要想使用这个类就必须进行连接,连接的第一步就是对这个类进行进一步验证,主要是对class静态结构进行语法和语义上的分析,保证其不会产生危害JVM的行为。

如果这一步验证也通过,那么JVM会姑且认为该class是安全的,但是后面在解析阶段还有一道符号引用的验证。

准备Preparation

在通过元数据字节码验证之后,JVM会姑且认为该class是安全的,这时进入准备阶段:为该类型中的静态变量赋类型0值。

解析Resolution

该阶段主要做的事情就是将符号引用替换为直接引用

  • 符号引用:

    假如A类中引用了B类,在编译阶段,A不知道B是否被编译了,而且此时B也一定没有被加载,所以A不知道B的实际地址,那么在A的class文件中,将使用一个字符串S来代替B的地址。S就是符号引用。

  • 直接引用:

    在运行时,如果A发生了类加载,到了解析阶段会发现B还未被加载,那么就会触发B的类加载,将B加载到虚拟机中,此时A中B的符号引用就会被替换为B的实际地址,这被称作为直接引用,此时A就能真正地调用到B。

    • 补充

      Java通过后期绑定的方式来实现多态,那么后期绑定是如何实现的呢?其实就是这里的动态解析。

      • 静态解析:

        如果A调用的B是一个具体的实现类,那么就称为静态解析。

      • 动态解析:

        假如上层Java代码使用了多态,这里的B可能是一个抽象类或者接口,它有两个具体的实现类C和D。此时B的具体实现并不明确,所以也就不知道使用哪个具体类的直接引用来进行替换。既然不知道,那就只能等到运行过程中发生了调用,此时JVM调用栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用来替换符号引用了。

        这也是为什么“解析”有时候会发生在“初始化”之后,这就是动态解析,用它来实现上层的后期绑定和多态,底层对应了invokevirtual指令来实现。

解析步骤完成,意味着整个连接部分的完成,这也就是说外部加载的Java类已经成功地引用到了程序中。

初始化Initialization

此时会判断代码中是否存在主动的资源初始化操作,如果有则执行。

这里的主动初始化操作不是构造函数,而是class层面的,比如成员变量、静态变量的赋值动作以及静态代码块的逻辑。

只有显示调用new指令,才会调用构造函数进行对象实例化,这是对象层面的。

类加载总结

image-20211108174504635

类加载器

加载过程是由类加载器完成的,具体来说,是由 ClassLoader 及其子类来实现的。

类加载器根据类的全限定名将class文件加载到JVM内存,转为Class对象。

什么时候需要加载类

  • 隐式加载:new(),会隐式地调用类加载器去加载类,获取对应Class后会自动进行实例化
  • 显示加载:直接调用loadClass()和Class.forName()

类加载器分类

image-20211109165236709

其它几种ClassLoader都只能从本地文件中获取字节码来进行加载,而 User ClassLoader 能够让用户获取任何来源的字节码,这也就印证了 “在类加载机制中,允许用户从各个渠道获取class文件的二进制流来进行加载” 的这个结论。

双亲委派机制

ClassLoader命名空间

类加载器的命名空间 是由类加载器本身以及所有父加载器所加载出来的binary name(full class name)组成

JVM规范:每个ClassLoader都有属于自己的命名空间,命名空间相互之间无法感知。

这也就是说,用不同的ClassLoader即使去加载同一个限定名的类,JVM也会认为它们是完全不同的类。

关于命名空间的一些规则:

  1. 在同一个命名空间里,不允许出现二个完全一样的binary name。
  2. 在不同的命名空间中,可以出现二个相同的binary name。此时二者对应的Class对象是相互不能感知到的,也就是说Class对象的类型是不一样的。
  3. 子加载器的命名空间中的binary name对应的类中可以访问父加载器命名空间中binary name对应的类,反之不行。

出现问题:一个类如果被不同的加载器加载,或者说不同的加载器加载了【限定名一样、但内容不一样】的类,这样会给程序带来混乱。

所以目前的需求是:默认情况下,一个限定名的类只会被一个ClassLoader加载并解析使用,这样在程序中,它就是唯一的,不会产生歧义。

如何实现这种需求?引入双亲委派模型

类加载器的层级结构
image-20211110112858318

区别于JDK中的类加载继承关系:ClassLoader <– SecureClassLoader <– URLClassLoader <– ExtClassLoader/AppClassLoader

image-20211130154751763

所以双亲委派机制就是:需要加载某个类时,会自底向上地把这个请求委派给父加载器来完成,当父加载器无法加载时,子加载器会自顶向下的尝试加载。

如何判断自己无法加载?根据类的限定名,ClassLoader没有在自己负责的加载路径中找到该类则为无法加载(即ClassNotFoundException)。

image-20211130153130194

双亲委派的实现体现在 ClassLoader.loadClass() 里,后面会在源码中具体分析、

采用双亲委派就能避免重复加载,比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的 Bootstrap ClassLoader 进行加载,这样就保证了使用不同的ClassLoader最终得到的都是同样一个Object对象。

相关源码
  • ClassLoader介绍

    image-20211115162116194

  • loadClass():根据限定名去加载指定的类

    image-20211115164311676

    image-20211115172237882

  • ClassLoader. getSystemClassLoader():获取系统类加载器

    Launcher的创建通过 Launcher.getLauncher() 来实现:

    image-20211115205834576

    可以看到在这个方法中初始化了ExtClassLoaderAppClassLoader

    • ExtClassLoader的创建:

      image-20211115210144329

    • AppClassLoader的创建:

      image-20211115210509929

破坏双亲委派

继承ClassLoader类,重写loadCLass和findClass方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值