java类加载机制
类加载器
能够通过一个类的全限定名来获取此类的二进制字节流的代码模块称之为类加载器。加载类的这个动作是放在JVM外部实现的,以便让应用程序决定如何获取所需的类。
虽然说类加载器的主要目的是完成这个动作,但是其作用却远远不仅于此。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都需要
每一个类在编译后都会产生一个Class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class文件),而类加载器实际上就是一条类加载链,原生的类加载器(java.lang.ClassLoader)只加载Java api等可信类,从本地加载,其他如果需要从网络上或者其他途径加载就需要使用额外的类加载器了。不过一般来说,原生的类加载器就够我们用了。
这里说的类加载链,在Java中是按照树形结构来组织的,也就是说会有父子加载器之分。类加载器的加载顺序也会考虑到这个。
类加载器的加载顺序有两种:
- 父类优先策律:算作是比较一般的情况了,比如常用的JDK就是这样(双亲委派模型),在加载某个类之前,会尝试先交给其父类加载器,只有当父类加载器找不到时,才会尝试自己去加载。
- 自己优先策略:与父类优先策略相反,当找不到自己的类加载器时,才会去找父类加载器加载。常见与web容器。
一般情况下,不需要关心类加载器的层次结构如何如何,但是当你需要干涉是使用哪个类加载器的话,可以考虑使用ClassLoader.loadClass(String)方法,因为该方法需要一个CLassLoader对象,所以可以根据需要指定使用哪一个ClassLoader对象。该方法只将.class文件加载到内存中,并不会执行类的初始化,只有当需要时才会进行初始化。属于动态加载。
JVM提供了3种类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载从JAVA_HOME\lib目录中的(rt.jar),或者通过-Xbootclasspath参数指定的类。这些类需要备虚拟机认可识别。
- 扩展类加载器(Extension ClassLoader):负载加载JAVA_HOME\lib\ext目录中的,活通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
双亲委派模型:
双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.
这样做的好处是,Java类随着它的类加载器一起具备了带有优先级的层次关系。比如存放在rt.jar中的Object类,无论那个类加载器加载哪个类,最终都是会让启动类加载器加载这个Object类,加载的都是同一个Object对象。如果由各自的加载器去加载的话,会不止出现一个Object类,最基础的行为都无法保证,应用程序会一派你混乱。
类加载机制
类使用的流程:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
其中,验证、准备、加载这三个部分统称为链接(Linking),加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,而解析与加载却并不一定,如解析可以在初始化阶段之后再执行,支持Java的动态绑定,加载可以根据需要动态加载。
加载(Loading)
加载是类加载的第一个阶段,分为两种加载
- 预加载:JVM启动时加载rt.jar下的.class文件,加载一些非常非常常见的class,如java.lang.*、java.util.*、java.io.*等等。
- 运行时加载:JVM在用到一个.class文件时,会先去内存中检查这个.class文件有没有被加载,如果没有,就按照类的全限定名来加载这个类
加载主要完成的了以下三件事:
获取.class文件的二进制流
将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
在内存中生成一个代表这个.class文件的java.lang.Class的对象,作为方法区这个类的各种数据的访问入口。
验证(Verification)
这一阶段的目的是为了确保.class文件中的字节流文件包含的信息符合当前虚拟机的要求,且不会危害虚拟机。之所以要检查,是因为.class文件可以由很多种方式得到,并不一定都要由java源码编译而来,甚至可以直接通过十六进制编辑器编辑。所以验证,就变得很重要了。
大概分为以下几个方面的验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备(Preparation)
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。被static修饰的变量也叫类变量,在准备阶段,会给那些不被final修饰的static变量分配零值,被final修饰的分配指定的值。
解析(Resulotion)
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。大概是一个映射吧。
符号引用是对类、变量、方法的描述,与虚拟机内存布局无关。
符号引用包含了三类常量
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
直接引用可以是直接指向目标的指针、相对偏移量、或者是一个句柄,与虚拟机的布局息息相关。
初始化(Initialization)
初始化阶段是类加载过程的最后一步,初始化过程就是一个执行类构造器
根据java虚拟机规范,所有java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化。
主动使用有以下6种:
- 创建类的实例
- 访问某个类或者接口的静态变量,或者对该静态变量赋值(如果访问静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
- 调用类的静态方法
- 反射(Class.forName(xxx.xxx.xxx))
- 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化(参见示例6)
- Java虚拟机被标明为启动类的类(包含main方法的)
类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。
使用(Using)
卸载(Unloading)
在一个类被加载,链接,初始化后,它的生命周期就开始了,当一个类的Class不再引用,或者说不可触及的时候,Class对象就会结束生命周期,此时,由GC负责卸载。(特殊的,Java虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,因为java虚拟机会始终引用这些类加载器,而这些类加载器会始终引用他们所加载类的Class对象,而由用户自定义的类加载器加载的类是可以被卸载)
类的引用关系
- 加载器和class对象
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用,另一方面,一个Class对象总是会引用他的类加载器,调用Class对象的getClassLoader方法就可以获得它的类加载器。由此可见,Class实例和加载它的加载器之间是双相关联关系。 - 类、类的class对象、类的实例对象
一个类的实例总是引用代表这个类的Class对象,在Object类中定义类getClass方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,他引用代表这个类的Class对象。
一个例子
loader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。