写点什么

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

用户头像
田维常
关注
发布于: 2020 年 11 月 08 日

关注“Java 后端技术全栈”


回复“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 found23                    // from the non-null parent class loader24                }2526                if (c == null) {27                    // If still not found, then invoke findClass in order28                    // to find the class.29                    long t1 = System.nanoTime();30                    // 如果都没有找到,则通过自定义实现的findClass去查找并加载31                    c = findClass(name);3233                    // this is the defining class loader; record the stats34                    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,它可以用来启用框架扩展和替换组件。


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


推荐阅读:


《系统架构:复杂系统的产品设计与开发》.pdf


《一线架构师实践指南》.pdf


《循序渐进Linux (第2版)》.pdf


关注公众号“Java 后端技术全栈”


免费获取 500G 最新学习资料



发布于: 2020 年 11 月 08 日阅读数: 39
用户头像

田维常

关注

关注公众号:Java后端技术全栈,领500G资料 2020.10.24 加入

关注公众号:Java后端技术全栈,领500G资料

评论

发布
暂无评论
JVM真香系列:轻松理解class文件到虚拟机(下)