Java 编译后的 Class 文件中描述的各种信息,都需要加载到内存中才能运行和使用。
JVM 把描述类的数据从 Class 文件中加载到内存中,并对数据进行校验,转化分析和初始化等,最终形成可以被 虚拟机 直接使用的 Java 类型,这就是 虚拟机的 类加载机制。
在 Java 语言中,类型的加载、连接和初始化都是在运行时完成的。
1. 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:
加载(Loading)、验证(Varification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和下载(Unloading)七个阶段,验证、准备和解析统称连接。
其中加载、验证、准备、初始化和卸载这五个阶段是确定的,解析阶段不一定。
Java 虚拟机规范并没有规定 加载 阶段什么时候执行,但是对于 初始化 阶段只有5中情况必须对类进行 初始化(加载 、验证和准备阶段肯定是在其之前运行的)
-
遇到 new、getstatic、putstatic 或者 invokestatic 这四条字节码指令时,如果类没有进行初始化,就需要先触发其初始化。Java代码场景:new 关键字示例化对象时,读取和设置一个静态字段时(被 final 修饰、已在编译期把结果放入到常量池的静态字段除外)时,以及调用类的静态方法时。
new 表示创建示例对象,getstatic/putstatic 表示访问类字段(static修饰),invokestatic 表示访问类方法(static修饰方法);具体细节可以去看一下 虚拟机的字节码指令(深入理解Java虚拟机)。 -
使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有被初始化,则先要对其进行初始化。
-
当初始化一个类的时候,如果父类还没有进行初始化,先触发父类的初始化。
-
当虚拟机启动时,用户需要制定一个要执行的主类(包含 main 方法的那个类),虚拟机会先初始化这个类。
-
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandler 示例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先将其初始化
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
-----------------------
SuperClass init
如上可以看到只输出了 SuperClass init,但是并没有输出 SubClass init。对于静态字段,只有直接定义这个字段的类才会被初始化。
是否需要加载子类,虚拟机规范并没有明确的规定,取决于虚拟机的具体实现。
Sun HotSpot 虚拟机可以通过 -XX:+TraceClassLoading 可以看到此操作会导致子类的加载。
打印结果:
如上所示,只是对子类进行加载,但是并没有对其进行初始化。
2. 类加载过程
1. 加载
加载时 类加载 的一个阶段,在加载阶段,虚拟机需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二级制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成代表的这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的方位入口
对于 HotSpot 虚拟机而言,Class 对象比较特殊,虽然其实对象,但是保存在方法区,作为程序访问方法区中的这些类型数据的外部接口。
2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
- 文件格式验证
验证字节流是否符合 Class 文件格式的规范,这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证,字节流才会进入内存的方法区中进行存储,所以后面的三个阶段都是基于方法区的存储结构进行的,不会直接操作字节流。 - 元数据验证
对字节码描述的信息进行语义分析,保证其描述的信息符合 Java 语言规范的要求 - 字节码验证
通过数据流和控制流分析,确定语义是合法的、符合逻辑的。 - 符号引用验证
解析阶段发生,将符号引用转化为直接引用
如果无法通过符号引用验证,就会抛出 java.lang.NoSuchFieldError, java.lang.NoSuchMethodError
这个经常出现在我们使用最新包依赖包的方法,但是依赖的确实旧版本的依赖包。
3. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
public static int value = 123;
变量 value 在准备阶段的额初始值是 0 并不是 123,因为这时候还没有执行任何的 Java 方法,而把 value 赋值为 123 的指令 putstatic 是程序被编译后,存放在 类构造器 < clinit >() 方法中,所以赋值为 123 实在初始化阶段执行的。
4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用
符号引用以一组符号来描述所引用的目标,符号可以上任何形式的字面量,只要使用时能无歧义地定位到目标即可。(我感觉就是你定义的变量名) - 直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。(我感觉就是真正的引用指针)
5. 初始化
类初始化阶段是类加载过程中的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。初始阶段是执行类构造器 () 方法的过程
类加载器
1. 类与类加载器
类加载器只用于实现类的加载动作。对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立起在 虚拟机 中的唯一性。
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = classLoader.loadClass("com.beng.classloader.SubClass").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof SubClass);
}
}
------------------------------------------------------------
SuperClass init
SubClass init
class com.beng.classloader.SubClass
false
- 启动类加载器(Bootstrap ClassLoader): 负责加载放在 < JAVA_HOME >\lib 目录中,或者被 -Xbootclasspath 参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。
- 扩展类加载器(Extension ClassLoader): 由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 < JAVA_HOME >\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库, 开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader): 这个类加载由 sum.misc.Launcher$AppClassLoader 实现。负责加载用户类路径(classpath)上指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,就默认使用这个。
上图中所呈现出的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。(不是继承关系)
双亲委派模型的工作过程是这样的:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个类加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
这样做的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object,它放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器来加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基本的行为也就无法保证了。
双亲委派模型对于保证 Java 程序运行的稳定性很重要,但它的实现很简单,实现双亲委派模型的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中,逻辑很清晰:先检查是否已经被加载过,若没有则调用父类加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先,检查请求的类是不是已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类抛出 ClassNotFoundException 说明父类加载器无法完成加载
}
if (c == null) {
// 如果父类加载器无法加载,则调用自己的 findClass 方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}