一、背景
Java代码被编译器变成生成Class字节码,但字节码仅是一个特殊的二进制文件
,无法直接使用。因此,都需要放到JVM系统中执行,将Class字节码文件放入到JVM的过程,简称类加载
。
二、整体流程
三、阶段逻辑分析
3.1 加载Loading
3.1.1 字节码来源
由于类的Class二进制字节码来源可能不同,JVM在此处做了扩展
,通过类的全限定名来加载不同来源的二进制字节码文件。以下是一些可能的来源:
- 本地文件中获取;
- 网络上获取;
- 压缩包中获取;
- 加密文件中获取;
- 运行时内存中获取。例如使用
动态代理技术
时,进行字节码重组,最终生成的二进制字节流就会在内存获取。
3.1.2 加载步骤
【步骤1】:
通过类的全限定名获取类的二进制字节流;
【步骤2】:
将二进制字节码中的静态存储结构转化为方法区的运行时数据结构
,同时在方法区会生成InstanceKlass对象。下面详细讲解一下字节码文件:
字节码的组成:
一般信息:
1. 魔数
2. 字节码文件对应的Java版本号
3. 访问标识(用于区分类、接口、枚举或注解等类型)
4. 子类、父类和接口的索引,用于找到子类、父类或结构的信息。索引指的在常量池中的位置
常量池:
1. 字符串常量
2. 类、接口名或字段名
其中的 #数字 即符号引用,表示在常量池中的位置。从图中可以看出,有String_info,Class_info,Methodref_info等信息,字符串常量池仅是其中的一小部分。
字段:当前类或接口声明的字段信息
方法:当前类或接口声明的方法信息
属性:类的属性
【步骤3】:
在内存【堆中】中生成一个当前类的Class对象
,作为访问方法区的入口
。
疑问:为什么有了InstanceKlass对象,还需要Class对象
1.InstanceKlass对象是C++语言生成的对象,因此Java代码无法直接操作InstanceKlass对象;
2.Klass对象中不仅包含类的基本信息,还包括虚方法表信息。虚方法表信息是给Java虚拟机使用的,开发者没有权限使用,因此创建一个简单的Class对象,给开发者使用,这样Java虚拟机就能很好的控制开发者访问数据的范围
3.2 链接Linking
3.2.1 验证
文件格式验证、元数据信息验证、字节码正确性验证以及符号引用存在性验证。
3.2.2 准备
static final
修饰的基本变量
,进行显示
初始化赋值。【隐士初始化在编译器阶段已经完成】- static 修饰的变量,
分配内存空间,并进行隐士初始化赋值
。JDK7放在方法区中,JDK8放在堆中。
3.2.3 解析
编译阶段:
由于尚未加载到内存,并不知道实际的内存引用关系,仅是通过特殊方式#数字
将具有引用关系的属性记录下来,最终形成符号引用;
运行阶段:
各种属性已经被分配过内存空间了,因此它们有实际的内存地址
。此时根据符号引用,将属性之间的引用关系转化为内存地址的实际引用关系,最终变成直接引用。
3.3 初始化Init
主要是编译器自动收集类中所有的静态变量以及静态代码块赋值动作
,生成<clinit>方法,按照代码赋予的值进行赋值。编译器收集的顺序主要是语句在代码中出现的顺序决定的。
触发类的初始化
的几种方式:
1. 访问一个类的```静态变量或静态方法```,但若变量是final修饰且等号右边是常量的,不会触发初始化;
2. 调用Class.forName(String className)方法;
3. 通过new 创建对象;
4. 执行Main方法的当前类
无法触发类的初始化
的几种方式:
1. 无静态代码块且无静态赋值语句;
2. 仅有静态变量的声明无赋值操作;
3. 静态变量的定义使用final修饰
父子类的初始化
规则:
1. 直接访问父类的静态变量,不会触发子类的初始化;
2. Java虚拟机保证子类的<clinit>方法执行之前,父类的<clinit>一定已经执行完毕,所以父类中的静态变量和静态代码块是优先于子类的静态变量和静态代码块执行的;
3.4 使用和卸载
使用:
表示当前类正在被使用
卸载:
表示已经被垃圾回收