《深入理解Java虚拟机》读书笔记——虚拟机类加载机制
2017-08-04 本文已影响9人
学无悔
一、概述
-
虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
-
类型的记载、连接和初始化都是在程序运行期间完成
-
Java里可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的
二、类加载的时机
-
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期(共7个阶段)包括:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
如下图所示,其中,验证、准备、解析3个部分统称为连接:
-
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这个顺序来开始;
-
但解析则不一定,某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称动态绑定或晚期绑定)。
-
虚拟机规范严格规定有且只有5中情况立即对类进行“初始化”(加载、验证、准备在这之前开始),前提是类还没初始化:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 当使用JDK 1.7时的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
三、类加载的过程
包括:加载、验证、准备、解析、初始化。中间三个合起来称为连接阶段。
-
加载
-
加载与类加载不同,“加载”是“类加载”过程的一个阶段
-
加载过程主要完成3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-
加载类的方式:
- 从zip包读取,很常见,最终成为日后JAR、EAR、WAR格式的基础。
- 从网络中获取
- 运行时计算生成
- 由其他文件生成
- 从数据库读取
-
加载阶段和连接阶段的部分内容是交叉进行的。
-
-
验证
- 验证是连接的第一步
- 目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 主要分为下面4个阶段的检验动作:
-
文件格式验证
- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 基于二进制字节流进行,只有通过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储。
- 主要目的:保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
-
元数据验证
- 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
- 主要目的:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
-
字节码验证
- 第二阶段是对元数据信息中的数据类型校验,这个阶段是对类的方法体进行校验分析。
- 保证被校验类的方法在进行时不会做出危害虚拟机安全的事件。
- 主要目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
-
符号引用验证
- 发生在虚拟机将符号引用转化为直接饮用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。
- 目的:确保解析动作能正常执行,否则将抛出异常
-
-
准备
- 是正式为类变量分配内存并设置类变量初始值的阶段;这些变量所使用的内存都将在方法区中进行分配。
- 这里所说的初始值通常情况下是数据类型的零值,如下代码中,在准备阶段过后的初始值为0而非123:
public static int value = 123;
特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,如下,这阶段过后的值为123:
public static final int value = 123;
- 解析
- 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用与直接引用的关联
- 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
- 符号引用与虚拟机实现的内存布局无关,而直接引用则与之相关。
- 主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号应用进行。
- 初始化
- 是类加载过程的最后一步,这个阶段,真正开始执行类中定义的Java程序代码(字节码)。
- 是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,收集的顺序由语句在源文件中出现的顺序所决定的。
- 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下
public class Test{
static{
i = 0; //给变量赋值可以正常编译通过
System.out.println(i); //这句编译器会提示“非法向前引用”
}
static int i = 1;
}
- 父类的<clinit>()方法先执行
- <clinit>()方法与类的构造函数(即实例构造器<init>()方法)不同,不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕
类加载器
-
从虚拟机角度来讲,只有2种不同的类加载器:
- 启动类加载器,这个类使用C++语言实现,是虚拟机自身的一部分;
- 所有其他的类加载器,由Java语言实现,独立于虚拟机外部,并都继承自抽象类java.lang.ClassLoader。
-
从开发人员的角度,分为2种:
- 启动类加载器
- 扩展类加载器
- 应用程序类加载器
-
类加载器的双亲委派模型
- 类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
-
好处:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
IMG_20170804_214249_HDR.jpg