WatchDogs

Knowledge of Backend Development

0%

什么是类的加载

在介绍类的加载机制之前,先来看看,类的加载机制在整个java程序运行期间处于一个什么环节,下面使用一张图来表示:

从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器的作用其实就是类的加载。

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区(1.8:元数据区)内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

Java 类加载过程

一个Class在虚拟机中的完整生命周期如下图所示:

上述的流程只是描述了逻辑上各个阶段的开始顺序,实际过程中,各个阶段可能是交错进行,并不是一个阶段等到另一个阶段完全完成才开始执行。

###加载
加载一个Class需要完成以下3件事:

  • 通过Class的全限定名获取Class的二进制字节流
  • 将Class的二进制内容加载到虚拟机的方法区
  • 在内存中生成一个java.lang.Class对象表示这个Class

获取Class的二进制字节流这个步骤有多种方式:

  • 从zip中读取,如:从jar、war、ear等格式的文件中读取Class文件内容
  • 从网络中获取,如:Applet
  • 动态生成,如:动态代理、ASM框架等都是基于此方式
  • 由其他文件生成,典型的是从jsp文件生成相应的Class

相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。

###校验
验证一个Class的二进制内容是否合法,主要包括4个阶段:

  • 文件格式验证,确保文件格式符合Class文件格式的规范。如:验证魔数、版本号等。
  • 元数据验证,确保Class的语义描述符合Java的Class规范。如:该Class是否有父类、是否错误继承了final类、是否一个合法的抽象类等。
  • 字节码验证,通过分析数据流和控制流,确保程序语义符合逻辑。如:验证类型转换是合法的。
  • 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。如:验证引用的类、成员变量、方法的是否可以被访问(IllegalAccessError),当前类是否存在相应的方法、成员等(NoSuchMethodError、NoSuchFieldError)。

###准备
在准备阶段,虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值。注意这里仅仅会为static变量分配内存(static变量在方法区中),并且初始化static变量的值为其所属类型的默认值。如:int类型初始化为0,引用类型初始化为null。即使声明了这样一个static变量:

1
public static int a = 123;

在准备阶段后,a在内存中的值仍然是0, 赋值123这个操作会在中初始化阶段执行,因此在初始化阶段产生了对应的Class对象之后a的值才是123 。

注意,在上面a是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是123了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。

###解析
解析阶段,虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类、接口、方法、成员变量等符号引用。在转换成直接引用后,会触发校验阶段的符号引用验证,验证转换之后的直接引用是否能找到对应的类、方法、成员变量等。这里也可见类加载的各个阶段在实际过程中,可能是交错执行。

###初始化
初始化阶段即开始在内存中构造一个Class对象来表示该类,即执行类构造器<clinit>()的过程。需要注意下,<clinit>()不等同于创建类实例的构造方法<init>(),<clinit>()方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。
虚拟机会确保先执行父类的<clinit>()方法。
如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成<clinit>()方法。
虚拟机会保证<clinit>()方法的执行过程是线程安全的。
因此,存在如下一种最简单的单例模式的实现:

1
2
3
4
5
public class Singleton {
public static final INSTANCE = new Singleton();
private Singleton() {
}
}

在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
1、创建类的实例,也就是new的方式访问某个类或接口的静态变量;
2、对类的静态变量赋值;
3、调用类的静态方法反射

1
Class.forName(“com.java.Test”)

4、初始化某个类的子类,则其父类也会被初始化
5、Java虚拟机启动时被标明为启动类的类( JavaTest)
6、直接使用 java命令来运行某个主类

类加载器

Bootstrap ClassLoader :最顶层的加载类,主要加载核心类库,也就是我们环境变量下面$JAVA_HOME/jre/lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。使用C++实现。

Extention ClassLoader :扩展的类加载器,加载目录$JAVA_HOME/jre/lib/ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。由sun.misc.Launcher$ExtClassLoader实现。

Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。我们看到java为我们提供了三个类加载器,应用程序都是由这三种类加载器互相配合进行加载的。由sun.misc.Launcher$AppClassLoader实现。如果有必要,我们还可以加入自定义的类加载器。

类加载的三种方式

(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。

(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。指定ClassLoader,初始化时不执行静态块.

(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。

双亲委派模型

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父-类加载器去完成,每一层都是如此。一直递归到顶层,当父-加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父加载器,没有mother。父加载器也不是我们平日所说的那种继承关系(父类),只是调用逻辑是这样。

阅读原文