虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
一、加载的时机
1、类的生命周期
类的生命周期有一下7个阶段:
- 加载 loading
- 验证 verification
- 准备 preparation
- 解析 resolution
- 初始化 initialization
- 使用 using
- 卸载 unloading
验证、准备、解析三个部分统称为连接。
其中,加载、验证、准备、初始化和卸载这5个阶段的顺序的确定的,但是解析阶段则不一定,有些情况下回在初始化之后进行。
2、初始化
对于初始化阶段,严格规定有且只有一下5种情况必须立即进行初始化(加载、验证、准备需要在此之前进行)。
- 遇到new、getstatic、putstatic、或invokestatic这4条字节码指令时。即使用new实例化一个对象、读取或设置一个类的静态字段(final修饰的常量除外)、及调用类的静态方法。
- 使用java.lang.reflect包的方法就类进行反射调用到时候
- 初始化一个类的时候,如果父类还没有初始化,要先初始化其父类
- 虚拟机启动时,会先初始化唯一的主类(包含main()那个类)
- JDK1.7 中动态语言支持的一些情况
只有这5中情况才会触发类的初始化,这5种场景中的行为被称为主动引用。除此之外,所有引用类的地方都不会触发初始化,被称为被动引用。下面是几个被动引用的例子:
- 通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化)。
- 通过数组定义类应用类:ClassA [] array=new ClassA[10]。触发了一个名为“[ClassA”的类的初始化,它是一个由虚拟机自动生成的、直接继承于Object的类,创建动作由字节码指令newarray触发。
- 常量会在编译阶段存入调用类的常量池。
二、加载的过程
1、加载
加载阶段,虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.class.Class对象,作为方法区这个类的各种数据的访问入口。
关于二进制流
可以来自很多地方,比如:
- ZIP包中,也是JAR、EAR、WAR格式的基础
- 从网络中获取,最典型的就是Applet
- 运行时计算生成,使用最多的就是动态代理
- 由其他文件生成,典型场景JSP
- 从数据库中读取
数组类的加载
对于非数组类,可以使用系统提供的引导类加载器加载,也可以使用用户自定义的加载器加载。
但是对于数组类情况就不同,数组类本身不通过类加载器创建,而是由java虚拟机直接创建。
2、验证
验证是连接的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。包含以下4个阶段:
- 文件格式校验:
- 以魔数0xCAFEBABE开头
- 主次版本号是否可处理
- 常量池的常量是否有不被支持的类型
- 等等。。。。
- 元数据验证
- 字节码验证
- 符号引用验证
3、准备
准备阶段是为类变量分配内存并设置初始值的阶段。
这一句话中包含三个关键信息:
仅仅是类变量(static修饰)而不包含实例变量
类变量的内存被分配在方法区中,而实例变量是在堆中
准备阶段变量的初始值都是零值,比如
1
2//在准备阶段value的值是0,初始化阶段才会赋值为123
public static int calue=123;
有一种特殊情况:如果类变量的字段属性表中存在ConstantValue属性,那么在准备阶段就会被直接初始化为ConstantValue属性指定的值:
1 | //准备阶段value值就是123 |
4、解析
解析是将常量池内符号引用替换为直接引用的过程。
首先区别下符号引用于直接引用:
- 符号引用:以一组符号来描述所引用的目标,与虚拟机的内存布局无关,引用的目标不一定已经加载到内存中。
- 直接引用:可以是直接指向目标的指针。相对偏移量或是一个能节间定位到目标的句柄。直接引用于虚拟机内存布局相关,如果有了直接引用,那么引用的目标必定在内存中存在。
下面是4中引用的解析:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
5、初始化
初始化是类加载的最后一步,到了初始化阶段,才真正开始执行java代码中的程序。
在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法<clinit>()
, 另一个是实例的初始化方法<init>()
。而初始化的过程就是执行类构造器<cinit>()
方法的过程。
关于 <cinit>()
:
<cinit>()
是由编译器自动收集类中都有类变量的赋值动作和静态语句块中语句合并产生的。<cinit>()
方法不是必需的,如果一个类中没有静态语句块或静态变量赋值,那么久可以不生成<cinit>()
方法。<cinit>()
不需要显示的调用,虚拟机会保证子类的<cinit>()
之前,父类的<cinit>()
已经执行完毕。所以虚拟机中第一个被执行的<cinit>()
方法肯定是java.lang.Object。编译器的收集顺序由代码顺序决定,静态语句块只能访问到之前的变量,对于之后的变量,只能赋值不能访问,比如
1
2
3
4
5
6
7public class Test{
static{
i=0; //给后面的变量赋值可以编译通过
System.out.println(i); //这句编译器会提示“非法向前引用”
}
static i=0;
}
三、类加载器
上面提到类的加载阶段需要完成三件事,这三件事就是通过类加载器完成的
1、类与类加载器
在Java中,任何一个类都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每个类加载器都有独立的类名称空间。也就是说,比较两个类是否相等,只有这两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的加载器不同,那么这两个类就不相等。这里的相等包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果。
比如:
1 | import java.io.IOException; |
输出为:
class interview.ClassLoaderTest
false
因为interview.ClassLoaderTest默认使用Application ClassLoader加载,而obj是通过自定义类加载器加载的,类加载不相同,因此不相等。
类加载器是个很强大的概念,很多地方被运用。最经典的例子就是AppletClassLoader,它被用来加载Applet使用的类,而Applets大部分是在网上使用,而非本地的操作系统使用。使用不同的类加载器,你可以从不同的源地址加载同一个类,它们被视为不同的类。J2EE使用多个类加载器加载不同地方的类,例如WAR文件由Web-app类加载器加载,而EJB-JAR中的类由另外的类加载器加载。有些服务器也支持热部署,这也由类加载器实现。你也可以使用类加载器来加载数据库或者其他持久层的数据。
2、双亲委派模型
2.1类加载器种类
从Java虚拟机的角度,类加载器只有两种:一种是启动类加载器(Bootstrap ClassLoader),这个加载器由C++语言实现是虚拟机自身的一部分。另一种是其他的类加载器,由Java语言实现,全部都继承自抽象类java.lang.ClassLoader。
从开发人员的角度,加载器分为三种:
- 启动类加载器(Bootstrap ClassLoader);负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
- 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
2.2、双亲委派模型
从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
- 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
- 每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
- 如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
注意几点:
- 这里类加载器之间的父子关系不是以继承的关系来实现,而是通过组合关系来复用父加载器的代码。
- 在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑。从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。在loadClass方法的逻辑里如果父类加载失败,就会调用自己的findClass()方法来完成加载。
3、类加载器的工作原理
类加载器的工作原理基于三个机制:委托、可见性和单一性。
- 委托机制:委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。
- 可见性:可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
- 单一性:单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。
4、如何显示加载一个类
Java提供了显式加载类的API:Class.forName(classname)和Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定类加载器的名称以及要加载的类的名称。类的加载是通过调用java.lang.ClassLoader的loadClass()方法,而loadClass()方法则调用了findClass()方法来定位相应类的字节码。