类的加载过程分为3个阶段
类的加载器,只负责加载 .class 文件,至于能不能执行,是执行引擎决定的。
- 通过类的全限定名获取此类的二进制流
- 将此类的二进制流中静态储存结构存储在运行时数据区的方法区(元空间 > 7.0 或永久代 < 7.0)
- 在内存中生成一个 java.lang.Class 的对象,作为方法区在这个类的各种数据的访问入口
虽然一般情况下,JVM 加载的是 .class 文件,其实只要是符合 JVM 的字节码都可以进行加载。比如:.jar 包中的文件,动态代理生成的字节码(可在运行时动态生成),还比如加密的 .class 文件,通过 JVM 解密后加载,可有效防止反编译。
- 初始化静态变量和静态块,此对应执行类构造器方法
(),此方法的指令按源文件中的顺序执行。如果没有相关静态变量或静态块,可能不会有 () 方法。 - 成员变量和局部变量对应 JVM 下的
()方法。
注意,他们不是继承关系,我们可以称他们为扩展关系。
public static void main(String[] args) {
ClassLoader app = ClassLoader.getSystemClassLoader();
System.out.println(app);
ClassLoader ext = app.getParent();
System.out.println(ext);
ClassLoader bootstrap = ext.getParent();
System.out.println(bootstrap);
}
结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@6d03e736
null
程序中的默认类加载器为 AppClassLoader 。一般情况下,Java 应用中的类都是由 AppClassLoader 加载器加载。其通过 ClassLoader.getSystemClassLoader() 方法获取。
Java 代码中不能直接获取引导类加载器实例。所以示例中 bootstrap 为 null
// 获取 Bootstrap 加载器的加载路径
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls){
System.out.println(url.getFile());
}
System.out.println("======================");
// 获取扩展类加载器加载的的路径
String dirs = System.getProperty("java.ext.dirs");
System.out.println(dirs);
结果
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/resources.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/rt.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/sunrsasign.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jsse.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jce.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/charsets.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/lib/jfr.jar
/C:/Program%20Files/Java/jdk1.8.0_181/jre/classes
======================
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
Bootstrap 加载器的加载路径下的 jar 包文件包含的类,由 Bootstrap 加载器加载,两个 ext 目录下的 jar 包下的类由 ExtClassLoader 加载器加强,我们的应用程序,classpath 属性下的类,由 AppClassLoader 加载。
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = ExecutorTest.class.getClassLoader();
System.out.println(classLoader1);
结果
null
sun.misc.Launcher$AppClassLoader@18b4aac2
说明 String 类是由引导类加载器加载,其引导类加载器无法在代码中获取,所以为 null 。ExecutorTest 是我们的示例对象,其由 AppClassLoader 加载器加载。当然,我们在 ext 目录下找到的 jar 包中的类,由 ExtClassLoader 加载器加载。
一般的 Java 程序中,使用引导类加载器、扩展类加载器、系统类加载器相互作用,即可。几乎不需要自定义类的加载器,我们可以在某些情况下进行自定义加载器。
package com.yyoo.jvm;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@Getter
@Setter
@AllArgsConstructor
public class MyClassLoader extends ClassLoader{
/**
* 当前加载器的 class 文件根路径
*/
private String classRootPath;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 使用 io 流读取.class 字节码文件,然后使用父类的 defineClass 方法返回为 Class 类
try (InputStream in = new FileInputStream(getClassRootPath()+"\\"+name.replaceAll(".","\\")+".class")){
byte[] b = new byte[1024];
int len = in.read(b);
return defineClass(name,b,0,len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
}
name 参数使用class的全类名即可。注:我们示例使用了 lombok 插件。
// 获取当前类的ClassLoader
Object o = new Object();
System.out.println(o.getClass().getClassLoader());
// 获取当前线程上下文的ClassLoder
System.out.println(Thread.currentThread().getContextClassLoader());
// 获取当前系统的 ClassLoder
System.out.println(ClassLoader.getSystemClassLoader());
以上整个过程称为双亲委派机制。
示例1
public static void main(String[] args) throws ClassNotFoundException {
String classRootPath = "D:\\work\\code\\mytest\\peixun\\target\\classes";
// 同一个 ClassLoader 实例,加载同一个 Class ,得到的是同一个 Class 对象
ClassLoader my = new MyClassLoader(classRootPath);
Class a = my.loadClass("com.yyoo.jvm.MyEmp");
Class b = my.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == b);// true
// 同一个 ClassLoader 的不同实例,加载同一个 Class 文件,得到的也是同一个 Class 对象
ClassLoader my1 = new MyClassLoader(classRootPath);
Class c = my1.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == c);// true
// 不同的 ClassLoader ,加载同一个 Class 文件,得到的也是同一个 Class 对象
ClassLoader app = ClassLoader.getSystemClassLoader();
Class d = app.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == d);// true
System.out.println(a.getClassLoader());// 系统类加载器
System.out.println(d.getClassLoader());// 系统类加载器
}
注:MyClassLoader 即为我们上面自定义的 ClassLoader。
根据双亲委派机制来解释该现象,我们自定义加载器的 classRootPath 其实就是我们应用的 classPath,而 classPath 下的类是由系统类加载器加载的,而且其加载的始终是 classPath 下的类,而我们的 ClassRootPath 下的类永远不会加载(除非我们自定义加载的类和系统类加载器加载的类全类名有不同的地方 或者 classPath 下没有该类)。
示例2
示例1的前提是,MyEmp 的 .class 文件存在于我们应用的 classPath 路径下,示例 2 ,我们将 classPath 路径下的 .class 文件删除,并按包路径创建文件夹在 D 盘根路径下(Java 的几大类加载器加载的路径和目录之外),再次执行
String classRootPath = "D:";
// 同一个 ClassLoader 实例,加载同一个 Class ,得到的是同一个 Class 对象
ClassLoader my = new MyClassLoader(classRootPath);
Class a = my.loadClass("com.yyoo.jvm.MyEmp");
Class b = my.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == b);
// 同一个 ClassLoader 的不同实例,加载同一个 Class 文件,得到的不是同一个 Class 对象
ClassLoader my1 = new MyClassLoader(classRootPath);
Class c = my1.loadClass("com.yyoo.jvm.MyEmp");
System.out.println(a == c);
System.out.println(a.getClassLoader());
System.out.println(c.getClassLoader());
结果:
true
false
com.yyoo.jvm.MyClassLoader@3f99bd52
com.yyoo.jvm.MyClassLoader@3a71f4dd
可以看到在 系统类加载器、扩展类加载器、启动类加载器加载的路径之外的 .class 文件会使用我们自定义的加载器加载,且不同的 MyClassLoader 实例加载的 Class 对象不是同一个。
SPI(service provider interface)是 jdk 内置的一种服务提供发现机制。可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,常用的关系型数据有 Mysql、Oracle、SQLServer、DB2 等,这些不同类型的数据库使用的驱动程序各不相同,那 JDK 不可能把所有厂商的驱动都实现,只能制定一个标准接口,其他不同厂商可以针对同一接口做出不同的实现,各个数据库厂商根据标准来实现自己的驱动,这就是SPI机制。
通俗点来说,SPI 就是某个应用程序只提供接口规则,具体的实现需要调用方(通常该接口会有多个实现,否则也就用不着 SPI 了)在使用时自行实现,其实现方式遵循 Java 的 SPI 机制。
比如:我们要提供一个文件上传的接口,其实现有如下几种方式:服务器本地存储、上传到 ftp 服务器、上传到 minio、上传到云服务等等
public interface FileUpload {
/**
* 上传文件
*/
void upload();
}
public class LocalFileUpload implements FileUpload{
@Override
public void upload() {
System.out.println("将文件存储到服务器本地路径下");
}
}
public class MinIOFileUpload implements FileUpload{
@Override
public void upload() {
System.out.println("将文件上传到 MinIOn 服务器");
}
}
在应用的 META-INF/services/ 目录下(目录不存在自行创建即可),创建文件名称为 com.yyoo.spi.FileUpload 的文本文件
com.yyoo.spi.LocalFileUpload
com.yyoo.spi.MinIOFileUpload
// load 方法,如果不传 ClassLoader 则默认使用当前线程上下文的 Thread.currentThread().getContextClassLoader()
// 这里即是系统类加载器,我们也可以使用重载方法 load(Class service,ClassLoader loader) 来指定加载器
ServiceLoader<FileUpload> serviceLoader = ServiceLoader.load(FileUpload.class);
// 获取迭代器或者直接使用 foreach 语句即可获取 META-INF/services/com.yyoo.spi.FileUpload 文件中配置的所有实现
for (FileUpload fileUpload : serviceLoader){
System.out.println(fileUpload.getClass());
System.out.println(fileUpload.getClass().getClassLoader());// 使用系统类加载器加载
fileUpload.upload();
}
执行结果:
class com.yyoo.spi.LocalFileUpload
sun.misc.Launcher$AppClassLoader@18b4aac2
将文件存储到服务器本地路径下
class com.yyoo.spi.MinIOFileUpload
sun.misc.Launcher$AppClassLoader@18b4aac2
将文件上传到 MinIOn 服务器
ServiceLoader 是 java.util 包提供的工具类,其主要作用就是通过读取 META-INF/services 下的配置,然后根据配置通过系统类加载器加载对应配置的实现类。
SPI 允许应用程序外部提供内部接口的实现,从而改变了内部接口的行为,在某种程度上规避了双亲委派机制思想(核心 API 实现被改变)。