深入理解java虚拟机——类加载机制及双亲委派模型详解
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhangjingao/article/details/86680206
定义
类加载机制指的是:虚拟机将描述类的数据从class文件加载到内存中,对加载的数据进行验证,解析,初始化,最后得到虚拟机认可后转化为直接可以使用的java类型的过程
类加载机制一共有七个阶段:加载,验证,准备,解析,初始化,使用,卸载。其中的验证,准备,解析合称为连接阶段。
加载,验证,准备,初始化,卸载的顺序是确定是,另两个由动态绑定等情况可能会在初始化后面。
画个草图:
依次来介绍下各个阶段的工作
1. 加载
加载就是将表示类的文件加载进内存中以便于虚拟机调用。但是类的来源,java并没有限制,可以class文件;可以是zip、jar、war;网络流(applet);运行时计算生成(动态代理);其他文件(jsp)。其实JVM的想法是成为一个跨平台,跨语言的虚拟机,而不是java的跨平台虚拟机,但是目前仅支持一部分小众语言。所以它并没有定义的很具体,类加载器也可以由用户自定义的来完成。
加载的过程分为三步:
1.1 通过类的全限定名获得类的二进制字节流;
1.2 将这个字节流的静态存储结构转化为方法区的运行时结构;
1.3 在内存中生成一个这个类的Class对象,作为方法区这类的入口。
一个特殊的是数组类型:
如果被加载的是一个数组类型,数组类型是一个比较特殊的类型,他不通过类加载器加载,而是由虚拟机去完成。但是他的引用却要考加载器加载,加载器会加载完数组的数据类型后将该数组绑定到相应的加载器上,然后与该类加载器一起绑定标识唯一性。
2. 验证(连接分为三步:验证,准备,解析)
目的是确保字节流符合虚拟机的规范,不会危害虚拟机。,如果其中的格式或者内容不符合虚拟机的规范,都会抛出异常。
2.1 文件格式验证
验证字节流是否符合class文件的规范,并且能被当前虚拟机处理。
比如:是否以魔数0xCAFEBABE开头;主次版本号是否在该虚拟机处理的范围内;
…
2.2 元数据验证
验证类是否符合规范。
是否继承有父类,除Object类外所有的类都要有父类;
是否继承了不允许被继承的类;
…
2.3 字节码验证
确定程序语义是合法的,符合逻辑的。
定义了一个int型,却按long型操作;
方法体内的类型转换是有效且可行的,比如可将子类赋值给父类,父类不能赋值给子类。
……
2.4 符号引用验证
对类自身信息进行匹配性校验。
引用的类或者方法是否存在;
指定的类中是否存在符合方法的字段;
……
3. 准备(连接分为三步:验证,准备,解析)
为类变量分配内存并设置初始值。类变量指被static修饰的变量,并且会被分配到方法区,实例变量才会分配到堆区。初始值一般为0,引用变量为null,如果类变量还被final修饰,那么会直接分配指定的值。
下面是各个基本数据类型的初始值:
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | ‘\u0000’ | reference | null |
byte | (byte)0 |
只有类变量被final修饰(比如下面这个类变量)就会用value生成一个COnstantValue属性,在准备阶段给变量初始化为指定的值。
public static final int value = 123;
4. 解析(连接分为三步:验证,准备,解析)
解析是将常量池中的符号引用替换成直接引用的过程。(符号引用可以理解为这个引用的名字(标志符),直接引用代表这个引用的目标指针、相对偏移量或者间接定位到目标的句柄)
解析主要针对:类或接口,类方法,接口方法,字段,方法属性,方法句柄,调用点限定符。
4.1 类或接口的解析
如果类N不是数组,那么会将N传给类加载器;
如果类N是数组并且元素类型是对象,那么先用类加载器加载元素类型,再由虚拟机创建对组对象。
判断加载这个被加载类类的调用类是否有权限访问被加载类,如果不允许访问,那么抛出IllegalAccessError异常。
4.2 字段的解析
字段的解析首先先找到所属类或者接口的符号引用。如果类或者接口中直接有,那就返回字段的直接引用;如果没有则由下而上找其实现的接口;还没有就由下而上搜索继承的父类;如果这两者中有字段的直接引用,那就中断搜索返回直接引用,如果没有的话,那么就解析失败,抛出NoSuchFieldException异常,相信很多都见过这个异常。
最后判断字段访问权限,如果该字段不允许访问,那么抛出IllegalAccessError异常。
4.3 类方法的解析
首先找到所属类,类和接口的方法的符号引用是分开的,如果发现类方法引用的是个接口,那么就会报异常;
如果所属类中有,那就返回直接引用;如果没有则查询其父类;再没有查询其父接口,注意,,这个时候如果在父接口中找到了,说明这是一个抽象类,抛出AbstractMethodErroe异常。如果查找成功了就检查方法权限,如果权限不允许调用类访问,那么抛出IllegalAccessError异常。
4.4 接口方法的解析
首先找到所属接口,和类方法类似,如果发现接口方法引用的是个类方法,那么就会报异常;
如果所属类中有,那就返回直接引用;在没有查询其父接口,有就返回直接引用;如果没有就抛出NoSuchFieldException异常,相信很多都见过这个异常。
最后判断字段访问权限,如果该字段不允许访问,那么抛出IllegalAccessError异常。
5. 初始化
前面准备阶段,已经对类变量分配了内存,现在开始对类变量和静态代码块进行赋值和执行。这个时候虚拟机会为有类变量和静态代码块的类或者接口生成()方法(不是构造方法)。这个方法有以下几个特点。
5.1 这个方法首先会调用父类的这个方法,确保父类的方法已经执行;
5.2 由于父类的方法首先执行,所以父类的静态代码块也会优先于子类执行。
5.3 如果类或者接口没有类变量和静态代码块(接口没有静态代码块,针对类)可以不存在
5.4 接口不需要先执行父接口的方法。
5.5 虚拟机会保证在多线程环境下方法能被正确的加锁,保证线程安全。
5.6 方法同一个加载器下只会执行一次。也就是说如果有多个线程同时请求加载一个类,只有一个线程会进入方法,其他线程被阻塞,当运行完方法后,释放锁,但是其他线程也不会在执行方法了。
双亲委派模型
类加载器有是三个:启动类加载器、扩展类加载器、应用程序加载器(系统加载器)
工作过程是:如果一个类加载器收到了一个类加载的请求,它首先不会去加载类,而是去把这个请求委派给父加载器去加载,直到顶层启动类加载器,如果父类加载不了(不在父类加载的搜索范围内),才会自己去加载。
1. 启动类加载器:加载的是lib目录中的类加载出来,包名是java.xxx(如:java.lang.Object)
2. 扩展类加载器:加载的是lib/ext目录下的类,包名是javax.xxx(如:javax.swing.xxx)
3. 应用程序扩展器:这个加载器就是ClassLoader的getSystemClassLoader的返回值,这个也是默认的类加载器。
双亲委派模型的意义在于不同的类之间分别负责所搜索范围内的类的加载工作,这样能保证同一个类在使用中才不会出现不相等的类,举例:如果出现了两个不同的Object,明明是该相等的业务逻辑就会不相等,应用程序也会变得混乱。