Java 底层原理 | Java 类加载过程详解!

类加载过程

简介

在Java中,类加载是一个重要的概念,它是Java虚拟机将类的字节码文件加载到内存并执行的过程。了解Java类加载的过程对于理解Java程序的运行机制以及解决一些常见的类加载问题非常有帮助。本文将深入探讨Java类加载的过程。

类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?主要分为以下几个过程。

加载

加载是类加载过程的第一个阶段。在加载阶段,其主要完成以下三件事:

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

Java 虚拟机并没有详细规范以上三点,所以对于一个类的二进制字节流来说,我们可以从任何地方获取,比如zip,网络等等。

什么是类的二进制字节流?

类的二进制字节流是指表示该类的字节码数据的一种数据流。在Java中,每个类都对应着一个Class对象,而Class对象包含了该类的所有信息,包括类名、方法、字段等。类的二进制字节流就是将这些信息以二进制形式进行编码后的数据。

加载类的过程主要是由类加载器来完成的,在Java中类加载器有很多种,当我们想要加载一个类的时候,具体使用什么类加载器是由双亲委派模型决定的,也就是说每个类使用的类加载器并不统一。

Java中都有哪些类加载器,如何使用这些类加载器进行加载,这些下次详细说明。

验证

我们都知道连接阶段又分为三个小部分,分别是验证、准备、解析。首先来看验证阶段,验证是连接阶段的第一步,其主要目的是确保Class文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段主要验证以下几部分:

  • 文件格式验证,验证字节流是否符合class文件格式的规范。
  • 元数据验证,对于字节码描述的信息进行语义分析,确保其符合规范。
  • 字节码验证,通过数据流和控制流分析,确定程序语义是否合法,是否符合逻辑。
  • 符号引用验证,验证类的正确性。

文件格式验证这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这个时候分配的变量都是类的变量,也就是一些静态变量。不包括实例变量,实例变量都是分配在堆中的。
  2. 理论上来说,类变量所使用的内存都应在方法区中,在jdk7之前,这种说法是没错的,但是在jdk7之后HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
  3. 这里设置的初始通常情况下都是默认值,除非静态变量加上了final关键字修饰,才会被赋值。

解析

**解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。**解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

什么是符号引用?

符号引用是一种编译时的引用,它以符号的形式来表示目标对象。在Java源代码中,使用类名、方法名、字段名等符号来引用其他类、方法或字段。符号引用是与具体内存地址无关的,它在编译阶段就已经确定了,包含了对目标对象的描述信息。也就是说符号引用其实就是一个符号表示。

其实就是说每个类都会有class文件,但是在编译期,一个类并不知道其引用的其他类的实际地址,因为还没有加载到内存,所以使用符号来代替,在加载的过程中再将符号转换为直接引用地址。

什么是直接引用?

直接引用是指向具体内存地址的引用,它可以直接定位到目标对象在内存中的位置。在类加载过程中,虚拟机会将符号引用转化为直接引用,使得程序能够准确地访问目标对象。直接引用是与具体内存地址相关的,它是在程序运行时动态生成的。其实就是类的元信息位于内存的地址串。

初始化

初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

<clinit> ()方法的作用是什么?

还记得么?在准备阶段,已经对类中static修饰的变量赋予了初始值。() 方法的作用,就是给这些变量赋予程序员实际定义的“值”。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  • 当遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。

  • 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。

  • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。

  • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  • MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。

  • 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类卸载

卸载类即该类的 Class 对象被 GC

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

参考:

类加载过程详解

Java类加载机制 - 知乎 (zhihu.com)

我的个人博客,欢迎交流

无限进步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小崔同学24

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

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

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

打赏作者

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

抵扣说明:

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

余额充值