JVM 系列()ClassLoader

在前面一节中,主要介绍了 Class 的装载过程,Class 的装载大体上可以分为加载类、连接类和初始化 3 个阶段。本小节将主要介绍绍 Java 语言中的 ClassLoader,类装载器。它主要工作在 Class 装载的加载阶段从系统外部获得 Class 二进制数据流。

一、ClassLoader

ClassLoader 是 Java 的核心组件,所有的 Class 都是由 ClassLoader 进行加载的, ClassLoader 负责通过各种方式将 Class 信息的二进制数据流读入系统,然然后交给 Java 虚拟机进行连接、初始化等操作。因此, Classloader在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader 去改变类的连接和初始化行为。

从代码层面看, ClassLoader 是一个抽象类,它提供了一些重要的接口,用于自定义 Class 的加载流程和加载方式。 Classloader 的主要方法如下

private final ClassLoader parent;

/**
* 给定一个类名,加载一个类,返回代表这个类的 Class 实例,如果找不到类,则返回 ClassNotFoundException 异常
*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
} /**
* 将二进制字节码流解析为 Class 实例
*/
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
} /**
* 查找一个类,这是一个受保护的方法,也是重载 ClassLoader 时,重要的系统扩展点。
* 这个方法会在 loadClass() 时被调用,用于自定义查找类的逻辑。
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
} /**
* 寻找已经加载的类
*/
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

在 ClassLoader 的结构中,还有一个重要的字段 parent,它也是一个 ClassLoader 的实例,这个字段所表示的 ClassLoader 也称为这个 ClassLoader 的双亲。在类加载的过程中, ClassLoader 可能会将某些请求交予自己的双亲处理。

二、ClassLoader 分类

在标准的 Java 程序中,Java 虚拟机会创建 3 类 ClassLoader 为整个应用程序服务。 它们分别是: BootstrapClassLoader(启动类加载器)、 ExtensionClassLoader(扩展类加载器)和 AppClassLoader(应用类加载器,也称为系统类加载器)。 此外,每一个应用程序还可以拥有自定义的 ClassLoader,扩展 Java 虚拟机获取 Class 数据的能力。

  • BootstrapClassLoader 加载 rt.jar
  • ExtensionClassLoader 加载 $JAVA_HOME/lib/ext/*.jar
  • AppClassLoader 加载 classpath 下的 *.jar

各个 ClassLoader 的层次自顶往下为启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。其中,应用类加载器的双亲为扩展类加载器,扩展类加载器的双亲为启动类加载器。当系统需要使用一个类时,在判断类是否已经被加载时,会从当前底层类加载器进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到到成功。

在这些些 ClassLoader 中,启动类加载器最为特别,它是完全由 C 代码实现的,并且在 Java 中没有对象与之对应。系统的核心类就是由启动类加载器进行加载的,它也是虚拟机的核心组件。扩展类加载器和应用类加载器都有对应的 Java 对象可供使用。

// 输出全部的类加载器
public class PrintClassLoaderTree { public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader);
classLoader = classLoader.getParent();
}
}
}

结果如下:

sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@1dac704

三、ClassLoader 的双亲委托模式

系统中的 ClassLoader 在协协同工作时,默认会使用双亲委托模式。即在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如如果双亲请求失败,则会自己加载。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); // (1)
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // (2)
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // (3) // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

(1) 当前 ClassLoader 试图查找该类是否已经被加载,如果已经被加载则直接返回。

(2) 如果没有被加载,则会请求其双亲加载(不是自己加载),如果双亲为 null 时,则使用启动类加载器加载。

(3) 如果双亲加载不成功,则由当前 ClassLoader 尝试加载。

四、双亲委托模式的弊端

在前文中已经提到,检查类是否已经加载的委托过程是单向的。这种方式虽然从结构上说比较清晰,使各个 ClassLoader 的职责非常明确,但是同时会带来一个问题,即顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类。

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问。比如,在系统类中,提供了一个接口,该接口需要在应用中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。拥有这种问题的组件有很多,比如 JDBC、 XmlParser 等。

五、双亲委托模式的补充

在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 ServiceProvider Interface,即 SPI。

下面以 javax.xml.parsers 中实现 XML 文件解析功能模块为例,说明如何在启动类加载器中,访问由应用类加载器实现的 SPI 接口实例。在 javax.xml.parsers.DocumentBuilderfactory 中有如下实现,用来构造一个 Documentbuilderfactory 实例,注意 Document Builderfactory是一个抽象类(加载在启动类加载器中),可以由应用程序自行实现,这里也将介绍该方法如何返回一个在应用类加载器中的实例。

public static DocumentBuilderFactory newInstance() {
return FactoryFinder.find(
/* The default property name according to the JAXP spec */
DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"
/* The fallback implementation class name */
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
}

FactoryFinder.find() 函数试图加载并返回一个 DocumentBuilderFactory 实例。当这个实例在应用层 jar 包里时,它会使用如下方法进行查找:

T provider = findServiceProvider(type);

其中 type 就是字符串 "Javax.xml.parsers.DocumentBuilderFactory", findServiceProvider 的主要内容如下代码所示,这段代码码并非 JDK 中的源码,为了节省版面,笔者做了适当的裁剪,只保留核心部分。

// jdk1.8 FactoryFinder 调用 ServiceLoader.load(type) 加载 jar 包中的实现类
private static <T> T findServiceProvider(final Class<T> type) {
try {
return AccessController.doPrivileged(new PrivilegedAction<T>() {
public T run() {
final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
final Iterator<T> iterator = serviceLoader.iterator();
if (iterator.hasNext()) {
return iterator.next();
} else {
return null;
}
}
});
} catch(ServiceConfigurationError e) {
final RuntimeException x = new RuntimeException(
"Provider for " + type + " cannot be created", e);
final FactoryConfigurationError error =
new FactoryConfigurationError(x, x.getMessage());
throw error;
}
} // ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

从以上代码可知,ServiceLoader 获得了一个名为上下文加载器的 ClassLoader,从以下代码可知,上下文加载器是从 Thread.currentThread().getContextClassLoader() 中得到的。并将此 ClassLoader 传入 ServiceLoader.load(service, cl) 方法,由这个 ClassLoader 去完成实例的加载和创建,而不是由这段代码所在的启动类加载器去加载。

六、突破双亲模式

双亲模式的类加载方式是虚拟机默认的行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,不少应用软件和框架都修改了这种行为,比如 Tomcat 和 OSGi 框架,都有各自独特的类加载顺序。在本小节中,将演示如何打破默认的双亲模式。

下面的代码通过重载 loadclass() 方法,改变类的加载次序,这里给出部分核心代码:

public class MyClassLoader extends ClassLoader {

    private String dir;

    public MyClassLoader(String dir) {
this.dir = dir;
} @Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findClass(className);
if (clazz == null) {
//System.out.println("can't load class:" + className + " need from parent");
return super.loadClass(className, resolve);
}
return clazz;
} @Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
if (className.startsWith("java")) {
return null;
} Class<?> clazz = super.findLoadedClass(className);
if (clazz == null) {
FileInputStream fis = null;
try {
String classFile = getClassFile(className);
fis = new FileInputStream(new File(classFile));
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (true) {
int len = fc.read(buf);
if (len == 0 || len == -1) {
break;
}
buf.flip();
wbc.write(buf);
buf.clear();
}
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
;
}
}
}
return clazz;
}
return super.findClass(className);
} public String getClassFile(String className) {
String path = className.replace(".", File.separator);
return dir + File.separator + path + ".class";
}
}

以上代码通过自定义 ClassLoader,重载 loadClass() 改变了默认的委托双亲加载的方式,通过 findClass() 读取 class 文件,并将二进制流定义为 Class 对象。如果加载不到,则委托双亲加载,这种方式颜倒了默认的加载顺序。

public static void main(String[] args) throws Exception, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader(
"F:\\doc\\java\\code-2018\\disruptor\\target\\test-classes");
Class<?> clazz = myClassLoader.loadClass(MyClass.class.getName(), true); ClassLoader classLoader = clazz.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader);
classLoader = classLoader.getParent();
} Method method = clazz.getMethod("sayHello");
method.invoke(clazz.newInstance());
}

自定义的类加载器默认的父加载器为系统类加载器:

com.github.binarylei.jvm.classloader.MyClassLoader@7f7052
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@899482

七、热替换的实现

但对 Java 来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件并无法述紧统再来加载并重定义这个类。因此,在 Java 中实现这一功能的一个可行的方法就是灵活运用 ClassLoader。由不同 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容。

import java.io.*;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel; /**
* @author: leigang
* @version: 2018-07-04
*/
public class HotClassLoader extends ClassLoader { private String dir; public HotClassLoader(String dir) {
this.dir = dir;
} @Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
if (!className.startsWith("com.github.binarylei")) {
return null;
}
Class<?> clazz = super.findLoadedClass(className);
if (clazz == null) {
FileInputStream fis = null;
try {
String classFile = getClassFile(className);
fis = new FileInputStream(new File(classFile));
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (true) {
int len = fc.read(buf);
if (len == 0 || len == -1) {
break;
}
buf.flip();
wbc.write(buf);
buf.clear();
}
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
;
}
}
}
return clazz;
}
return super.findClass(className);
} public String getClassFile(String className) {
String path = className.replace(".", File.separator);
return dir + File.separator + path + ".class";
}
}

准备一个要替换的类:

package com.github.binarylei.jvm.classloader;

public class DemoA {
public void sayHello() {
System.out.println("++++++++++++");
//System.out.println("------------");
}
}

测试:

public static void main(String[] args) throws Exception, InstantiationException {
while (true) {
HotClassLoader myClassLoader = new HotClassLoader("D:\\tmp\\clz");
Class<?> clazz = myClassLoader.loadClass("com.github.binarylei.jvm.classloader.DemoA"); Method method = clazz.getMethod("sayHello");
method.invoke(clazz.newInstance()); Thread.sleep(1000);
}
}

参考:

本文转载至《实战JAVA虚拟机 JVM故障诊断与性能优化》第十章


每天用心记录一点点。内容也许不重要,但习惯很重要!

JVM 系列 ClassLoader的更多相关文章

  1. JVM系列文章(四):类载入机制

    作为一个程序猿,只知道怎么用是远远不够的. 起码,你须要知道为什么能够这么用.即我们所谓底层的东西. 那究竟什么是底层呢?我认为这不能一概而论.以我如今的知识水平而言:对于Web开发人员,TCP/IP ...

  2. JVM系列五:JVM监测&工具

    JVM系列五:JVM监测&工具[整理中]  http://www.cnblogs.com/redcreen/archive/2011/05/09/2040977.html 前几篇篇文章介绍了介 ...

  3. jvm系列(八):jvm知识点总览-高级Java工程师面试必备

    在江湖中要练就绝世武功必须内外兼备,精妙的招式和深厚的内功,武功的基础是内功.对于武功低(就像江南七怪)的人,招式更重要,因为他们不能靠内功直接去伤人,只能靠招式,利刃上优势来取胜了,但是练到高手之后 ...

  4. jvm系列 (二) ---垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 前言:本文基于<深入java虚拟机>再加上个人的理解以及其他相关资料,对内容进行整理浓缩总结.本文中的图来自网络,感谢图的作者.如果有不正确的地方,欢迎指出. 目 ...

  5. jvm系列 (五) ---类的加载机制

    类的加载机制 目录 jvm系列(一):jvm内存区域与溢出 jvm系列(二):垃圾收集器与内存分配策略 jvm系列(三):锁的优化 jvm系列 (四) ---强.软.弱.虚引用 我的博客目录 什么是类 ...

  6. jvm系列 (五) ---类加载机制

    类的加载机制 目录 jvm系列(一):jvm内存区域与溢出 jvm系列(二):垃圾收集器与内存分配策略 jvm系列(三):锁的优化 jvm系列 (四) ---强.软.弱.虚引用 我的博客目录 什么是类 ...

  7. jvm系列(四):jvm知识点总结

    原文链接:http://www.cnblogs.com/ityouknow/p/6482464.html jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 ...

  8. JVM系列——从菜鸟到入门

    持续更新系列. 参考自<深入理解Java虚拟机>.<Java性能权威指南>.<分布式Java应用基础与实践>. Java的内存结构 JVM系列——运行时数据区 JV ...

  9. jvm系列四、jvm知识点总结

    原文链接:http://www.cnblogs.com/ityouknow/p/6482464.html jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 ...

随机推荐

  1. 10.Action中的method属性

    转自:https://wenku.baidu.com/view/84fa86ae360cba1aa911da02.html 在struts1.x中我们知道通过继承DispatchAction可以实现把 ...

  2. Springmvc Exception

    对于异常处理,大多使用一个共同的异常类统一处理从dao,service,controller层抛出的异常,将页面跳转到共同的error页面. public class CommonException ...

  3. python 之九九乘法表

    for i in range(1,10): for j in range(1,i+1): print(f"{j}*{i}={i*j}",end='\t') print() 运行结果 ...

  4. AI-人工智能-参考文档

     人工智能——目录汇总: https://blog.csdn.net/qq_27297393/article/details/80685474   人工智能——高数篇: https://blog.cs ...

  5. ABAP-关于隐式与显式的DB Commit

    转载:https://www.cnblogs.com/liaojunbo/archive/2011/07/11/2103491.html 1.显式的DB Commit 显式的DB Commit并没有对 ...

  6. Effective C++笔记——day01

    1.当我们看到赋值符号时,请小心,因为"="也可以用来调用copy构造函数 Widget w3 = w2; //调用copy构造函数,而不是copy赋值操作符 2.不明确的行为: ...

  7. C++连接Oracle之OCCI(windows)

    上一节我们讲过了ADO连接Oracle,这一节我们尝试通过OCCI的方式,来在windows平台下连接Oracle数据库,下一节讨论在Linux环境下通过OCCI的方式连接远程的Oracle数据库. ...

  8. Virtualbox [The headers for the current running kernel were not found] (操作过程后还是失败,显示相同问题)

    在笔记本安装Ubuntu11.04增强功能失败 引用 fuliang@fuliang-VirtualBox:~$ sudo /etc/init.d/vboxadd setup Removing exi ...

  9. Null value was assigned to a property of primitive type setter of"原因及解决方法

    在action请求数据的过程中报出"Null value was assigned to a property of primitive type setter of"错误,搜索之 ...

  10. Servlet Response 重定向

    重定向 response.sendRedirect("index.jsp");       //登录用户名不存在,重定向到index.jsp 1重定向在客户端发挥作用,通过浏览器重 ...