2019-11-08
1 JVM架构整体架构
1.1 类加载器子系统
1.1.1 加载
1.1.2 链接
1.1.3 初始化
1.2 运行时数据区(Runtime Data Area)
1.3 执行引擎
1.4 示例
2 classloader加载class文件的原理和机制
2.1 Classloader 类结构分析
2.2 实现类的热部署
2.3 类加载器的双亲委派模型
2.4 类加载的三种方式
2.5 自定义类加载器的两种方式
参考
1 JVM架构整体架构
图1 JVM整体架构图
JVM被分为三个主要的子系统:
- 类加载器子系统
- 运行时数据区
- 执行引擎
1.1 类加载器子系统
图2 类加载器
Java的动态类加载功能是由类加载器子系统处理。当它在运行时(不是编译时)首次引用一个类时,它加载、链接并初始化该类文件。
1.1.1 加载
加载器:类由此组件加载。启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。
- 启动类加载器 – 负责从启动类路径中加载类,无非就是rt.jar。这个加载器会被赋予最高优先级。
- 扩展类加载器 – 负责加载ext 目录(jrelib)内的类.
- 应用程序类加载器 – 负责加载应用程序级别类路径,涉及到路径的环境变量等etc.上述的类加载器会遵循委托层次算法(Delegation Hierarchy Algorithm)加载类文件,这个在后面进行讲解。
加载过程:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。
1.1.2 链接
校验: 字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误。
- 文件格式验证:基于字节流验证,验证字节流符合当前的Class文件格式的规范,能被当前虚拟机处理。验证通过后,字节流才会进入内存的方法区进行存储。
- 元数据验证:基于方法区的存储结构验证,对字节码进行语义验证,确保不存在不符合java语言规范的元数据信息。
- 字节码验证:基于方法区的存储结构验证,通过对数据流和控制流的分析,保证被检验类的方法在运行时不会做出危害虚拟机的动作。
- 符号引用验证:基于方法区的存储结构验证,发生在解析阶段,确保能够将符号引用成功的解析为直接引用,其目的是确保解析动作正常执行。换句话说就是对类自身以外的信息进行匹配性校验。
准备:分配内存并初始化默认值给所有的静态变量。
public static int value=33;
这据代码的赋值过程分两次,一是上面我们提到的阶段,此时的value将会被赋值为0;而value=33这个过程发生在类构造器的<clinit>()方法中。
解析:所有符号引用被方法区(Method Area)的直接引用所替代。
举个例子来说明,在com.sbbic.Person类中引用了com.sbbic.Animal类,在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.sbbic.Animal来代表Animal真实的内存地址。在解析阶段,JVM可以通过解析该符号引用,来确定com.sbbic.Animal类的真实内存地址(如果该类未被加载过,则先加载)。
主要有以下四种:类或接口的解析,字段解析,类方法解析,接口方法解析
解析理解:
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
其中:
- 全限定名:就是完整类名把.改为/。
- 描述符:字段的类型,方法的返回类型和参数列表(参数列表又包含每个参数的类型)。
符号引用和直接引用的区别与关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量(看下图6)或是一个能间接定位到目标的句柄(看下图5)。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
1.1.3 初始化
这是类加载的最后阶段,这里所有的静态变量会被赋初始值, 并且静态块将被执行。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻初始化:
- 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类;
- 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化;
- 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化;
- 虚拟机启动时,用户会先初始化要执行的主类(含有main);
- jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化;
1.2 运行时数据区(Runtime Data Area)
- The 运行时数据区域被划分为5个主要组件:
- 方法区 (线程共享) 常量 静态变量 JIT(即时编译器)编译后代码也在方法区存放
- 堆内存(线程共享) 垃圾回收的主要场地
- 程序计数器 当前线程执行的字节码的位置指示器
- Java虚拟机栈(栈内存) :保存局部变量,基本数据类型以及堆内存中对象的引用变量
- 本地方法栈 (C栈):为JVM提供使用native方法的服务
图4 运行时数据区
1.3 执行引擎
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
解释器:解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。
编译器:JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。
- 中间代码生成器– 生成中间代码
- 代码优化器– 负责优化上面生成的中间代码
- 目标代码生成器– 负责生成机器代码或本机代码d. 探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。
垃圾回收器: 收集并删除未引用的对象。可以通过调用"System.gc()"来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。Java本地接口 (JNI): JNI会与本地方法库进行交互并提供执行引擎所需的本地库。本地方法库:它是一个执行引擎所需的本地库的集合。
1.4 示例
通过以下代码看JVM类加载执行过程
1 package com.example.demo.classloader; 2 /** 3 * 从JVM调用的角度分析java程序堆内存空间的使用: 4 * 当JVM进程启动的时候,会从类加载路径中找到包含main方法的入口类HelloJVM 5 * 找到HelloJVM会直接读取该文件中的二进制数据,并且把该类的信息放到运行时的Method内存区域中。 6 * 然后会定位到HelloJVM中的main方法的字节码中,并开始执行Main方法中的指令 7 * 此时会创建Student实例对象,并且使用student来引用该对象(或者说给该对象命名),其内幕如下: 8 * 第一步:JVM会直接到Method区域中去查找Student类的信息,此时发现没有Student类,就通过类加载器加载该Student类文件; 9 * 第二步:在JVM的Method区域中加载并找到了Student类之后会在Heap区域中为Student实例对象分配内存, 10 * 并且在Student的实例对象中持有指向方法区域中的Student类的引用(内存地址); 11 * 第三步:JVM实例化完成后会在当前线程中为Stack中的reference建立实际的应用关系,此时会赋值给student 12 * 接下来就是调用方法 13 * 在JVM中方法的调用一定是属于线程的行为,也就是说方法调用本身会发生在线程的方法调用栈: 14 * 线程的方法调用栈(Method Stack Frames),每一个方法的调用就是方法调用栈中的一个Frame, 15 * 该Frame包含了方法的参数,局部变量,临时数据等 student.sayHello(); 16 */ 17 public class HelloJVM { 18 //在JVM运行的时候会通过反射的方式到Method区域找到入口方法main 19 public static void main(String[] args) {//main方法也是放在Method方法区域中的 20 /** 21 * student(小写的)是放在主线程中的Stack区域中的 22 * Student对象实例是放在所有线程共享的Heap区域中的 23 */ 24 Student student = new Student("spark"); 25 /** 26 * 首先会通过student指针(或句柄)(指针就直接指向堆中的对象,句柄表明有一个中间的,student指向句柄,句柄指向对象) 27 * 找Student对象,当找到该对象后会通过对象内部指向方法区域中的指针来调用具体的方法去执行任务 28 */ 29 student.sayHello(); 30 } 31 } 32 class Student { 33 // name本身作为成员是放在stack区域的但是name指向的String对象是放在Heap中 34 private String name; 35 public Student(String name) { 36 this.name = name; 37 } 38 //sayHello这个方法是放在方法区中的 39 public void sayHello() { 40 System.out.println("Hello, this is " + this.name); 41 } 42 }