JavaJVM_虚拟机类加载机制
1. 类加载时机
一个类从被加载进内存,到卸载出内存,完整的生命周期包括:加载,验证,准备,解析,初始化,使用,卸载。
不同的阶段往往是穿插着进行的
,加载阶段中可能会激活验证的开始,而验证阶段又有可能激活准备阶段的赋值操作等,但整体的开始顺序是不会变的。
2. 类加载过程
.1. 加载
- 通过一个
类的全限定名
来获取定义此类的二进制字节流
。 - 将这个字节流所代表的
静态存储结构
转化为方法区的运行时数据结构
。 - 在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
.2. 验证
目的是为了
确保Class文件的字节流中包含的信息符合当前虚拟机的要求
,并且不会危害虚拟机自身的安全
。验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以
0xCAFEBABE
开头、主次版本号
是否在当前虚拟机的处理范围之内、常量池中的常量
是否有不被支持的类型。- **元数据验证:**对字节码描述的信息进行语义分析(注意: 对比
javac
编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object
之外。- **字节码验证:**通过
数据流和控制流分析
,确定程序语义是合法的、符合逻辑的。- **符号引用验证:**确保解析动作能正确执行。
如果所引用的类经过反复验证,那么可以考虑采用
-Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
.3. 准备
准备阶段
是正式为类变量分配内存
并设置类变量初始值
的阶段,这些变量所使用的内存都将在方法区中进行分配。
这时候进行
内存分配的仅包括类变量
(被static修饰的变量
),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。这里所设置的初始值通常情况下是
数据类型默认的零值
(如0
、0L
、null
、false
等),而不是被在Java代码中被显式地赋予的值
。
比如:假设一个类变量的定义为: public static int value = 3
;那么变量value在准备阶段过后的初始值为0
,而不是3
,因为这时候尚未开始执行任何Java方法,而把value赋值为3的put static
指令是在程序编译后,存放于类构造器()
方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行
。
- 对
基本数据类型
来说,对于类变量(static)和全局变量
,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值
,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被
static
和final
修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 - 对于引用数据类型
reference
来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null
。 - 如果在
数组初始化时没有对数组中的各元素赋值
,那么其中的元素将根据对应的数据类型而被赋予默认的零值。 - 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量
value就会被初始化为ConstValue属性所指定的值
。假设上面的类变量value被定义为:public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。我们可以理解为static final
常量在编译期就将其结果放入了调用它的类的常量池中
.4. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
,解析动作主要针对类
或接口
、字段
、类方法
、接口方法
、方法类型
、方法句柄
和调用点
限定符7类符号引用进行。
-
符号引用
就是一组符号来描述目标,可以是任何字面量。 -
直接引用
就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
.5. 初始化
- 使用
new
关键字实例化对象的时候。- 读取或设置一个
类型的静态字段
(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。- 调用一个类型的
静态方法
的时候。- 使用java.lang.reflect包的
方法对类型进行反射调用的时候
,如果类型没有进行过初始化,则需要先触发其初始化。- 当初始化类的时候,如果发现
其父类还没有进行过初始化
,则需要先触发其父类的初始化。- 当虚拟机启动时,用户需要
指定一个要执行的主类
(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 通过
子类引用父类的静态字段
,只会触发父类的初始化,而不会触发子类的初始化。- 定义
对象数组,不会触发该类的初始化
。常量在编译期间会存入调用类的常量池中
,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。通过类名获取 Class 对象,不会触发类的初始化
。- 通过
Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化
,其实这个参数是告诉虚拟机,是否要对类进行初始化。- 通过
ClassLoader 默认的 loadClass 方法
,也不会触发初始化动作。
3. 类加载器
- 一种是
启动类加载器(Bootstrap ClassLoader)
,这个类加载器使用C++语言实现
,是虚拟机自身的一部分; - 另一种就是所有
其他的类加载器
,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
.1. 类加载器
- 启动类加载器: 这个类将器负责将存放
在<JAVA_HOME>\lib目录中的
,或者被-Xbootclasspath参数所指定的路径中的
,并且是虚拟机识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。 - 扩展类加载器: 这个加载器由
sun.misc.Launcher$ExtClassLoader实现
,它负责加载<JAVA_HOME>\lib\ext目录中的
,或者被java.ext.dirs系统变量所指定的路径中的所有类库
,开发者可以直接使用扩展类加载器。 - 应用程序类加载器: 这个类加载器
由sun.misc.Launcher$AppClassLoader来实现
。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库
,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
.2.加载方式
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
// Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
// Class.forName("Test2", false, loader);
}
}
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
- Class.forName(): 将类的.class文件加载到jvm中之外,还会
对类进行解释
,执行类中的static块; - ClassLoader.loadClass(): 只干一件事情,就是
将.class文件加载到jvm中
,不会执行static中的内容,只有在newInstance才会去执行static块
。 - Class.forName(name, initialize, loader)
带参函数也可控制是否加载static块
。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
.3. 加载机制
-
全盘负责: 当一个类加载器负责加载某个Class时,
该Class所依赖的和引用的其他Class也将由该类加载器负责载入
,除非显示使用另外一个类加载器来载入。 -
缓存机制: 缓存机制将会保证
所有加载过的Class都会被缓存
,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区
。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。 -
双亲委派机制: 如果一个类加载器收到了类加载的请求,它
首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行