本章深入了解Java字节码以及加载Java字节码的类加载器相关姿势!
Java字节码
Java 在刚刚诞生之时曾经提出过一个非常著名的口号: “一次编写,到处运行(write once,run anywhere)”,这句话充分表达了软件开发人员对冲破平台界限的渴求。“与平台无关”的理想最终实现在操作系统的运用层上: 虚拟机提供商开发了许多可以运行在不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写到处运行”。
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式—字节码(ByteCode),因此,可以看出字节码对 Java 生态的重要性。之所以被称为字节码,是因为字节码是由十六进制组成的,而 JVM(Java Virtual Machine)以两个十六进制为一组,即以字节为单位进行读取。在 Java 中使用 javac 命令把源代码编译成字节码文件,一个 .java 源文件从编译成 .class 字节码文件的示例
由此可见,Java字节码是“与平台无关”的关键点。且要说明的是,只要能生成符合 JVM 字节码规范的文件,都可以认为是Java字节码文件,来源不一定是.java
文件或者javac
编译生成,python也可以,Golang也行。
甚至,只要能够在JVM中恢复为一个类的字节序列,也可以称之为Java字节码,例如BCEL
码
后续非特殊情况简称字节码
Java类加载器
什么是Java类加载器
定义完字节码,还需要了解一个东西,叫做类加载器 - ClassLoader
Java的ClassLoader
是根据字节码加载类的最基础的方法,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象。
ClassLoader
会告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader
就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime
在Java中,每个类都由类加载器加载,并在运行时被创建为一个Class对象。类加载器负责从文件系统、网络或其他来源中加载类的字节码,并将其转换为可执行的Java对象。类加载器还负责解析类的依赖关系,即加载所需的其他类。
Java类加载器有哪些
Java虚拟机定义了三个主要的类加载器:
- 启动类加载器(Bootstrap Class Loader):也称为根类加载器,它负责加载Java虚拟机的核心类库,如
java.lang.Object
等。启动类加载器是虚拟机实现的一部分,它通常是由C++
等底层语言实现的,而不是Java实现。 - 扩展类加载器(Extension Class Loader):它是用来加载Java扩展类库的类加载器。扩展类库包括
javax
和java.util
等包,它们位于jre/lib/ext
目录下。 - 应用程序类加载器(Application Class Loader):也称为系统类加载器,它负责加载应用程序的类。它会搜索应用程序的类路径(包括用户定义的类路径和系统类路径),并加载类文件。
除了这三个主要的类加载器,Java还支持自定义类加载器,开发人员可以根据需要实现自己的类加载器。
Java类加载器做什么
类加载器的工作可以简化为三个步骤:
-
加载(Loading):根据类的全限定名(包括包路径和类名),定位并读取类文件的字节码。
-
链接(Linking):将类的字节码转换为可以在虚拟机中运行的格式。
链接过程包括三个阶段:
1)验证(Verification):验证字节码的正确性和安全性,确保它符合Java虚拟机的规范。
2)准备(Preparation):为类的静态变量分配内存,并设置默认的初始值。
3)解析(Resolution):将类的符号引用(比如方法和字段的引用)解析为直接引用(内存地址)。
- 初始化(Initialization):执行类的初始化代码,包括静态变量的赋值和静态块的执行。
Java类加载器怎么做
Java类加载器实行的是双亲委派机制
最上方的
ClassLoad
是最父类的Loader,目前可以这样理解
在图中可以明显看出来,在符合Java的推荐标准的情况下,类加载器的继承顺序是
User Define ClassLoader -> Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader
但是加载顺序却不一定,ClassLoader
收到加载请求时,会做大致两件事来确定加载顺序
例如:需要加载类Boy
时:
ClassLoaderA
查看自身是否已经加载过Boy
,如果有直接返回,如果没有,会将请求给到父类ClassLoaderB
- 如果下一个
ClassLoaderB
并没有加载或者返回Boy
类并结束了,ClassLoaderA
会尝试加载Boy
ClassLoaderB
会做和ClassLoaderA
相同的事
如果结合图来看,再给个完全的栗子:
假设类Boy
没有被加载过,且Boy
类字节码被加密,最终只能被User Define ClassLoader
解密加载,那么执行流会如下
User Define ClassLoader
查看自身是否已经加载过Boy
,由于Boy
没有被加载过,所以失败,请求给到父类Application ClassLoader
Application ClassLoader
查看自身是否已经加载过Boy
,由于Boy
没有被加载过,所以失败,请求给到父类Extension ClassLoader
Extension ClassLoader
查看自身是否已经加载过Boy
,由于Boy
没有被加载过,所以失败,请求给到父类Bootstrap ClassLoader
Bootstrap ClassLoader
查看自身是否已经加载过Boy
,由于Boy
没有被加载过,所以失败,没有父类,尝试加载,加载失败,因为不是Java虚拟机的核心类,返回- 执行流回到
Extension ClassLoader
,父类没有加载,尝试加载,加载失败,因为Boy
类不是Java扩展类,返回- 执行流回到
Application ClassLoader
,父类没有加载,尝试加载,加载失败,Boy
类无法识别(因为是用户加密了),返回- 执行流回到
User Define ClassLoader
,父类没有加载,尝试解密并加载,加载成功
双亲委派的优劣
优点
-
因为双亲委派是向上委托加载的,所以它可以确保类只被加载一次。
-
共享功能:一些framework层级的类一旦被顶层加载器加载,缓存在内存。在其他任何地方用到时,都遵守双亲加载机制,派发到顶层加载器因已经加载,所以都不需要重新加载,避免重复加载
-
避免核心类被串改:Java的核心API都是通过引导类加载器进行加载的,如果别人通过定义同样路径的类比如
java.lang.Integer
,类加载器通过向上委托,两个Integer,那么最终被加载的应该是jdk的Integer类,而并非我们自定义的,这样就避免了我们恶意篡改核心包的风险 -
隔离功能:保证核心类库的纯净和安全,防止恶意加载。
优点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