今天,xiao--xin打算来简单介绍一下JVM的类加载器和类的加载过程,话不多说,老板请速速上车。
基本概念
类加载器负责将类的字节码文件加载到JVM中,并负责解析类之间的依赖关系,确保类的正确加载和隔离。
类加载器的种类
引导类加载器(Bootstrap ClassLoader)
引导类加载器是最顶层的类加载器,是由C++编写的,属于JVM的一部分,也就是说它没有父类加载器,引导类加载器加载的类是Java的核心类库,这些类是JVM自身运行所需要的。另外,引导类加载器无法直接被Java程序引用。
引导类加载器还有另一种叫法——启动类加载器。
扩展类加载器(Extension ClassLoader)
扩展类加载器是由Java语言编写的,它继承自ClassLoader类。顾名思义,扩展类加载器就是用来加载Java的扩展类库。扩展类加载器由引导类加载器进行加载,并且它的父类加载器就是引导类加载器。
系统类加载器(System ClassLoader)
系统类加载器也是由Java语言编写的,系统类加载器负责加载用户类路径(ClassPath)上的指定类库,是我们平时编写Java程序默认使用的类加载器。系统类加载器的父类加载器是扩展类加载器,系统类加载器可以通过调用ClassLoader.getSystemClassLoader()方法获得。
系统类加载器还有另一种叫法——应用类加载器(Application ClassLoader)
自定义类加载器(Custom ClassLoader)
我们可以根据我们的业务需要自定义类加载器,来控制类的加载。比如:从数据库加载类、从网络加载类甚至从加密文件中加载类。自定类加载器是Java动态性的一个体现,可以用来扩展Java的灵活性和安全性。
讲到类加载器,那就不得不提一下类加载器的双亲委派机制了。
双亲委派机制
层次结构基础
类加载器之间形成了一种层次结构,从上到下一次是引导类加载器、扩展类加载器、系统类加载器、自定义类加载器。
具体过程
当类加载器要加载一个类的时候,它不会马上加载,而是会将加载请求传递给父类加载器,如果父类加载器能够加载,则由父类加载器进行加载,若父类加载器也无法加载,则继续向上传递询问,直到传递到引导类加载器为止。这个过程,自下而上,一层一层的向上委托父类加载器加载。
当加载请求传递到引导类加载器时,若引导类加载器能够加载,则由引导类加载器加载。但是,如果引导类加载器无法加载,又因为引导类加载器没有爸爸(父类加载器),所以引导类加载器只能把请求还回去,还给扩展类加载器,告诉扩展类加载器自己不能加载,再由扩展类加载器尝试加载。如果扩展类加载器可以加载,则由扩展类加载器加载,若不行,就继续往子类加载器传递回去,直到有一个子类加载器能够加载或者已经到最后一个子类加载器为止。这个过程,自上而下,类加载器们一层一层尝试加载。
简单来说,双亲委派机制就是只有当父类加载器没办法加载的时候,自己才会尝试加载。
好处
保证安全性
因为Java的核心类库只能由引导类加载器进行加载,而引导类加载器只会加载Java的标准核心类库,因此当有恶意代码想要伪装成核心类库侵入时,恶意代码的类会被先传递到引导类加载器,被发现不是核心类库从而不会被加载进核心类库,防止了恶意代码替换核心类库,增强系统的安全性。
保证类的唯一性
通过向上委托的机制,确保了所有的类都会被先传递到引导类加载器,避免了不同的类加载器重复加载类的情况,保证了核心类库的统一性,也能防止核心类库被替换。
支持类的隔离和层次划分
不同的类加载器加载不同层次的类库,引导类加载器加载核心类库,扩展类加载器加载扩展类库,系统类加载器加载用户代码,有利于实现沙箱安全机制,同时每一个加载器的职责明确,便于管理和维护。
简化了加载流程
通过委派,确保了大部分正确的类都能够被加载,同时减少了每一个类加载器需要加载的类的数量,简化了加载流程,提高了加载效率。
介绍完类加载器的种类,接下来我们来看看类是如何被加载的。
类的加载过程
类的加载过程分为三个部分,加载、链接和初始化,链接又包括验证、准备和解析。其中加载和初始化是由类加载器来完成的,而链接是由JVM内部来完成的。
加载(loading)
加载阶段是通过类的全限定名(包名+类名,例如:(例如com.example.MyClass))从文件系统或其他位置(网络或数据库等数据源)获取字节码文件,并将字节码数据其转化成Class对象存放在方法区中。
链接(Linking)
加载阶段完成后,就进入链接阶段。链接包括验证准备解析。
验证(Verification)
验证是确保Class文件中的字节流信息符合当前虚拟机的要求,保证这个被加载的类的正确,不会危害虚拟机。验证包括以下四个方面:
文件格式验证
文件格式验证指检查字节码文件是否符合Java类文件的要求。
元数据验证
元数据验证指检查类是否有正常的继承关系,确保继承链的完整性。
字节码验证
检查字节码指令是否正确,确保不会发生非法操作(访问未初始化的局部变量)。
符号引用验证
检查符号引用是否正确,确保指向的类、字段和方法等都存在。
准备(Preparation)
准备阶段为类的静态变量分配内存并设置初始化值。
分配内存
分配内存即在方法区为类的静态变量分配内存。
设置初始化值
准备阶段的设置初始化值就是静态变量设置默认值(如 设置int类型的静态变量为0)。特别注意的是,被final修饰的static字段不会被初始化,因为final在编译阶段就分配好了。
解析(Resolution)
解析阶段是将类文件中的符号引用(类文件常量池中描述字段和方法的符号表示形式)转化为直接引用(指向内存中的实际数据,比如方法区的字段数据或方法入口地址)。
初始化(Initialization)
初始化阶段是类的加载过程的最后一个阶段,初始化阶段会执行编译器生成的构造方法(即初始化代码,注意不是我们用户自己写的构造方法)进行类的初始化包括执行静态代码块,给静态变量赋值和调用父类初始化(如果有父类,父类先初始化)。
补充类的卸载:
如果类的所有实例都已经被回收,即堆中没有该类任何实例,或者加载该类的ClassLoader已经被回收,或者类对应的java.lang.Class对象没有被任何实例引用,也无法在任何地方通过反射获取该类的实例时,类会被卸载。
最后
今天,我们简单了解了一下类加载器、双亲委派机制,以及类加载器是如何加载类的。要值得注意的是,类的加载过程中,加载阶段和初始化阶段是在类加载器完成的,而链接阶段是在JVM内部完成的,不要想当然的以为类的加载过程就全都是由类加载器来完成的哈。
好了,该去吃饭、摸鱼、打游戏了,拜拜~