1.概述
定义如下: 虚拟机把描述类的数据从Class文件(已经编译成Class文件了)加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
类从被加载到到虚拟机内存开始 ->到卸载出内存为止,生命周期如下:
其中验证,准备,解析三个部分统一称为连接。
加载,验证,准备,初始化,和卸载这5个阶段的顺序时确定的。而解析则不一定。在某些情况下可以在初始化之后再开始解析,目的就是为了支持java语言的动态绑定(即运行时绑定或晚期绑定)。
虚拟机规定了5种情况必须立即对类进行初始化:
(1)遇到new,getstatic,putstatic,invokestatic这四条指令时。
new->实例化对象
getstatic,putstatic->读取,设置一个类的静态字段(final修饰,已在编译期把结果放入常量池的静态字段除外)
invokestatic->调用一个类的静态方法
(2)使用java.lang.reflect包的方法对类进行反射调用的时候
(3)初始化一个类时其父类还没有初始化,显初始化父类
(4)虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)
(5)动态语言,一个java.lang.invoke.MethodHandle实例最后的极细结果有REF_getStatic,REF_putStatic,REF_involeStatic的方法句柄时。
1.1加载
加载是类加载(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流(并未指定从哪儿获取,怎样获取,也可以从jar读取,也可以从网络中获取,也可运行时计算生成,如动态代理,甚至类加载器也可以自己写)
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口(p.s. 这里和反射是一样的)
数组类的加载本身不通过类加载器创建,由java虚拟机直接创建。
(1)若数组类的组件类型时引用类型,就遍历按照类的加载方法去加载
(2)若不是引用类型,如int[]数组,虚拟机会把数组与引导类加载器关联
(3)数组类的可见性与它的组件类型的可见性保持一致
1.2.1 验证
目的是确保class文件的字节流中的信息符合当前虚拟机的要求,并且不会危害虚拟机。包括
(1)文件格式验证。如是否以魔数0xCAFEBABE开头等
(2)元数据验证。对字节码描述的信息进行语义分析,保证其描述的信息符合规范。是否有父类,是否继承了不允许被继承的类
(3)字节码验证。确保程序语义是合法的,符合逻辑的
(4)符号引用验证。符号引用中通过字符串描述的全限定名是否能找到对应的类
1.2.2 准备
为类变量分配内存并设置类变量的初始值。这些类变量所使用的内存将在方法区中进行分配。注意仅包括类变量不包括实例变量。
两个关键字:类变量+初始值
初始值是数据类型的零值。例如
public static int value = 123;
变量value在准备阶段过后的初始值为0,而不是123。而把value赋值为123的putstatic指令是在程序编译之后,存放于类构造器<clinit>()方法之中。所以把value赋值为123的动作在初始化阶段才会执行。
但是如果类字段表的字段属性表中存在ConstantValue属性,那么在准备阶段value就会被初始化为ConstantValue属性所指定的值。如:
public static final int value = 123;
编译时javac将会为 value生成ConstantValue属性,并在准备阶段就根据ConstantValue的设置将value赋值为123.
1.2.3 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关
- 直接引用(Direct References):直接指向目标的指针,相对偏移量或一个能间接定位到目标的句柄
详细解析步骤参见深入理解java虚拟机。
1.3 初始化
类加载过程的最后一步。在前面的过程中除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作全由虚拟机主导和控制。
初始化阶段,才真正开始指向类中定义的java程序代码(字节码。初始化阶段是执行类构造器<clinit>()方法的过程。
- <clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生。编译器收集的顺序按照代码源文件顺序,静态语句块只能访问到定义在静态语句块之前的变量
- <clinit>()方法与类的构造函数,或者说是实例构造器<init>()方法不同,它不需要显示的调用父类构造器。虚拟机会保证在子类的<clinit>()执行之前,父类的<clinit>()方法已经执行完毕。可以想到,虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
- 由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。例如
public class TestClinit{
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args){
System.out.println(Sub.B);
}
}
打印的结果字段B的值将会是2而不是1
- <clinit>()方法对于类或接口不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,则编译器可以不生成<clinit>()方法
- 接口仍然有变量初始化的赋值操作,接口与类一样都会生成<clinit>()方法。但接口的<clinit>()方法不需要先执行父接口的<clinit>()方法方法。只有当父接口中的变量使用时,父接口才会初始化。接口的实现类在初始化时一样不会执行接口的<clinit>()方法。
- 虚拟机会保证<clinit>()方法在多线程环境下的正确性
2.类加载器
定义:通过一个类的全限定名来获取描述此类的二进制字节流, 并且这个动作是放到java虚拟机外部去实现的,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为-类加载器。
(1)对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟中的唯一性,每一个类加载器,都拥有一个独立的名称空间。
(2)带来的影响:比较两个类是否"相等", 只有在这两个类是由同一个类加载器加载的前提下才有意义。即使两个类来源于同一个Class文件,被同一个虚拟机加载,但是他们的类加载不同,那这两个类就必定不相等。
(3)这里所指的"相等",包括代表类的Class对象的equals()方法,isAssignableForm()方法,isInstance()方法返回的结果相同
2.1双亲委派模型
从虚拟机角度来讲,存在两种不同的类加载器
- 启动类加载器 -Bootstrap ClassLoader。C++语言实现,是虚拟机自身的一部分
- 其他的类加载器-Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
从java开发人员角度来看,有三种3种类加载器
- 启动类加载器 -Bootstrap ClassLoade
- 扩展类加载器-加载<JAVA_HOME>lib\ext目录中所有类库
- 应用程序类加载器-加载用户类路径(ClassPath)上指定的类库