深入理解类加载机制
作者:zen
概述
Java程序的运行,需要将class的字节码读入到内存中,将其放入运行时数据区的方法区内,然后在堆内存中完成对象的初始化。将class文件加载到java虚拟机内存中的模块,即为类加载器。类加载器,通过运行时,动态将所需类加载到虚拟机内存。类加载器可以由使用者指定,以便让应用程序决定如何获取所要加载的类。
1、加载类的过程
链接过程分为验证、准备、解析三部分
加载
类加载的过程即为类加载器将class文件读入java虚拟机内存的的过程,在此阶段,虚拟机主要完成3部分:
通过一个类的全名获取该类的字节码数据流(JAR ZIP WAR EAR等格式)
通过该class定义的数据结构,转化为方法区运行时的数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口
注意:生成代表这个类的Class对象时,java虚拟机规范并没有明确规定对象是在java堆中,对于Hotspot虚拟机而言,Class对象比较特殊,他虽然是对象,但是存放在方法区里面。因此如果说所有的对象都放在堆里,是不准确的。
链接
1. 验证
java虚拟机规范中,并未要求class文件必须由java编译生成,因此可以通过任何方式生成,因此虚拟机在进行类加载时,有必要对字节码进行安全验证。整体分为四类:
文件格式验证
元数据验证
字节码验证
符号引用验证
2. 准备
类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
3. 解析
虚拟机将常量池内的符号引用替换成直接引用。
符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。
直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量。
如果类中有语句:
private static int a = 10
它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
java虚拟机对java类的使用分为主动使用和被动使用,只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下八种:
创建类的实例,也就是new的方式
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
通过反射调用类
初始化某个类的子类,则其父类也会被初始化
如果一个接口声名了非抽象、非静态的方法,对直接或者非直接实现该接口的子类进行初始化时,该接口也会被初始化
使用动态代理时,会初始化被代理类
自jdk8后,接口中加入了默认方法(通过default关键字修饰),接口的实现类发生了初始化,则该接口作为父类要先进行初始化
jdk8后接口中支持声名静态方法,需要有实现体。单不可以被实现类集成,因为一个类可以实现多个接口。如果2个接口具有相同的静态方法,它们都将被继承,编译器就不知道要调用哪个接口。因此接口的静态方法,只能通过接口名称直接调用。
- Java虚拟机启动时被标明为启动类的类或者接口,即有main方法的类或者接口
2、类加载机制(双亲委派)
图中的层次关系,称为类加载器的双亲委派模型(是委派关系而非继承关系)。双亲委派模型要求除了顶层的根类加载器以外,其余的类加载器都应该有自己的父类加载器。如果一个类收到类加载请求,它首先请求父类加载器去加载这个类,只有当父类加载器无法完成加载时(其目录搜索范围内没找到需要的类),子类加载器才会自己去加载。
双亲委派的优势:使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object(存放于rt.jar中),是所有类的父类,所以任意一个类启动类加载时,都需要先加载Object类。在类加载器来看,所有的加载Object类的请求,都会逐级委托,最后都委托给Bootstrap根类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。(否则,系统中出现的Object类都不尽相同则会出现一片混乱)
3、类加载器的种类
JDK默认有三种类加载器,Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)
启动类加载器(Bootstrap class loader):该加载器使用C++语言实现,是虚拟机自身的提供的,即为虚拟机的一部分,启动类加载器并不会继承java.lang.ClassLoader。
扩展类加载器(Extensions class loader):该类加载器在此目录里面查找并加载 Java 类。扩展类加载器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dirs指定位置中的类库加载到内存中。由于扩展类加载器是由java代码实现的,开发者可以在程序开发的过程中,直接使用。
应用类加载器(Application class loader):应用类加载器(又叫系统类加载器)是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。开发的的程序及三方代码如果在没自定义类加载器的情况下,默认由该类加载器进行加载。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4、类加载器源码(openjdk11)
我们通过看一下ClassLoader源码,研究一下双亲委派机制是如何运行的。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查该类是否已被加载
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果父加载器不为空,则根据双亲委派机制,委托给父加载器加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父加载器没有加载成功,则通过自己的findClass来查找该类
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过源码可以看出,loadClass()方法是public的,因此可以被重写,通过递归调用的方式来完成双亲委托的类加载逻辑,在一个类需要加载时,类加载器会先将加载请求委托给父加载器,当父加载器无法加载此类的时候,才会交给子加载器进行加载。
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
在ClassLoder中findClass其主要职责是找到class文件,然后将class字节码读取到虚拟机内存中,方法需要其实现类去完成方法的实现。
// URLClassLoader.java
protected Class> findClass(final String name)
throws ClassNotFoundException
{
final Class> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<>() {
public Class> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 得到类的Class对象
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
findClass方法在读取到class文件字节码数组之后,然后调用defineClass()方法,得到这个类的Class对象。defineClass() 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。
5、自定义类加载器
为什么需要自定义类加载器?
1.默认类加载器只能加载固定路径下的class,如果有特定路径下的class,就需要自定义自己的类加载器
2.安全性:系统自身需要一些jar,class,如果业务类代码中也有相同的class,破坏系统,类似双亲委托安全性
此处参考tomcat自定义类加载器的原因
a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。
Tomcat应用容器自定义了WebAppClassLoader类加载器,打破了双亲委派模型。WebAppClassLoader 类加载器具体实现是重写了 ClassLoader 的两个方法:loadClass() 和 findClass()。其大致工作过程:首先类加载器自己尝试去加载某个类,如果找不到再委托代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。这也正是一个Tomcat能够部署多个应用实例的根本原因。下面我们看下tomcat如何实现自己的类加载器
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
if (log.isDebugEnabled()) {
log.debug("loadClass(" + name + ", " + resolve + ")");
}
Class> clazz = null;
this.checkStateForClassLoading(name);
//现在自定义类加载器缓存中,查找是否加载过该类
clazz = this.findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
this.resolveClass(clazz);
}
return clazz;
} else {
// 从系统类加载器的缓存中,查询是否加载过该类
clazz = this.findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
this.resolveClass(clazz);
}
return clazz;
} else {
String resourceName = this.binaryNameToPath(name, false);
ClassLoader javaseLoader = this.getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
tryLoadingFromJavaseLoader = javaseLoader.getResource(resourceName) != null;
} catch (Throwable var13) {
ExceptionUtils.handleThrowable(var13);
tryLoadingFromJavaseLoader = true;
}
Class var10000;
if (tryLoadingFromJavaseLoader) {
label210: {
try {
// 使用ExtClassLoader类加载器来加载
clazz = javaseLoader.loadClass(name);
if (clazz == null) {
break label210;
}
if (resolve) {
this.resolveClass(clazz);
}
var10000 = clazz;
} catch (ClassNotFoundException var15) {
break label210;
}
return var10000;
}
}
if (log.isDebugEnabled()) {
log.debug(" Searching local repositories");
}
try {
// 在本地目录中加载该类
clazz = this.findClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from local repository");
}
if (resolve) {
this.resolveClass(clazz);
}
var10000 = clazz;
return var10000;
}
} catch (ClassNotFoundException var17) {
;
}
if (!delegateLoad) {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader at end: " + this.parent);
}
try {
// 通过系统类加载器,加载该类,Class.forName()默认类加载器为系统类加载器
clazz = Class.forName(name, false, this.parent);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
if (resolve) {
this.resolveClass(clazz);
}
var10000 = clazz;
} catch (ClassNotFoundException var14) {
throw new ClassNotFoundException(name);
}
return var10000;
} else {
throw new ClassNotFoundException(name);
}
}
}
}
}
主要有如下几个步骤:
1)先在本地缓存 Cache 查找该类是否已经加载过,即 Tomcat 自定义类加载器 WebAppClassLoader 是否已加载过。
2)如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
3)如果系统类加载器也没有加载过,此时,会让 ExtClassLoader 扩展类加载器去加载,很关键,其目的防止 Web 应用自己的类覆盖 JRE 的核心类。
因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里有类似上面举的例子自定义了 Object 类,如果先加载这些JDK中已有的类,会导致覆盖掉JDK里面的那个 Object 类。
这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,JRE里的类由BootstrapClassLoader安全加载,然后返回给 Tomcat 的类加载器。
这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
4)如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
5)如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意:Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
6)如果上述加载过程全部失败,抛出 ClassNotFoundException 异常。
public Class> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
try {
if (log.isTraceEnabled()) {
log.trace(" findClassInternal(" + name + ")");
}
try {
// 先从本地应用目录查找
clazz = this.findClassInternal(name);
} catch (AccessControlException var5) {
} catch (RuntimeException var6) {
}
if (clazz == null && this.hasExternalRepositories) {
try {
// 如果本地目录没有找到,则调用父加载器查找
clazz = super.findClass(name);
} catch (AccessControlException var7) {
throw var8;
}
}
if (clazz == null) {
if (log.isDebugEnabled()) {
log.debug(" --> Returning ClassNotFoundException");
}
// 父加载器没有加载到,则报出异常
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException var9) {
}
return clazz;
}
在 findClass() 重写的方法里,主要有三个步骤:
1)先在 Web 应用本地目录下查找要加载的类。
2)如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
3)如何父加载器也没找到这个类,抛出 ClassNotFoundException 异常。
6、总结
本文整体讲了加载类的整体过程:加载->链接->初始化,以及类加载的机制,双亲委托加载模型中几个类加载器的委派关系。通过阅读ClassLoader类的源码,loadClass()方法如何实现双亲委派实现的细节,findClass()方法如何查找class文件,都很清晰了。最后通过tomcat自定的类加载器WebAppClassLoader类,学习了tomcat如何打破双亲委派模型,来实现其对类加载场景的业务支持。