目录
1. 简述
JVM讲一个class文件加载内存,然后进行验证、准备、解析(这三步也可以合称为链接)及初始化的完整过程称为类加载过程。这个过程种,各步骤的顺序是确定的:
在Java虚拟机规范中,没有强制约束什么时候要开始类的加载。但是严格规定了几种情况是必须进行类的初始化的(加载和链接需要在初始化之前开始):
1)遇到 new、getstatic、putstatic、或者invokestatic 这4条字节码指令,如果没有类没有进行过初始化,则触发初始化
2)使用java.lang.reflect包的方法,对垒进行反射调用的时候,如果没有初始化,则先触发初始化
3)初始化一个类时候,如果发现父类没有初始化,则先触发父类的初始化
2. 加载(Loading)
加载就是通过指定的类全限定名,获取此类的二进制字节流,然后将此二进制字节流转化为方法区的数据结构,在内存中生成一个代表这个类的Class对象。在JVM中,类的加载由类加载器ClassLoader负责,JVM自身提供的类加载器有三个,分别是:
- Bootstrap ClassLoader:由C++直接实现,是Java的启动器类加载器,负责加载JVM自身工作需要的类。实际Bootstrap ClassLoader会将%JAVA_HOME%\lib下的class文件或者通过JVM参数-Xbootclasspath指定的路径下的类加载到JVM中
- Extension ClassLoader:这个类加载器位于sun.misc.Launcher下,由Java实现。它负责加载%JAVA_HOME%\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库
- Application ClassLoader:该类加载器同样位于sun.misc.Launcher下,由Java实现。它负责在家用户类路径(classpath)下的指定类库,通常我们在项目中编写的类文件,如果没有额外指定类加载器就会由Application ClassLoader负责加载。
另外,Extension ClassLoader和Application ClassLoader作为Java类,且是JVM自身运行所需的重要类,它们的加载由Bootstrap ClassLoader负责,这也贴合Bootstrap ClassLoader自身的职责。
关于ClassLoader的知识其实不少,比如双亲委派模型,自定义类加载器等等,后续我再写一遍博客单独介绍吧,此处就先略过。
3 链接(Linking)
链接这一阶段实际分为三个小段:验证、准备和解析,我们逐个部分来分析JVM都在干些啥
3.1 验证(Verification)
Java虚拟机在这一阶段首先需要校验加载的字节码是否符合规范,是否是一个正确的Java类文件。
其次JVM还会对字节码进行语义分析,判断其是否符合Java语言规范,比如:这个类是否有父类(按照Java语言规范,除了java.lang.Object类,其他类都有父类)、这个类的父类是否继承自一个final类、如果该类不是抽象类,则是否实现了所有应该实现的方法(接口或父类抽象方法)、类中的字段、方法是否与父类有矛盾。
除此之外,验证阶段还会检查类中的符号引用是否能正确解析。但是这一步比较特殊,它实际发生在“解析”这一阶段,因为在这个时候JVM才会去处理符号引用。这一步实际会校验一下内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private,protected,public,default)是否可以被当前类访问
只有通过了符号引用验证,JVM才能确保解析步骤继续执行
3.2 准备(Preparation)
JVM在准备阶段需要为类或接口的静态成员变量分配内存并赋上默认值(也叫零值)。在这一阶段JVM实际不会执行任何Java代码,所谓的默认值也是JVM自己规定的默认值,它和类的初始值是不同的,JVM为各类型的成员变量赋的默认值见下表:
数据类型 | 默认值 |
---|---|
int | 0 |
long | 0L |
short | 0 |
char | ‘\u0000’ |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
在JVM中,用 static final修饰的String类型和基本数据类型变量又被称为ConstantValue变量。对于该类型变量,JVM在准备阶段会直接赋予其实际值,而不再是默认值。
public static final String author = "JerryGao";
比如上面代码定义的变量author,JVM会在准备阶段就将“JerryGao”赋值给author变量,而不是String的默认值null。
3.3 解析(Resolution)
解决阶段实际就是想class文件常量池中的符号引用转化直接引用的过程。所以在了解解析阶段JVM做了什么之前,有必要简单介绍一下到底什么是符号引用和直接引用:
-
符号引用:符合引用其实就是一组用来描述所引用目标的符号,它可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,因为引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为java虚拟机规范在class文件的格式中有明确定义符号引用的字面量形式。总共有7类符号引用,分别是:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符
-
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关。同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不相同。如果有了直接引用,那引用的目标必定已经在内存中存在
JVM虚拟机规范并未要求解析阶段应该在何时发生,但有明确提出在执行16个特别的字节码指令前需要先进行类的解析,这16个字节码以及对应含义如下:
- anewarray、multianewarray:创建数组
- checkcast、instanceof:检查类实例类型
- getfield、getstatic、putfield、putstatic:访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)
- invokeinterface:调用接口方法,他会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
- invokespecial:调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
- invokestatic:调用类方法(static方法)
- invokevirtual:调用对象实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言中最常见的分派方式
- invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行改方法
- ldc:将一个常量加载到操作数栈的指令
- ldc_w:将一个常量加载到操作数栈的指令
- new:创建类实例
对相同的符号引用进行多次解析在JVM的运行过程中很常见,所以为了优化性能,JVM会缓存符号引用第一次解析后的结果(在运行时常量池中记录直接引用,并把常量标识为已解析状态)。invokedynamic命令除外,因为该命令本来就是对于动态语言的支持,所以每次调用该命令时的符号应用解析都是可能动态变化的,无法缓存。
4 初始化(Initializing)
初始化阶段实际就是执行类或接口的初始化方法。只有在第一次主动调用某个类时,JVM才会初始化该类,主动调用有以下六种情况,其余时候的调用都属于被动调用(不会触发初始化):
- 一个类的实例被创建(new操作、反射、cloning,反序列化)
- 调用类的static方法
- 使用或对类/接口的static属性进行赋值时(这不包括final的与在编译期确定的常量表达式)
- 当调用 API 中的某些反射方法时
- 当其子类被初始化时
- 被设定为JVM启动时的启动类(即main方法类)
那什么時候属于被动调用呢,这里简单举个例子:假设C是A的子类,A有一个用public修饰的静态成员变量name。我们使用C.name的方式来通过C调用其父类A的静态变量,此时对于C来说,它属于被动调用,JVM不会初始化C。但JVM会去初始化C的父类A,因为name是A的静态变量,我们虽然是通过C.name来获取的name而不是A.name,但本质上还是在获取A的静态成员变量值,属于对A的6种主动调用的一种。
在初始化阶段,JVM需要对类的静态成员变量(也叫类变量),实例变量进行赋值。还需要执行类中的静态代码块以及普通代码块,以及类的构造方法。同时一个类的初始化阶段还会引起它的父类或实现的接口的初始化。JVM会遵循一个固定的顺序去完成上述所有初始化阶段需要做的事,这里用一个流程图表示会更清晰: