Featured image of post Java字节码与类加载器

Java字节码与类加载器

Java语言中字节码的动态加载的前置知识

本章深入了解Java字节码以及加载Java字节码的类加载器相关姿势!

Java字节码

Java 在刚刚诞生之时曾经提出过一个非常著名的口号: “一次编写,到处运行(write once,run anywhere)”,这句话充分表达了软件开发人员对冲破平台界限的渴求。“与平台无关”的理想最终实现在操作系统的运用层上: 虚拟机提供商开发了许多可以运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写到处运行”。

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式—字节码(ByteCode),因此,可以看出字节码对 Java 生态的重要性。之所以被称为字节码,是因为字节码是由十六进制组成的,而 JVM(Java Virtual Machine)以两个十六进制为一组,即以字节为单位进行读取。在 Java 中使用 javac 命令把源代码编译成字节码文件,一个 .java 源文件从编译成 .class 字节码文件的示例

image-20241106165423165

由此可见,Java字节码是“与平台无关”的关键点。且要说明的是,只要能生成符合 JVM 字节码规范的文件,都可以认为是Java字节码文件,来源不一定是.java文件或者javac编译生成,python也可以,Golang也行。

image-20241106165923055

甚至,只要能够在JVM中恢复为一个类的字节序列,也可以称之为Java字节码,例如BCEL

后续非特殊情况简称字节码

Java类加载器

什么是Java类加载器

定义完字节码,还需要了解一个东西,叫做类加载器 - ClassLoader

Java的ClassLoader是根据字节码加载类的最基础的方法,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象。

ClassLoader会告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime

在Java中,每个类都由类加载器加载,并在运行时被创建为一个Class对象。类加载器负责从文件系统、网络或其他来源中加载类的字节码,并将其转换为可执行的Java对象。类加载器还负责解析类的依赖关系,即加载所需的其他类。

Java类加载器有哪些

Java虚拟机定义了三个主要的类加载器:

  1. 启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如java.lang.Object等。启动类加载器是虚拟机实现的一部分,它通常是由C++等底层语言实现的,而不是Java实现。
  2. 扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库的类加载器。扩展类库包括javaxjava.util等包,它们位于jre/lib/ext目录下。
  3. 应用程序类加载器(Application Class Loader):也称为系统类加载器,它负责加载应用程序的类。它会搜索应用程序的类路径(包括用户定义的类路径和系统类路径),并加载类文件。

除了这三个主要的类加载器,Java还支持自定义类加载器,开发人员可以根据需要实现自己的类加载器

Java类加载器做什么

类加载器的工作可以简化为三个步骤:

  1. 加载(Loading):根据类的全限定名(包括包路径和类名),定位并读取类文件的字节码。

  2. 链接(Linking):将类的字节码转换为可以在虚拟机中运行的格式。

    链接过程包括三个阶段:

​ 1)验证(Verification):验证字节码的正确性和安全性,确保它符合Java虚拟机的规范。

​ 2)准备(Preparation):为类的静态变量分配内存,并设置默认的初始值。

​ 3)解析(Resolution):将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)。

  1. 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。

Java类加载器怎么做

Java类加载器实行的是双亲委派机制

image-20241106171432448

最上方的ClassLoad最父类的Loader,目前可以这样理解

在图中可以明显看出来,在符合Java的推荐标准的情况下,类加载器的继承顺序是

User Define ClassLoader -> Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader

但是加载顺序却不一定,ClassLoader收到加载请求时,会做大致两件事来确定加载顺序

例如:需要加载类Boy时:

  1. ClassLoaderA查看自身是否已经加载过Boy,如果有直接返回,如果没有,会将请求给到父类ClassLoaderB
  2. 如果下一个ClassLoaderB并没有加载或者返回Boy类并结束了,ClassLoaderA会尝试加载Boy

ClassLoaderB会做和ClassLoaderA相同的事

如果结合图来看,再给个完全的栗子:

假设类Boy没有被加载过,且Boy类字节码被加密,最终只能被User Define ClassLoader解密加载,那么执行流会如下

  1. User Define ClassLoader 查看自身是否已经加载过Boy,由于Boy没有被加载过,所以失败,请求给到父类Application ClassLoader
  2. Application ClassLoader查看自身是否已经加载过Boy,由于Boy没有被加载过,所以失败,请求给到父类Extension ClassLoader
  3. Extension ClassLoader查看自身是否已经加载过Boy,由于Boy没有被加载过,所以失败,请求给到父类Bootstrap ClassLoader
  4. Bootstrap ClassLoader查看自身是否已经加载过Boy,由于Boy没有被加载过,所以失败,没有父类,尝试加载,加载失败,因为不是Java虚拟机的核心类,返回
  5. 执行流回到Extension ClassLoader,父类没有加载,尝试加载,加载失败,因为Boy不是Java扩展类,返回
  6. 执行流回到Application ClassLoader,父类没有加载,尝试加载,加载失败,Boy类无法识别(因为是用户加密了),返回
  7. 执行流回到User Define ClassLoader,父类没有加载,尝试解密并加载,加载成功

双亲委派的优劣

优点

  1. 因为双亲委派是向上委托加载的,所以它可以确保类只被加载一次。

  2. 共享功能:一些framework层级的类一旦被顶层加载器加载,缓存在内存。在其他任何地方用到时,都遵守双亲加载机制,派发到顶层加载器因已经加载,所以都不需要重新加载,避免重复加载

  3. 避免核心类被串改:Java的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如java.lang.Integer,类加载器通过向上委托,两个Integer,那么最终被加载的应该是jdk的Integer类,而并非我们自定义的,这样就避免了我们恶意篡改核心包的风险

  4. 隔离功能:保证核心类库的纯净和安全,防止恶意加载。

优点1,2是由于只会加载一次,优点3,4是因为加载优先级是从父类到子类

缺点

双亲委派模型的典型问题是加载 SPI 实现类的场景,比如 JNDI(Java Naming and Directory Interface,Java 命名与目录接口)服务。

它的代码由启动类加载器去加载(在 JDK 1.3 时放进 rt.jar),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的 classpath 下的 JNDI 接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,这就双亲委派模型的问题,JDBC 也是同样的问题。

参考链接

https://zhuanlan.zhihu.com/p/382020126

https://blog.csdn.net/qq_21484461/article/details/131421264

https://zhuanlan.zhihu.com/p/603047338

https://frank909.blog.csdn.net/article/details/54973413

https://www.cnblogs.com/hackerxian/p/10871667.html

Dan❤Anan
Built with Hugo
主题 StackJimmy 设计