(一)概述
咱们都知道Java代码会被编译成class文件,在class文件中描述了该类的各类信息,class类最终须要被加载到虚拟机中才能运行和使用。java
虚拟机把Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成虚拟机能够直接使用的Java类型,这就是虚拟机的类加载机制。数据库
(二)类加载的过程一个类从被加载到卸载出内存,一共包含下面七个阶段:网络
加载、验证、准备、解析、初始化、使用、卸载 加载的来源有如下部分:app
一、本地磁盘ide
二、网络下载的.class文件spa
三、war,jar下加载.class文件线程
四、从专门的数据库中读取.class文件(少见)代理
五、将java源文件动态编译成class文件,典型的就是动态代理,经过运行时生成class文件code
加载的过程是经过类加载器实现的。有关类加载的其余过程我会在下一章中介绍。blog
(三)类加载器的分类类加载器分为系统级别和用户级别:
系统级别的类加载器有:
一、启动类加载器(底层使用C++实现)
二、扩展类加载器(底层使用java实现,是ClassLoader的子类)
三、应用程序类加载器(底层使用java实现,是ClassLoader的子类)
用户级别的类加载器咱们统一称为自定义类加载器。
首先咱们来看看启动类加载器加载了哪些类,启动类加载器负责加载sun.boot.class.path:
public static void bootClassLoaderLoadingPath(){ //获取启动列加载器加载的目录 String bootStrapLoadingPath=System.getProperty("sun.boot.class.path"); //把加载的目录转为集合 List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";")); for (String bootPath:bootLoadingPathList){ System.out.println("启动类加载器加载的目录:"+bootPath); } }
经过上面的代码咱们能够获取到启动类加载器所加载的类:
扩展类加载器加载负责加载java.ext.dirs,咱们一样写一段代码去加载它:
public static void extClassLoaderLoadingPath(){ //获取启动列加载器加载的目录 String bootStrapLoadingPath=System.getProperty("java.ext.dirs"); //把加载的目录转为集合 List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";")); for (String bootPath:bootLoadingPathList){ System.out.println("拓展类加载器加载的目录:"+bootPath); } }
能够看到,除了加载了JDK目录下的ext外,还加载了Sun目录下的ext
最后是应用类加载器,它负责加载java.class.path:
public static void appClassLoaderLoadingPath(){ //获取启动列加载器加载的目录 String bootStrapLoadingPath=System.getProperty("java.class.path"); //把加载的目录转为集合 List<String> bootLoadingPathList= Arrays.asList(bootStrapLoadingPath.split(";")); for (String bootPath:bootLoadingPathList){ System.out.println("应用程序类加载器加载的目录:"+bootPath); } }
它负责加载工程目录下classpath下的class以及jar包。
(四)双亲委派模型所谓双亲委派模型,就是指一个类接收到类加载请求后,会把这个请求依次传递给父类加载器(若是还有的话),若是顶层的父类加载器能够加载,就成功返回,若是没法加载,再依次给子加载器去加载。 咱们先经过代码来看一下类加载器的层级结构:
public class ClassLoaderPath { public static void main(String[] args) { System.out.println(ClassLoaderPath.class.getClassLoader()); System.out.println(ClassLoaderPath.class.getClassLoader().getParent()); System.out.println(ClassLoaderPath.class.getClassLoader().getParent().getParent()); } }
编写一个类,依次输出这个类的类加载器,父类加载器,父类的父类加载器
能够看到首先是应用程序类加载器,它的父类是扩展类加载器,扩展类加载器的父类输出了一个null,这个null会去调用启动类加载器。若是你不信,咱们看源码:ClassLoader类
接着从父类加载器往下调用findClass,若是能够加载,就直接返回class,若是不能加载,就依次向下。若是到了自定义加载器仍是没法被加载,就会抛出ClassNotFound异常。
我画了一个流程图来展现双亲委派模型的全过程:
双亲委派模型保证了Java程序的稳定运行,能够避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
(五)破坏双亲委派双亲委派模型并非绝对的,spi机制就能够打破双亲委派模型。
首先咱们须要了解什么是spi,spi(Service Provider Interface)是一种服务发现机制,Java在核心库中定义了许多接口,而且针对这些接口给出调用逻辑,可是并未给出具体的实现。开发者要作的就是定制一个实现类,在 META-INF/services 中注册实现类信息,以供核心类库使用。最典型的就是JDBC。
Java提供了一个Driver接口用于驱动各个厂商的数据库链接,Driver类位于JAVA_HOME中jre/lib/rt.jar中,应该由Bootstrap类加载器进行加载。根据类加载机制,当被加载的类引用了另一个类的时候,虚拟机就会使用加载该类的类加载器加载被引用的类,所以若是其余数据库厂商定制了Driver的实现类以后,按理说也得把这个实现类放到启动类加载器加载的目录下,这显然是很不合理的。
因而Java提供了spi机制,即便Driver由启动类加载器去加载,可是他可让线程上下文加载器(Thread Context ClassLoader)去请求子类加载器去完成加载,默认是应用程序类加载器。可是这确实破坏了类加载机制。