类加载机制

类加载机制主要内容可以通过以下几个问题来解释:
初识类加载
严格来说,类加载机制指的就是将描述数据的字节码文件加载到内存中,然后对其进行验证,准备,解析以及初始化的过程,最终生成 Class 对象的过程;简单来说就是通过将字节码文件加载到内存中生成 Class 对象的过程
使用类加载机制的原因非常简单,主要分为三点
- 因为现代的操作系统都是要求程序必须加载到内存中才能够运行,所以类加载机制的出现是必然的
- 其次类加载机制会对字节码进行验证,所以可以防止字节码文件危害到虚拟机自身的安全,并且还通过双亲委派机制,避免用户编写相同的类从而覆盖核心类库
- 类加载机制允许用户自定义类加载器,使得用户可以自定义自己的加载逻辑
注:这里可能会问到什么是双亲委派机制,这个在后面补充
类加载机制是由各个不同层次的类加载器负责的,且类加载都是独立于虚拟机存在的(除启动类加载器)便于用户自定义类加载器
类加载器主要分为以下几类:
- 如果从虚拟机的角度来看,那么仅分为两类:第一类就是启动类加载器,第二类就是自定义类加载器
- 如果从程序的角度来看,那么可以分为三类:第一类依然是类加载器,第二类是扩展类加载器,第三类是应用类加载器
各个类加载器主要负责以下的功能:
启动类加载器:主要负责 lib 目录下的类的加载,并且会根据文件的名字来进行加载,不符合条件的类即使添加到 lib 目录下也是不可以加载的;此外,还要负责将扩展类加载器和应用类加载器加载到内存中
扩展类加载器:主要负责 lib/ext 目录下的类的加载,可以将扩展的类添加到目录下进行加载
应用类加载器:主要负责 classpath 下配置的用户自定义类的加载
注:启动类加载器在 JDK 9 以前都是完全采用 C++ 在虚拟机内部实现的,在 JDK 9 之后就采用 Java 和 混合实现 C++
类加载流程
主要分为三个过程:加载,连接,初始化,连接又可以细分为验证,准备,解析三个阶段
加载:根据全限定名将相应的字节码加载到内存中,然后在方法区中生成 Klass 对象并且在堆区中生成 Class 对象
连接:主要分为三个阶段
- 第一个阶段就是验证:主要就是通过文件验证,元数据验证以及字节码验证从而确保字节码文件不会危害虚拟机自身的安全
- 第二个阶段就是准备:主要就是给所有静态变量赋默认值
- 第三个阶段就是解析:主要就是将常量池中的部分符号引用解析成直接引用,这个过程称为静态解析,与之相对的就是动态连接
初始化:主要就是收集类中的静态变量,静态代码块然后添加到 clinit 方法中,然后执行 clinit 方法,从而初始化静态变量的值
双亲委派机制

双亲委派机制指的就是在系统向子类加载器发起类加载请求的时候,子类并不会立刻去加载这个类而是交给自己的父类加载器去处理;每层的类加载器都是如此,直到父类加载器没有办法处理器这个类加载请求的时候,再有子类加载器自己去加载
注:双亲委派机制中提到的父类加载器通常指的是上层的类加载器,而不是指的具有继承关系的子类父类
双亲委派机制存在的理由非常简单,就是为了避免虚拟机内部存在多份具有相同名字的字节码文件,从而导致虚拟机内部系统混乱的情况发生
比如,用户按照自己的逻辑也编写一个名字叫 Object 的类,然后在编写一个相应的类加载器去加载自己写的 Object 类,那么在没有双亲委派的情况下最终就会导致虚拟机存在两份相同的字节码文件,会导致系统混乱
注:双亲委派机制的具体实现就是在 loadClass( ) 这个方法中的
历史上有三次破坏双亲委派机制,是哪三次?如何破坏双亲委派机制?
第一次破坏其实出现在 JDK 1.2 之前,也就是在双亲委派机制还没有引入的时候:
在没有引入双亲委派机制的时候,用户可以在 loadClass 这个方法中自定义类加载的逻辑,在引入双亲委派后,loadClass 的逻辑就变成了双亲委派的实现,所以在此之前都算是对双亲委派的破坏
实际上,因为 Java 的社区规范要求之后的版本更新都必须兼容以前的代码,所以 JDK 官方只是推荐使用双亲委派并没有强制,也就意味着现在依然可以重写 loadClass 破坏双亲委派机制
第二次破坏其实是由于 SPI(Service Provider Interface) 服务造成的,本质上是双亲委派机制天生的缺陷
比如说 JDBC 服务就会造成双亲委派的破坏,JDK 中采用 DriverManager 加载并管理 Driver,但是 JDK 中仅定义了 Driver 接口,不同的数据库的驱动的实现肯定是不同的,而 DriverManager 是由启动类加载器加载的,是没有办法去加载各个具体的驱动的
所以 JDK 后面就想出了办法,直接在当前线程的上下文中设置类加载器为应用类加载器,也就是相当于由父类委托子类去加载各个数据库具体的驱动,这就违背了双亲委派机制
第三次破坏其实出现在 JDK 9 之后,采用模块化的思想代替了双亲委派机制,实际上也就不存在双亲委派
注:JDK 9 这个模块化说实话有点难以解释,暂时放着
初始化顺序
父类实体类:
class Father{ |
子类实体类:
class Son extends Father{ |
课程实体类:
class Course{ |
测试方法:
public static void main(String[] args) { |
在连接的准备阶段给所有的静态变量赋默认值,然后在初始化阶段给给所有的静态变量赋初始值,也就是会执行父类和子类中所有静态代码块中的内容,所以实例变量的初始化在类加载的阶段是不会执行的
既然如此,加载类的过程中就只会执行静态代码块中的内容,而不会执行其他任何方法中的内容或者初始化实例变量
创建对象的前提是对应的类必须被加载到内存中,那么也就是说创建对象之前必须执行类加载,所以静态代码块中的方法会先于其余所有的方法执行,在类加载完成之后才是分配内存,初始化实例变量,初始化对象头,执行构造函数的过程
从这个过程就可以看出来,如果直接在定义实例变量的时候就赋值的话,就会先于构造函数执行,并且这个值是引用类型的话就会提前于构造函数创建对象;此外,子类对象在创建之前必定会查找自己的父类,然后在创建父类对象完成之后才会继续自己的创建,所以父类对象的构造函数会先于子类构造函数执行
所以总结下来就是:
父类,子类的静态代码块最早执行
父类所有实例变量初始化,父类构造函数执行
子类所有实例变量初始化,子类构造函数执行