类的加载过程
类的加载过程
加载 –> 验证 –> 准备 –> 解析 –> 初始化
加载
首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据
获取二进制字节流并没有规定必须从class文件中获取,怎么获取,所以有了从ZIP包中读取,例如war,jar,ear格式,从网络中获取,例如Applet ,运行时计算生成,例如动态代理技术,其他文件生成,例如jsp应用
加载完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区内
验证
目的时为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会产生危害,主要包括4个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证
- 文件格式验证
1、是否以魔数开头(每种文件类型的固定标记)
2、jdk的版本号是否在虚拟机的处理范围之内
3、常量池中的常量是否有不被支持的类型
。。。
通过这个阶段的验证后,字节流才会存储到内存的方法区,之后的所有阶段都是基于方法区的存储结构进行 - 元数据验证
语言规范校验,是否有父类,是否继承了被final修饰的类。。。
- 字节码验证
数据流和控制流分析,例如声明了一个int类型的字段,却存储了long类型的值,方法体中的类型转换都是有效的,这些校验还都是用程序来做, 所以就是用程序来校验程序,并不能做到绝对的准确,在jdk1.6之后进行了优化,增加了StackMapTable(栈图)的属性,不需要程序推导合法性
- 符号引用
能否找到对应的类,字段,方法,是否可以访问,不会报NoSuchMethod,NoSuchFieldError错误,
准备
为类变量(static修饰)分配内存并设定初始值,只是初始值而不执行真正的赋值动作
解析
将常量池中的符号引用替换为直接引用
初始化
执行类构造器
有以上特点,所以有继承关系的类的加载顺序为
父类的静态字段或者静态语句块 –> 子类的静态字段或静态语句块 –> 父类普通变量以及语句块 –> 父类构造方法被加载 –> 子类变量或者语句块被加载 –> 子类构造方法被加载
虚拟机在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,只会有一个线程去执行这个类的
类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身共同确定在虚拟机中的唯一性,比较两个类是否相等,这两个条件必须同时满足
- 类加载器分类
启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader), 应用程序类加载器(Application ClassLoader)
这几种加载器之间的关系如下图:
这种层次关系称为类加载器的双亲委派模型,如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,所有的加载请求最终都会传送到顶层的启动类加载器,只有当父类加载器无法完成时(搜索范围内没有找到该类),子加载器才会尝试自己加载 这种模型的好处是
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全