JVM:03.类加载

一、类加载阶段

当需要某个类的时候,jvm会加载.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载

类加载分为:加载、链接、初始化三个阶段

类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。简言之:把类(class)装载进内存

1. 加载

重点:

  • 加载就是将类的字节码载入到方法区,并在堆中创建类.class 对象
  • 如果此类的父类没有加载,先加载父类
  • 加载是懒惰执行

细节:

将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类。这个instanceKlass 有个重要的属性叫_java_mirror, 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用。可以理解为是加载对象时java和c++的桥梁。随后在堆中创建类.class对象

如图所示,JVM首先将Person类的字节码载入元空间,用一个instanceKlass 来描述Person类,并在堆中创建了一个Person.class。instanceKlass 的属性_java_mirror记录了Person.class地址,Person.class记录了instanceKlass 地址。

Person实例想要访问类方法时,便会现根据自己的对象头中记录的class地址去访问Person.class,再去访问instanceKlass ,在其中查看各种信息,如父类、成员变量、方法等

image-20220304093901695

2. 链接

重点:

加载和链接可能是交替运行的

链接分为如下几个阶段:

  • 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
  • 准备 – 为 static 变量分配空间,设置默认值
  • 解析 – 将常量池的符号引用解析为直接引用(类A里用了类B,符号引用就是只知道有类B这样一个符号,直接引用知道B的地址)

细节:

准备阶段会为 static 变量分配空间,设置默认值,其中

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成将常量池中的符号引用解析为直接引用

3. 初始化

重点:

初始化即调用 <cinit>()V ,JVM会保证这个类的『构造方法』的线程安全

类初始化是【懒惰的】

细节:

初始化发生的时机:

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况:

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化

视频52-54可以复习,更好理解static及初始化,其中有懒惰初始化单例模式

二、类加载器

以jdk8为例,类加载器有:

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

除了 BootstrapClassLoader ,其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

1. 启动类加载器

主要加载JAVA_HOME/jre/lib目录下的类,都是些核心类,也可通过在控制台输入指令,使得类被启动类加器加载

2. 扩展类加载器

主要加载JAVA_HOME/jre/lib/ext目录下的类

如果 classpath 和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用扩展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载。

3. 双亲委派模式

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • 能找到这个类,由上级加载,加载后该类也对下级加载器可见
  • 找不到这个类,则下级类加载器才有资格执行加载

注意:

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系,只是级别不一样

我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。

官方 API 文档的描述: The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

比如想要加载一个classpath的Application 类,则会先看Extension 有没有加载,看Extension 有没有加载时又会先看 Bootstrap 有没有加载,发现Bootstrap 没有加载,然后又发现Extension 也没有加载,则在Application 里加载

image-20220304125301954

类加载器

双亲委派的目的

  1. 让类的加载有优先次序,避免类的重复加载,保证核心类优先加载
  2. 让上级类加载器中的类对下级共享(反之不行,保证核心类不会被篡改),即能让你的类能依赖到 jdk 提供的核心类

打破双亲委派模式

想打破双亲委派模型需要重写 loadClass() 方法

能不能自己写个类叫 java.lang.System并加载?

  • 自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的

  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败

  • 以上也仅仅是假设。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

细节–loadClass源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,查找该类是否已经被该类加载器加载过了
Class<?> c = findLoadedClass(name);
// 如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
// 看是否被它的上级加载器加载过了,其中 Extension 的上级是Bootstarp,但它显示为null
if (parent != null) {
c = parent.loadClass(name, false); // 递归调用
} else {
// 上级加载器都没有加载过,使用启动类加载器尝试加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器没能加载完成,因为找不到该类
// 捕获异常,但不做任何处理
}

if (c == null) {
// 如果还是没有找到,先让拓展类加载器调用 findClass 方法去找到该类,如果还是没找到,就抛出异常
// 然后让应用类加载器去找 classpath 下找该类,即自己尝试加载
long t1 = System.nanoTime();
c = findClass(name);

// 记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

4. 自定义类加载器

使用场景

  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤

  • 继承 ClassLoader 父类
  • 要遵从双亲委派机制,重写 findClass 方法
    不是重写 loadClass 方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法

JVM:03.类加载
http://jswanyu.github.io/2022/04/24/JVM/03-类加载/
作者
万宇
发布于
2022年4月24日
许可协议