java类加载机制

JAVA类是在运行时进行转载的,这种动态机制虽然降低了些许性能,但是使用起来更加灵活。相比编译时候需要连接的语言C++等来说。
类加载过程分为:加载-验证-准备-解析-初始化-使用-卸载。其中加载、验证、初始化的开始顺序是固定,但是解析有可能放在初始化后面。并且他们也有可能是交叉进行的。

初始化阶段

jvm对加载没有强制的规定,但是对类的初始化有了强制的规定。即对一个类的主动引用,主动因为指的是一下5中情况。

  1. new对象、调用对象的static属性(final除外因为final直接进入了运行时常量池)、调用对象的static方法。
  2. 初始化一个类时候发现父类没有初始化要对父类进行初始化
  3. main方法所以在的类
  4. 反射时候如果发现类没有初始化
  5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(实际上是因为第一条的规定)

    注意:接口初始化和类很类似也需要初始化父类接口的class对象,不过不需要初始化所有父类接口,只有使用父类时候才会初始化(使用父类接口的常量)。

加载过程

加载

这里只的是类加载的“加载过程”。记载分为3个过程

  1. 根据类全限定名找到对应外部二进制文件
  2. 将二进制文件加载到内存中
  3. 在方法区创建对应的class类做外部访问数据的接口

    几种外部的二级制文件加载方式zip包、网络、运行时、数据库等等
    注意:数组的加载方式,由虚拟机直接产生。去掉数组的一维,如果是引用类型的话即组件类,该组件类的Classloader加载该类,并且数组类会在组件类的Classloader的的类空间进行标示。如果是int等非引用类型,我们会调用引导类的类加载器。访问方式取决于组件类的访问方式。

验证

java虚拟机校验类文件的合法性,防止恶意篡改、类错误导致虚拟机或者程序崩溃。主要有以下四个阶段

  1. 文件格式校验:判断class的魔数、版本等,通过校验后会将二进制流读入到方法区,后续的校验会针对方法区的二进制流进行校验(检查文件)
  2. 元数据校验:对字节码元数据进行分析,查看是否有父类,父类是否合法。(检查类)
  3. 字节码校验:保证方法体字节码的合法性,比如是否有非法的类型转换、赋值能。jdk1.7后增加了StackMapTable用来保存方法体中初始的状态,校验时候直接通过读取StackMapTable即可(检查方法)
  4. 符合引用校验:字段的引用是否合法,是否访问了不存在的字段、方法、以及超过了访问权限等

准备

java虚拟机为类的变量分配内存的过程,这里指的是static修饰的变量因为实例变量会在实例创建时候赋值。对于一般情况变量分配完内存后要赋0值,真实的值保存在<clinit>()方法中,在初始化时候才会赋值。final修饰的变量会在该阶段赋值真实值

解析

java将class文件中的符号引用改为直接引用的过程

字段的解析

  • 普通引用比如对于字段N类型C来说,将N交给类的ClassLoader加载C的类型
  • 数组加载数组的ComponentType的类,然后由虚拟机生成数组的直接应用
  • 校验范围权限

    类方法的解析

  • 检查类的class_index如果是类方法,class_index是接口方法直接抛出java.lang.IncompatibleClassChangeError异常。

  • 类中有方法的简单名称和描述符返回,否则去父类查找如果有返回,否则去接口查找如果接口有抛异常java.lang.AccessMethodError,如果没有抛出java.lang.NoSuchMethodError
  • 检查访问权限

    接口方法解析:步骤同上只是不用检查权限

初始化

在该阶段开始执行java代码(字节码),生成方法<clinit>()。

  • clinit方法是由静态字段和静态代码块共同合并生成,且顺序自上到下
  • 如果有父类,且父类有有静态类和静态字段,父类的clinit方法会先调用
  • 接口因为也可能有静态阶段,接口也可能生成clinit,如果有父类接口,只有用到父类接口的变量才会生成父类接口的clinit方法

注意clinit出于线程安全考虑会加锁这就是为什么static代码块只执行一次。如果static里阻塞会阻塞类的创建

类加载器

唯一性:ClassLoader+类=唯一,每个ClassLoader都有自己的类命名空间,这个会影响到instanceOf等结果。

java的classloader分为3种

  1. 启动类加载器:Bootstrap ClassLoader,由C++编写,java无法直接调用,如果将类委托给该加载器调用改classloader回报异常。用于加载JAVA_HOME/lib以及 -Xbootclasspath对应地址的类并且要符合命名规则,比如rt.jar
  2. 扩展类加载器:Extension ClassLoader,由java编写,java可直接调用。JAVA_HOME/ext/lib或者java.ext.dirs指定的类
  3. 应用类加载器:Application ClassLoader,由java编写,java可直接调用。classpath中的类,如果不指定自己的classloader他是默认的classloader

类加载的双亲委派模型:即类加载器会优先调用父类的加载器,父类加载器会调用直到启动类加载器,之后在调用自己的类加载器。好处在于对于一些系统的类全局只有一个唯一的Class类(受唯一性约束)。顺序是:启动类加载器–>扩展类加载器–>应用类加载器–>自定义类加载器。

这是java建议的使用方法

双亲委派模型的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized Class<?>loadClass(String name,boolean resolve)throws ClassNotFoundException {
//首先,检查请求的类是否已经被加载过了
Class c=findLoadedClass(name)
if(c==null){
try{
if(parent!=null){
c=parent.loadClass(name,false);
} else {
c=findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
} if(c==null){
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
c=findClass(name);
}