写点什么

类加载机制

用户头像
Geek_571bdf
关注
发布于: 1 小时前

线索:

1. 什么是虚拟机的类加载机制?

2. 类加载机制分为哪几个阶段?每个阶段分别做了什么?

3. 类加载器的继承结构?什么是双亲委派模型?

4. 如何确定一个类的全局唯一性?

5. 枚举类初始化的触发情况;(8 点)

 

1. 什么是虚拟机的类加载机制?

虚拟机将描述类的数据从 class 文件加载至内存,并对其进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的 java 类型,这个过程称为虚拟机的类加载机制。

 

2. 类加载的过程,按先后顺序,分为 加载、链接、初始化。

但并非所有的都需要经过这三个阶段。我们知道,Java 类型分为基本类型和引用类型,引用类型具体是:类、接口、泛型参数、数组类。其中,泛型参数会在编译过程中被擦除,因此 Java 虚拟机实际上只有前三种。

 

其中,基本类型是由 Java 虚拟机预先定义好的。因此不需要经过类加载机制。

 

数组类没有对应的字节流,是由 Java 虚拟机直接生成的,因此不需要进行加载。而其他两种都有对应的字节流。不过,无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行连接和初始化。

 

3. 在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。事实上,Java 语言天生支持动态扩展的特性就是依赖于运行期动态加载和动态链接这个特性实现的。动态扩展说的是,比如,面向接口编程时,我们可以等到运行时再指定其实际的实现类。

 

4. 对于字节流,最常见的形式是由 Java 编译器生成的 class 文件。除此之外,我们也可以在程序内部直接生成,或者从网络中获取(例如网页中内嵌的小程序 Java applet)字节流。这些不同形式的字节流,都会被加载到 Java 虚拟机中,成为类或接口。

 

5. 加载阶段。

加载,是查找字节流,并据此创建类的过程。具体说,在加载阶段,虚拟机需要完成下面这三件事:

1)       根据类的全限定名,获取定义该类的二进制字节流;

2)       将该字节流所代表的静态存储结构,转换为方法区的运行时数据结构;

3)       在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

 

加载需要用到类加载器完成。Java 平台有着多种且具有对应等级的类加载器。

它们有着共同的祖师爷:启动类加载器(bootstrap class loader)。启动类加载器由 C++实现,因此没有对于的 Java 对象,在 Java 中用 null 指代。谁也联系不到启动类加载器。

除了启动类加载器之外,其它的类加载器都是 java.lang.ClassLoader 的子类,是有对应的 Java 对象的。因此,这些类加载器需要先由另一个类加载器加载至虚拟机(比如启动类加载器),才能去执行它们自己的类加载功能。

除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器和应用类加载器,均由 Java 核心类库提供。从上图可以看到,扩展类加载器的父类加载器是启动类加载器,应用类加载器的父类加载器是扩展类加载器。除了由 Java 核心类库提供的类加载器外,我们还可以加入自定义的类加载器,来实现特殊的加载方式。比如,我们可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。

 

1) 在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。扩展类加载器负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。应用类加载器负责加载应用程序路径下的类。(这里的应用程序路径指虚拟机参数 -cp/-classpath、系统变量 java.class.)

2) 在 Java9 之后,引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

 

此外,类加载器除了加载功能,还提供了命名空间的作用。在虚拟机中,类的唯一性是由“类加载器实例+类的全名”一同确定。也就是说,即便是同一串字符流,通过不同的类加载器加载,也回得到两个不同的类。在实践中,我们可以利用这样的特性来运行同一个类的不同版本。

 

双亲委派模型:

在进行类加载时,有个规定:双亲委派机制。说的是,当一个类加载器收到加载请求时,它首先并不会去尝试加载这个类,而是将请求委派给父类加载器。以此类推。可以看到,所有的类加载请求都会被向上委派到启动类加载器中。

 

只有在父类加载器没有找到所请求的类的情况下(该类的 Class 文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,层层下去,直到该类被成功加载。

若找不到该类,则 JVM 会抛出 ClassNotFoud 异常。

 

6. 链接阶段。

链接阶段分为 验证、准备、解析三个阶段。

 

1. 验证:验证的目的是确保被加载类 满足 java 虚拟机的约束条件。一般而言,由 java 编译器生成的类文件必然满足 java 虚拟机的约束条件。

2. 准备:准备阶段除了 为被加载类的静态字段分配内存外,部分 Java 虚拟机还会在此阶段构造 跟类层次相关的数据结构,比如,用来实现虚方法的动态绑定的方法表。

3. 解析:解析是将符号引用解析为实际引用的过程。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载,但未必触发这个类的链接以及初始化。

 

什么是符号引用?

在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用。比如,对于一个方法调用,编译器会生成一个包含 “目标方法所在类名+目标方法名+接受参数类型+返回值类型” 的符号引用,来指代所要调用的方法。

 

不过,Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

7. 初始化阶段。

在 Java 代码中,我们可以通过直接赋值或者在静态代码块中,对一个静态字段进行初始化。

如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,该字段会被 Java 编译器标记成常量值 ConstantValue,其初始化直接由 Java 虚拟机完成。

 

除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。

 

类加载的最后一步是初始化,是对标记为 ConstantValue 的字段的赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。只有当初始化完成之后,类才正式成为可执行的状态。

 

8. 类的初始化在什么情况下会被触发?JVM 规范枚举了下述多种触发情况:(8 点)

 

1.        当虚拟机启动时,初始化用户指定的主类(程序启动需要指定一个主类,即 main()方法所在类);

2.        当遇到 new 指令时,初始化 new 指令的目标类;(Object o = new Object())// 新建目标类实例的 new 指令

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

4.        当遇到 putstatic、getstatic 指令时,初始化该静态字段所在的类;// 访问静态字段的指令

5.        子类的初始化会触发父类的初始化;// 子类初始化时,若发现父类还未初始化,则会先触发父类的初始化

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

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

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


public class Singleton {  private Singleton() {}  private static class LazyHolder {    static final Singleton INSTANCE = new Singleton(); // <clinit>,类初始化,执行<clinit>方法  }  public static Singleton getInstance() {    return LazyHolder.INSTANCE; // 访问静态字段  }}
复制代码

上述这段代码是在著名的单例延迟初始化例子中,只有当调用 Singleton.getInstance 时,程序才会访问 LazyHolder.INSTANCE,才会触发对 LazyHolder 的初始化(对应第 4 种情况),继而新建一个 Singleton 的实例。由于类初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个 Singleton 实例。

用户头像

Geek_571bdf

关注

还未添加个人签名 2019.06.13 加入

还未添加个人简介

评论

发布
暂无评论
类加载机制