类的加载到执行

在命令行中输入java xxx指令后,java执行程序会在JRE安装目录中寻找JVM启动文件,如果在windows中,就是jvm.dll文件,启动JVM后,接着JVM产生Bootstrap Loader类加载器,Bootstrap Loader类加载器接着产生Extended Loader,并且设置该加载器的父加载器为Bootstrap Loader,接着有产生System Loader,并且设置其父加载器为Extended Loader.
在java中,除了Bootstrap Loader之外,其他的类加载器都有父加载器。Bootstrap由C语言编写,其他的由java语言编写。
三种类型的加载器的主要功能如下:

  1. 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

加载

当以上过程完成后,System Loader就开始加载运行类了,也就是在运行中,需要用到新的类时,在默认情况下就由System Loader来负责加载。每一个类加载器在加载类时,都会把加载工作交给其父类加载器来完成,一层一层的往上提交,如果父类加载器不能完成加载工作,才由当前的类加载器来完成加载工作。这就是所谓的”类加载代理模式”。之所以采用该模式主要是为了保证java核心库的类型安全。在java虚拟机中,判定两个类是否相同,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

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
package com.example;

public class Sample {
private Sample instance;

public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
//测试类是否相同
public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}

//运行错误输出
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more

所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

当类对应的.class文件加载到JVM后,会创建一个java.lang.Class对象,一个Class对象对应一个.class文件,主要记录该文件的关于类的所有信息。我们可以通过该对象的newInstance()函数来生成类的实例,这种情况只适合在类具有无参数构造函数的情况下。默认情况下,JVM只会用一个Class实例来代表一个.class文件(确切说,应该是通过同一类加载器载入的.class文件),每一个类的实例都会知道自己由哪一个Class实例生成,可以听过对象.getClass()或是类名.class或是Class.forName("类名")来获得类的Class实例。Class实例记录了类的所有信息,可以通过该实例来得到具体类的对象,java的反射机制就是通过Class来实现的。

链接

当类被加载后,系统就为之创建一个对应的Class对象,接着就会进入连接阶段。连接阶段会负责吧类的二进制数据合并到JRE中。类连接又可以分为如下三个阶段:

  1. 验证:检验被加载的类是否有正确的内部结构,并和其它类协调一致。

  2. 准备:负责为类的静态属性分配内存,并设置默认初始值。

  3. 解析:将类的二进制数据中的符号引用替换成直接引用。

初始化

JVM负责对类进行初始化,也就是对静态属性进行初始化。在Java类中,对静态属性指定初始值的方式有两种:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。
默认情况下都是在Class实例生成后,对类进行初始化。但是也可以对其推迟,直到需要生成类的实例时,才进行初始化,而且只在第一次生成类的实例前才执行初始化。Class.forName("类名", bool值初始化与否, 类加载器)可以自定义初始化的时间。
完成以上工作后,程序就可以继续执行了。