您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
源码剖析JVM类加载机制
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
源码剖析JVM类加载机制
自猿其说Tech
2022-01-18
IP归属:未知
30800浏览
计算机编程
### 1 前言 我们平常开发中,都会部署开发的项目或者本地运行main函数之类的来启动程序,那么我们项目中的类是如何被加载到JVM的,加载的机制和实现是什么样的,本文给大家简单介绍下。 ### 2 类加载运行全过程 当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM,通过Java命令执行代码的大体流程如下 ![](//img1.jcloudcs.com/developer.jdcloud.com/8aa267bc-461d-4f8f-983d-e1608e78750f20220118144342.png) 从流程图中可以看到类加载的过程主要是通过类加载器来实现的,那么什么是类加载器呢? ### 3 类加载器 #### 3.1 什么是类加载器 类加载器负责在运行时将Java类动态加载到JVM(Java 虚拟机)。此外,它们是JRE(Java运行时环境)的一部分。所以由于类加载器,JVM不需要知道底层文件或文件系统来运行Java程序。 Java类加载器的作用是寻找类文件,然后加载Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。 #### 3.2 类加载器种类 #### 3.2.1 启动类加载器(Bootstrap ClassLoader) 它主要负责加载JDK内部类,一般是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心库。此外,Bootstrap类加载器充当所有其他ClassLoader实例的父级。 Bootstrap ClassLoader是JVM核心的一部分,是用native引用编写的。它本身是虚拟机的一部分,所以它并不是一个JAVA类,我们无法直接使用该类加载器。 ##### 3.2.2 扩展类加载器(Extension ClassLoader) 负责加载支撑JVM运行的位于$JAVA_HOME/jre/lib目录下的ext扩展目录中的JAR 类包。我们可以直接使用这个类加载器。 ##### 3.2.3 应用程序类加载器(Application ClassLoader) 负责加载用户类路径(classpath)上的指定类库,主要就是加载你自己写的那些类。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 ##### 3.2.4 自定义类加载器 通过继承ClassLoader类实现,主要重写findClass方法。 下面通过代码来看下了解不同的类是使用的哪种类加载器来加载的: ```java System.out.println("Classloader of this class : " + ClassLoaderDrill.class.getClassLoader()); System.out.println("Classloader of Logging : " + Logging.class.getClassLoader()); System.out.println("Classloader of String : " + String.class.getClassLoader()); System.out.println("-----------"); System.out.println("Classloader : " + ClassLoaderDrill.class.getClassLoader()); System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent()); System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent().getParent()); ``` 下面是运行结果: ![](//img1.jcloudcs.com/developer.jdcloud.com/86b3e642-a540-4999-a306-c5a8f9fb2b4920220118144951.png) 通过运行结果,我们会发现我自定义的当前运行类的类加载器是AppClassLoader,Logging这个类的类加载器是ExtClassLoader,而且类加载器之间是有父子关系关联的。但String的类加载器却为null,ExtClassLoader的父加载器也为null,是意味着String类不是通过类加载器加载的?那如果可以加载它又是怎么被加载的呢?为什么我们获取不到BootstrapClassLoader呢?后面我们会进行解读。 #### 3.3 类加载器的机制 上面介绍了都有哪些类加载器,那么一个类是如何被类加载器加载的,这些类加载器之间又有什么关联关系呢,接下来就介绍下类加载器的机制。 ##### 双亲委派机制 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时(它搜索的范围没有找到所需的类),子加载器才会尝试自己取加载。 **双亲委派机制说简单点就是:对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。** ![](//img1.jcloudcs.com/developer.jdcloud.com/442f22ca-3e2b-42d7-ba90-bc3099ac6f9920220118145021.png) #### 3.4 类加载机制的源码实现 参见最开始类运行加载全过程图可知,流程中会创建JVM启动器实例:sun.misc.Launcher。 sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。JVM默认使用Launcher的getClassLoader(),这个方法返回的类加载器(AppClassLoader)的实例加载我们的应用程序。 ```java public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); 。。。。。。 //省略一些不需关注代码 } ``` 从上面Launcher构造方法的源码中,我们看到了AppClassLoader和ExtClassLoader这两种类加载器的定义,并且在创建AppClassLoader时将ExtClassLoader设置为父类,也符合上面说的类加载器之间的关联。 但是BootstrapClassLoader仍然没有出现,并且也没有给ExtClassLoader设置父加载器,那它又是和ExtClassLoader如何关联的?下面的双亲委派机制实现的源码会为我们解答。 我们来看下AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下: - 首先检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。 - 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);),或者是调用bootstrap类加载器来加载。 - 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法,在文件系统本身中查找类,来完成类加载。 - 如果最后一个子类加载器也无法加载该类,则会抛出 java.lang.NoClassDefFoundError。 ```java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 检查当前类加载器是否已经加载了该类 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果当前加载器的父加载器不为空,则委托父加载器加载 c = parent.loadClass(name, false); } else { //如果当前加载器父加载器为空,则委托启动类加载器加载 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(); // 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { // 解析、链接指定的Java类 resolveClass(c); } return c; } } ``` 上面就是双亲委派机制实现原理的源码。从中我们可以看到有一个逻辑点会调用findBootstrapClassOrNull()这个方法,那么至此,我们有个疑团也就解开了:ExtClassLoader和BootstrapClassLoader(启动类加载器)就是在这里关联上的。因为ExtClassLoader在定义的时候,没有设置父类加载器(parent),所以执行到了这个逻辑,委托了BootstrapClassLoader进行加载。上面说的类加载器之间层级关系的实现和关联,也是在块逻辑里实现的。从源码这里的逻辑,也符合前面我们介绍BootstrapClassLoader所说的:Bootstrap类加载器充当所有其他ClassLoader实例的父级。 这个疑团是解开了,但是之前还有一个疑团仍然没有说明,在开始我们获取不同的类的加载器的时候,String的类加载器是null。在类加载的源码里面,我们看到了BootstrapClassLoader加载器的获取,为什么获取不到是null呢。这个我们要看下findBootstrapClassOrNull()这个方法的实现,看看BootstrapClassLoader到底是怎么定义的。 ```java /** * Returns a class loaded by the bootstrap class loader; * or return null if not found. */ private Class<?> findBootstrapClassOrNull(String name) { if (!checkName(name)) return null; return findBootstrapClass(name); } // return null if not found private native Class<?> findBootstrapClass(String name); ``` 通过源码可以看到最终调用了findBootstrapClass这个方法来返回,但是这个方法的修饰符是native,那么就容易理解我们为什么获取不到这个BootstrapClassLoader了。 #### 3.5 为什么设计双亲委派 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改 ,防止了恶意代码的注入,安全性的提高和保障。 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。如果每个加载器都自己加载,那么可能会出现多个同名类,导致混乱。 #### 3.6 双亲委派机制的打破 双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。 若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码时,就需要破坏双亲委派模型了。下面就介绍几种破坏了双亲委派机制的场景。 ##### 3.6.1 JNDI破坏双亲委派模型 JNDI是Java标准服务,它的代码由启动类加载器去加载,但JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。为了解决这个问题,引入了一个线程上下文类加载器(ContextClassLoader)。可通过Thread.setContextClassLoader()设置。利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。 ##### 3.6.2 Spring破坏双亲委派模型 Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。 那么Spring是如何访问WEB-INF下的用户程序呢?----使用线程上下文类加载器 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。利用这个来加载用户程序,即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。 ##### 3.6.3 Tomcat破坏双亲委派机制 - 不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。 - 部署在同一个web容器中相同的类库相同的版本可以共享 - web容器也有自己依赖的类库,不能与应用程序的类库混淆。 - web容器要支持jsp的修改,需要支持 jsp 修改后不用重启。 ![](//img1.jcloudcs.com/developer.jdcloud.com/e4a68612-765b-4374-b4fd-b8543389c37e20220118145316.png) #### 3.7 自定义类加载器 在介绍类加载器种类的时候,一共有四种,前面所说的都是前三种类加载器的一些机制,那如果我们想自己自定义个类加载器要如何实现呢? 自定义类加载器,只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)。下面是个自定义类加载器的例子: ```java public class ClassLoaderDrill { static class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = loadByte(name); //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节 数组。 return defineClass(name, data, 0, data.length); } catch (Exception e) { e.printStackTrace(); throw new ClassNotFoundException(); } } private byte[] loadByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class"); int len = fis.available(); byte[] data = new byte[len]; fis.read(data); fis.close(); return data; } } public static void main(String args[]) throws Exception { //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader MyClassLoader classLoader = new MyClassLoader("D:/test"); //创建 /com/xxx/xxx 的几级目录,跟你要加载类的目录一致 Class clazz = classLoader.loadClass("com.test.jvm.User"); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sout", null); method.invoke(obj, null); System.out.println(clazz.getClassLoader().getClass().getName()); } } ``` 注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。这个在ClassLoader的构造方法实现里可以看到。 ### 4 类加载的过程 上述我们介绍了类加载器及相关机制和实现源码,但是类加载器获取所需要的类这个动作,只是类加载全过程中的一部分。类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析三个部分统称为链接,这7个阶段的发生顺序如图: ![](//img1.jcloudcs.com/developer.jdcloud.com/60004ca2-6591-44b4-a605-7957541ea03120220118145424.png) 下面也给大家简单介绍下每个阶段所执行的具体动作 #### 4.1 加载 JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存(JVM)中,并生成一个代表该类的 java.lang.Class 对象。该阶段JVM完成3件事: - 通过类的全限定名获取该类的二进制字节流(需要特别说明的是我们上述所说的类加载器相关动作,就是类加载过程中的这个阶段) - 将字节流所代表的静态存储结构转化为方法区的运行时数据结构 - 在内存中生成一个该类的java.lang.Class对象,作为该类在方法区的各种数据的访问入口 #### 4.2 验证 主要确保加载进来的字节流符合JVM规范。JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行,该阶段是保证 JVM 安全的重要屏障。 验证阶段会完成以下4个阶段的检验动作: - 文件格式验证:基于字节流验证 - 元数据验证(是否符合Java语言规范):基于方法区的存储结构验证 - 字节码验证(确定程序语义合法,符合逻辑):基于方法区的存储结构验证 - 符号引用验证(确保下一步的解析能正常执行):基于方法区的存储结构验证 #### 4.3 准备 该步主要为静态变量在方法区分配内存,并设置默认初始值。JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化。 #### 4.4 解析 虚拟机将常量池内的符号引用替换为直接引用的过程,即将常量池中的符号引用转化为直接引用。 #### 4.5 初始化 在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。 #### 4.6 使用 使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。当使用阶段完成之后,java类就进入了卸载阶段。 #### 4.7 卸载 关于类的卸载,在类使用完之后,如果满足下面的情况,jvm就会在方法区垃圾回收的时候对类进行卸载。类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。 - 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。 - 加载该类的ClassLoader已经被回收。 - 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。 ### 5 总结 最后介绍了下类加载的整个过程及执行的具体动作,其实每个节点去深挖也是有很多内容的,感兴趣的小伙伴可以再去深入了解。 ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:孙靖凯
原创文章,需联系作者,授权转载
上一篇:ElasticSearch深度分页详解
下一篇:京东APP鸿蒙工程组件化探索
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号