【JVM】(一)类的加载过程

本文详细介绍Java的类加载机制,包括加载、验证、准备、解析和初始化等过程,以及类加载器的分类和双亲委派机制。

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

1.Write Once,Run Anywhere

Write Once,Run Anywhere,每一位Javaer都知道这一句由SUN公司喊出的口号。我们编写同样的一份Java代码,可以在不同的操作系统中运行而不需要做更多额外的操作,而Java这样的特性,正是因为编译后的Java代码,都是通过底层的虚拟机运行的。

下面是从官网弄出来了一张Java架构图:

Java架构图
图中可以看到,JDK是JRE的超集,里面包括了JRE拥有的所有东西,而JVM是JRE的其中一部分。

2.Java文件的编译

我们在初学Java时就已经知道了,编写好的Java文件是通过JDK中的javac指令进行编译的,编译完成后会生成一个字节码(.class)文件,再使用java指令运行字节码文件得到运行结果。
编译的过程和使用到的工具可以了解一下:

Person.java → 词法分析器 → tokens流 → 语法分析器 → 语法树/抽象语法树 → 语义分析器→ 注解抽象语法树 → 字节码生成器 → Person.class文件

上面说到了,每一份Java代码编译后的字节码文件,都是运行在虚拟机中的。
而此时字节码文件是在磁盘中的,要使它顺利的运行,就需要把这个字节码文件加载到JVM中,这个加载的过程就涉及到一个重要的机制——类加载机制。

3.类加载机制

虚拟机将字节码文件从硬盘加载到内存中,并进行一系列的验证、转换、解析、初始化的过程,将字节码文件转换成可供Java应用程序直接使用的类,这就是类加载机制。Java中类加载是在运行期完成的而不是编译期

类的生命周期从加载到内存到从内存卸载总共有七个过程。
在这里插入图片描述

其中验证、准备、解析三个步骤统称为连接,使用和卸载严格的说不属于类加载的过程,下面聊一下类加载过程中每个环节都做了什么。
在这里插入图片描述

3.1.加载

加载阶段一共需要完成三件事:

  • 通过完全限定名获取一个类的二进制字节流。
  • 将这里二进制字节流转化成方法区的运行时数据结构。
  • 实例化一个与之对应的java.lang.Class对象,放入堆中作为这个类的访问入口。

第一点中的二进制字节流不一定是从磁盘中的Class文件中获取,也可以是Jar包、war包、网络(Applet)或者运行时生成字节码(动态代理)等。

3.2.连接

3.2.1.验证

验证阶段是用来保证将要加载到虚拟机的字节流不会危害虚拟机的安全,大致会包括文件格式验证、元数据验证、字节码验证、符号引用验证

  • 文件格式验证:验证字节流所代表的文件是否符合虚拟机规范,这个阶段是字节流在加载到虚拟机之前进行的。
  • 元数据验证:验证文件内容是否符合Java的语法。
  • 字节码验证:验证方法体中的内容是否合法。
  • 符号引用验证:验证当前类中的对象能否正确引用其他其他对象,比如调用的字段、方法是否存在,字段是否是private等等,我们常见的java.lang.IllgalAccessError,java.lang.NoSuchMethodError,java.lang.NoSuchFieldError等就是在这个阶段抛出的,这个阶段是发生的虚拟机将符号引用转换为直接引用时。

3.2.2.准备

验证合法之后,就需要为当前类中的变量分配内存。
在准备阶段,会给类变量(static修饰的变量)在方法区中分配内存,并初始化默认值,各个数据类型的默认值如下。

数据类型默认值
byte(byte)0
short(short)0
int0
long0L
float0.0f
double0.0d
char‘\u0000’
booleanfalse
referencenull
假设定义一个int类型的变量:
public static int value = 10;

在准备阶段,value的值为int的默认值0,而不是10。

public static final int value = 10;

此外如果是静态常量的话,准备阶段value的值就会初始化为10。

3.2.3.解析

解析阶段是将符号引用替换为直接引用。

  • 符号引用:就是用一组符号来描述目标,可以是任何的字面量。
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

3.3.初始化

经过上面的两个阶段后,自然就进入了初始化阶段,为静态变量赋值,并执行静态代码块。

3.4.类加载小结

类加载的过程:
装载–>链接(验证、准备、解析)–>初始化

装载:查找和导入class文件

  • 通过类的完全限定名找到类并获取它的字节流。
  • 将字节流中类信息保存到方法区。
  • 在堆中生成一个对应的Class对象。

链接:

验证:保证被加载类的正确性,包括文件格式验证、元数据验证、字节码验证、符号引用验证。
准备:给类中的静态变量分配内存,并初始化默认值。
解析:把符号引用转化为直接引用。

初始化:给准备好的静态变量赋值,并执行静态代码块。

4.类加载器(ClassLoader)

在一个项目跑起来的时候,有各种各样的类需要加载到JVM中,有JRE中自带的,有三方中间件的,也有自己编写的代码。这些来自不同地方的类文件,需要不同的类加载器分门别类的加载。

在上面提到的装载阶段中,通过文件的完全限定名将类转化成二进制字节流就是通过类加载器来完成的。

4.1.类加载器的分类

  • BootStrapClassLoader:负责加载$JAVA_HOME$中 jre/lib/rt.jar中的所有class或 -Xbootclasspath选项指定的jar包。
  • ExtensionClassLoader:负责加载Java平台的扩展功能jar包,如$JAVA_HOME$中 jre/lib/*.jar或 -Djava.ext.dirs指定目录下的jar包。
  • AppClassLoader:负责加载classpath中指定的jar包及Djava.class.path 所指定目录下的类和ar包。
  • CustomerClassLoader:通过java.lang.ClassLoader的子类自定义加载class属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

4.2.双亲委派机制

双亲委派图解

在这里插入图片描述

双亲委派的定义

一个类加载器收到一个加载类的请求时,它自己不会首先去加载,而是尝试把这个加载的请求委派给父加载器加载,如果父加载器加载了类,则返回成功。只有父加载器无法完成加载任务时,才会尝试自己去加载。

为什么要有双亲委派机制

前面提到了,如果有完全限定名完全一致的类,如果没有限制的话,会出现重复加载的问题。此外,也可以任意写Java核心API中的类,如Object,通过没有限制的类加载器来篡改核心API。

为了解决这些问题,使用双亲委派机制,越上层的类加载器优先级越高,类加载完成后,如果有相同的类则不会再次加载,避免重复加载的问题。
同时,优先级最高的启动类加载器(BootStrapClassLoader)加载完核心API中的类后,就算有相同的例如Object这样的类,也不会再次加载了,避免了核心API被篡改的问题。

如何破坏双亲委派机制

写一个类继承ClassLoader,重写loadClass方法可以破坏双亲委派。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

挥之以墨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值