(目录)
Java ClassLoader
ClassLoader 做什么的?
众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载
到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流
的过程.
有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。
每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。
class Class<T> {
...
private final ClassLoader classLoader;
...
}
双亲委派模型
说到 Java 的类加载器,必不可少的就是它的双亲委派模型
,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
- 其他的类加载器,都是由 Java 实现,在虚拟机的外部,并且全部继承自
java.lang.ClassLoader
在 Java 内部,绝大部分的程序都会使用 Java 内部提供的默认加载器.
启动类加载器(Bootstrap ClassLoader)
BootstrapClassLoader
负责加载 JVM 运行时核心类
,这些类位于 JAVA_HOME/lib/rt.jar 文件
中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器
」。
扩展类加载器(Extension ClassLoader)
ExtensionClassLoader
负责加载 JVM 扩展类
,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar
中,有很多 jar 包。
应用程序类加载器(Application ClassLoader)
AppClassLoader
才是直接面向我们用户的加载器
,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录
。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader
工作流程:
- 收到类加载的请求
- 首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
- 如果父类加载器没有,继续寻找父类加载器.
- 搜索了一圈,发现都找不到,然后才是自己尝试加载此类.
程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?
虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。
何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。
因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。
自定义加载器(Custom ClassLoader)
运用场景
- 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于
自定义路径中的class类文件的加载,我们需要自己的ClassLoader
- 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要
自己实现加载类的逻辑
,当然其他的特殊处理也同样适用。- 可以
定义类的实现机制
,实现类的热部署
, 如OSGi中的bundle模块
就是通过实现自己的ClassLoader实现的。
ClassLoad加载class文件逻辑
ClassLoader的loadClass采用 双亲委派型实现,因为我们实现的ClassLoader都继承于java.lang.ClassLoader类,父加载器都是AppClassLoader,所以在上层逻辑中依旧要保证该模型,所以一般不覆盖loadClass函数
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,resolve);
else
c = findBootstrapClassOrNull(name);
}catch ( ClassNotFoundException e ){
//如果父加载器无法加载
}
if( c == null ){//父类不能加载,由当前的类加载器加载
c = findClass(name);
}
}
if( resolve ){//如果要求立即链接,那么加载完类直接链接
resolveClass();
}
//将加载过这个类对象直接返回
return c;
}
从上面的代码中,我们可以看到在父加载器不能完成加载任务时,会调用findClass(name)函数
。
这个就是我们自己实现的ClassLoader的查找类文件的规则,所以在继承后,我们只需要覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,而且还不会破坏双亲委托模型
注意:
不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库
自定义类加载器,加载自定义路径下的class文件
覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,不会破坏双亲委托模型
package com.company;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.ByteBuffer;
/**
* Created by liulin on 16-4-20.
*/
public class MyClassLoader extends ClassLoader {
private String classpath;
public MyClassLoader( String classpath){
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getClassFile( name );
byte[] classByte=null;
try {
classByte = getClassBytes(fileName);
}catch( IOException e ){
e.printStackTrace();
}
//利用自身的加载器加载类
Class retClass = defineClass( null,classByte , 0 , classByte.length);
if( retClass != null ) {
System.out.println("由我加载");
return retClass;
}
//System.out.println("非我加载");
//在classPath中找不到类文件,委托给父加载器加载,父类会返回null,因为可加载的话在
//委派的过程中就已经被加载了
return super.findClass(name);
}
/***
* 获取指定类文件的字节数组
* @param name
* @return 类文件的字节数组
* @throws IOException
*/
private byte [] getClassBytes ( String name ) throws IOException{
FileInputStream fileInput = new FileInputStream(name);
FileChannel channel = fileInput.getChannel();
ByteArrayOutputStream output = new ByteArrayOutputStream();
WritableByteChannel byteChannel = Channels.newChannel(output);
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int flag;
while ((flag = channel.read(buffer)) != -1) {
if (flag == 0) break;
//将buffer写入byteChannel
buffer.flip();
byteChannel.write(buffer);
buffer.clear();
}
}catch ( IOException e ){
System.out.println("can't read!");
throw e;
}
fileInput.close();
channel.close();
byteChannel.close();
return output.toByteArray();
}
/***
* 获取当前操作系统下的类文件合法路径
* @param name
* @return 合法的路径文件名
*/
private String getClassFile ( String name ){
//利用StringBuilder将包形式的类名转化为Unix形式的路径
StringBuilder sb = new StringBuilder(classpath);
sb.append("/")
.append ( name.replace('.','/'))
.append(".class");
return sb.toString();
}
public static void main ( String [] args ) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader("/home/liulin/byj");
try {
myClassLoader.loadClass("java.io.InputStream");
myClassLoader.loadClass("TestServer");
myClassLoader.loadClass("noClass");
}catch ( ClassNotFoundException e ){
e.printStackTrace();
}
}
}
从结果我们看,因为我们加载的类的父加载器是系统加载器,所以调用 双亲委托 的loadClass,会直接加载掉java.io.InputStream类,只有在加载双亲中没有的TestServer类,才会用到我们自己的findClass加载逻辑加载指定路径下的类文件
Thread.contextClassLoader(线程上下文类加载器)源码分析
阅读 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别
public class Thread implements Runnable {
// 这里省略了无关代码.....
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 这里省略了无关代码....
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
// 继承父线程的 上下文类加载器
this.contextClassLoader = parent.contextClassLoader;
// 这里省略了无关代码....
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 这里省略了无关代码.....
}
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。
你可以使用下面这种方式来显示使用它:
Thread.currentThread().getContextClassLoader().loadClass(name);
程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。
有了Thread ContextClassLoader,就可以实现父ClassLoader让子ClassLoader去完成一个类的加载任务,即父ClassLoader加载的类中,可以使用ContextClassLoader去加载其无法加载的类)。
contextClassLoader 究竟是做什么用的?
跨线程共享类
,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader
如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来
举例:
如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。
如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。
在 SPI 中 的使用
Java 中所有涉及SPI机制的类加载基本上都是采用这种方式,最常见的就是JDBC Driver的加载
。
Java定义的JDBC接口
位于JDK的rt.jar
中(java.sql
包),因此这些接口会由BootstrapClassLoader进行加载
;
而数据库厂商提供的Driver驱动包一般由我们自己在应用程序中引入
(比如位于CLASSPATH下),这已经超出了BootstrapClassLoader的加载范围,即这些驱动包中的JDBC接口的实现类无法被BootstrapClassLoader加载,只能由AppClassLoader或自定义的ClassLoader来加载
。
这样,SPI机制就没有办法实现。
SPI 为什么需要 ThreadContextClassLoader 才能实现
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。
SPI接口中的代码经常需要加载具体的实现类
。SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)**来加载的;
SPI的实现类是由**系统类加载器(System ClassLoader)**来加载的,依照双亲委派模型,
BootstrapClassloader无法委派AppClassLoader来加载类
。
要解决这个问题,就需要使用Thread Context ClassLoader
,线程上下文类加载器破坏了“双亲委派模型”
,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器
。
JDBC SPI 实现分析
平时是如何使用mysql获取数据库连接的:
// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
12345
以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写Class.forName
被注释掉了,但依然可以正常运行,这是为什么呢?
这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。
那到底是在哪一步自动注册了mysql driver的呢
?
重点就在
DriverManager.getConnection()
中。我们都是知道
调用类的静态方法会初始化该类,进而执行其静态代码块
DriverManager的静态代码块就是:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
下面就查看下JDK中的DriverManager
类的源码,来看看其中Thread ContextClassLoader的使用。
public class DriverManager {
// 省略无关代码......
static {
loadInitialDrivers(); // 在静态代码块中加载当前环境中的 JDBC Driver
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
// 省略无关代码.....
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通过 ServiceLoader#load 方法来加载 Driver 的实现(如 MySQL、Oracle、PostgreSQL 提供的 Driver 实现)
// 即 SPI 机制
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
}
DriverManager
类在被加载的时候就会执行通过ServiceLoader#load方法来加载数据库驱动(即Driver接口的实现)。
由于每个类都会使用加载自己的ClassLoader去加载其他的类(即它所依赖的类),因此可以简单考虑以上代码的类加载过程为:
DriverManager类由BootstrapClassLoader加载,DriverManager类依赖于ServiceLoader类,因此BootstrapClassLoader也会尝试加载ServiceLoaer类,这是没有问题的;
再往下,ServiceLoader的load方法中需要加载数据库(MySQL等)驱动包中Driver接口的实现类,即ServiceLoader类依赖这些驱动包中的类;
此时如果是默认情况下,则还是由BootstrapClassLoader来加载这些类,但驱动包中的Driver接口的实现类是位于CLASSPATH下的,BootstrapClassLoader是无法加载的,这就有问题了。
因此,在ServiceLoader#load方法中实际是指明了由ContextClassLoader来加载驱动包中的类:
public final class ServiceLoader<S> implements Iterable<S> {
// 省略无关代码
public static <S> ServiceLoader<S> load(Class<S> service) {
// 需要注意的是,这里使用的是 当前线程的 ContextClassLoader 来加载实现,这也是 ContextClassLoader 为什么存在的原因。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
到此总结一下:
使用TCCL,也就是说
把自己加载不了的类加载到TCCL中
TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader
ContextClassLoader
默认存放了AppClassLoader
的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()
取出应用程序类加载器来完成需要的操作。
线程上下文类加载器(TCCL)的适用场景:
- 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
- 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
标签:java,name,ClassLoader,SPI,线程,Java,Class,加载 From: https://blog.51cto.com/panyujie/8741301