JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制
类的生命周期
加载、验证、准备、初始化和卸载5个阶段的顺序是确认的,类加载过程必须按照这种顺序按部就班的开始,在动态绑定中解析可以在初始化后完成。
JVM规范中初始化的条件
- 使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候;
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则 需要先触发其父类的初始化;
- 当JVM启动时,用户需要制定一个要执行的主类(main 方法),JVM会先初始化这个主类;
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果;REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
被动引用不会触发初始化
测试代码
/**
* @Author: vhtk
* @Description: 被动引用
* @Date: 2020/6/21
*/
public class InitClass {
public static void main(String[] args) {
// 通过子类引用父类的静态字段,不会导致子类初始化
System.out.println(SubClass.value);
// 通过数组来引用类,不会触发该类的初始化
SupperClass[] supperClasses = new SupperClass[3];
// 常量在编译阶段会存入常量池,本质上并没有直接引用到类定义常量的类,因此不会触发定义常量类的初始化
System.out.println(SubClass.HELLO_WORLD);
}
static class SupperClass {
static {
System.out.println("SupperClass init!");
}
public static int value = 123;
}
static class SubClass extends SupperClass {
static {
System.out.println("SubClass init!");
}
public static final String HELLO_WORLD = "hello world";
}
}
类加载的过程(从功能层面讲)
加载:
非数组类
- 将一个类的全限类名来获取定义此类的二进制字节流,可以通过ZIP包,网络,运行时生成等各种方式获取,开发人员可控性最强的阶段;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
数组类
如果是引用类型,则去掉维度类型,递归的采用非数组类的加载方式加载,如果基本类型,JVM会标记为与引导类加载器相关联。
验证
一切为了安全
文件格式验证
- 是否以魔数0xCAFEBABE开头,魔数是.class字节码文件的标识;
- 主次版本号是否在当前JVM处理范围内;
- 常量池中是否有不被支持的常量类型;
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据;
- Class文件中各部分及文件本身是否有被删除的或附加的其他信息。
元数据验证
- 当前类是否有父类;
- 当前类是否继承了不允许被继承的类;
- 如果当前类不是抽象类,是否实现了其父类或接口之中要求实现的方法;
- 类中字段、方法是否与父类产生矛盾。
字节码验证
通过数据流和控制流分析,确定程序语义是合法的,合乎逻辑的,保证类运行时不会做出危害JVM的安全事件。
符号引用验证
对类自身以外的信息进行匹配性校验:
- 符号引用中通过字符串描述的权限定名是否能找到对应的类;
- 指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
- 符号引用中的类、字段、方法是否合乎访问权限。
准备
正式的为类变量分配内存并设置类变量初始值
解析
将符号引用替换为直接引用的过程,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符号引用进行解析,对同一个符号引用多次解析是很常见的,JVM会对第一次解析的结果进行缓存,除了invokedynamic指令以外,invokedynamic指令主要用来动态加载。
符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能 无歧义的定义到目标即可。符号引用与JVM实现的内存布局无关;
直接引用:直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,也就是对象定位的两种方式。
初始化
执行类构造器的方法clinit的过程,clinit方法由javac指令生成,JVM定义了以下规范:
- 编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生,编译器收集的顺序是 由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量;
- 不需要显式的调用父类构造器,JVM会保证在子类的构造器执行之前父类构造器以及执行完毕;
- 如果一个类没有静态语句块,则不生成clinit方法;
- 接口中有静态变量的初始化动作,因此接口也会生成clinit方法;
- JVM会保证clinit方法在多线程环境中正确的加锁和同步。
类加载器:
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。JVM预定义有三种类加载器:
根类加载器(BootstrapClassLoader)
加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
测试代码:
import java.net.URL;
/**
* @Author: vhtk
* @Description:
* @Date: 2020/6/22
*/
public class BootstrapClassLoaderTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
测试结果:
扩展类加载器(ExtClassLoader)
它负责加载JRE的扩展目录,负责加载$JAVA_HOME中jre/lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
测试代码:
/**
* @Author: vhtk
* @Description:
* @Date: 2020/6/22
*/
public class ExtClassLoaderTest {
public static void main(String[] args){
ClassLoader classLoader = sun.security.ec.SunEC.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
}
}
测试结果:
应用类加载器(AppClassLoader)
负责在JVM启动时,加载来自在命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。
测试代码:
/**
* @Author: vhtk
* @Description:
* @Date: 2020/6/22
*/
public class AppClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
}
}
测试结果:
类加载过程(从流程层面讲)
JVM类加载机制
全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派
先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制
所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
双亲委派模型
优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。