类加载过程
- 加载 :加载是整个类加载的一个阶段,他们是两个不同的概念。在加载阶段,主要是:
- 通过类的全限定名来获取此类的二进制字节流。
- 将这个字节流锁代表的静态存储结构转化为方法区的运行时数据结构。
- 在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
- 连接 :这个阶段又可分为三个阶段:
-
验证 :目的是确保Class文件的字节流中的信息符合JVM规范,防止运行后危害虚拟机自身安全。大致上从以下四个阶段来完成验证阶段:
- 文件格式验证:验证字节流是否符合Class的规范,例如是否以魔数开头,主、次版本号是否能被接受等等。
- 元数据验证:对类的元数据信息进行语义检验,保证其符合《Java语言规范》。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行,例如通过全限定名是否能找到对应的类,符合引用中的类、字段、方法的访问权限。
-
准备 :准备阶段是正式为类变量(即static修饰的变量)分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。例如:
javapublic static int a = 100;
变量a在准备阶段过后的初始值为0而不是100 ,而将a赋值为100的putstatic指令实际上是在程序被编译后,存放在类构造器
<clinit>()
中,在初始化阶段的时候完成赋值。还有一种特殊情况,static final修饰的字段在javac编译时生成comstantValue属性,在类加载的准备阶段直接把constantValue的值赋给该字段。 -
解析 :解析阶段是Java虚拟机将常量池内的符号引用 替换为直接引用 的过程。
- 符号引用 :简单的理解就是字符串,在Class文件中的CONSTANT_Class_info、CONSTANT_Field_info等类型的常量。比如引用一个类,java.util.ArrayList 这就是一个符号引用,字符串引用的对象不一定被加载。
- 直接引用 :指针或者地址偏移量。引用对象一定在内存,也就是说目标必定已经被加载到内存中。
-
- 初始化 :初始化阶段就是执行类加载器
<clinit>()
的过程。<clinit>()
是javac编译的自动产物,是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>()
方法,也即<clinit>()
对于类和接口来说不是必须的。
类什么时候初始化?
在《JVM虚拟机规范中》没有对加载阶段进行强制约束,而对于初始化阶段,却有严格的规定,有且只有以下情况必须立即对类进行初始化(而加载、验证、准备则自然在这之前开始):
- 通过new关键字创建对象
- 访问类的静态变量,包括读取和更新(被final修饰、在编译时已经在常量池中的除外)
- 访问类的静态方法
- 对某个类进行反射操作
- 初始化子类会导致父类的的初始化
- 执行该类的main函数
除了以上几种的主动引用,以下情况被动使用,不会触发初始化: - 通过子类引用弗雷的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,就比如我们在准备阶段提到的使用final static修饰的变量。
- 使用:当JVM完成初始化阶段之后,JVM便开始从入口方法开始执行用户的程序代码。
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。
类加载器(ClassLoader)
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。实现这个动作的代码被称为"类加载器"(Class Loader)。对于
任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性
JVM提供了3中种类加载器:
- 启动类加载器 (Bootstrap):也叫引导类加载器,是由C++代码实现的。负责加载
JAVA_HOME\lib
目录的类库。 - 扩展类加载器(Extension):负责加载 JAVA_HOME\lib\ext 目录中的
- 应用程序类加载器(Application):负责加载用户路径(classpath)上的类库
JVM通过双亲委派 模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
值得注意的是:
- AppClassLoader的父加载器是ExtClassLoader;ExtClassLoader的父加载器是Bootstrap;Bootstrap是根加载器。三者之间是没有继承关系的。
- AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader。ClassLoader由一个parent字段,过设置该字段引用,指定父加载器。(是组合关系,即A has B)。
- AppClassLoader的parent指向ExtClassLoader,ExtClassLoader的parent指向null,(null的原因是因为Bootstrap是C++实现的,通过代码中逻辑判断来转向Bootstrap)
双亲委派
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派的好处 :
采用双亲委派的一个好处是比如加载位于rt.jar包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
双亲委派的好处 :
采用双亲委派的一个好处是比如加载位于rt.jar包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
如何打破双亲委派:AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader,在ClassLoader中有一个loadClass方法,就是实现双亲委派的代码实现,所以我们只要自定义的类加载器重新这个方法,就可以打破双亲委派机制。Tomcat就是打破了双亲委派。