导航
- 一、过程概述
- 二、Loading
- 2.1 类加载器
- 2.2 双亲委派机制
- 2.3 类在内存中的结构
- 三、Linking
- 四、Initializing
一、过程概述
java 源文件编译后会生成一个 .class文件存储在硬盘上。
在程序运行时,会将用到的类文件加载到 JVM 内存中。从磁盘到内存的过程总共分为三个步骤:加载、连接、初始化。
- Loading
- Linking
- Initializing
二、Loading
Loading 过程是把一个 class 文件加载到内存中去。
2.1 类加载器
JVM 加载类的方式是按需动态加载,采用双亲委派机制。
认识几种系统提供的类加载器:
-
Bootstrap 启动类加载器,负责加载指定目录——JAVA_HOME/lib下的 rt.jar、charset.jar 等核心类库,由 C++实现,是JVM 的不可分割的一部分。类加载范围可以在 Launcher 类中查看:
-
ExtClassLoader 扩展类加载器,负责加载指定目录——JAVA_HOME/lib/ext 下的扩展包,或者也可以由 -Djava.ext.dirs 参数指定。类加载范围可以在 Launcher 类中查看:
-
AppClassLoader 应用类加载器,加载用户应用的 classpath 下的 class 文件,这是应用程序的默认类加载器,用户自定义的类都是通过这个类加载器来加载。类加载范围可以在 Launcher 中查看:
-
自定义类加载器,开发者自定义的 ClassLoader,继承自 ClassLoader 抽象类,并重写 findClass(…) 。
类加载器(除 C++实现的 Bootstrap 外)本身就是一个普通的 class,JVM 所有的 class 都是被类加载器加载到内存的。
public class TestLoadClass {public static void main(String[] args) {System.out.println(TestLoadClass.class.getClassLoader());System.out.println(TestLoadClass.class.getClassLoader().getParent());// Bootstrap 由 C++ 实现,Java 中没有具体的类与之对应,故返回 nullSystem.out.println(TestLoadClass.class.getClassLoader().getParent().getParent());}
}
output:
sun.misc.Launcher$AppClassLoader@58644d46
sun.misc.Launcher$ExtClassLoader@4554617c
null
2.2 双亲委派机制
JVM 加载类时处于安全考虑,基础类和扩展类等,都必须由指定的类加载器来加载,不同的类加载器有自己的命名空间,同一个类,如果由不同的类加载器加载,会在内存中存在多份类对象。也正因如此,JVM 的类加载机制要求诸如 java.lang.Object 这种基础类必须由最基础的 Bootstrap 来加载。
因此,整个 Loading 的过程就成了:底层类加载器收到类加载请求后,必须将请求层层传递给父级加载器检查,确认是否应该由父级加载器加载,若由于父加载器的指定类路径中没有该类文件,就会再层层向下返回,最终才会去加载:
Tip:注意,AppClassLoader 的父加载器是 ExtClassLoader,ExtClassLoader的父加载器是Bootstrap 加载器。
这里的父加载器并不是 Java 多态中语法的 extends 继承关系,而是一种架构上的层级关系,AppClassLoader 和 ExtClassLoader 之间没有任何继承关系,它们在语法上,都继承自 ClassLoader 抽象类,实际上 ClassLoader 中维护了一个 ClassLoader parent 引用,这才是 “双亲” 的真实面目,即 AppClassLoader 的父加载器实际上是其内部的一个组合对象。
2.3 类在内存中的结构
加载的时候,创建了两块内容,第一块内容是把 Xxx.class 二进制扔到内存中,第二块内容是生成一个 Class 类的对象,该对象中的变量会指向前一块的实际内存地址(这一步实际上是Resolution 的过程)。
三、Linking
Linking,连接,这是一个大的步骤,其中又分为三个小步骤:
- Verification 验证
- Preparation 准备
- Resolution 解析
验证:校验 class 文件格式是否符合 jvm 规范,如开头的魔数等。
准备:将 class 中的静态变量赋默认值,所谓默认值,举个例子,int 类型默认值是 0,String 类型默认值是 null。
解析:把class 文件常量池中的符号引用转化为直接内存地址。
四、Initializing
初始化,区别于 Linking 中的 Preparation ,此过程将静态变量赋初始值。比如:
public static int num = 8;
这个 num 静态变量会在 Preparation 阶段赋值 0 ,在 Initializing 阶段赋值 8。
Java初始化的时机,JVM规范中有明确规定,自然,加载也一定是在初始化之前完成。JVM规范中定义了以下这些必须初始化完毕的场景:
- new
- 读取和设置 static 变量,只触发直属类的初始化(例如,子类直接引用父类中的 static 变量,只初始化父类)
- 调用 static 方法
- 子类初始化之前,父类必须完成初始化
- main 函数所在的类在执行之前必须完成初始化
- 通过反射获取类信息
注意:对于 static final 类型的属性,在编译之后即存储在字节码文件的常量池中,直接引用它不会触发初始化。
记忆技巧:
- new 是创建对象,而对象的创建一定需要类信息的支持
- static 变量和方法,又称为类变量、类方法,它们都需要类指针来访问。
- main也属于static 方法
- 子类是父类的扩展,子类都要初始化了,父类当然一定要先初始化
- 反射是使用类信息的一种技术,当然需要先初始化类