JVM从入门到上天之类加载子系统与SPI

类加载器

以实现方式分类

1、c++实现

2、java实现

以功能分类

1、启动类加载器

2、扩展类加载器

3、应用程序类加载器

其中启动类加载器由c++实现,其它的的类加载器均又java实现,由java实现的类加载器都继承自类java.lang.ClassLoader

类加载器之间的联系以及功能

如下图所示。

类加载器.png
注:类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们没有真正的从属关系,即不是继承关系

启动类加载器

JVM将C++处理类加载的一套逻辑定义为启动类加载器,所以它不像其它类加载器是有实体的,因此无法被java程序调用。

查看启动类加载器的加载路径

代码
 URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
 for (URL url:urLs) {
       System.out.println(url);
 }
打印结果
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/classes
可以通过-Xbootclasspath指定

扩展类加载器

查看扩展类加载器加载的路径

ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urLs = urlClassLoader.getURLs();
for (URL url:urLs) {
     System.out.println(url);
}

打印结果

file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/zipfs.jar
可以通过java.ext.dirs指定

应用类加载器

默认加载用户程序的类加载器

查看应用类加载器的路径

 String[] urls = System.getProperty("java.class.path").split(":");
 for (String url:urls) {
     System.out.println(url);
 }

 System.out.println("=======================================");

 URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
 URL[] urLs = classLoader.getURLs();
 for (URL url:urLs) {
     System.out.println(url);
 }

打印结果

上面演示的是两种方法,打印结果是一样的

file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/deploy.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/ext/zipfs.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/javaws.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jfxswt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/management-agent.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/plugin.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_191/jre/lib/rt.jar
file:/F:/study/lb/%e7%ac%ac%e4%b8%80%e9%98%b6%e6%ae%b5%20jvm/2%20%e7%b1%bb%e5%8a%a0%e8%bd%bd%e5%ad%90%e7%b3%bb%e7%bb%9f%e4%b8%8eSPI/luban-jvm-research/target/classes/
file:/F:/java_dev_env/maven-repository/org/openjdk/jol/jol-core/0.10/jol-core-0.10.jar
file:/F:/java_dev_env/maven-repository/cglib/cglib/3.3.0/cglib-3.3.0.jar
file:/F:/java_dev_env/maven-repository/org/ow2/asm/asm/7.1/asm-7.1.jar
file:/F:/Program%20Files%20(x86)/IntelliJ%20IDEA%202019.3.4/lib/idea_rt.jar

自定义类加载器

当我们写一个类继承自java.lang.ClassLoader之后,这就是我们的自定义加载器了,自定义加载器的的父类加载器默认使用系统类加载器(AppClassLoader)。

对应的无参构造函数如下

 protected ClassLoader() {
     this(checkCreateClassLoader(), getSystemClassLoader());
 }

代码实现

//继承ClassLoader
public class MyClassloader extends ClassLoader {
​
 public static void main(String[] args) {
     MyClassloader classloader = new MyClassloader();
     try {
         Class<?> clazz = classloader.loadClass(ClassloaderToLoad.class.getName());
         System.out.println(clazz);
         System.out.println(clazz.getClassLoader());
     } catch (ClassNotFoundException e) {
         e.printStackTrace();
     }
 }
​
 public static final String SUFFIX = ".class";

 //重写findClass方法
 @Override
 protected Class<?> findClass(String className) throws ClassNotFoundException {
     System.out.println("Classloader_1 findClass");
     //将包名转换为路径名获取文件字节数组
     byte[] data = getData(className.replace('.', '/'));
     return defineClass(className, data, 0, data.length);
 }
 //通过路径名获取字节数组
 private byte[] getData(String name) {
     InputStream inputStream = null;
     ByteArrayOutputStream outputStream = null;
​
     File file = new File(name + SUFFIX);
     if (!file.exists()) return null;
​
     try {
         inputStream = new FileInputStream(file);
         outputStream = new ByteArrayOutputStream();
​
         int size = 0;
         byte[] buffer = new byte[1024];
​
         while ((size = inputStream.read(buffer)) != -1) {
         outputStream.write(buffer, 0, size);
   }
​
     return outputStream.toByteArray();
   } catch (FileNotFoundException e) {
       e.printStackTrace();
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
     try {
         inputStream.close();
         outputStream.close();
      } catch (Exception ex) {
         ex.printStackTrace();
   }
 }
     return null;
   }
}
//要去加载的类
class ClassloaderToLoad {
​
}

打印结果

class com.cloud.classloader.Classloader_1_A
sun.misc.Launcher$AppClassLoader@18b4aac2

打印出来的类加载器居然不是咱们自己定义的类加载器,难道翻车了?

然而并没有,此处需要引出类加载器之间一个很重要的机制——双亲委派。

双亲委派

类加载器的类存储

首先先了解一下,类加载器加载的类是如何存储的,废话不说,先上图。

类加载器的类存储.png

值得一提的是ClassLoader本省对于java而言,也是一个分配在堆中的一个对象,它管理着自己在方法区的一个区域。对于对象来说,即使名称相同,如果是不同类加载器加载的,那么它们就是不同的。

概念

如果某个类加载器收到了加载某个类的请求,这个类加载器并不会立即去加载这个类,而是把请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器,只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器时,子类加载器才会尝试自己去加载。

上图

双亲委派.png

上面我们自己实现的自定义类加载器之所以没有加载咱们想加载的类是因为应用程序类加载器已经帮我们加载了,如果想要用我们自己定义的类加载器加载,需要去加载三个预定义类加载器加载范围之外的类。

优势

避免重复加载 + 避免核心类篡改

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设我们自己定义了一个java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载我们自己定义的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

劣势

通过双亲委派机制的原理可以得出一下结论:由于BootstrapClassloader是顶级类加载器,BootstrapClassloader无法委派AppClassLoader来加载类,也就是说BootstrapClassloader中加载的类中无法使用由AppClassLoader加载的类。可能绝大部分情况这个不算是问题,因为BootstrapClassloader加载的都是基础类,供AppClassLoader加载的类调用的类。但是万事万物都不是绝对的,比如最经典的例子—— jdbc加载数据库驱动。

driver.png

很明显,接口:java.sql.Driver,定义在java.sql包中,包所在的位置是:jdk\jre\lib\rt.jar中,java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类,很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,我们的问题是DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖,那它是怎么解决这个问题的?这就引申了我们第二个问题:如何打破双亲委派机制?

打破双亲委派

思路很简单,取反呗,双亲委派是向上委派,打破双亲委派,那咱们就是不委派,或向下委派。

自定义类加载器
public class TestClassLoader extends ClassLoader {
​
 private String name;
​
 public TestClassLoaderN(ClassLoader parent, String name) {
     super(parent);
     this.name = name;
 }
​
 @Override
 public String toString() {
     return this.name;
 }
​
 @Override
 public Class<?> loadClass(String name) throws ClassNotFoundException {
     Class<?> clazz = null;
     if(name.startsWith("com.cloud")){
     clazz = findClass(name);
   }  else{
     ClassLoader system = getSystemClassLoader();
   try {
     clazz = system.loadClass(name);
   } catch (Exception e) {
     // ignore
   }
   if (clazz != null)
     return clazz;
  }
     return clazz;
 }
​
 @Override
 public Class<?> findClass(String name) {
     InputStream is = null;
     byte[] data = null;
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
   try {
     is = new FileInputStream(new File("F:/test/Test.class"));
     int c = 0;
     while (-1 != (c = is.read())) {
     baos.write(c);
   }
     data = baos.toByteArray();
   } catch (Exception e) {
     e.printStackTrace();
   } finally {
   try {
     is.close();
     baos.close();
   } catch (IOException e) {
     e.printStackTrace();
   }
 }
     return this.defineClass(name, data, 0, data.length);
 }
​
 public static void main(String[] args) {
     TestClassLoaderN loader = new TestClassLoaderN(
     TestClassLoaderN.class.getClassLoader(), "TestLoader");
     Class clazz;
     try {
       clazz = loader.loadClass("com.cloud.Test");
       Object object = clazz.newInstance();
     } catch (Exception e) {
       e.printStackTrace();
     }
   }
}

自定义类加载器,重写loadClass方法,让其不去父类加载器中查找,用我们自己的findClass方法查找即可。

SPI机制

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

上面已经提到DriverManager是启动类加载器加载的,根据双亲委派,它不可能调用到由应用程序类加载器加载的驱动程序,而事实上他却可以调用,实现了向下委派,典型破坏了双亲委派,而这里就应用了SPI机制,咱们来看下源码。

 // 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()
​
 AccessController.doPrivileged(new PrivilegedAction<Void>() {
 public Void run() {
 //ServiceLoader.load 获取驱动的关键代码,点进ServiceLoader看看
 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
 Iterator<Driver> driversIterator = loadedDrivers.iterator();
 public final class ServiceLoader<S> implements Iterable<S>
 {
 //很明显这个类就是去"META-INF/services/" 查找对应的驱动类,这不就是前面解释的SPI机制吗?
 private static final String PREFIX = "META-INF/services/";
​
 // The class or interface representing the service being loaded
 private final Class<S> service;
​
 // The class loader used to locate, load, and instantiate providers
 private final ClassLoader loader;
​
 // The access control context taken when the ServiceLoader is created
 private final AccessControlContext acc;

为了验证一下我们去mysql驱动包里去看一下,如下图

mysql驱动.png
线程上下文类加载器

在看jdbc的源码时,我们会看到它是如下获取类加载器,其中ContextClassLoader就是上下文类加载器。

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

获取
Thread.currentThread().getContextClassLoader()

设置
Thread.currentThread().setContextClassLoader(new Classloader());

如果不做任何设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器,它的存在就是为了打破双亲委派,实现逆向委派。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。