本文内容
问题
Class.getCLassLoader()获取到的类加载器是什么时候绑定在一起的,有什么语义- 我们使用
SomeClass a = new SomeClass()的时候,SomeClass这个类是被什么类加载器加载的 - 两个不同的类加载器加载同一个类,是否会初始化两次
- Class.forName() 和 classLoader.loadClass() 的区别
- 默认的线程上下文类加载器是什么
- 线程上下文类加载器有什么作用
- 怎么自定义类加载器
说明
本文的源码均来自于我的 Mac 上安装的 adoptopenjdk-8.jdk ,非 Oracle JDK,因此部分细节有可能与 Oracle JDK 8 有所不同,但是应该基本上来说都是一致的。
运行代码时,如果没有明确说明,那么均是使用了 IDEA 提供的 springboot 2.x 模板运行的,主要是为了方便。没有明确说明的时候,都是使用 IDE 的运行按钮去运行的代码,而不是打成 springboot 的 fat jar 运行的。
本文不是很权威,仅仅是说下我个人的理解,因为一直对类加载器这一块儿模模糊糊的,最近正好有时间看这一块儿的内容,决定好好记录一下自己的研究过程和研究成果。
参考
BootstrapClassLoader
这个类加载器实际上并不存在于 Java 中,它是由 C++ 编写的 JVM 实现的一部分,它的主要作用就是加载 JDK 的核心类库,以 java.* 开头的那些类(可能应该还有一些 sun 开头的类)。
当我们启动 JVM 的时候(视作一个用 C++ 写的普通的进程),BootstrapClassLoader(存在于 C++ 进程中)就会去加载 JDK 的核心类库,以前我们要配置 CLASSPATH 环境变量应该也是为了去告诉它去哪里加载核心类库所在的 jar 包。现在来说其实是不需要配置 CLASSPATH,JVM 会自动去找到核心类库的 jar 包路径(rt.jar),其实我猜测应该也就是相对于 java 命令本身所在的路径吧(../lib/rt.jar)或者是相对于 $JAVA_HOME 的路径
在我安装的
adoptopenjdk-8.jdk中,实际上不存在 $JAVA_HOME/lib/rt.jar,只有 $JAVA_HOME/src.zip,BootstrapClassLoader 加载的是这个 zip 包里的核心类库。(jar包其实就是zip格式)可以使用 -Xbootclasspath 来改变它加载的路径,具体看我前面的文章
classpath和java命令
sun.misc.Launcher
一直传说的类加载器的三层模型:BootstrapClassLoader ---> ExtClassLoader ---> AppClassLoader,其实后面 2 个 classLoader 都是 Launcher 的静态内部类。
首先看下 Launcher 的关键逻辑:
public class Launcher {
// 关键逻辑,这里去调用构造函数构造一个实例了
private static Launcher launcher = new Launcher();
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
// Finally, install a security manager if requested
String s = System.getProperty("java.security.manager");
if (s != null) {
// init FileSystem machinery before SecurityManager installation
sun.nio.fs.DefaultFileSystemProvider.create();
SecurityManager sm = null;
if ("".equals(s) || "default".equals(s)) {
sm = new java.lang.SecurityManager();
} else {
try {
sm = (SecurityManager)loader.loadClass(s).newInstance();
} catch (IllegalAccessException e) {
} catch (InstantiationException e) {
} catch (ClassNotFoundException e) {
} catch (ClassCastException e) {
}
}
if (sm != null) {
System.setSecurityManager(sm);
} else {
throw new InternalError(
"Could not create SecurityManager: " + s);
}
}
}
}
private static Launcher launcher = new Launcher();初始化的逻辑:- 因为 Launcher 属于核心类库,因此它被 BootstrapClassLoader 加载后执行了初始化,因此这里就调用构造方法构造了 Launcher 实例
public Launcher()构造方法的逻辑:使用
ExtClassLoader.getExtClassLoader()构造 ExtClassLoader 实例使用
loader = AppClassLoader.getAppClassLoader(extcl);构造 AppClassLoader 实例,实际上也就是 AppClassLoader 内部持有 ExtClassLoader 实例Thread.currentThread().setContextClassLoader(loader);- 将 AppClassLoader 设置为线程上下文类加载器
使用
java.security.manager定义的 SecurityManager 类名,使用 AppClassLoader 加载后作为系统的 SecurityManager,如果没有定义,就使用java.lang.SecurityManager(这一块儿以后再研究)
总结
- ExtClassLoader 和 AppClassLoader 都是 Launcher 的静态内部类
- Launcher 被 BootstrapClassLoader 加载初始化后,先后构造了 ExtClassLoader 和 AppClassLoader 实例,然后将 AppClassLoader 设置为线程的上下文类加载器,这就是 Launcher 的主要逻辑
- AppClassLoader 被 Launcher 持有作为它给出的 ClassLoader
- AppClassLoader 内部持有 ExtClassLoader 作为上层的 ClassLoader
ExtClassLoader
ExtClassLoader 是用来加载以 javax.* 开头的那些 java 扩展类库,这些类库的 jar 包存在于 $JAVA_HOME/jre/lib/ext 目录下,说白了这个 ExtClassLoader 就是专门去加载这个路径下的 jar 包的,我们把自己的 jar 包扔到这个路径下面也会被它加载,可以使用 java 系统属性:java.ext.dirs 来改变它加载的路径
初始化逻辑
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
private static volatile ExtClassLoader instance = null;
public static ExtClassLoader getExtClassLoader() throws IOException
{
if (instance == null) {
synchronized(ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
final File[] dirs = getExtDirs();
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
}
被 Launcher 构造之前进行初始化
初始化
ClassLoader.registerAsParallelCapable();的逻辑:@CallerSensitive protected static boolean registerAsParallelCapable() { Class<? extends ClassLoader> callerClass = Reflection.getCallerClass().asSubclass(ClassLoader.class); return ParallelLoaders.register(callerClass); }- 使用反射获取到调用这个方法的类,也就是 ExtClassLoader
- 将自己注册为可并行的类加载器
初始化主要是注册自己为可并行的类加载器,这种使用反射 Reflection.getCallerClass() 的模式就很有用,注册了之后,ClassLoader 就知道:这个 ClassLoader 可以并行加载,也就是说它的 loadClass 方法可以安全地被多个线程调用去加载类
getExtClassLoader 方法
public static ExtClassLoader getExtClassLoader() throws IOException
{
if (instance == null) {
synchronized(ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
- 双重判空来确保单例
- 使用 createExtClassLoader 方法构造出单例对象作为自己的静态字段
没啥太多说的,主要是确保只有一个单例,最终逻辑还是在 createExtClassLoader 方法
createExtClassLoader 方法
private static ExtClassLoader createExtClassLoader() throws IOException {
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
final File[] dirs = getExtDirs();
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
- 使用 getExtDirs 里获取到的文件路径,最后构造了一个 ExtClassLoader 实例
AppClassLoader
AppClassLoader 是用来加载我们自行传入的应用程序所包含的类的,我们传入的 classpath 最终就会被 AppClassLoader 去加载,通过 java -cp dir1:dir2:dir3:aaa.jar:bbb.jar 传入多个 classpath
初始化逻辑
static class AppClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
}
- 被 Launcher 构造之前进行初始化
ClassLoader.registerAsParallelCapable();将自己注册为可并行的类加载器
getAppClassLoader 方法
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
final URLClassPath ucp;
/*
* Creates a new AppClassLoader
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
ucp.initLookupCache(this);
}
- Launcher 使用 getAppClassLoader 获取一个 AppClassLoader 实例,传入的是 ExtClassLoader
- 我们通过
java -cp传递的 classpath,在 java 程序中通过System.getProperty("java.class.path");访问到 - 将 classpath 转换成 url 的集合,缓存到它继承的 URLClassPath 中去
ClassLoader 类源码分析
说明
源码是 mac 下的 adoptopenjdk-8,主要是进行 ClassLoader 的一些关键逻辑的分析,一些细枝末节和一些跟安全性相关的内容没有仔细去看和了解。主要是着眼于 ClassLoader 加载类的流程,以及它加载的一些机制。
静态变量
// The class loader for the system // @GuardedBy("ClassLoader.class") private static ClassLoader scl;- 系统类加载器,也就是 AppClassLoader
// Set to true once the system class loader has been set // @GuardedBy("ClassLoader.class") private static boolean sclSet;- 表示系统类加载器是否已经设置了,一旦 AppClassLoader 被设置给 scl,这个值就是 true
总结
也就是说,ClassLoader 的静态变量也会持有一个系统类加载器,也就是 AppClassLoader
实例变量
// The parent class loader for delegation // Note: VM hardcoded the offset of this field, thus all new fields // must be added *after* it. private final ClassLoader parent;- 每个 ClassLoader 的实现类,都持有一个 parent 的 ClassLoader,因此 ClassLoader 的层次结构类似于一棵树,任何一个 ClassLoader 最后都通过 parent 逐步向上到达树根:null(BootstrapClassLoader)
// Maps class name to the corresponding lock object when the current // class loader is parallel capable. // Note: VM also uses this field to decide if the current class loader // is parallel capable and the appropriate lock object for class loading. private final ConcurrentHashMap<String, Object> parallelLockMap;- 这个 parallelLockMap 只有注册了并行加载能力的 ClassLoader 才有,没有注册并行加载能力的 ClassLoader,这个字段的值将会是 null
- ClassLoader 使用这个字段来判断:定义的具体的 ClassLoader 是否具有并行加载能力(loadClass 方法是否能够被不同线程同时访问)。
- 如果没有并行加载能力,loadClass 将会是一个同步方法,锁对象就是当前的 ClassLoader 实例。如果有并行加载能力,将会从这个 map 里获取到锁对象(map是类名到锁对象的映射),如果没有锁对象的话(表示当前这个类没有其他线程在加载),就新生成一个 Object 作为锁对象,然后在加载这个特定类的时候使用这个锁对象加锁
- 也就是说:
- 默认加载所有的类都使用当前的 ClassLoader 实例加锁
- 注册了并行加载后,仅在加载 A 类时使用 A 类对应的某个特定锁对象 lockA,加载 B 类时使用 B 类对应的某个锁对象 lockB,这样就大大减小了锁的粒度
// The classes loaded by this class loader. The only purpose of this table // is to keep the classes from being GC'ed until the loader is GC'ed. private final Vector<Class<?>> classes = new Vector<>();- 每个 ClassLoader 都持有它自己加载过了的所有的类,唯一的目的就是保持一点:这些被加载后生成的 Class 对象不会被 GC 回收掉,因为:这个数组持有所有自己加载了的 Class 对象的强引用
- findLoadedClass(name),也就是查找当前 ClassLoader 是否已经加载过某个类的方法,并不是依靠这个 classes 来实现的,真正实现这个逻辑的在 native 方法中,也就是 JVM 实现里。因为很显然我们这里只能得到类的名称,并没有办法直接与 Vector<Class<?>> 去查找然后判断自己是否加载过这个类(当然我们也可以自己重写这种逻辑)
private final ProtectionDomain defaultDomain = new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, this, null);- 主要关注点在:构造这个 ProtectionDomain 的时候,把 this 指针,也就是当前 ClassLoader 加入进去了。
- 当真正让 JVM 去加载类的时候,其实是调用了 defineClass 方法,最终将这个 ProtectionDomain 传递给了这个 native 方法。因此 native 构造出 Class 对象的时候,是可以通过 ProtectionDomain 获取到当前 ClassLoader 对象实例的,所以当我们调用 someClass.getClassLoader() 的时候可以获取到它的类加载器,这个类加载器就是通过 ProtectionDomain 加入进去的
// The packages defined in this class loader. Each package name is mapped // to its corresponding Package object. // @GuardedBy("itself") private final HashMap<String, Package> packages = new HashMap<>();- 表示这个 ClassLoader 加载过了的所有的包,map的内容是包名到 Package 这个存储了包的定义的映射
- 我们可以调用 someClassLoader 来获得某个包的定义,如果这个包没有被这个 ClassLoader 加载过,默认的 ClassLoader 实现会将这个包名的查询也转交给持有的上层的 ClassLoader 去查询
总结
- 每个 ClassLoader 都持有一个 parent 对象,表示它的上层的 ClassLoader,AppClassLoader 上层是 ExtClassLoader,ExtClassLoader 上层是 null(BootstrapClassLoader)
- 由于这种持有的关系,所以 ClassLoader 是一种树状的关系,最上层的是 BootstrapClassLoader
- 一个 ClassLoader 的实现类默认是不能并行处理的,在 loadClass 方法中会对当前 ClassLoader 实例加锁。需要在静态初始化代码块中调用:
ClassLoader.registerAsParallelCapable();将自己注册为可并行的类加载器。此时这个 ClassLoader 只有在加载同一个类的时候会进行线程同步,加载不同的类是可以并行的。 - 每个 ClassLoader 实例构造出来的每个 Class 实例,都默认绑定了这个 ClassLoader 实例,通过 Class 实例的 getClassLoader 方法可以获取到真正加载它的 ClassLoader 实例(真正 define 这个 Class 的 ClassLoader)
- 由于委托模式的存在,
Class a = classLoaderA.load("xxxName"),最终 a 代表的 Class 不一定是 classLoaderA 加载的,可能是它的 parent 加载的,也就是说a.getClassLoader()不一定是 classLoaderA,可能是classLoaderA.getParent(),或者是classLoaderA.getParent().getParent() - 每个 ClassLoader 实例都保存了它加载过的 package,我们可以传入完整的包名给 ClassLoader 实例,然后获取到这个包名对应的 Package 对象,从而获取包的更多信息
静态方法
registerAsParallelCapable
@CallerSensitive
protected static boolean registerAsParallelCapable() {
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
return ParallelLoaders.register(callerClass);
}
- 如果一个 ClassLoader 想要实现并行加载,那么在静态初始化的时候,需要调用这个静态方法,将自己注册到并行加载器里面
- 它的主要逻辑就是使用反射获取调用类:也就是哪个类调用了这个方法(
Reflection.getCallerClass(),这个可以值得关注
getSystemResource
public static URL getSystemResource(String name) {
ClassLoader system = getSystemClassLoader();
if (system == null) {
return getBootstrapResource(name);
}
return system.getResource(name);
}
- 获取系统类加载器:AppClassLoader
- 使用 AppClassLoader 实例的 getResource 方法获取资源 URL
- 实际上 AppClassLoader 并没有重写 getResource 方法,最后调用的就是 ClassLoader 实例的 getResource 方法
- 语义是:使用系统类加载器去寻找某个资源,例如 "META-INF/MANIFEST.MF" 就表示找到某个 jar 包下的这个文件,多个 jar 包是可以存在相同的文件的,而这个方法仅仅是找到其中一个,然后就直接返回
getSystemResources
public static Enumeration<URL> getSystemResources(String name)
throws IOException
{
ClassLoader system = getSystemClassLoader();
if (system == null) {
return getBootstrapResources(name);
}
return system.getResources(name);
}
- 寻找的逻辑与上面一致,唯一的区别是:将所有找到的资源都返回,返回的是一个枚举,我们可以迭代这个枚举从而遍历所有找到的资源
getSystemResourceAsStream
public static InputStream getSystemResourceAsStream(String name) {
URL url = getSystemResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
- 寻找的逻辑与上面一样,唯一的区别是,这里找到的资源返回的是 InputStream,实际上来说仅仅是比上面多了一步,多帮我们调用了一个
url.openStream(),没什么其它好说的
getSystemClassLoader
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
- 获取系统类加载器,也就是 AppClassLoader
总结
- 具体的 ClassLoader 实现类通过 ClassLoader 的静态方法 registerAsParallelCapable,在将自己注册为可并行加载的类加载器(多线程同时执行这个 ClassLoader 实例的 loadClass 方法)
- 使用 getSystemResource 和 getSystemResources 方法,我们传入某个资源在某个 jar 包里的完整路径,比如 "META-INF/MANIFEST.MF" 这种,获取到这个资源的 URL
- 这两个方法基本等价于 AppClassLoader 实例的 getResource/getResources 方法
- 与类加载的委托机制类似,寻找资源的时候也是先让父加载器去寻找,最后才自己寻找
- getSystemResource 只找到第一个资源就返回,getSystemResources 则是找到所有资源,然后返回所有 URL 的枚举,我们可以遍历处理
- getSystemResourceAsStream 方法几乎与 getSystemResource 一致,仅仅是多调用了一个 url.openStream()
- getSystemClassLoader 获取到系统类加载器 AppClassLoader 实例
实例方法
构造方法
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
- 可以看到,前两个构造方法都要传入父类类加载器 parent
- 第三个构造方法使用 getSystemClassLoader 获取系统类加载器 AppClassLoader 实例
loadClass
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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);
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// 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;
}
}
- 两个方法只是不同的重载,不传入 resolve 参数的 loadClass 方法表示:加载该类,但是并不解析,解析的工作是后续由 JVM 去完成的(也就是我们并不需要显式调用
loadClass(someClass, true)) - 首先调用
findLoadedClass(name)判断自己是否加载过这个类 - 然后委托父加载器 parent 去加载该类
- 如果加载不到,就自己去加载该类,加载的逻辑就在 findClass 方法中,这个方法的语义是:根据类名找到该类并且加载
findClass
// ClassLoader.java
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// URLClassLoader.java
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
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;
}
ClassLoader 的 findClass 方法直接抛出异常,它的语义是让具体的 ClassLoader 一定要重写这个方法,也就是说,将 "根据全类名找到某个类并且加载" 这个动作交给了子类去实现,赋予了子类重写方法的权限。因此也就给我们自定义类加载器留下了一个很重要的口子
AppClassLoader 和 ExtClassLoader 都继承自 URLClassLoader,它们俩并未再次重写 findClass 这个方法,而是直接使用的 URLClassLoader 的 findClass 方法
String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false);- 核心逻辑就是将类名的点号替换为目录的分隔符,然后加上类的扩展名
class,然后使用ucp.getResource去找到这个类文件- 实际上 AppClassLoader 的 getResource 的核心逻辑就是
ucp.getResource,ucp对象里保存了 AppClassLoader 应该加载的 classpath,从这些 classpath 里去寻找资源文件
- 实际上 AppClassLoader 的 getResource 的核心逻辑就是
- 核心逻辑就是将类名的点号替换为目录的分隔符,然后加上类的扩展名
return defineClass(name, res);,真正加载了类并且返回 Class 对象的就是这个 defineClass 方法private Class<?> defineClass(String name, Resource res) throws IOException { long t0 = System.nanoTime(); int i = name.lastIndexOf('.'); URL url = res.getCodeSourceURL(); if (i != -1) { String pkgname = name.substring(0, i); // Check if package already loaded. Manifest man = res.getManifest(); definePackageInternal(pkgname, man, url); } // Now read the class bytes and define the class java.nio.ByteBuffer bb = res.getByteBuffer(); if (bb != null) { // Use (direct) ByteBuffer: CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); return defineClass(name, bb, cs); } else { byte[] b = res.getBytes(); // must read certificates AFTER reading bytes. CodeSigner[] signers = res.getCodeSigners(); CodeSource cs = new CodeSource(url, signers); sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0); return defineClass(name, b, 0, b.length, cs); } }这里的逻辑主要是根据 Resource 然后去读取这个类的字节码,存放在数组中,最终调用了 ClassLoader 的方法
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }
getResource
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
- 首先调用持有的父类加载器的 getResource 方法
- 对于 ExtClassLoader 和 AppClassLoader 来说,它们俩都没有重写这个方法
- 因此它们最终都走到了 else 分支,调用 getBootstrapResource 方法了
- 也就是说加载资源也遵循父类优先的模式
- 首先让 parent 去加载资源
- parent 加载不到的话就自己去加载资源
- 如果同一个资源有多个的话,最先被找到的那个资源被返回,例如我们可能加载了多个 jar,多个 jar 可能有多个相同的资源,比如 jar 包里都有某个清单文件,此时只有第一个被找到的才返回
- 寻找的模式是 jar 包里的文件,例如说我们想要找到 java.lang.String 这个类的类文件,可以使用
ClassLoader.getSystemClassLoader().getResource("java/lang/String.class")
getResources
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
与上面的类似,只不过返回的是这个 ClassLoader 实例的所有找到的资源,上面 getResource 仅仅找到第一个就直接返回
getResourceAsStream
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
与 getResource 基本一致,仅仅是帮我们调用了 url.openStream()
getParent
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
获取到父加载器,这里还会根据当前调用这个获取父加载器方法的类,检查这个类是否有权限去获取
总结
ClassLoader 构造方法决定了,每个类必有一个 parent 父加载器,如果实现的子类 ClassLoader 没有传入 parent 父加载器的话,默认是系统类加载器 AppClassLoader 实例
loadClass 方法默认不对类进行解析工作(解析由 JVM 自己完成)
loadClass 方法的默认行为是双亲委托,即:一个 ClassLoader 实例 loadClass 的时候,首先交给它的 parent 去 loadClass,如果 parent 加载失败了,自己才去尝试加载这个类,加载的逻辑在 findClass 方法中。这样的双亲委托保证我们使用核心类库的时候使用的是相同的版本(后面详细说明)
findClass 是我们定义一个 ClassLoader 实现类的最重要的逻辑,它的语义是
根据一个类名,获取到类的二进制字节数组
最终要负责调用
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)就实现了加载类的功能
getResource 系列的方法,默认也遵循双亲委托的模式,先由 parent 去找到资源,最后再自己去找
类如何与类加载器绑定在一起的
class ClassLoader{
private final ProtectionDomain defaultDomain =
new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
null, this, null);
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
}
- 任何 ClassLoader 的实现类,都有来自于基类 ClassLoader 的 ProtectionDomain 属性
- 在这个属性中传入了当前实现类对象的指针
new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, this, null);(倒数第二个 this 代表实现类) - define class 的时候,将 defaultDomain 传给了 native 方法,因此通过这个 defaultDomain 与加载这个类的 classloader 绑定在了一起
引用某个类的时候,使用的是什么 ClassLoader
引用某个类的方式
我们有多种方式引用某个类,例如:
A a = new A();直接引用Class.forName()someClassLoader.loadClass(className)A.class
一个类完整的命名空间其实是:加载这个类的 ClassLoader 实例 + 完整类名
我们在引用一个类的时候其实都隐含了一个 ClassLoader 实例,下面分析这些引用方式用到的 ClassLoader
直接引用
当我们在 A 类中引用 B 类的时候,默认将使用 A 类的类加载器去加载 B 类(但是由于委托模式的存在,最终真正加载 B 类的不一定是 A 类的类加载器)
public class JavaDemoApplication {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
System.out.println("系统启动");
MyClassLoader myClassLoader = new MyClassLoader(
"/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
Class<?> a = myClassLoader.loadClass("Main");
Method mainMethod = a.getMethod("main",String[].class);
mainMethod.invoke(null, (Object) null);
}
public static class MyClassLoader extends ClassLoader{
private Path startPath;
public MyClassLoader(String startPath){
// MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
super();
this.startPath = Paths.get(startPath);
}
@SneakyThrows
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
byte[] bytes = Files.readAllBytes(classPath);
return defineClass(name, bytes, 0, bytes.length);
}
}
}
// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.java
public class Main {
public static void main(String[] args) {
A a = new A();
a.hello();
System.out.println(a.getClass().getClassLoader().getClass().getName());
}
}
// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.java
public class A {
public void hello(){
System.out.println("hello, ClassLoader");
}
}
- JavaDemoApplication 是我们的主类,并且定义了一个静态内部类 MyClassLoader
- MyClassLoader 的 parent 类加载器是系统类加载器 AppClassLoader
- MyClassLoader 加载某个固定路径下的 class 文件
- 使用 MyClassLoader 加载了
/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.class - 然后调用 Main 类的 main 方法,main 方法中使用
A a = new A();引用到了 A 类(/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.class) - 此时就会由 Main 类的类加载器 MyClassLoader 去尝试加载 A 类,于是输出中就可以看到 A 类的类加载器是 MyClassLoader
完整输出
系统启动
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader
Class.forName()
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
Class.forName(String className)使用的是调用这个方法的类的类加载器来加载这个新类,也就是与直接引用的方式是一致的,并且这个类会被初始化forName(String name, boolean initialize, ClassLoader loader)使用的是我们手动传入的类加载器来加载这个新类,也就是说我们可以自己决定谁来加载这个类,并且可以控制是否初始化。
总结
- 在 A 类中引起 B 类的加载的时候,会使用 A 类的类加载器去加载 B 类
- 主要包括有
A a = new A()这种直接引用和Class.forName(className)这种加载,两者都是使用 A 类的加载器去加载新类
两个不同的类加载器是否会让一个类初始化多次
public class JavaDemoApplication {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
System.out.println("系统启动");
MyClassLoader myClassLoader = new MyClassLoader(
"/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
MyClassLoader2 myClassLoader2 = new MyClassLoader2(
"/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
Class<?> a = myClassLoader.loadClass("Main");
Class<?> b = myClassLoader2.loadClass("Main");
System.out.println(a.getClassLoader().getClass().getName());
System.out.println(b.getClassLoader().getClass().getName());
System.out.println(a == b);
Method mainMethod = a.getMethod("main",String[].class);
Method mainMethod2 = b.getMethod("main",String[].class);
mainMethod.invoke(null, (Object) null);
mainMethod2.invoke(null, (Object) null);
System.out.println("系统结束");
}
public static class MyClassLoader extends ClassLoader{
private Path startPath;
public MyClassLoader(String startPath){
// MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
super();
this.startPath = Paths.get(startPath);
}
@SneakyThrows
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
byte[] bytes = Files.readAllBytes(classPath);
return defineClass(name, bytes, 0, bytes.length);
}
}
public static class MyClassLoader2 extends MyClassLoader{
public MyClassLoader2(String startPath){
super(startPath);
}
/**
* 先尝试自己加载,然后才转移给父类
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> aClass = null;
try {
aClass = findClass(name);
} catch (Exception e) {
return super.loadClass(name, resolve);
}
if(resolve){
resolveClass(aClass);
}
return aClass;
}
}
}
// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.java
public class Main {
public static void main(String[] args) {
A a = new A();
a.hello();
System.out.println(a.getClass().getClassLoader().getClass().getName());
}
}
// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.java
public class A {
public void hello(){
System.out.println("hello, ClassLoader");
}
}
输出如下:
系统启动
com.example.javademo.JavaDemoApplication$MyClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader2
false
Main 类被加载了
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader
Main 类被加载了
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader2
系统结束
总结
- 两个不同的类加载器可以对一个类初始化多次
- 归根结底是因为使用了不同的加载器加载一个类之后,它在 JVM 看来压根儿就是毫不相干的两个类,哪怕他们的字节码完全一致
- 因此当我们使用 2 个不同的 ClassLoader 去加载相同的字节码之后,初始化是可以执行 2 次的
- 必须要主动使用了 Class 的功能,比如调用它的方法,获取它的字段,这样 JVM 才会主动去初始化这个类
Class.forName() 和 classLoader.loadClass() 的区别
Class.forName(className)- 约等于
Class.forName(className, true, ClassLoader.getClassLoader(caller)) - 也就是说,它使用当前调用这个方法的类的类加载器来加载这个 className 对应的类
- 并且默认会执行类的初始化行为
- 约等于
Class.forName(className, initialize, classLoader)- 我们可以控制这个类被加载后是否初始化
- 使用我们传入的 classLoader 来加载这个类
classLoader.loadClass()- 使用这个类加载器来加载这个类
- 类不会被初始化,我们可以传入第二个参数表示类是否进行链接操作
- 所以主要的区别在于
Class.forName(className)会对类进行初始化操作
默认的线程上下文类加载器
- 每个线程有一个默认的线程上下文类加载器
- 每个线程将继承其父线程的上下文类加载器
- 初始的线程的上下文类加载器是 AppClassLoader
- 因此一般来说默认的线程上下文类加载器就是 AppClassLoader
为什么要有线程上下文类加载器
- 当我们在类 A 里引用类 B 的时候,默认使用的是类 A 的类加载器去加载类 B
- 在 spi 机制中,java 只定义了一组接口,而具体的实现是第三方厂商去定义的,这就出现了一种情况
- jdk 内部定义使用 spi 功能的时候,肯定是按照接口编程的,那么这些接口都是被 BootstrapClassLoader 加载的
- 而 spi 的具体实现都是厂商实现的,都是在 classpath 里,应该被 AppClassLoader 加载
- jdk 内部使用 spi 的时候加载这些第三方类库,BootstrapClassLoader 是无法加载它们的,必须交给别的加载器去加载
- 因此其实是可以使用 AppClassLoader 去加载的,java 选择添加了线程上下文类加载器的方式,使用上下文类加载器去加载
- 我的理解是
- 使用 ClassLoader.getSystemClassLoader 也是可以加载 spi 的
- 使用线程上下文类加载器是为了添加一种机制:当 BootstrapClassLoader 无法加载某类的时候将控制权交给线程上下文类加载器
- 实际上此时将控制权交给 ClassLoader.getSystemClassLoader 也就是 AppClassLoader 也是可以的
- 唯一的不同是线程上下文类加载器会更灵活一点,不同的线程的类加载控制权可以交给不同的类加载器,如果是系统类加载器,那么全都交给了这一个加载器,不够灵活
怎么自定义类加载器
- 继承 ClassLoader
- 重写 findClass 方法
- 如果想要屏蔽双亲委托的行为,可以重写 loadClass 方法
- findClass 中,最终要调用 defineClass 方法
public class MyClassLoader extends ClassLoader{
private Path startPath;
public MyClassLoader(String startPath){
// MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
super();
this.startPath = Paths.get(startPath);
}
@SneakyThrows
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
byte[] bytes = Files.readAllBytes(classPath);
return defineClass(name, bytes, 0, bytes.length);
}
}
public class MyClassLoader2 extends MyClassLoader{
public MyClassLoader2(String startPath){
super(startPath);
}
/**
* 先尝试自己加载,然后才转移给父类
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> aClass = null;
try {
aClass = findClass(name);
} catch (Exception e) {
return super.loadClass(name, resolve);
}
if(resolve){
resolveClass(aClass);
}
return aClass;
}
}
对双亲委托的一种理解
class A {
public void some() {
B b = new B();
b.call();
}
}
- 考虑上面的代码,当我们实例化了一个 A 类的对象,调用它的 some 方法的时候
- 这个 A 类的实例将会引起 B 类的加载,
new B();在语义上基本等价于A.class.getClassLoader().loadClass(“B”).newInstance(),也就是使用 A 类的加载器去尝试加载 B 类 - 如果 B 类就是 String 类,而 A 类的类加载器不遵守双亲委托,那么 B 类(String)的加载可能将会在中途被截胡,最后加载到的 String 可能就不是 BootstrapClassLoader 加载的 String 了
总结
BootstrapClassLoader 加载核心类库
AppClassLoader 和 ExtClassLoader 都是被 BootstrapClassLoader 加载的
默认的线程上下文类加载器就是 AppClassLoader
任何一个对象实例,都可以通过 getClass 方法,获取到它的类
任何一个类(Class),都可以通过 getClassLoader,获取到加载这个类的 ClassLoader 实例
类的真正命名空间,是:classloader 实例 + 类的全限定类名。只有这两个都相等的才被认为是同一个类,如果 classloader 实例不同,就会出现
java.lang.String can not cast to java.lang.String这类的错误不同的类加载器为类提供了额外的命名空间,所以相同名称的类可以并存在 JAVA 虚拟机中
我们通常在 A 类里加载 B 类,有一个默认的语义
class A { public void some() { B b = new B(); b.call(); } }B b = new B();在语义上相当于B b = A.class.getClassLoader().loadClass(“B”).newInstance()- 也就是说,我们在一个A类里面加载另一个B类的时候,默认使用的是A类的classloader去加载B类
ClassLoader 的并行加载能力就是:一个 ClassLoader 是否可以被不同的线程同时使用 loadClass 去加载类,ExtClassLoader 和 AppClassLoader 都是可以的。我们自定义的类加载器可以使用
static { ClassLoader.registerAsParallelCapable(); }这种静态初始化,将 ClassLoader 注册为可以并行加载。如果不注册的话,loadClass 方法将会是一个线程同步的方法,同一时间只能有一个线程执行这个方法
Class.forName 会执行类的初始化 ,ClassLoader 的 loadClass 不会初始化,直到第一次真正使用该类才会由 JVM 执行初始化
类可以被初始化多次:前提的这个类被多个不同的类加载器加载