1 Java 虚拟机中类加载器
在JVM中定义了4类加载器分别为:启动(Bootstrap)类加载器,扩展(Extension)类加载器,系统(System)类加载器,以及用户自定义加载器
启动(Bootstrap)类加载器
引导类加载器是负责加载并管理
启动(Bootstrap)类加载器是扩展(Extension)类加载器的父加载器,是最高等级的加载器,由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。
//从系统属性中获取启动(Bootstrap)类加载器加载并管理核心类库
System.getProperty("sun.boot.class.path")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\sunrsasign.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\classes
扩展(Extension)类加载器
扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责加载并管理
//从系统属性中获取扩展(Extension)类加载器加载并管理核心类库
System.getProperty("java.ext.dirs")
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext;
C:\windows\Sun\Java\lib\ext
系统(System)类加载器
系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责加载并管理用户类路径(java -classpath或-Djava.class.path变量所指定的URL资源)中class文件对应的Class对象。开发者可以直接使用系统类加载器。
//从系统属性中获取系统(System)类加载器加载并管理核心类库
System.getProperty("java.class.path")
...省略
D:\project_alibaba\jvm-in-action\target\classes;
...省略
案例
package jvm;
import java.lang.reflect.Constructor;
/**获取JVM类加载器 **/
public class ClassLoaderTest {
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@45ee12a7
null
2 类加载(双亲委派机制)
首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,直到最终交给不存在父类加载器的启动(Bootstrap)类加载器加载。
[图片上传失败...(image-d354d5-1564996480026)]
实现流程
双亲委派模型对于保证Java程序的稳定运作很重要,它的具体实现在java.lang.ClassLoader类loadClass()方法中。
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先判断该类是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载,这里是一个递归调用的过程。
c = parent.loadClass(name, false);
} else { // 递归终止条件
// 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
// parent == null就意味着由启动类加载器尝试加载该类,
// 即通过调用 native方法 findBootstrapClass0(String name)加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值
// 注意,若自身也加载不了,也会产生ClassNotFoundException异常并向上抛出
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
//没错!此方法没有具体实现,只是抛了一个异常,而且访问权限是protected。这充分证明了:这个方法就是给开发者重写用的,即自定义类加载器时需实现此方法!
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
- AppClassLoader,ExtClassLoader 对findClass实现都继承自URLClassLoader,
protected Class> findClass(final String name)
throws ClassNotFoundException
{
final Class> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction>() {
public Class> run() throws ClassNotFoundException {
//将类的包路径转换为url资源路径
String path = name.replace('.', '/').concat(".class");
//通过URLClassLoader URLClassPath ucp中获取url路径对应的二进制数据
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//读取二进制获取调用defineClass获取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;
}
案例
ClassLoaderTest,EventID,Sdp属于被不同类加载器管理的类对象,使用相同系统类加载器加载时,由于采用双亲委派机制,获取Class对象是由不同的类加载器加载。
@Test
public void classLoaderLoadClass2() throws IOException {
try {
//调用加载当前类的类加载器(这里即为系统类加载器)加载ClassLoaderTest
Class typeLoaded = ClassLoaderTest.class.getClassLoader().loadClass("com.wuhao.jvm.classLoader.ClassLoaderTest");
//查看被加载的ClassLoaderTest对象是被那个类加载器加载的
System.out.println(typeLoaded.getClassLoader());
//调用加载当前类的类加载器(这里即为系统类加载器)加载EventID
Class typeLoaded1 = ClassLoaderTest.class.getClassLoader().loadClass("com.sun.java.accessibility.util.EventID");
//查看被加载的ClassLoaderTest对象是被那个类加载器加载的
System.out.println(typeLoaded1.getClassLoader());
//调用加载当前类的类加载器(这里即为系统类加载器)加载Sdp
Class typeLoaded2 = ClassLoaderTest.class.getClassLoader().loadClass("com.oracle.net.Sdp");
//查看被加载的ClassLoaderTest对象是被那个类加载器加载的
System.out.println(typeLoaded2.getClassLoader());
} catch (Exception e) {
}
}
模式优点
双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载).
例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。
模式缺点
由于双亲委派模型存在,一个类被谁加载是由类加载器管理资源所决定,即使某个类存在于多个类加载器管理资源中。也只会被更上程的类加载器加载。这样就会导致一个在被上层类加载中Class中获取的上程类加载器,是无法加载下层类加载器中管理Class对象。
线程上下文类加载器
为了解决这个问题JVM引用了线程上下文类加载器
这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使用系统程序类加载器。
如何破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式。大多数的类加载器都遵循这个模型,双亲委派模型的具体逻辑实现在ClassLoader的loadClass方法,如果我们自定义类加载器,采用双亲委派模型:只需要重写ClassLoader的findClass()方法即可,破坏双亲委派模型:重写ClassLoader的整个loadClass()方法(因为双亲委派模型的逻辑主要实现就在此方法中,若我们重写即可破坏掉。)
3 tomcat中类加载器
3.1 Tomcat 如果使用默认的类加载机制行不行
Tomcat是个web容器,那么它要解决什么问题
- 一个web容器可能需要部署多个个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享给所有的应用程序。节省资源。
- web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。
Tomcat 如果使用默认的类加载机制是无法解决上面的问题,
- 对于第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
- 第三个问题和第一个问题一样。
- 第四个问题,我们想我们要怎么实现jsp文件的热部署。jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。我们只能将jsp文件Class对象从类加载器中卸载,在重新加载。
3.2 Tomcat类加载器
- commonLoader: Tomcat 通用类加载器, 加载的资源可被 Tomcat 和 所有的 Web 应用程序共同获取
- catalinaLoader: Tomcat 类加载器, 加载的资源只能被 Tomcat 获取
- sharedLoader: Tomcat 各个Context的父加载器, 这个类是所有 WebappClassLoader 的父类, sharedLoader 所加载的类将被所有的 WebappClassLoader 共享获取
- WebappClassLoader: 每个Context 对应一个 WebappClassloader, 主要用于加载 WEB-INF/lib 与 WEB-INF/classes 下面的资源
3.3 实例化commonLoader,catalinaLoader,sharedLoader
Bootstrap的初始化中实例化的tomcat类加载器主要包括commonLoader,catalinaLoader,sharedLoader,其本质是URLClassLoader。其核心思想是“根据类加载器的名称从读取/conf/catalina.properties中读取不同类加载加载class文件资源路径,转为为URL数组,来实例化成URLClassLoader”
源码
initClassLoaders方法中构造类加载器被封装到createClassLoader方法中。第一个参数用来作为需要构造类加载器的名称,第二个参数用来作为构造加载器的父类加载器。
private void initClassLoaders() {
try {
/** 实例化commonLoader,如果未创建成果的话,则使用应用程序类加载器作为commonLoader **/
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
/** 实例化catalinaLoader,其父加载器为commonLoader **/
catalinaLoader = createClassLoader("server", commonLoader);
/** 实例化sharedLoader,其父加载器为commonLoader **/
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
createClassLoader方法中根据类加载器的名称从读取/conf/catalina.properties中读取不同类加载加载class文件资源路径,
将读取的资源转换为Repository类型的列表。Repository类用来表示一个资源。其内部存在2个属性location,location。分别表示资源的路径和资源的类型
调用ClassLoaderFactory.createClassLoader创建一个ClassLoader
/**
* 按照名称创建不同tomcat 类加载器
* @param name
* @param parent
* @return
* @throws Exception
*/
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
/** 根据类加载器的名称从CatalinaProperties配置中读取不同类加载加载class文件资源路径,
*
* CatalinaProperties配置来源于tomcat工作目录/conf/catalina.properties
* 默认配置如下
* common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
* server.loader=
* shared.loader=
*/
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
/** 替换资源路径字符串中属性遍历占位符,比如:${catalina.base}、${catalina.home} **/
value = replace(value);
/**
* 定义一个Repository类型的列表,
* Repository类用来表示一个资源。其内部存在2个属性location,location。分别表示资源的路径和资源的类型
**/
List repositories = new ArrayList<>();
/** 读取配置中的资源按,分隔字符数组**/
String[] repositoryPaths = getPaths(value);
/** 遍历repositoryPaths **/
for (String repository : repositoryPaths) {
/** 检查资源路径是否为URL **/
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
/** 创建一个Repository,类型为URL添加到repositories **/
repositories.add(
new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
/** 判断资源是否为某个目录下所有*.jar文件 **/
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
/** 创建一个Repository,类型为GLOB添加到repositories **/
repositories.add(
new Repository(repository, RepositoryType.GLOB));
}
/** 判断资源是否为某个目录下.jar文件 **/
else if (repository.endsWith(".jar")) {
/** 创建一个Repository,类型为JAR添加到repositories **/
repositories.add(
new Repository(repository, RepositoryType.JAR));
}
/** 判断资源是否目录 **/
else {
/** 创建一个Repository,类型为目录添加到repositories **/
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
//创建一个ClassLoader
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
public enum RepositoryType {
//目录
DIR,
//目录下*.jar
GLOB,
//单个jar
JAR,
//URL
URL
}
public static class Repository {
/**
* 资源路径
*/
private final String location;
/**
* 资源类型
*/
private final RepositoryType type;
public Repository(String location, RepositoryType type) {
this.location = location;
this.type = type;
}
public String getLocation() {
return location;
}
public RepositoryType getType() {
return type;
}
}
createClassLoader方法中遍历列表中Repository,将其转换为URL,添加到定义URL集合中。最后将URL集合转化为数组,构造URLClassLoader。
public static ClassLoader createClassLoader(List repositories,
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled())
log.debug("Creating new class loader");
/** 定义个URL集合,用来作为构造URLClassLoader时需要URL集合参数 **/
Set set = new LinkedHashSet<>();
/** 读取遍历repositories,将资源转换为URL放入set集合中 **/
if (repositories != null) {
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
/** 通过指定资源路径构造URL **/
URL url = buildClassLoaderUrl(repository.getLocation());
if (log.isDebugEnabled())
log.debug(" Including URL " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.DIR) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
/** 校验目录资源 **/
if (!validateFile(directory, RepositoryType.DIR)) {
continue;
}
/** 获取文件目录URL **/
URL url = buildClassLoaderUrl(directory);
if (log.isDebugEnabled())
log.debug(" Including directory " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.JAR) {
File file=new File(repository.getLocation());
file = file.getCanonicalFile();
/** 校验jar文件资源 **/
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
/** 获取jar文件URL **/
URL url = buildClassLoaderUrl(file);
if (log.isDebugEnabled())
log.debug(" Including jar file " + url);
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory=new File(repository.getLocation());
directory = directory.getCanonicalFile();
/** 校验jar文件资源 **/
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including directory glob "
+ directory.getAbsolutePath());
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
/** 遍历目录中文件,找到jar文件,添加到set中 **/
for (int j = 0; j < filenames.length; j++) {
String filename = filenames[j].toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar"))
continue;
File file = new File(directory, filenames[j]);
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
if (log.isDebugEnabled())
log.debug(" Including glob jar file "
+ file.getAbsolutePath());
URL url = buildClassLoaderUrl(file);
set.add(url);
}
}
}
}
/** 将集合URL转换为数组 **/
final URL[] array = set.toArray(new URL[set.size()]);
if (log.isDebugEnabled())
for (int i = 0; i < array.length; i++) {
log.debug(" location " + i + " is " + array[i]);
}
/** 创建URLClassLoader **/
return AccessController.doPrivileged(
new PrivilegedAction() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
参考链接
Tomcat源码学习--WebAppClassLoader类加载机制
Tomcat 源码分析 WebappClassLoader 分析 (基于8.0.5)
深入理解Tomcat(五)类加载机制
深入理解Java类加载器
双亲委派模型与线程上下文类加载器
深入理解 Tomcat(四)Tomcat 类加载器之为何违背双亲委派模型