简介
在上一篇文章中,我们有提到在给对象分配内存之前需要经过类加载过程,类被虚拟机加载、解析、初始化后才会根据对象大小进行内存分配。那虚拟机是如何进行类加载、解析、初始化的呢?类加载过程中虚拟机进行了哪些工作呢?什么是类加载器?什么是双亲委派?为什么又有打破双亲委派?接下来小刀为你一一揭晓。
类被虚拟机加载到内存,再到被卸载出内存整个生命周期一共需要经历过以下几个阶段:加载、验证、准备、解析、初始化、使用和卸载。
其中如上图所示,验证、准备、解析这三个阶段可以合并称链接。我们要注意一点的是,这几个阶段是按照顺序开始的,但是不一定按照顺序进行或者完成的。
加载
“加载”是整个过程的第一个阶段,虚拟机在这个阶段完成三件事:
- 通过一个类全限定名,找到这个类,获取这个类的二进制流
- 将这个字节流所代表的静态存储结构转化成方法区的数据结构
- 在内存中生成一个代表这个类的
Class
对象,作为方法区各个数据的访问入口,这个对象有时候也被叫做“元对象”
注意,加载过程中不一定要从一个 Class
文件中获取,也可以从 jar
包或者 war
包中读取,或者是通过动态代理生成,或者由其他文件生成(由jsp
文件生成对应的Class
类)
类加载器
完成类加载这个过程的那部分代码模块我们叫做类加载器
。虚拟机只是实现了“启动类加载器”,还有其他类加载器独立于虚拟机之外,让应用程序自己去决定如何获取所需要的类实现类加载这个动作。
所以这样子来看的话,对于虚拟机来说只有两种类加载器,启动类加载器和其他类加载器
- 启动类加载器:它使用
C++
实现(这里仅限于Hotspot
,也就是JDK1.5
之后默认的虚拟机,有很多其他的虚拟机是用Java
语言实现的),是虚拟机自身的一部分。 - 其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类
java.lang.ClassLoader
,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
但是对于 Java 开发人员来说,类加载器是可以分为启动类加载器、扩展类加载器、应用程序类加载器这三大类
- 启动类加载器:它负责加载存放在
JDK\jre\lib
下,或被-Xbootclasspath
参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar
,所有的java.*
开头的类均被Bootstrap ClassLoader
加载)。启动类加载器是无法被Java
程序直接引用的。 - 扩展类加载器:该加载器由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如javax.*
开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器:该类加载器由
sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath
)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
在我们的应用程序中的类都是由这三个加载器配合加载,如果有必要,我们还可以加入自定义的类加载器
所以整个类加载器的层级关系就如同上图所示,我们把每一层的上一层加载器叫做父类加载器,注意,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。
双亲委派
接下来我们来聊聊双亲委派,首先我们来看看什么是双亲委派。双亲委派就是某一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException
异常后,再调用自己的findClass()
方法进行加载,依次类推
那为什么要通过双亲委派这个机制去加载类呢?首先虚拟机判定两个类是否为同一个类是通过类全限定名是否相同,加载这个类的加载器是否相同来判定这两个类是否为同一个类。,其次,有一个好处是 java 随着他的类加载器一起具备了一种带有优先级层次关系。
那么这样的话,如果没有双亲委派模型,让所有类加载器自行加载的话,假如用户自己编写了一个称为java.lang.Object
的类,并放在程序的ClassPath
中,系统就会出现多个不同的Object类, Java类型体系中基础行为就无法保证。同时双亲委派也保证了内存中不会出现多份同样的字节码。
打破双亲委派
刚刚我们知道双亲委派如果可以被委托的父类加载器加载,他就不能被当前的类加载器加载。那么这种情况下你就知道 String
类一定是被BootstrapClasserLoader
加载的 /lib
下的那个 rt.jar
的那个java/lang/String.class
但是我们要清楚,虚拟机不是强制要求类加载机制一定要按照双亲委派的模型。这个模型也会有一些问题,比如我们经常会通过数据库提供商的驱动来链接操作数据库。但是 jdk 只是提供了一个规范接口,没有提供实现。数据库提供商提供了具体的驱动实现。但是我们总不可能把数据库驱动放在 jdk 的目录里面吧。
这里我们通过加载 mysql 驱动来举一个例子,首先看下面的代码:
Class clz = Class.forName("java.sql.Driver");
Driver driver = (Driver)clz.newInstance();
复制代码
应为java.sql.Driver
是在 rt.jar
下的类文件,所以按照我们上面的描述,这个应该会被启动类加载器加载,问题是java.sql.Driver
是个接口,无法真的实例化,就报错了。
那么我们要正确加载驱动需要在 classpath
里加一个 mysql-connector-java.jar
,然后再编写如下代码:
Class clz = Class.forName("com.mysql.jdbc.Driver");
Driver driver = (Driver)clz.newInstance();
复制代码
这里我们直接使用应用程序类加载器来加载 mysql-connector-java.jar
中的 com.mysql.jdbc.Driver
在JDBC4.0
以后,开始支持使用spi
的方式来注册这个Driver
,具体做法就是在mysql
的jar
包中的META-INF/services/java.sql.Driver
文件中指明当前使用的Driver
是哪个,然后使用的时候就直接这样就可以了,具体代码可以写成下面这样子:
Connection conn= DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK",
"root", "");
复制代码
DriverManager
就根据"jdbc:mysql"
这个提示去找具体实现,从META-INF/services/java.sql.Driver
文件中获取具体的实现类名“com.mysql.cj.jdbc.Driver”
。加载这个类用class.forName(“com.mysql.jdbc.Driver”)
来加载,我们可以用代码编译器清楚的看到这个文件
这里就有个问题,Class.forName()
加载用的是调用者的Classloader
,这个调用者DriverManager
是在rt.jar
中的,ClassLoader
是启动类加载器,而com.mysql.jdbc.Driver
肯定不在<JAVA_HOME>/lib
下,所以肯定是无法加载mysql
中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
这个问题如何解决呢?由于这个mysql
的drvier
只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器。
线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则
通过以上的例子我们知道了在某些情况下是需要违反双亲委派这种模型来加载类的,到目前为止双亲委派模型主要出现过三次大规模的“破坏”的情况:
- 第一次:在双亲委派模型发布之前,即
JDK1.2
之前。为了兼容之前JDK
版本中自定义类加载器的实现。(即没有按照双亲委派模型来设计)
解决办法:把自己的类加载器逻辑写到findClass()
方法中,在loadClass()
方法的逻辑里如果父类加载失败,则会调用自己写的findClass()
方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
- 自身的缺陷所致。
JNDI
服务需要调用独立厂商实现并部署在应用程序的ClassPath
下的JNDI
接口提供者(SPI)的代码,但是启动类加载器不认识这些代码。
解决办法:引入上下文类加载器(Thread Context ClassLoader)
。这个类加载器可以通过java.lang.Thread
类的 setContextClassLoaser()
方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围都没有设置过的话,那这个类加载器默认就是应用程序的类加载了。类似的服务还有:JDBC、JCE、JAXB、JBI
等等。
- 第三次:用户对动态性的追求而导致的,例如:代码热替换、热部署
解决办法:OSGI
实现模块化热部署的关键是它自定义的类加载机制实现的。OSGi
每个模块都有自己独立的classpath
。
好了,这次小刀就先和大家先聊到这,大家拿去怼面试官吧。
原创不易,感谢大家在看点赞,我们下期再见!!