源码剖析JVM类加载机制

article/2025/9/22 4:04:38

1 前言

我们平常开发中,都会部署开发的项目或者本地运行main函数之类的来启动程序,那么我们项目中的类是如何被加载到JVM的,加载的机制和实现是什么样的,本文给大家简单介绍下。

2 类加载运行全过程

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM,通过Java命令执行代码的大体流程如下

从流程图中可以看到类加载的过程主要是通过类加载器来实现的,那么什么是类加载器呢?

3 类加载器

3.1 什么是类加载器

类加载器负责在运行时将Java类动态加载到JVM(Java 虚拟机)。此外,它们是JRE(Java运行时环境)的一部分。所以由于类加载器,JVM不需要知道底层文件或文件系统来运行Java程序。

Java类加载器的作用是寻找类文件,然后加载Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

3.2 类加载器种类

3.2.1 启动类加载器(Bootstrap ClassLoader)

它主要负责加载JDK内部类,一般是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心库。此外,Bootstrap类加载器充当所有其他ClassLoader实例的父级。

Bootstrap ClassLoader是JVM核心的一部分,是用native引用编写的。它本身是虚拟机的一部分,所以它并不是一个JAVA类,我们无法直接使用该类加载器。

3.2.2 扩展类加载器(Extension ClassLoader)

负责加载支撑JVM运行的位于$JAVA_HOME/jre/lib目录下的ext扩展目录中的JAR 类包。我们可以直接使用这个类加载器。

3.2.3 应用程序类加载器(Application ClassLoader)

负责加载用户类路径(classpath)上的指定类库,主要就是加载你自己写的那些类。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

3.2.4 自定义类加载器

通过继承ClassLoader类实现,主要重写findClass方法。

下面通过代码来看下了解不同的类是使用的哪种类加载器来加载的:

System.out.println("Classloader of this class : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader of Logging : " + Logging.class.getClassLoader());
System.out.println("Classloader of String : " + String.class.getClassLoader());System.out.println("-----------");System.out.println("Classloader : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent().getParent());

下面是运行结果:

通过运行结果,我们会发现我自定义的当前运行类的类加载器是AppClassLoader,Logging这个类的类加载器是ExtClassLoader,而且类加载器之间是有父子关系关联的。但String的类加载器却为null,ExtClassLoader的父加载器也为null,是意味着String类不是通过类加载器加载的?那如果可以加载它又是怎么被加载的呢?为什么我们获取不到BootstrapClassLoader呢?后面我们会进行解读。

3.3 类加载器的机制

上面介绍了都有哪些类加载器,那么一个类是如何被类加载器加载的,这些类加载器之间又有什么关联关系呢,接下来就介绍下类加载器的机制。

双亲委派机制

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它搜索的范围没有找到所需的类),子加载器才会尝试自己取加载。

双亲委派机制说简单点就是:对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。

3.4 类加载机制的源码实现

参见最开始类运行加载全过程图可知,流程中会创建JVM启动器实例:sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。

在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。JVM默认使用Launcher的getClassLoader(),这个方法返回的类加载器(AppClassLoader)的实例加载我们的应用程序。

public Launcher() {Launcher.ExtClassLoader var1;try {var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);。。。。。。 //省略一些不需关注代码}

从上面Launcher构造方法的源码中,我们看到了AppClassLoader和ExtClassLoader这两种类加载器的定义,并且在创建AppClassLoader时将ExtClassLoader设置为父类,也符合上面说的类加载器之间的关联。
但是BootstrapClassLoader仍然没有出现,并且也没有给ExtClassLoader设置父加载器,那它又是和ExtClassLoader如何关联的?下面的双亲委派机制实现的源码会为我们解答。

我们来看下AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

  • 首先检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  • 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false)😉,或者是调用bootstrap类加载器来加载。
  • 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法,在文件系统本身中查找类,来完成类加载。
  • 如果最后一个子类加载器也无法加载该类,则会抛出 java.lang.NoClassDefFoundError。
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 检查当前类加载器是否已经加载了该类Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {//如果当前加载器的父加载器不为空,则委托父加载器加载c = parent.loadClass(name, false);} else {//如果当前加载器父加载器为空,则委托启动类加载器加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();
// 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {// 解析、链接指定的Java类resolveClass(c);}return c;}}

上面就是双亲委派机制实现原理的源码。从中我们可以看到有一个逻辑点会调用findBootstrapClassOrNull()这个方法,那么至此,我们有个疑团也就解开了:ExtClassLoader和BootstrapClassLoader(启动类加载器)就是在这里关联上的。因为ExtClassLoader在定义的时候,没有设置父类加载器(parent),所以执行到了这个逻辑,委托了BootstrapClassLoader进行加载。上面说的类加载器之间层级关系的实现和关联,也是在块逻辑里实现的。从源码这里的逻辑,也符合前面我们介绍BootstrapClassLoader所说的:Bootstrap类加载器充当所有其他ClassLoader实例的父级。

这个疑团是解开了,但是之前还有一个疑团仍然没有说明,在开始我们获取不同的类的加载器的时候,String的类加载器是null。在类加载的源码里面,我们看到了BootstrapClassLoader加载器的获取,为什么获取不到是null呢。这个我们要看下findBootstrapClassOrNull()这个方法的实现,看看BootstrapClassLoader到底是怎么定义的。

    /*** Returns a class loaded by the bootstrap class loader;* or return null if not found.*/private Class<?> findBootstrapClassOrNull(String name){if (!checkName(name)) return null;return findBootstrapClass(name);}// return null if not foundprivate native Class<?> findBootstrapClass(String name);

通过源码可以看到最终调用了findBootstrapClass这个方法来返回,但是这个方法的修饰符是native,那么就容易理解我们为什么获取不到这个BootstrapClassLoader了。

3.5 为什么设计双亲委派

沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改 ,防止了恶意代码的注入,安全性的提高和保障。

避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。如果每个加载器都自己加载,那么可能会出现多个同名类,导致混乱。

3.6 双亲委派机制的打破

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。

若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码时,就需要破坏双亲委派模型了。下面就介绍几种破坏了双亲委派机制的场景。

3.6.1 JNDI破坏双亲委派模型

JNDI是Java标准服务,它的代码由启动类加载器去加载,但JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。为了解决这个问题,引入了一个线程上下文类加载器(ContextClassLoader)。可通过Thread.setContextClassLoader()设置。利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。

3.6.2 Spring破坏双亲委派模型

Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。

那么Spring是如何访问WEB-INF下的用户程序呢?——使用线程上下文类加载器

Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。利用这个来加载用户程序,即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

3.6.3 Tomcat破坏双亲委派机制

  • 不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 部署在同一个web容器中相同的类库相同的版本可以共享
  • web容器也有自己依赖的类库,不能与应用程序的类库混淆。
  • web容器要支持jsp的修改,需要支持 jsp 修改后不用重启。

3.7 自定义类加载器

在介绍类加载器种类的时候,一共有四种,前面所说的都是前三种类加载器的一些机制,那如果我们想自己自定义个类加载器要如何实现呢?

自定义类加载器,只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)。下面是个自定义类加载器的例子:

public class ClassLoaderDrill {static class MyClassLoader extends ClassLoader {private String classPath;public MyClassLoader(String classPath) {this.classPath = classPath;}protected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] data = loadByte(name);//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。return defineClass(name, data, 0, data.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}private byte[] loadByte(String name) throws Exception {name = name.replaceAll("\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + name+ ".class");int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}}public static void main(String args[]) throws Exception {//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoaderMyClassLoader classLoader = new MyClassLoader("D:/test");//创建 /com/xxx/xxx 的几级目录,跟你要加载类的目录一致Class clazz = classLoader.loadClass("com.test.jvm.User");Object obj = clazz.newInstance();Method method = clazz.getDeclaredMethod("sout", null);method.invoke(obj, null);System.out.println(clazz.getClassLoader().getClass().getName());}
}

注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。这个在ClassLoader的构造方法实现里可以看到。

4 类加载的过程

上述我们介绍了类加载器及相关机制和实现源码,但是类加载器获取所需要的类这个动作,只是类加载全过程中的一部分。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析三个部分统称为链接,这7个阶段的发生顺序如图:

下面也给大家简单介绍下每个阶段所执行的具体动作

4.1 加载

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存(JVM)中,并生成一个代表该类的 java.lang.Class 对象。该阶段JVM完成3件事:

  • 通过类的全限定名获取该类的二进制字节流(需要特别说明的是我们上述所说的类加载器相关动作,就是类加载过程中的这个阶段)
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个该类的java.lang.Class对象,作为该类在方法区的各种数据的访问入口

4.2 验证

主要确保加载进来的字节流符合JVM规范。JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行,该阶段是保证 JVM 安全的重要屏障。

验证阶段会完成以下4个阶段的检验动作:

  • 文件格式验证:基于字节流验证
  • 元数据验证(是否符合Java语言规范):基于方法区的存储结构验证
  • 字节码验证(确定程序语义合法,符合逻辑):基于方法区的存储结构验证
  • 符号引用验证(确保下一步的解析能正常执行):基于方法区的存储结构验证

4.3 准备

该步主要为静态变量在方法区分配内存,并设置默认初始值。JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化。

4.4 解析

虚拟机将常量池内的符号引用替换为直接引用的过程,即将常量池中的符号引用转化为直接引用。

4.5 初始化

在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

4.6 使用

使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段。

4.7 卸载

关于类的卸载,在类使用完之后,如果满足下面的情况,jvm就会在方法区垃圾回收的时候对类进行卸载。类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

5 总结

最后介绍了下类加载的整个过程及执行的具体动作,其实每个节点去深挖也是有很多内容的,感兴趣的小伙伴可以再去深入了解。


http://chatgpt.dhexx.cn/article/8wZIL4Dq.shtml

相关文章

JVM类加载机制简单介绍

本文为《深入理解Java虚拟机JVM高级特效与最佳实践&#xff08;第三版&#xff09;》一书的摘要总结 类加载时机 Java虚拟机把描述类的数据从Class文件加载到内存&#xff0c;并对数据进行校验、转换解析和初始化&#xff0c;最终形成可以被虚拟机直接使用的Java类型&#xff0…

JVM的类加载机制

一、类加载机制 类的加载指的是将类的.class文件中的二进制数据读入到内存中&#xff0c;将其放在运行时数据区的方法区内****&#xff0c;然后在堆区创建一个java.lang.Class对象&#xff0c;用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象&…

深入JVM类加载机制

从ClassLoad开始说起 ClassLoader顾名思义就是我们所常见的类加载器,其作用就是将编译后的class文件加载内存当中.在应用启动时,JVM通过ClassLoader加载相关的类到JVM当中.在具体了解ClassLoader之前我们先来了解下JVM的类加载机制. 1. 类加载机制 虚拟机将class文件加载到内…

java面试题-JVM类加载机制

类加载的生命周期&#xff1f; 1. 加载阶段&#xff08;Loading&#xff09; 在Java程序中&#xff0c;当需要使用某个类时&#xff0c;JVM会使用类加载器来查找并加载该类文件。类加载器会首先从文件系统或网络中查找相应的 .class 文件&#xff0c;读取类的二进制数据&#x…

JVM面试 类加载机制

JVM的类加载机制 一、JVM的运行机制 JVM 是用于运行Java字节码的虚拟机&#xff0c;包括一套字节码指令集&#xff0c;一组程序寄存器&#xff0c;一个虚拟机栈&#xff0c;一个虚拟机堆&#xff0c;一个方法区和一个垃圾回收器。JVM运行在操作系统之上&#xff0c;不与硬件设…

JVM--详解类加载机制

JVM--详解类加载机制 转载&#xff1a;https://blog.csdn.net/championhengyi/article/details/78680700 Java虚拟机的体系结构 前面我们探讨了Class文件的结构&#xff0c;如果你还没有学习&#xff0c;将不利于这部分知识的吸收与掌握&#xff0c;所以请移步&#xff1a;JV…

JVM类加载机制

文章目录 概述1. 类加载器2.类加载过程3.双亲委派机制总结 概述 Class文件由类装载器装载后&#xff0c;在JVM中将形成一份描述Class结构的元信息对象&#xff0c;通过该元信息对象可以获知Class的结构信息&#xff1a;如构造函数&#xff0c;属性和方法等&#xff0c;Java允许…

JVM:类加载机制

类加载器 什么是类加载器 ​ 类加载器的作用负责从磁盘中或者网络中加载class文件&#xff0c;classloader只负责加载class文件&#xff0c;类加载器通过一个类的全限定名来获取描述此类的二进制字节流。类加载器虽然用于实现加载动作&#xff0c;但它在Java程序中起到的作用…

JVM类的加载机制

1 类的加载机制 类的加载指的是将类的.class文件中的二进制数据读入到内存中&#xff0c;将其放在运行时数据区的方法区内&#xff0c;然后在堆区创建一个java.lang.Class对象&#xff0c;用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象&#xf…

jvm之java类加载机制和类加载器(ClassLoader)的详解

手把手写代码&#xff1a;三小时急速入门springboot—企业级微博项目实战--->csdn学院 当程序主动使用某个类时&#xff0c;如果该类还未被加载到内存中&#xff0c;则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外&#xff0c;JVM将会连续完成3个步…

二叉树的遍历

遍历一棵二叉树有很多种方法。假如用D、L、R分别代表二叉树的根结点、左子树、右子树&#xff0c;那么要遍历这棵二叉树&#xff0c;方法就有6种&#xff1a;DLR、DRL、LDR、LRD、RDL、RLD。一般在遍历时遵循先左后右的原则&#xff0c;因此常用的遍历方法有三种&#xff1a;DL…

二叉树的遍历详解

概述 二叉树的遍历是一个很常见的问题。二叉树的遍历方式主要有&#xff1a;先序遍历、中序遍历、后序遍历、层次遍历。先序、中序、后序其实指的是访问父节点的次序。在遍历过程中&#xff0c;若访问顺序是父节点-左孩子节点-右孩子节点&#xff0c;就是先序遍历&#xff0c;…

“二叉树遍历“详解 以及 二叉树的实现

目录 一.二叉树的遍历 1.二叉树的遍历的解释&#xff1a; 2.二叉树的遍历有三种递归结构 (1) 实现先序遍历&#xff1a; (2) 实现中序遍历&#xff1a; (3) 实现后序遍历&#xff1a; (4) 二叉树的层序遍历 层序遍历代码&#xff1a; 二.二叉树的递归实现相关函数讲解…

二叉树遍历详解

二叉树的遍历方式是最基本&#xff0c;也是最重要的一类题目&#xff0c;我们将从「前序」、「中序」、「后序」、「层序」四种遍历方式出发&#xff0c;总结他们的递归和迭代解法。 一、二叉树定义 二叉树&#xff08;Binary tree&#xff09;是树形结构的一个重要类型…

讲透学烂二叉树(三):二叉树的遍历图解算法步骤及JS代码

二叉树的遍历是指不重复地访问二叉树中所有结点&#xff0c;主要指非空二叉树&#xff0c;对于空二叉树则结束返回。 二叉树的遍历分为 深度优先遍历 先序遍历&#xff1a;根节点->左子树->右子树&#xff08;根左右&#xff09;&#xff0c;有的叫&#xff1a;前序遍历…

二叉树的中序遍历算法(Java三种实现方法)

文章目录 题目一、二叉树的节点定义二、三种遍历方法1.递归算法思想 2.迭代算法思想 3.Morris 中序遍历算法思想 总结 题目 给定一个二叉树的根节点 root &#xff0c;返回它的 中序 遍历 一、二叉树的节点定义 public class TreeNode {int val;TreeNode left;TreeNode righ…

二叉树遍历的几种常见方法

二叉树的遍历方法 一.二叉树分类&#xff1a; 完全二叉树满二叉树扩充二叉树平衡二叉树 二.二叉树的四种遍历方式&#xff1a; 前序遍历&#xff08;先根&#xff0c;再左&#xff0c;最后右&#xff09;中序遍历&#xff08;先左&#xff0c;再根&#xff0c;最后右&#…

二叉树的三种遍历方式

目录 1.二叉树的结构&#xff1a; 2.二叉树的前序遍历&#xff1a; 3.二叉树的中序遍历&#xff1a; 4.二叉树的后序遍历&#xff1a; 5.二叉树前、中、后序的代码实现&#xff1a; 前序遍历函数&#xff1a; 中序遍历函数&#xff1a; 后序遍历&#xff1a; 完整代码&am…

图解二叉树的三种遍历

1、二叉树的遍历 前序遍历&#xff1a;根结点 —> 左子树 —> 右子树 中序遍历&#xff1a;左子树—> 根结点 —> 右子树 后序遍历&#xff1a;左子树 —> 右子树 —> 根结点 层次遍历&#xff1a;仅仅需按层次遍历就可以 前序遍历&#xff1a;1 2 4 5 7…

二叉树的遍历【 详细讲解 】

二叉树的遍历 一共有4种遍历 先看图&#xff0c;对于这个图进行4种遍历的讲解 1、 先序遍历 定义&#xff1a;若二叉树为空&#xff0c;则空操作&#xff1b;否则 &#xff08;1&#xff09;访问根节点&#xff08;2&#xff09;先序遍历左子树&#xff08;3&#…