JVM——类的加载机制

JVM——类的加载机制

1.装载(load)

  1. 找到类文件位置

    不同的 类加载器ClassLoader 装载不同目录下的类,jvm分为以下4种类加载器(如下图)

    1.BootStrap ClassLoader

    2.Extension ClassLoader

    3.App ClassLoader

    4.Custom ClassLoader

BootStrapClassLoader:装载$JAVA_HOME中/jre/lib/rt.jar里所有的class或xbootclassoath选项指定的jar包
ExtensionClassLoader:加载Java平台中扩展的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
AppClassLoader:加载classpath中指定的jar包,以及Djava.class.path所指定目录下的类或jar包
CustomClassLoader:通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自己需要自定义的ClassLoader

JVM装载类时并不是相互隔离地各自干各自的活,而是采用双亲委派机制装载所需类(我先把这个概念抛出来,后面详说),什么是双亲委派机制呢?简单来说就是在装载一个类的时候,当前类加载器并不会立即装载,而是向父装载器反馈,如果父装载器已经装载了,那么当前类加载器就不会再装载了;而父装载器也不会立即装载,而是向爷装载器反馈,如果爷装载器装载了,那么父装载器和当前类装载器都不需要装载了,以此类推,直到反馈到BootStrapClassLoader为止;如果说父装载器反馈自己没有找到,那么就由自己装载了。那么这样做有一个好处就是:每个类只会被装载一次,而且是保证被对应的类加载器正确装载。

举一个例子描述上述的双亲委派机制

package org.example;
public class Hello {
  
}
// 测试类
public class MainTest {
    public static void main(String[] args) throws ClassNotFoundException {
        MainTest maintest = new MainTest();
        ClassLoader classLoader = maintest.getClass().getClassLoader();
        classLoader.loadClass("org.example.Hello");
    }
}

我一共建了2个类——HelloMainTest,在MainTest中获取到类加载器,再通过类加载器加载Hello类,下面点进loadClass方法看看其具体实现

⚠️ 看源码的时候,会在不同的方法之间跳来跳去地看,所以请耐心看。

(1)loadClass(String name)

public Class<?> loadClass(String name) throws ClassNotFoundException {
  return loadClass(name, false);//调用另一个类加载方法,往下看
}

(2)loadClass(String name, boolean resolve)

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
  synchronized (getClassLoadingLock(name)) {
    // First, check if the class has already been loaded 检查类是否已经被加载
    Class<?> c = findLoadedClass(name);// 先从内存中查找该类【请转至(3)findLoadedClass】
            if (c == null) { // 如果c为null证明此类并未被加载
                long t0 = System.nanoTime();// 获取当前时间的纳秒表示
                try {
                    if (parent != null) {// 判断当前加载器是否有父类加载器
                        c = parent.loadClass(name, false);// 重点!!!反馈给父加载器,让父类加载器加载此类
                    } else {//如果当前加载器已经没有父类加载器了,说明什么呢?说明已经到了BootStrapClassLoader,这个时候就调用findBootstrapClassOrNull方法查找BootstrapClassloader加载的类中是否存在被查找类
                        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();// 获取当前的时间的纳秒标识
                    c = findClass(name);// 若不重写findClass方法则抛出类没有被找到的异常【转至(6)findClass(String name)】

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);// 记录父类加载器委托时间
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);// 记录寻找类所花费的时间
                    sun.misc.PerfCounter.getFindClasses().increment();//增加“找到的类”的计数。每当类加载器找到一个类时,这个计数器就会增加,从而可以用来跟踪类加载器已经加载了多少类
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

(3)findLoadedClass

protected final Class<?> findLoadedClass(String name) {
  // 检查类名,若类名不符合规矩则直接返回null【跳转至(4)checkName(String name)查看具体实现】
  if (!checkName(name)) 
    return null;
  return findLoadedClass0(name);// 这是一个本地方法,查找类,被加载了就直接返回此类,没有被加载则返回null
}

(4)checkName(String name)

private boolean checkName(String name) {
  // 检查类名是否为空
  if ((name == null) || (name.isEmpty()))
    return true;
  // 检查name中是否有 反斜杠 符号
  if ((name.indexOf('/') != -1)
      || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
    return false;
  return true;
}

(5)findBootstrapClassOrNull(String name)

private Class<?> findBootstrapClassOrNull(String name) {
  if (!checkName(name)) return null;// 检查类名是否合规【转至(4)checkName(String name)】
  return findBootstrapClass(name);// 这是一个本地方法,查找Bootstrap类加载器中的类并返回,找到则返回被查找类,没找到则返回null
}

(6)findClass(String name)

protected Class<?> findClass(String name) throws ClassNotFoundException {
   throw new ClassNotFoundException(name);// 抛出类没被找到的异常
}
  1. 类文件信息交给JVM:类文件找到了,那么就需要把它交付给JVM,那么这个交付过程也值得细细了解

    JVM篇——.class详解中我已经记述了.class文件的具体构成。交付时拆开.class,分门别类的把.class中的各种描述信息存储到JVM的的对应空间中。但是拆开前,.class已经被转换为了文件字节码流,需要把文件字节码流 存储在JVM里面的一块特殊区域(此区域专用于存储文件字节码流)——method area,

  2. 类文件对应的对象class交给jvm:

2.链接

2.1 验证:保证被加载的类的正确性

  • 文件格式验证:确保类文件遵从Java类文件的固定格式。这包括检查magic数、主版本号、常量池、访问标志、接口、字段、方法等的格式是否正确
  • 元数据验证:确保类本身符合Java语言的语法规定。这包括对类名、父类名、实现的接口名、方法签名等进行校验。它还会验证final类型的类是否有子类,以及final类型的方法是否被覆盖
  • 字节码验证:确保字节码流可以被Java虚拟机安全地执行。字节码验证是验证过程中最为复杂的一个过程,它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。但需要注意的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题
  • 符号引用验证:校验器还会进行符号引用的验证,确保符号引用转换为直接引用时没有问题。符号引用验证发生在解析阶段

2.2 准备:为类的静态变量分配内存空间,并将静态变量的值赋值为默认值

2.3 解析:将类中的符号引用转化为直接引用

符号引用:符号引用是一种数据结构,它包含了足够的信息来唯一标识一个类、接口、字段或方法,但并不直接指向内存中的具体位置。符号引用主要用于在类文件的编译阶段生成,而在类加载的解析阶段,会被转换成直接引用。符号引用可以是任何形式的字面量,比如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。它的作用是在程序运行时能够找到对应的目标。

直接引用:直接引用则是一种直接指向目标的内存地址或者偏移量,它可以是指向对象实例的指针、指向类的静态变量的指针、指向类的方法的指针等。直接引用是在程序运行时生成的,它依赖于具体的内存地址,可以直接被CPU所执行

3.初始化

为静态变量初始化为真正的值,那么这一步和链接的准备阶段不是有矛盾的吗?其实不矛盾!链接的准备阶段是对静态变量赋默认值,比如说我的Java类中定义的此变量是10,链接准备阶段此变量赋值为0,然后轮到初始化阶段将此变量的值0置为10。

关于类加载时的依赖问题的解释

在类的加载过程中,链接的解析阶段会将类的符号引用转化为直接引用,如果现存在2个类person和order,person和order是组合关系,person中有order属性,先加载person,此时order并未被加载,此时JVM是如何处理的呢?此时JVM会暂停加载person,而开始去加载order,加载完order后,再继续person的加载过程。

posted @   勤匠  阅读(19)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示