Java中ClassLoader的加载过程-刘宇
作者:刘宇
CSDN博客地址:https://blog.csdn.net/liuyu973971883
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。
1、基本概念
1.1、如何结束JVM生命周期
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
1.2、类加载的过程
- 加载:查找并加载类的二进制
- 连接
校验:确保被加载的类的正确性
准备:为类中的静态变量分配内存,并初始化为默认值
解析:把类中的符号引用转换为直接引用 - 初始化:为类的静态变量赋予正确的初始值
- 使用:正常的new一个实例
- 卸载:将类的字节码从内存中卸载
流程图
1.3、Java程序对类的使用方式主要分为两种
所有java虚拟机实现必须在每个类或者接口被java程序首次主动使用时才能初始化他们。
1.3.1、主动使用:
- new,直接使用
- 访问某个类或者接口的静态变量,或者对该静态变量进行赋值操作
- 调用静态方法
- 反射某个类
- 初始化一个子类
- 启动类
- 从JDK1.7开始提供动态语言支持,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对呀的累没有初始化,则初始化它们
1.3.2、被动使用:
除上述以外都属于被动使用,都不会导致类的初始化。
1.3.3、特殊使用情况:
- 子类访问父类的静态变量不会导致子类初始化
- 某个类的数组在new该数组的时候不会初始化该类
- 当一个接口或它的实现类初始化时,并不要求其父接口或实现的接口完成初始化,但是会导致父接口的加载。只有在真正使用到父接口的时候(如引用接口中所定义的非编译阶段能确定的常量时),才会初始化。这里和类的机制还是不一样的,类机制是子类需要父类完成初始化的。
- 调用静态的常量变量不会初始化该类,因为会在编译期间将常量放入调用该常量的方法所在类中的常量池中。如该常量的值不能在编译阶段得到时,则会初始化,如:
public static final int x = new Random().nextInt(100);
1.4、类加载过程小练习
出现以下问题是因为在类加载过程中初始化阶段的先后顺序导致的
1.4.1、初始化过程练习1
public class Singleton {
public static int x = 0;
public static int y;
private static Singleton instance = new Singleton();
private Singleton(){
x++;
y++;
}
public static Singleton getInstance(){return Singleton;}
public static void main(Stirng[] args){
Singleton singleton = getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
输出结果:
1
1
1.4.2、初始化过程练习2
public class Singleton {
private static Singleton instance = new Singleton();
public static int x = 0;
public static int y;
private Singleton(){
x++;
y++;
}
public static Singleton getInstance(){return Singleton;}
public static void main(Stirng[] args){
Singleton singleton = getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
输出结果:
0
1
2、JVM简单介绍
2.1、虚拟机所管理的内存
2.2、程序计数器
-
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
-
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2.3、Java 虚拟机栈
-
线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
-
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
-
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
-
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
2.4、本地方法栈
- 区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。
2.5、Java 堆
- 对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。
- 内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
- 与堆相关的一个重要概念是垃圾收集器,现代几乎所有的垃圾收集器都是采用的分代收集算法,所以堆空间也对其做了相应的划分:新生代与老年代。Eden空间、From Survivor空间、To Survivor空间。
- OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
2.6、方法区
- 属于共享内存区域,存储元信息,就是已被虚拟机加载的class文件结构信息(包括常量池、字段描述、方法描述)、常量、静态变量、即时编译器编译后的代码等数据。也有人称为永久代(Permanent Generation),但是从JDK1.8开始,已经彻底废弃了永久代,使用元空间(meta space)。
3、类加载的三个阶段详解
3.1、加载阶段
是将class文件中的二进制数据读取到内存中,将其放在方法区中,然后在堆区中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装在方法区的数据结构
3.1.1、加载类的方式
- 本地磁盘中直接加载
- 内存中直接加载
- 通过网络加载.class
- 从zip、jar等归档文件夹中加载.class
- 数据库中提取.class文件
- 将Java源文件动态编译为.class文件
3.1.2、Class和Object对象在内存中的对应关系
3.1.3、创建一个对象的过程(句柄模式)
3.1.4、创建一个对象的过程(指针模式)
3.2、连接阶段
- 在加载阶段完成之后,虚拟机外部的二进制数据量就会按照虚拟机所需要的格式存储在方法区中,然后在堆区中创建一个Class对象,这个对象作为程序访问方法区中数据结构的外部接口
- 加载阶段与连接阶段的部分内容可以交叉进行,比如一部分代码加载完成就可以进行验证,提高效率。
3.2.1、验证阶段
验证主要的目的是确保Class文件中的字节流中包含的信息符合虚拟机要求,并且不会损害到JVM自身的安全。如果出现验证失败则会出现VerifyError或其子类
-
文件格式验证
-
- 魔术因子是否正确,如exe、jpg等文件的魔术因子都是不同的
-
- 主从版本号是否符合当前虚拟机
-
- 常量池中的常量类型是不是支持
元数据验证
-
- 是否有父类
-
- 是否实现了抽象方法
-
- 是否覆盖了父类的final字段
-
- 其他语义检查
字节码验证
-
- 主要进行数据流和控制流分析,不会出现这样的情况,在操作栈中放置了一个int类型,但是却给了long类型的数据
符号引用验证
-
- 调用了一个不存在的方法、字段等
3.2.2、准备阶段
为类的变量分配存储空间和初始值
3.2.3、解析阶段
主要是用来解析类或接口、字段、类方法、接口方法等,把类中的符号引用转换为直接引用
3.3、初始化阶段
-
初始化阶段是执行构造函数<clinit>方法和<init>方法的过程
-
<clinit>构造函数方法是由编译器自动收集类中的所有静态变量的赋值动作和静态语句块中的语句合并产生的。
-
<init>,针对源代码中的每一个类的构造方法,java编译器都会产生一个<init>方法,而<init>用于处理对变量的赋值操作(不包含静态变量)。
-
静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,只能赋值,不能访问
-
<clinit>方法与类的构造函数有点区别,他不需要显示的调用父类的构造函数,虚拟机会保证子类的<clinit>执行之前先执行父类的<clinit>,因此在虚拟机找那个首先被执行的是Object的<clinit>方法
-
由于父类的<clinit>方法要先执行,也就也就意味着父类中定义的静态语句块,要先于子类执行。如下代码输出:2