什么是类加载?
在JVA虚拟机实现规范中,通过ClassLoader类加载器把*.class字节码文件(文件流)加载到内存,并对字节码文件内容进行验证、准备、解析和初始化,最终形成可以被虚拟机直接使用的java.lang.Class对象,这个过程被称作类加载。
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
类的生命周期
类的生命周期包括以下7个阶段:
加载
验证
准备
解析
初始化
使用
卸载
结束类生命周期的几种场景:执行System.exit()方法
程序正常执行结束
程序执行中遇到了异常或错误而异常终止
操作系统出现错误或强制结束程序而导致JVM虚拟机进程终止
类加载过程
类加载过程包含:加载、验证、准备、解析和初始化,一共包括5个阶段。
1.加载
在加载阶段 ,JVM主要完成以下3件事:
- 通过类的完全限定名称获取定义该类的***.class**字节码文件的二进制字节流。
- 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。
由于JVM虚拟机对加载***.class** 字节码文件的来源并未做限制,所以出现了以下的***.class**字节码文件加载方式:
- 本地文件系统直接读取;
- 从网络中通过服务器响应读取。例如:Web Applet技术;
- 从 JAR、EAR、WAR等压缩文件中读取;
- 运行时通过动态代理技术生成字节码文件。例如:在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件或容器生成。例如:由tomcat将***.jsp** 文件翻译成***.java** 文件后,编译生成对应的 *.class字节码文件。
在加载阶段完成之后,*.class字节码文件的类信息数据就会存储在元空间,同时在JVM虚拟机堆区生成一个该类的Class对象。
2.验证
在验证阶段,JVM主要确保***.class**字节码文件中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全。
验证阶段会完成下面四个阶段的检验:
- 文件格式验证:验证字节流是否符合*.class字节码文件格式的规范,且能被当前版本的虚拟机处理。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候 , 这个转化动作将在连接的第三个阶段------解析阶段中发生。确保解析动作能正常执行。
3.准备
- 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
- 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
- 初始值一般为 0 值。
4.解析
将常量池的符号引用替换为直接引用。
5.初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划 去初始化类变量和其它资源。
<clinit>()是由编译器自动收集类中所有类变量的赋值动作 和静态语句块中的语句 合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值 ,不能访问。
例如:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后面。
java
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示"非法向前引用"
}
static int i = 1;
}
类加载的时机
1.主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:
- 当遇到new 、 getstatic、putstatic 或invokestatic这 4 条字节码指令时,比如 new 一个对象,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行new指令时会加载类。即:当程序创建一个类的实例对象。
- 当 jvm 执行getstatic指令时会加载类。即:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行putstatic指令时会加载类。即:程序给类的静态变量赋值。
- 当 jvm 执行invokestatic指令时会加载类。即:程序调用类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用时如Class.forname("...") , 或newInstance() 等等。如果类没初始化,需要触发类的加载。
- 加载一个类,如果其父类还未加载,则先触发该父类的加载。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含**main()**方法的类),虚拟机会先加载这个类。
- 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
2.被动引用
除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。
被动引用的常见例子包括:
通过子类引用父类的静态字段,不会导致子类加载。
java
System.out.println(SubClass.value); // value 字段在 SubClass类的父类中定义
通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
java
SuperClass[] sca = new SuperClass[10];
类加载器
1.什么是类加载器
在类加载过程的加载阶段,通过类的完全限定名,获取描述类的二进制流的实现类,被称为"类加载器"。
2.类加载器分类
从 JVM 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机的一部分;
- 其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
3.什么情况下需要自定义类加载器?
- 隔离加载类。在某些框架内进行中间件与应用的模块之间进行隔离,把类加载到不同的环境。
- 修改类加载方式。
- 扩展加载源。比如:从数据库、网络、电视机顶盒进行类加载。
- 防止源码泄漏。比如:编译时字节码进行加密,需要通过自定义类加载器对字节码进行解密还原。
对象的创建过程
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查 通过后,接下来虚拟机将为新生对象分配内存 。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式 有 "指针碰撞 " 和 "空闲列表" 两种。
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除 ",还是"标记-整理"。
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置 ,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
step5:执行 init 构造方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java 程序的视角来看,对象创建才刚开始,<init> 构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 **<init>**构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。