Java中的类加载器
一、概述
以 JDK 8 为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
BootstrapClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 App |
- 启动类加载器(bootstrap class loader):由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)
- 扩展类加载器(extension class loader):Java 核心类库提供。负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
- 应用类加载器(application class loader):Java 核心类库提供。负责加载应用程序路径下的类。(这里的应用程序路径,是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
- 自定义类加载器:除了由 Java 核心类库提供的类加载器外,可以加入自定义的类加载器,来实现特殊的加载方式。比如可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。
除了加载功能之外,类加载器还提供了命名空间的作用。
在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,往往借助这一特性,来运行同一个类的不同版本。
启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。
二、启动类加载器
public class F {
static {
System.out.println("bootstrap F init");
}
}
public class Load {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.hcx.jvm.load.F");
System.out.println(aClass.getClassLoader()); // AppClassLoader ExtClassLoader
}
}
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. com.hcx.jvm.load.Load
bootstrap F init
null
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath: (完全替换)
- java -Xbootclasspath/a:<追加路径>(后追加)
- java -Xbootclasspath/p:<追加路径>(前追加 可以替换)
三、扩展类加载器
public class G {
static {
System.out.println("classpath G init");
}
}
/**
* 演示 扩展类加载器
* 在 C:\Program Files\Java\jdk1.8.0_91 下有一个 my.jar
* 里面也有一个 G 的类,观察到底是哪个类被加载了
*/
public class Load {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.hcx.jvm.load.G");
System.out.println(aClass.getClassLoader()); //sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
写一个同名的类
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包,将 jar 包拷贝到 JAVA_HOME/jre/lib/ext,重新执行 Load
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
四、双亲委派模型
指调用类加载器的 loadClass 方法时,查找类的规则
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if(c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if(resolve) {
resolveClass(c);
}
return c;
}
}
public class Load {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Load.class.getClassLoader());
Class<?> aClass = Load.class.getClassLoader().loadClass("com.hcx.jvm.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
- sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
- sun.misc.Launcher A p p C l a s s L o a d e r / / 2 处,委派上级 s u n . m i s c . L a u n c h e r AppClassLoader // 2 处,委派上级 sun.misc.Launcher AppClassLoader//2处,委派上级sun.misc.LauncherExtClassLoader.loadClass()
- sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
- sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找
- BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
sun.misc.Launcher$ExtClassLoader
// 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher$AppClassLoader的 // 2 处- 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了
一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。
好处:双亲委派模可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。
打破双亲委派:重写loadClass方法
public static void main(String[] args) throws Exception {
//类加载器加载的目录
System.out.println("启动类加载器:"+System.getProperty("sun.boot.class.path")); // Bootstrap class loader:加载 %JAVA_HOME%/lib路径下的jar包
System.out.println("扩展类加载器:"+System.getProperty("java.ext.dirs")); // Extension class loader:加载 %JAVA_HOME%/jre/lib/ext路径下的jar包
System.out.println("应用类加载器:"+System.getProperty("java.class.path")); // Application class loader:加载CLASSPATH路径下指定
}
打印结果:
启动类加载器:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/sunrsasign.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/classes
扩展类加载器:/Users/hongcaixia/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
应用类加载器:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre/lib/jce.jar
自定义加载器:
public class MyCustomClassLoader extends ClassLoader{
private String path;
private String clName;
public MyCustomClassLoader(String path, String clName) {
this.path = path;
this.clName = clName;
}
@Override
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
name = path+name.replace(".","\\")+".class";
InputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(name);
out = new ByteArrayOutputStream();
int i = 0;
while ((i=in.read())!=-1){
out.write(i);
}
}catch (Exception e){
}finally {
//关闭流
}
return out.toByteArray();
}
}
使用自定义加载器加载Person类
public static void main(String[] args) throws Exception {
//自定义加载器
MyCustomClassLoader myCustomClassLoader = new MyCustomClassLoader("/Users/hongcaixia/Documents/study/myjvm/src/main/java/com/hcx/jvm/cloader/", "MyClass");
Class<?> personClazz = myCustomClassLoader.loadClass("com.hcx.jvm.cloader.Person");
Object person = personClazz.newInstance();
System.out.println(person.getClass().getClassLoader());//MyCustomClassLoader
System.out.println(person.getClass().getClassLoader().getParent());//appClassLoader
System.out.println(person.getClass().getClassLoader().getParent().getParent());//extClassLoader
System.out.println(person.getClass().getClassLoader().getParent().getParent().getParent());//null 为null时自动调用bootstrap
}
当把person.class放在classpath下时,父类加载器变成了appClassLoader;
当把person.class放在/jre/lib/ext下时,父类还是appClassLoader;因为ext下只能加载jar;打包成jar时才会直接是extClassLoader。
双亲委派加载:
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) {
// 父类不为空,就一直调用父类的loadClass方法
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;
}
}
五、线程上下文类加载器
我们在使用 JDBC 时,都需要加载 Driver 驱动。
如果不写Class.forName("com.mysql.jdbc.Driver")
,也是可以让 com.mysql.jdbc.Driver 正确加载的:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
= new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
DriverManager 的类加载器:在启动类路径下
System.out.println(DriverManager.class.getClassLoader()); //null
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
// 1.使用 ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2.使用 jdbc.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);
}
}
}
2步骤发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载
1步骤它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称:
这样就可以使用:
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
六、自定义类加载器
什么时候需要自定义类加载器:
- 想加载非 classpath 随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass 方法
注意不是重写 loadClass 方法,否则不会走双亲委派机制 - 读取类文件的字节码
- 调用父类的 defineClass 方法来加载类
- 使用者调用该类加载器的 loadClass 方法
准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下
public class Load {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c1 = classLoader.loadClass("MapImpl1");
Class<?> c2 = classLoader.loadClass("MapImpl1");
System.out.println(c1 == c2); //true
MyClassLoader classLoader2 = new MyClassLoader();
Class<?> c3 = classLoader2.loadClass("MapImpl1");
System.out.println(c1 == c3); //false
c1.newInstance();
}
}
class MyClassLoader extends ClassLoader {
@Override // name 就是类名称
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "e:\\myclasspath\\" + name + ".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path), os);
// 得到字节数组
byte[] bytes = os.toByteArray();
// byte[] -> *.class
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到", e);
}
}
}