1 类加载器概念
1.1 加载概念
- 加载指的是将类的 class 文件读入到内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。
- 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的自定义类加载器。
1.2 类加载器概念
- 加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的
newInstance()
方法就可以创建出该类的一个对象。 - 实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
1.3 JDK8类加载器
1.4 类加载器加载 Class 的流程
- 检测此 Class 是否载入过,即在缓冲区中是否有此 Class ,如果有直接进入第8步,否则进入第【2】步。
- 如果没有父类加载器,则要么 Parent 是启动类加载器,要么本身就是启动类加载器,则跳到第【4】步,如果父类加载器存在,则进入第【3】步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第【8】步,否则接着执行第【5】步。
- 请求使用启动类加载器去载入目标类,如果载入成功则跳至第【8】步,否则跳至第【7】步。
- 当前类加载器尝试寻找 Class 文件,如果找到则执行第【6】步,如果找不到则执行第【7】步。
- 从文件中载入 Class ,成功后跳至第【8】步。
- 抛出 ClassNotFountException 异常。
- 返回对应的 java.lang.Class 对象。
1.5 JVM的类加载机制
缓存机制:缓存机制将会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象,存入缓冲区中。这就是为什么修改了 Class 后,必须重新启动 JVM ,程序所做的修改才会生效的原因。
-
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该 Class ,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
双亲委派机制的优势:避免重复加载 + 避免核心类篡改。采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的 Integer.class ,这样便可以防止核心API库被随意篡改。
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用其他 Class 也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
2 启动类加载器
用 Bootstrap 类加载器加载类
3 扩展类加载器
- 测试一:当我们自己编写一个普通类时,由应用程序加载类加载
public class G {
static {
// 1、打包:jar -cvf my.jar com\jvm\t10_class_loader\T02_G.class
// 2、放入:jdk\jre\lib\ext\ 下
//System.out.println("ext G init");
System.out.println("classpath G init");
}
}
// 用扩展类加载器 Extension ClassLoader加载类
public class TestDemo {
public static void main(String[] args) throws ClassNotFoundException {
// forName 可以完成类的加载,也可以类的链接、初始化操作
Class<?> aClass = Class.forName("com.yqj.G");
System.out.println(aClass.getClassLoader());
}
}
Result: 自己编写一个普通类时,由应用程序类加载器加载
classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2
- 测试二:如果同名类也在拓展类加载器路径下,则会由扩展类加载器进行加载
public class G {
static {
// 1、打包:jar -cvf my.jar com\jvm\t10_class_loader\T02_G.class
// 2、放入:jdk\jre\lib\ext\ 下
System.out.println("ext G init");
//System.out.println("classpath G init");
}
}
cd /Users/yorick/Documents/study/code/test-tmp/target/classes # 进入classpath下
jar -cvf my.jar com/yqj/G.class # 打包jar
sudo mv my.jar /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/lib/ext/ # 移动到ext目录下,再运行程序
Result:如果同名类也在拓展类加载器路径下,则会由扩展类加载器进行加载,即 G 类是由扩展类加载器加载
ext G init
sun.misc.Launcher$ExtClassLoader@29453f44
总结:双亲委派模式,当我们应用程序类加载器去加载类时得去问问它的上级类加载器是否已经加载,上级就找到了拓展类加载器,结果拓展类加载器就找到了同名的 G类文件,加载以后结果就是应用类加载器就没有机会加载了。优先级最高的是启动类加载器、其次是扩展类加载器,第三才是应用程序类加载器。
四 双亲委派源码分析
双亲委派:就是指调用类加载器的 loadClass 方法时,查找类的规则。
//查看 loadClass的源码(类加载 ClassLoader 在 package java.lang 包下的 ClassLoader.java 文件中)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 1、检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2、有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 如果没有上级了(ExtClassLoader), 则委派 BootstrapClassLoader
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();
// 4、每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// this is the defining class loader; record the stats
// 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;
}
}
五 线程上下文类加载器
线程上下文类加载器是当前线程使用的类加载器,默认就是应用类加载器。
案例:加载JDBC的Driver驱动时,需要打破双亲委派的机制。
- 我们在使用 JDBC 时,都需要加载 Driver 驱动,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让com.mysql.jdbc.Driver 正确加载 - 查看 DriverManager的类加载器,打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib/ 下显然没有 mysql-connect-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
System.out.println(DriverManager.class.getClassLoader()); //null
- 深入查看 DriverManager 的源码,查看其静态代码块中存在
loadInitialDrivers()
方法,用于初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
- 继续看 loadInitialDrivers() 方法:JDK 在某些情况下需要打破双亲委派模式。(若此处不打破双亲委派机制,则已经是启动类加载器加载的DriverManager无法在其路径下找到mysql的驱动包,势必会加载失败)
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;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用ServiceLoader 机制加载驱动,即 Service Provider Interface(SPI),约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称。即根据接口找到文件,文件内容是要加载类的类名
//得到文件中定义的全部实现类
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);
}
}
}
- 深入观察ServiceLoader.load()方法,看看是如何打破双亲委派,从中加载mysql的实现类的。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器(应用程序类加载器)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//将应用程序类加载器传入
return ServiceLoader.load(service, cl);
}
- 深入 ServiceLoader.load(),查看如何使用的应用程序类加载器加载的类对象
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//ServiceLoader构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
//reload方法
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
//LazyIterator构造方法
//Class<S> service;
//ClassLoader loader;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// LazyIterator 的 nextService方法,在该方法中发现是使用应用程序类加载器加载的 c 这个类
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 使用 Class.forName 实现了对于指定类的加载
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
}
// LazyIterator 的 hasNextService() 方法中具体实现SPI的过程
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//PREFIX=META-INF/services
//META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//加载SPI中定义的全部实现类名到list集合
pending = parse(service, configs.nextElement());
}
//获取第一个实现类名
nextName = pending.next();
return true;
}
- 当加载某个Driver类的初始化阶段,会调用DriverManager将该类注册到其中的list集合保存
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
//注册驱动
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
- DriverManager最终将驱动保存
//private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
//加入驱动的集合中
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
六 自定义类加载器
6.1 自定义类加载器的步骤
- 继承 ClassLoader 父类
- 要遵从双亲委派机制,重写 findClass() 方法
注意:不是重写 loadClass() 方法,否则不会走双亲委派机制 - 读取类文件的字节码
- 调用父类的 defineClass() 方法来加载类
- 使用者调用该类加载器的 loadClass() 方法
6.2 代码实例
public class TestDemo extends ClassLoader {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassLoader classLoader1 = new TestDemo();
Class<?> aClass1 = classLoader1.loadClass("HelloWorld");
Class<?> aClass2 = classLoader1.loadClass("HelloWorld");
System.out.println(aClass1 == aClass2); //true 类文件只会加载一次,第一次加载放在自定义类加载器缓存中,第二次加载时发现缓存中已经有了
ClassLoader classLoader2 = new TestDemo();
Class<?> aClass3 = classLoader2.loadClass("HelloWorld");
System.out.println(aClass1 == aClass3); //false 确认唯一类,要包名、类名相同,同时类加载器也得相同。虽然类同名,但是类加载器不一致
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = "/Users/yorick/Documents/study/code/myFunc/" + 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);
}
}
}