Java虚拟机与垃圾回收知识点梳理(一)Java内存区域与HotSpot虚拟机中堆中对象的创建,存储,访问
Java虚拟机与垃圾回收知识点梳理(二)垃圾收集算法
Java虚拟机与垃圾回收知识点梳理(三)HotSpot虚拟机的七种垃圾收集器及它们之间的关系
Java虚拟机与垃圾回收知识点梳理(四)内存分配与回收策略和垃圾收集器实例演示
Java虚拟机与垃圾回收知识点梳理(五)虚拟机类加载机制深入解析
虚拟机类加载机制深入解析
JVM整体架构
JVM由三个主要的子系统构成,它们分别是
- 类加载器子系统
- 运行时数据区(内存结构)
- 执行引擎
运行时数据区前面已经重点介绍过,今天主要介绍类加载器子系统。
类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。
在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的
类加载的时机
下图是类的生命周期
加载,验证,准备,初始化和卸载这五个阶段顺序是确定的,而 解析阶段可以在初始化之后开始。比如:Java语言的运行时绑定特性(动态绑定或者晚期绑定)。
必须对类进行初始化的六种情况
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令,如果类型没有被初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型的Java代码场景有:
- 使用new关键字实例化对象的时候。
- 读取/设置一个类型的静态字段(被final修饰的静态常量除外)。
- 调用一个类型的静态方法的时候。
- 对类型进行反射调用的时候。
- 初始化子类的时候,父类还未初始化,先触发父类的初始化
- 虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类)。
- 使用jdk7新加入的动态语言支持。
- 当一个接口中定义了jdk8新加入的默认方法。如果该接口的实现类发生了初始化,那该接口要在其之前被初始化。
以上6种情况会触发类型进行初始化的场景,这六种场景中的行为称为对一个类型的主动引用。
被动引用
案例一
package test5;
public class SuperClass {
static{
System.out.println("SuperClass init");
}
public static int value = 123;
}
package test5;
public class SubClass extends SuperClass {
static{
System.out.println("subClass init");
}
}
package test5;
public class Main {
public static void main(String[] args) {
//子类引用父类的静态字段 父类被动初始化
System.out.println(SubClass.value);
}
}
运行结果如下:
总结:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。对HotSpot虚拟机来说,使用-XX:+TraceClassLoading参数可以观察类加载的详细过程。
案例二
package test6;
public class SuperClass {
static{
System.out.println("SuperClass init");
}
public static int value = 123;
}
package test6;
public class NotInitialization {
public static void main(String[] args) {
//通过数组定义来引用类,不会触发此类的初始化
SuperClass[] csa = new SuperClass[10];
}
}
运行结果如下:
案例三
package test7;
public class ConstClass {
static{
System.out.println("constClass init");
}
public static final String Hello = "hello";
}
package test7;
public class Main7 {
public static void main(String[] args) {
System.out.println(ConstClass.Hello);
}
}
运行结果如下:
上述问题的原因是:
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类加载的过程(各个阶段)
加载阶段
Java虚拟机完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。(开发人员可控性最强的阶段)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接阶段(验证、准备、解析)
验证阶段(重要,但不是必须要执行,可以设置参数关闭此阶段)
作用:确保Class文件的字节流中包含的信息符号《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段要进行以下几项:
- 文件格式验证
- 元数据验证
对字节码描述的信息进行语义分析
例如:这个类是否有父类(除了Object类之外,所有的类都应当有父类) - 字节码验证
对类的方法体进行校验分析 - 符号引用验证
主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的有:
java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
准备
准备阶段是正式为类中定义的变量(静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,和虚拟机实现的内存布局无关,符号引用的字面量形式明确定义在《Java虚拟机规范》Class文件格式中。
直接引用:可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。和虚拟机实现的内存布局直接相关。
初始化
类的初始化阶段是类加载过程的最后一个步骤
初始化阶段就是执行类构造器< clinit >()方法的过程
介绍< clinit >
- < clinit >方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
- < clinit >()方法与类的构造函数不同,它不需要显式调用父类构造器,Java虚拟机会保证在子类的< clinit >()方法执行前,父类的< clinit >()已经执行完毕。因此Java虚拟机中第一个被执行的< clinit >()方法的类型肯定是java.lang.Object。
- 由于父类的< clinit >()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
package test9;
public class Test {
static class Parent{
public static int A=1;
static{
A = 2;
}
}
static class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
}
运行结果如下:
package test10;
public class Test {
static class DeadLoopClass{
static{
if(true) {//必加
System.out.println(Thread.currentThread() + "init");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+" start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread()+" run over");
}
};
Thread t1=new Thread(script);
Thread t2=new Thread(script);
t1.start();
t2.start();
}
}
运行结果如下:
类加载器
实现了类 “加载阶段”中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作的代码就叫做“类加载器”(Class Loader)
双亲委派模型
类加载器种类
- 启动类加载器:负责加载JRE的核心类库,如jre目标下的rt.jar,charsets.jar等
- 扩展类加载器:负责加载JRE扩展目录ext中JAR类包
- 系统类加载器:负责加载ClassPath路径下的类包
- 用户自定义加载器:负责加载用户自定义路径下的类包
package cn.yemuxia;
public class TestJDKClassLoader {
public static void main(String[] args){
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}
运行结果如下:
双亲委派模型
先委托父类加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类
双亲委派优势
沙箱安全机制:自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次