0%

JVM如何进行类加载

Jvm在进行类加载时分为三个环节,分别为加载,链接以及初始化。

加载

加载是指查找字节流,并且据此创建类的过程。加载的class文件可以来源于本地磁盘,也可以来自于网络或者运行时计算生成等等。在加载阶段需要完成以下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

在加载完成以后,外部的二进制流就会按照设定的格式存储在方法区之中。

加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。这会影响例如equals()方法,isAssignableForm()方法以及instanceof的判定。下面的代码就演示了不同的类加载器对instanceof的影响,其中Java虚拟机中同时存在了两个Main类,一个由虚拟机的应用程序加载类进行加载,另一个使用自定义的ClassLoader进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
InputStream in = getClass().getResourceAsStream(name+".class");
if (in == null) {
return super.loadClass(name);
}
byte[] b = in.readAllBytes();
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = loader.loadClass("Main").newInstance();
System.out.println(obj.getClass()); //class Main
System.out.println(obj instanceof Main); //false
}
}

除了启动类加载器,其余的加载器都是java.lang.ClassLoader的子类。

启动类加载器(bootstrap class loader)由C++实现,没有对应的Java对象。其他的类加载器都需要先由另一个类加载器加载至java虚拟机中,才能执行类加载的工作。

启动类加载器负责加载最为基础重要的类(比如JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容)。启动类加载器只会加载包名为java,javax和sun开头的类。

扩展类加载器(JAVA9以后称为平台类加载器)的父类加载器是启动类加载器,在sun.misc.Launcher$ExtClassLoader中以Java实现。它负责加载相对次要、但又通用的类,(从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库)。

应用类加载器的父类加载器则是扩展类加载器,在sun.misc.Launcher$AppClassLoader中以Java实现。。它负责加载应用程序路径下的类。(负责加载环境变量classpath或系统属性java.class.path指定路径下的类库)。如果应用程序中没有定义过自己的加载器,那么会是程序中默认的加载器。

除了Java提供的类加载器之外,还可以加入自定义的类加载器,来实现特殊的加载方式。例如可以对class文件进行加密,加载时再利用自定义的类加载器进行解密。

双亲委派机制

当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。这一规则被称为双亲委派机制。双亲委派模型要求除了启动类加载器,都必须要有自己的父类加载器。双亲委派机制可以避免类的重复加载,同时可以防止核心API库被篡改。双亲委派模型的代码在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
41
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);
if (c == null) {
long t0 = System.nanoTime();
// 从父加载器加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 父类是启动类加载器
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);

// 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;
}
}

从上面的代码也可以看出破环双亲委派模式的方法,就是直接在子加载器中覆盖loadClass()方法。在编写自己的类加载逻辑时,应该尽量去重写findClass方法来完成加载,来防止双亲委派模型被破坏。

链接

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

验证

在验证阶段,需要确保被加载的类能够满足java虚拟机的约束条件。

准备

准备阶段的目的是为了给被加载类的静态字段分配内存,将其初始化为默认值。例如以下一段Java代码

1
public static int value=1; //在准备阶段赋值为0

该代码在准备阶段只会将value的值设置为0,而将value设置为1则会在之后的初始化阶段进行。而如果该字段加上了final修饰,那么会在准备阶段赋值为1。

1
public static final int value=1; //在准备阶段赋值为1

解析

在加载至JVM之前,这个类无法知道其他类及其方法,字段所对应的具体位置。因此在引用这些成员时,需要生成符号引用。

解析阶段的目的在于将这些符号引用解析成为实际引用。如果在该过程中出现符号引用指向了一个未被加载的类,字段或者方法,那么将触发这个类的加载(不一定触发链接以及初始化)。

初始化

对于静态字段的直接赋值操作以及所有静态代码块的代码,JVM会将其置于同一个方法中,成为,然后在初始化阶段运行。在初始化阶段,JVM会通过加锁的方式来保证方法只会执行一次。只有当初始化完成以后,类才正式的成为了可执行的状态。

接下来分析一下单例模式延迟初始化。当调用Singleton.getInstance()方法时,程序会访问LazyHolder.INSTANCE这一静态字段,触发对于LazyHolder的初始化,继而创建一个新的Singleton实例。由于类初始化是线程安全的,因此可以保证在任何情况下,有且只有一个Singleton实例。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
private Singleton(){}
private static class LazyHolder{
static final Singleton INSTANCE=new Singleton();
}
public static Singleton getInstance(){
return LazyHolder.INSTANCE;
}
public static void main(){

}
}