在最初学习java时,我们都知道我们编写的java代码在执行前会被编写成class字节码文件,然后通过类加载器加载到jvm虚拟机中去执行,并开辟内存空间存储其中的字节码对象与初始化赋值。在平时编写代码时,我们其实也常常与类加载器打交道,比如反射调用,读取配置文件,动态代理等。。那么,类加载器又是如何工作的呢,其中又是怎么加载对象,给变量赋值的呢,这就是这次我要学习的问题了。
类加载的过程
类加载的生命周期分为加载、验证、准备、解析、初始化、使用、卸载,共七个步骤,其中加载、验证、准备、初始化、卸载这五个步骤的顺序是固定的,依次执行,而解析和使用的执行时间会视代码的编码情况来确认。
那么类在加载过程中都做了什么呢?
1.加载:
在加载阶段,虚拟机需要完成以下3件事情
(1) 通过一个类的全限定名来获取定义此类的二进制字节流
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
这里的加载和类的加载不是一个性质,这里是类加载器加载入jvm的第一步,通俗点讲就是类加载器根据类的全类名找到类在文件夹中的位置,将这个文件转换为二进制流,通过读取流,把对应的数据结构转换为jvm可运行的数据结构存储到jvm的方法区中,最后再在堆内存中生成一个字节码对象与这个方法区的数据结构对应。
这里我们再复习一下jvm虚拟机中的内存结构
2.验证:
确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编译规则,这一步就是要过滤掉这部分不合法文件
3.准备:
准备阶段是正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配(因为这里的变量都是类变量,实例变量在堆,类变量在方法区),在这里进行的是初始化赋值,就是将类变量的值赋为默认值,如int为0,boolean为false,引用为null等等。。下面是例子
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行
4.解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用是在class字节码文件最初未被jvm虚拟机执行的时候不知道实际的地址,所以由符号来代替,在解析后,再转换成虚拟机可以识别的直接引用。
5.初始化:
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段变量已经付过一次系统要求的初始值,而在初始化阶段,则根据代码中编写的赋值去初始化类变量和其他资源
6.使用和卸载:
到这两个阶段的时候类的加载其实已经进入了尾声,内存中已经为类创建了对应的内存空间,为相应的变量赋值,并完成代码中的使用过程与使用过后的销毁。
到此,类的加载过程就已经解析完了,那么类中各种属性的加载时间与加载顺序又是什么样的呢?
类加载器的加载机制与加载顺序
类加载器在java中有三个内置的类加载器,他们负责加载不同的类。
(1) Bootstrap ClassLoader : 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用
(2) Extension ClassLoader : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
(3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。
类加载器使用的是双亲委派机制,来进行加载类。
双亲委派机制的原理:
如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
好处:
java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
类在加载时的执行顺序,代码如下:
public class Test {
private static Test test = new Test();
private static final int FINALNUM = 123;
private int num = 111;
private static int staticNum = 99;
{
System.out.println("FINALNUM="+FINALNUM+",staticNum="+staticNum+",num="+num);
System.out.println("=========1===========");
}
static {
System.out.println("FINALNUM="+FINALNUM+",staticNum="+staticNum);
System.out.println("**********2**********");
}
public Test() {
System.out.println("FINALNUM="+FINALNUM+",staticNum="+staticNum+",num="+num);
System.out.println("----------3----------");
}
public static void main(String[] args) {
System.out.println("FINALNUM="+FINALNUM+",staticNum="+staticNum);
System.out.println("##########4###########");
}
}
执行结果:
这里就要说一下类加载器的加载顺序了,为什么13会在最上方,13的staticNum会为0,所有的FINALNUM都会有值呢
总结:
因为类在加载类变量的时候会在准备阶段先把int的值赋值为0默认值,并且类加载器在加载静态变量时,是按照代码的执行顺序来加载的,也就是说会先执行静态的实例化对象的过程,而在实例化对象时,构造代码块的执行又是最优先级的,所以出现了这样的执行顺序,而被final修饰的变量,在加载时会被类认为是不再改变的,所以会优先直接写入到内存中,所以始终会被读取到值