Java 类加载机制
最近在看druid的源代码,在加载资源的时候,有看到下面的代码:
在加载类的时候使用两种机制来加载,之前有了解过JVM加载类是通过双亲委托的方式来完成的,但是一致没有深究这个问题,今天抽时间看了一下并总结成文案
加强一下记忆。
概述
JVM的类加载机制就是将字节码加载到堆内存中,并为之生成一个Class的对象。JVM 将类的加载操作剥离了 JVM,这样做的好处就是使得类的加载更加的灵活, 用户可以实现自定义的类加载器完成类的加载操作。
类的加载流程
在说类的加载流程之前,我们先想一下类加载的目的,这样接下来的分析或许才记得更清楚(这里说的加载并不是整个流程的加载阶段,而是整个初始化的过程)。
类加载的目的是为后续生成类对应的对象做铺垫,待加载的类中可能会引用其他的类(继承、组合),也有可能会包含静态字段(类级别的)、静态代码块。因此整
个加载的流程就需要正对上述的情况做处理。
如上图所示,整个加载流程可以分为:加载 -> 连接 -> 初始化
类初始化的条件
- 当遇到new(生成一个新的对象)、 getstatic、putstatic(上面两条指令是操作静态字段)、invokestatic(操作静态方法)这4个字节码的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 初始化一个类的时候,如果发现父类还没有初始化,会优先初始化父类
- MethodHandle实例最后的解析结果为static的引用的情况下(没有使用过这种方法)
- 虚拟机启动的时候指定的主类(一般是包含了Main函数的类)
总结一下,创建对象、调用静态的方法或者字段、通过反射的方式创建对象会导致类及父类的初始化。
加载
类加载阶段是通过的类的全限定名加载类的二进制字节流,并在堆内存生成相应的class对象,该class对象将代码中定义的字段转化成class中对应的数据, 这个class对象将作为方法区中相应字段的访问的入口,这里是访问的类的数据,并不是类生成的对象的数据。
连接
连接的整个过程又可以细分为以下步骤
验证
验证的主要目的是验证字节码符合JVM规范
- 文件格式校验:魔数、版本号的验证,确保字节流能够正确的解析并存在于方法区中
- 元数据校验:主要是语义校验,如final类型的变量是否被继承
- 字节码校验:方法体的校验,通过数据流和控制流分析程序的语义是正确的
- 符号引用的校验:将符号引用转化成直接引用(内存中的偏移量),并确定直接引用是否有足够的权限
总结一下:由表及里、由内而外的校验
准备
为类变量分配方法区中的内存并初始化类变量,这里是初始化类字段为对应类型的0值。
1 | public static int x = 1; |
在准备阶段并不会将x初始化为1,而是初始化成int对应的零值:0。
上述操作会存放于类构造器
解析
解析阶段将符号引用替换成直接引用:使用内存中的偏移量替换掉类中的符号。这里待替换的符号为
- 类或接口
- 类、接口中的字段、方法
初始化
初始化阶段是执行类构造器
1 | static { |
上面的操作中引用是非法的,但是赋值操作i=0是正常的,只不过i最终的值为1(由于执行的顺序是从上至下),虚拟机会保证在子类的
上面概要的说明了类初始化的整个流程,接下来看一下类加载相关的知识点。
双亲委托模型
JVM将类的加载放到虚拟机之外实现,这样做的好处是足够的灵活,我们完全可以按照自己的需要从外部读取字节码。JVM推荐的类加载的方式是双亲委托的方式
在收到加载类的请求的时候,当前的类加载器会将该请求委托给父类来加载,这样递归到BootstrapClassLoader,只有父类在确定无法完成该类的加载的时候,
才会将该请求传递给子ClassLoader,完成类的加载,对应的代码如下:
在遵循双亲委托的前提下,通常我们只需要实现ClassLoader的findClass()方法来定制类加载器即可(这样上面的双亲委托并没有被破坏),
不推荐覆盖loadClass方法,这样很可能在不知情的情况下破坏双亲委托的模式。启用双亲委托的方式来加载类的好处是java类随着类加载器具备了优先级的
层次关系,这样如果使用者写了一个和java类库中同路径、同名的类的时候,尽管编译成字节码没有问题,但是在运行的时候会报错。
双亲委托模型是推荐的类加载机制,但是有时候却不一定合适,由于本人做的比较多的是web开发,因此通过一个web应用来描述一下双亲委托的缺陷。
通常情况下,一个tomcat是可以部署多个web应用的,假设两个不同的web应用同时依赖两个不同版本的java类库,如果采用双亲委托的方式,
这些类库的载入必然使用到同样的类加载器,由于所加载的类仅仅只是版本有所不同,类的全路径是完全相同的,因此必然有一个类库无法加载成功。
这样我们势必要破坏这种双亲委托:由子类加载器完成类的加载,不再委托父类完成类的加载(重写类加载器的loadClass方法,在类加载的时候不去寻找
parent的类加载器即可)。上述实现思路可行,但是我们总要找到一种设置我们自定义类加载器的方式,这种方式就是线程上下文类加载器,也即是Thread
的setContextClassLoader、getContextClassLoader。通过线程上下文类加载器,我们就完成了子类加载器的委托。
发散思维:假设存在一种情况,接口的类加载器和实现的类加载器不一致,如下

1 | HelloService helloService = (HelloService)new HelloServiceImpl() |
上述代码会报ClassNotFoundException,这是因为JVM是通过类的全限定名和类加载器来确定一个类的,因此尽管实现了接口,但类加载器不同,因此没有办 法相互转换,如果要解决这种问题可以通过JDK动态代理来实现(有指定对应的classLoader)