类加载的过程
1 加载
注意:“加载”是“类加载”(Class Loading)过程的第一步
1.1 加载的过程
在加载过程中,JVM主要做3件事情
- 通过一个类的全限定名来获取定义此类的二进制字节流(class文件)
在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程 - 将这个字节流的静态存储结构转化为方法区的运行时数据结构
- 在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口
程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口
1.2 从哪里加载
JVM规范对于加载过程给予了较大的宽松度.一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取
- 从ZIP包中读取
Jar、War、Ear等 - 其它文件生成
由JSP文件中生成对应的Class类. - 从数据库中读取
将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发. - 从网络中获取
从网络中获取二进制字节流.典型就是Applet. - 运行时计算生成
动态代理技术,用PRoxyGenerator.generateProxyClass为特定接口生成形式为"*$Proxy"的代理类的二进制字节流.
1.3 类和数组加载过程的区别
数组也有类型,称为“数组类型”.如:
String[] str = new String[10];
这个数组的数组类型是Ljava.lang.String
,而String只是这个数组的元素类型.
当程序在运行过程中遇到new关键字创建一个数组时,
由JVM直接创建数组类,再由类加载器创建数组中的元素类型.
而普通类的加载由类加载器创建.既可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成(即重写一个类加载器的loadClass()方法).
1.4 加载过程的注意点
- JVM规范并未给出类在方法区中存放的数据结构
类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,虚拟机规范并没有指定 - JVM规范并没有指定Class对象存放的位置
在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类的对象,作为本类的外部访问接口.既然是对象就应该存放在Java堆中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象.HotSpot将Class对象存放在方法区. - 加载阶段和链接阶段是交叉的
类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制.也就是说,类加载过程中,必须按照如下顺序开始:
加载、链接、初始化,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉
2 验证
验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间
2.1 验证的目的
验证是为了保证二进制字节流中的信息符合虚拟机规范,并没有安全问题.
2.2 为什么需要验证
虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行.也就是说,Java语言的安全性是通过编译器来保证的.
但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。
通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到的都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证!
2.3 验证的过程
- 文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.
本验证阶段是基于二进制字节流进行的,只有通过了本阶段的验证,才会被允许存入到方法区中存储.
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流.
通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区。
而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区。
也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作。这个过程印证了:加载和验证是交叉进行的。
- 元数据验证
对字节码描述信息进行语义分析,确保符合Java语法规范. - 字节码验证
本阶段是验证过程的最复杂的一个阶段.
本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件.
字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全 - 符号引用验证
发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行.
3 准备
准备阶段完成两件事情:
- 为已在方法区中的类的静态成员变量分配内存
-
为静态成员变量设置初始值
初始值为0、false、null等
public static final int value = 123;
准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123).
4 解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程.
5 初始化
真正开始执行类中定义的Java程序代码(或者说是字节码)
初始化阶段就是执行类构造器clinit()的过程.
clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。
初始化过程的注意点
- clinit()方法是IDE自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,IDE收集的顺序是由语句在源文件中出现的顺序所决定的.
- 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
public class Test {
static {
i=0;
System.out.println(i);//编译失败:"非法向前引用"
}
static int i = 1;
}
- 实例构造器init()需要显示调用父类构造函数,而类的clinit()不需要调用父类的类构造函数,虚拟机会确保子类的clinit()方法执行前已经执行完毕父类的clinit()方法.因此在JVM中第一个被执行的clinit()方法的类肯定是java.lang.Object.
- 如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会为此类生成clinit()方法.
- 接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。
- 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法.不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法.
- 虚拟机会保证在多线程环境中一个类的clinit()方法别正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的clinit()方法,其它线程都被阻塞等待,直到活动线程执行clinit()方法完毕.
其他线程虽会被阻塞,只要有一个clinit()方法执行完,其它线程唤醒后不会再进入clinit()方法.同一个类加载器下,一个类型只会初始化一次.