目录
1. 类与类加载器
对于任意一个类,都需要有加载它的类加载器和这个类本身一同确认其在java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
2. 双亲委派机制
2.1 加载器分类
从java虚拟机的角度来看,只存在两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):C++语言实现,是java虚拟机的一部分;
- 其他所有的类加载器:这些都是有Java语言实现的,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。
从开发人员的角度来看,绝大部分Java程序都会使用以下三种类加载器,可以分为:
- 启动类加载器(
Bootstrap ClassLoader
):负责<JAVA_HOME>/lib或者被-Xbootclasspath参数指定的路径。 - 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现。负责加载<JAVA_HOME>/lib/ext目录以及被java.ext.dirs系统参数指定的类库。
- 应用类加载器(Application ClassLoader):这个类由sun.misc.Launcher$AppClassLoader实现。加载用户类路径(classPath)的类。
2.2 从源码来看双亲委派机制
双亲委派机制的图示如下,其其工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派其父类加载器去完成,每一个层次的类加载器都是如此,因此最终所有的加载请求都应该传递给顶层的启动类加载器,只有父类加载器无法完成这个加载请求时,子加载器才会尝试自己去加载。
除了用C++实现的启动类加载器,其余所有的类加载器都要继承抽象类ClassLoader方法,下面分析其中几个关键方法的源码:
2.3.1 构造方法
//指定一个父类加载器,指定null,则父类加载器为Bootstrap ClassLoader
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
//默认父类加载器为AppClassLoader
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
2.3.2 loadClass(String name, boolean resolve)
/**
* 双亲委派机制是通过loadClass方法实现的
*/
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//类加载的解析
resolveClass(c);
}
return c;
}
}
2.3.3 findClass(String name)
ClassLoder
中的findClass
方法是抽象方法,需要子类进行实现。ExtClassLoader
和AppClassLoader
都继承了URLClassLoader
,而URLClassLoader
继承了ClassLoder
,并实现了findClass
方法。
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");
//找到对应的clss资源
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//根据class字节流生产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;
}
2.3.4 defineClass
所有的类加载器最后都要掉这个方法,它内部会调本地方法,负责将class字节流文件转换成Class实例。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
//先进行校验,比如以java开头的类名,不能加载。
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
3. 自定义类加载器
- 如果要打破双亲委派机制,重写
loadClass()
方法; - 否则,重写
findClass()
方法即可。
3.1 自定义类加载器(加载指定目录)
package com.lihui.study.jvm;
import java.io.*;
/**
* @author ex_lihui4
* @ClassName MyClassLoader
* @Description 自定义一个普通的类加载器,通过dir执行相应的类目录
* @date 2020-11-17 11:50
*/
public class MyClassLoader extends ClassLoader {
private String dir;
public MyClassLoader(ClassLoader parent, String dir) {
super(parent);
this.dir = dir;
}
public MyClassLoader(String dir) {
//如不指定父类加载器,
this.dir = dir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//拼接类路径
String className = name.substring(name.lastIndexOf(".") + 1)+".class";
String classPth = "\\"+name.substring(0, name.lastIndexOf(".")).replace(".","\\")+"\\";
String fileName = dir+classPth+className;
//获取二进制数据
File file = new File(fileName);
try (FileInputStream is = new FileInputStream(file)){
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ((length = is.read(buffer)) !=-1){
os.write(buffer,0,length);
}
byte[] bytes = os.toByteArray();
//返回类文件
return defineClass(name,bytes,0,bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4. 补充一下当前线程类加载器
JNDI服务:它由启动类加载器去加载,但JNDI就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的classPath下的JNDI接口提供者。
那么现在问题来了,启动类加载器加载的代码怎么去加载classPath下的类。默认使用调用者的类加载器,也就是启动类加载器去加载classpath下的接口提供者,是加载不了的。这个时候直接使用ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader()
进行加载。如果创建线程时为指定线程类加载器,会从父类进行继承。如果全部范围都没有指定,默认为应用程序类加载器。所以解决了JNDI接口提供者的加载问题。