虚拟机是如何加载一个类的?

加载(双亲委派模型)

加载是指查找字节流,每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器(最终都会传到顶层启动类加载器)。在父类加载器没有找到所请求的类的情况下,该类加载器才会依次向下的尝试去加载。

启动类加载器(Bootstrap ClassLoader):C++编写 由JVM控制 开发人员不可进行操作 主要负责加载JDK的基础和核心类库

扩展类加载器(Extension ClassLoader):负责加载相对次要、但又通用的类,比如存放在JRE的lib/ext目录下jar包中的类

应用程序类加载器(Application ClassLoader):负载加载classpath目录下的所有jar和class文件

为什么要使用双亲委派模型加载类?

  • 避免同一个类被多个类加载器重复加载 另外加载类的时候还加了锁机制 在多线程下也避免了重复加载

  • 安全性 保证了内存中不会出现多份同样的字节码 避免核心类库被开发人员自定义修改(假如用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中类加载器也不会去加载,在向上委托时在顶层加载器可以找到JDK自带的Object)

如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

破坏双亲委派模型

只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制,破坏双亲委派规则有:自定义ClassLoader,重写loadClass方法

常见的破坏双亲委派规则

  • 使用SPI机制:如ServiceLoader.load是需要启动类加载器加载,那么所依赖的实现是在我们classpath目录下,那么启动类加载器是加载不到的,这时就会获取当前线程上下文加载器的类加载器(默认AppClassLoad)去加载所依赖的类

  • Tomcat的自定义类加载器:Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找,避免多个Web应用相同全限定名导致不可加载问题。

线程上下文加载器:由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。

以上方式到底有没有破坏双亲委派模型呢

那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由BootStrap ClassLoader进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」

有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。

课外知识点:

Class.forName():类默认会被初始化,即执行内部的静态块代码以及保证静态属性被初始化,默认会使用当前的类加载器来加载对应的类

ClassLoader.loadClass():与Class.forName()不同,类不会被初始化,只有显式调用才会进行初始化。提供一种灵活度,可以根据自身的需求继承ClassLoader类实现一个自定义的类加载器实现类的加载

链接

链接,是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

  • 验证:确保被加载类能够满足Java虚拟机的约束条件

  • 准备:为被加载类的static+final字段基本数据类型初始化分配内存,实现虚方法的动态绑定的方法表

  • 解析:将这些符号引用解析成为实际引用。在class文件被加载至Java虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。举例来说,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

初始化

在Java代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

如果直接赋值的静态字段被final所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被Java编译器标记成常量值(ConstantValue),其初始化直接由Java虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被Java编译器置于同一方法中,并把它命名为< clinit >

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行< clinit >方法的过程。需要注意的是如果该类有父类则会有限初始化父类< clinit >, Java虚拟机会通过加锁来确保类的< clinit >方法仅被执行一次。

只有当初始化完成之后,类才正式成为可执行的状态

那么,类的初始化何时会被触发呢?JVM规范枚举了下述多种触发情况:

  • 当虚拟机启动时,初始化用户指定的主类;

  • 当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;

  • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;

  • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;

  • 子类的初始化会触发父类的初始化;

  • 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

  • 使用反射API对某个类进行反射调用时,初始化这个类;

  • 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

这段代码是在著名的单例延迟初始化例子中,只有当调用Singleton.getInstance时,程序才会访问LazyHolder.INSTANCE,才会触发对LazyHolder的初始化(对应第4种情况),继而新建一个Singleton的实例。

由于类初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个Singleton实例。

posted @ 2022-06-30 20:16  吃小孩的哥哥  阅读(137)  评论(0)    收藏  举报