类的加载过程(浅显小白版)《深入理解Java虚拟机》---p267
加载Loading ==> 链接Linking ==> 初始化Initialization
一、加载阶段:
- 通过类的全限定名,获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,JDK 7 之前成为永久代,之后成为元空间
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载class文件的方式
- 本地系统中直接加载
- 通过网络获取,典型场景: Web Applet
- 从zip压缩包中读取 ,成为日后jar 、war 格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 从其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
二、链接阶段 : 验证 ——> 准备 ——> 解析
① 验证Verify
- 确保Class文件的字节流中包含信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证(0xCAFEBABE开头),元数据验证(继承关系),字节码验证(复杂的 合法的 符合逻辑的 安全的 验证工作),符号引用验证
② 准备Prepare
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
分配内存:JDK7之前使用永久代来实现方法区时,逻辑成立;JDK8之后类变量会随着Class对象一起存放在Java堆中了
类变量的默认初始值:int : 0 boolean : false reference : null char : '\u0000'... 注意:内存分配仅包括类变量而不包括实例变量,实例变量会在对象实例化的时候随着对象一起分配在Java堆中。如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量值就会被初始化为ContantValue属性所指定的初始值。(编译时javac会为value赋值为123)
public static final int value = 123 ;
③解析 Resolve
- 将常量池内的符号引用转换为直接引用
事实上,解析操作往往会伴随着JVM再施行完初始化之后再执行
符号引用:一组符号来描述所引用的目标,与内存布局无关,引用的目标不一定是已经加载到虚拟机内存中的内容
直接引用: 可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄 - 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONTANT_Field_info、CONSTANT_Methodref_info等等
三、初始化阶段
- 初始化阶段就是执行类构造器<clinit>()方法的过程, <clinit>()方法是Javac编译器的自动生成物</clinit></clinit>
- <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中的出现顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,但可以为定义静态语句块之后的变量赋值。</clinit>
- < clinit>()方法与类的构造函数的区别:
clinit()方法不需要显式地调用父类构造器,Java虚拟机会保证在子类的clinit执行前,父类的clinit已经执行完毕,因此在Java虚拟机中第一个被执行的clinit方法的类为java.lang.Object
- clinit方法并不是必须的。没有对类变量的赋值或者static代码块的话,编译器可以不为这个类生成clinit
- 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口也有clinit
- 接口的clinit不需要先执行父接口的clinit,因为只有父接口的变量被使用的时候,父接口才会被初始化
- Java虚拟机必须保证一个类的clinit方法在多线程环境中被正确的加锁同步。多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待,直到活动线程执行完毕clinit方法。