分享

JVM真香系列:轻松理解class文件到虚拟机(下)

 田维常 2020-11-08

回复“000”获取大量电子书

类加载器

类加载器是很多人认为很硬的骨头。其实也没那么可怕,请听老田慢慢道来。

在装载(Load)阶段,通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的。

上面我们自定义一个String出了问题,问题在于JVM不知道我们想用哪个类,于是JVM就定义了个规范。

把这种类装载器分成几类。

Bootstrap ClassLoader 

负责加载$JAVA_HOME中 jre/lib/rt.jar里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。

Extension ClassLoader 

负责加载Java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或 -Djava.ext.dirs指定目录下的jar包。

App ClassLoader 

负责加载classpath中指定的jar包及 Djava.class.path所指定目录下的类和jar包。

Custom ClassLoader

通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcatjboss都会根据j2ee规范自行实现ClassLoader

图解类加载

加载原则

检查某个类是否已经加载:顺序是自底向上,从Custom ClassLoaderBootStrap ClassLoader逐层检查,只要某个Classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。

加载的顺序:加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

ClassLoader类分析

java.lang.ClassLoader中很重要的三个方法:

loadClass方法

findClass方法

defineClass方法

loadClass方法
1 public Class<?> loadClass(String name) throws ClassNotFoundException {
2  return loadClass(name, false);
3 }
4 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
5  //使用了同步锁,保证不出现重复加载
6  synchronized (getClassLoadingLock(name)) {
7   // 首先检查自己是否已经加载过
8   Class<?> c = findLoadedClass(name);
9   //没找到
10   if (c == null) {
11 long t0 = System.nanoTime();
12 try {
13  //有父类
14  if (parent != null) {
15   //让父类去加载
16   c = parent.loadClass(name, false);
17  } else {
18   //如果没有父类,则委托给启动加载器去加载
19   c = findBootstrapClassOrNull(name);
20  }
21 } catch (ClassNotFoundException e) {
22  // ClassNotFoundException thrown if class not found
23  // from the non-null parent class loader
24 }
25
26 if (c == null) {
27  // If still not found, then invoke findClass in order
28  // to find the class.
29  long t1 = System.nanoTime();
30  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
31  c = findClass(name);
32
33  // this is the defining class loader; record the stats
34  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
35  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
36  sun.misc.PerfCounter.getFindClasses().increment();
37 }
38   }
39   //是否需要在加载时进行解析
40   if (resolve) {
41 resolveClass(c);
42   }
43   return c;
44  }
45 }

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。

loadClass实现也可以知道,如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoaderloadClass方法获取到class对象。

findClass方法
1 protected Class<?> findClass(String name) throws ClassNotFoundException {
2  throw new ClassNotFoundException(name);
3 }

JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。

从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。

需要注意的是,ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常。同时应该知道的是,findClass方法通常是和defineClass方法一起使用的。

defineClass方法
1 protected final Class<?> defineClass(String name, byte[] b, int off, int len,
2 ProtectionDomain protectionDomain) throws ClassFormatError{
3  protectionDomain = preDefineClass(name, protectionDomain);
4  String source = defineClassSourceLocation(protectionDomain);
5  Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
6  postDefineClass(c, protectionDomain);
7  return c;
8 }

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。

通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象 。

如何自定义类加载器

用户根据需求自己定义的。需要继承自ClassLoader,重写方法findClass()

如果想要编写自己的类加载器,只需要两步:

  • 继承ClassLoader

  • 覆盖findClass(String className)方法

**ClassLoader**超类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。
如果要实现该方法,必须做到以下几点:

1.为来自本地文件系统或者其他来源的类加载其字节码。
2.调用
ClassLoader超类的defineClass方法,向虚拟机提供字节码。

浅谈双亲委派模型

这个在面试中也是频率相当高。

如果一个类加载器在接到加载类的请求时,先查找是否已经加载过,如果没有被加载过,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归。

如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

优势

Java类随着加载它的类加载器一起,具备了一种带有优先级的层次关系。

比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。

如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。

打破双亲委派模型的案例

tomcat

tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的。简单看一下 tomcat 类加载器的层次结构。

对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的 .class 文件,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。

如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。

那么 tomcat 是怎么打破双亲委派机制的呢?

可以看图中的 WebAppClassLoader,它加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它却可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能。

但是你自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。

OSGi

OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。

OSGi 是服务平台的规范,旨在用于需要长运行时间、动态更新和对运行环境破坏最小的系统。

OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。

比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但我们不难想象,这种与直觉相违背的加载方式,肯定是由专用的类加载器来实现的。

随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统),我个人认为,现在的 OSGi,意义已经不是很大了。

OSGi 是一个庞大的话题,你只需要知道,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。

SPI

Java 中有一个 SPI 机制,全称是 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

后面会再专门针对这个写一篇文章,这里就不细说了。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多