深入理解JVM类加载机制
前言什么是类加载?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。加载什么?
前面的定义已经讲了是加载描述类的数据,也就是Class文件,关于Class文件,我在《深入解析Class类文件的结构》一文中进行了分析。谁来加载?
加载描述类的类文件的二进制流是由类加载器完成的,已有的三种类加载和自定义的类加载器组成了类加载器子系统,关于类加载器,下文会详细讲述。怎么加载?
这就是本文的重点,类加载机制中的类加载流程。
可以通过下图整体上看一下类加载在JVM体系中的位置
JVM体系结构类的生命周期
类的生命周期共有7个阶段,分别如下图:
类的生命周期
前5个阶段属于类加载流程的范围,其中验证、准备、解析又被称为连接,类加载的5个阶段并不是按照顺序依次完成的,除了解析可能会在初始化之后开始,其他的几个阶段的开始顺序是确定的,但结束顺序不一定,可能会交叉着进行,加载还没完成,连接可能已经开始。类加载流程
类加载分为5个过程,分别是加载、验证、准备、解析、初始化,下面分别对这几个过程进行讲述,尽量简短明了。加载
"加载"是"类加载"流程的一个阶段
加载阶段主要干的3件事:通过一个类的全限定名获取定义此类的二进制字节流将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class实例,作为访问入口
在这三件事里,开发人员能干预的是第一件事,我们可以使用系统的三个类加载器去加载我们想要加载的类文件,也可以自定义类加载器去获取二进制字节流。
定义类的二进制字节流不一定是经过编译后存储在磁盘上的.class文件,有可能是以下来源:从ZIP包中读取,如:JAR、EAR、WAR从网络中获取,如:Applet运行时计算生成,如:动态代理技术由其他文件生成,如:JSP文件生成.class从数据库中读取,中间件服务器,如:SAP Netweaver
Hotspot虚拟机中,Class实例不是在堆上分配空间,而是存放在方法区中,这个实例在代码中可以轻松的获取到,并通过它可以获取代表某个类的各种数据结构。验证
验证是对输入的字节流进行检查的过程
为什么要有验证这个过程呢?就是因为加载的对象:描述类的二进制字节流,来源广泛,不得不防止它被小人利用,损害虚拟机的正常运行,导致崩溃。所以总共有四个验证过程,分别如下图:
4个验证过程文件格式验证
这个阶段直接操作字节流,后面的三个阶段是基于方法区的存储结构,这个阶段主要是验证文件本身的字节码是不是符合规范,目的是保证输入的字节流可以被正确的存储在方法区内。上图中的四个检查项只是其中的一小部分,真正的验证点还有很多。元数据验证
这个阶段主要是验证类的元数据信息是否符合Java语言规范,比如检查是否有父类,除了Objec,其他类都应该要有父类,否则就不符合规范了;被final修饰的不允许被继承。字节码验证
这个阶段主要是对类的方法体进行验证,保证类方法的运行不会对虚拟机造成危害。这是4个验证里最复杂的一个,因为要通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的。符号引用验证
上面三个阶段是对类本身进行验证,而符号引用验证阶段主要是对类以外的信息进行验证,后面会讲到解析是将符号引用替换成直接引用,所以这里验证的目的是确保符号引用是正确的,确保后面的解析过程能顺利的进行。准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
注意这里是为类变量分配内存,而且是分配在方法区中,实例变量是后面随着实例一起分配在堆上的。
设置初始值也不是代码里赋的值,而是各个数据类型规定的零值,比如基础类型是相应类型不同字节长度的0,引用类型是null。
不是每个类变量都是设置为零值,被final修饰的常量,因为在编译期带有一个ConstantValue属性,属性值则是该常量在代码里赋的值,这个值在准备阶段前就已经确定了,所以在准备阶段设置值的时候,直接取的ConstantValue给类常量。
下面的例子可以很好的了解准备阶段,准备阶段过后,a、b、c分别是多少?public class Test { public static int a; public static int b = 1; public static final int c = 2; public void say(){ System.out.println("Hello"); } }
答案揭晓:0, 0, 2 原因上文里写的很明白解析
解析是将常量池内的符号引用替换为直接引用的过程
那什么是符号引用和直接引用呢?符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析的时机根据虚拟机实现不同而不同,可以是类加载器加载时解析,也可以是符号引用使用前解析
解析主要是对7类符号引用进行:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符
7类符号引用初始化
初始化是执行类构造器()方法的过程
类初始化阶段是类加载流程的最后一个阶段,是执行()方法的阶段,这个阶段才真正开始执行开发人员的代码。
()方法是编译器按照源文件中定义的顺序收集类变量和静态语句块形成的方法。它的一些特点和细节如下:编译器自动收集静态变量和静态代码块合并产生的不需要显示的调用父类的,虚拟机保证父类先执行父类定义的静态语句块优先于子类变量赋值操作没有静态变量和静态语句块,可以不生成()方法接口也会有这个方法,但不需要先执行父类的()方法虚拟机保证该方法在多线程环境下被正确的加锁和同步什么时候发生初始化?
对一个类进行主动引用的时候必须初始化,主动引用的场景如下:遇到new、getstatic、putstatic、invokestatic这四条指令时使用java.lang.reflect包的方法对类进行反射调用时初始化一个其父类还没被初始化的类时虚拟机启动时,包含main方法的主类还没被初始化时当使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法所对应的类没有进行初始化时什么时候不发生初始化?
对一个类进行被动引用的时候不初始化,被动引用的场景有下面一些:通过子类引用父类的静态字段,不会导致子类的初始化通过数组定义来引用类,不会触发此类的初始化引用类的常量时,不会触发此类的初始化类加载器什么是类加载器?
实现"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作的代码模块就叫做类加载器
类加载不仅仅是加载二进制字节码的作用,还起着独立的类名称空间的作用,确定一个类的唯一性由三个因素决定:同一个java虚拟机同一个类加载器同一个全限定类名双亲委派模型
下图中各个加载器之间的层次关系被称为类加载器的双亲委派模型
双亲委托模型图
图中可以看到,系统提供了三个类加载器:启动类加载器、扩展类加载器和应用程序类加载器,java程序启动的时候,三个类加载器分别从各自指定的路径中加载所需的类。最下面是开发人员自定义的类加载器,继承自ClassLoader,重写findClass()方法。
一般我们自己写的类是默认由应用程程序加载器加载的,自定义的类加载器的父类加载器默认是应用程序加载器,应用程序加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器,这种父子关系不是一般的继承或实现关系,而是子加载器持有父加载器的引用,是一种组合关系。自定义类加载器时,可以在构造函数中传入指定的父类加载器。双亲委派模型的工作原理
一个类加载器收到了类加载的请求时,它首先会先检查自身有没有加载过这个类,实质就是在JVM的常量池中查找该类的符号引用是否存在,如果有就直接返回,否则把这个请求委派给父类加载器,直至委派给启动类加载器,只有当父类加载器加载失败,子类加载器才会尝试自己去加载。
下面是实现双亲委派模型的主要代码,代码简单易懂://ClassLoader.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) { //有父类加载器,则交给父类加载器加载,递归执行loadClass方法 c = parent.loadClass(name, false); } else { //没有父类加载器,交给启动类加载器加载,执行一个本地方法 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 除了启动类加载器之外的类加载器加载类失败抛异常,此处不进行任何处理 } if (c == null) { // 父类加载器未成功加载到类,则调用本加载器的findClass方法 long t1 = System.nanoTime(); c = findClass(name); // 记录一些状态 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //验证解析 if (resolve) { resolveClass(c); } return c; } }
虽然易懂,但配合下面的图更容易加深理解,下面是这段代码的数据流程图:
双亲委派模型数据流程图
下面按照一般的双亲委派模型来分析,假设是自定义的类加载器调用了loadClass方法,触发了类加载的过程,则下面的过程会依次执行:自定义的类加载器首先会调用findLoadedClass(name)方法查看有没有被加载的这个类,如果有直接返回,否则执行下面步骤检查是否存在父类,如果有则递归调用父类的loadClass方法,否则说明父类加载器是启动类加载器,本类加载器是扩展类加载器,调用findBootstrapClassOrNull(name)使用启动类加载器进行类加载启动类加载器加载成功则返回,失败则调用扩展类加载器的findClass(name)方法来加载,成功则返回,失败则继续调用应用类加载器的findClass(name)方法,同样成功返回,失败调用自定义类加载器的findClass(name)我们自定义的类加载器一般会重写findClass方法,使用自定义的类加载器加载一个父类加载器加载不了的类的时候,就会执行自定义的findClass方法,在此方法中,会指定二进制字节码的路径读入字节数组,最后调用defineClass返回加载成功的类
下面是自定义类加载器的示例代码:public class MyClassLoader extends ClassLoader{ private String classpath; //指定父类加载器的构造函数 public MyClassLoader(String classpath,ClassLoader classLoader) { super(classLoader); this.classpath = classpath; } //默认父类加载器为应用程序加载器的构造函数 public MyClassLoader(String classpath) { this.classpath = classpath; } //重写findClass,加载类文件,返回类 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String classFilePath = null; String finalName = name.replace(".", "/"); classFilePath = classpath + "/" + finalName + ".class"; Path path = Paths.get(classFilePath); if (!Files.exists(path)) { return null; } try { byte[] classData = Files.readAllBytes(path); return defineClass(name, classData, 0, classData.length); } catch (IOException e) { throw new RuntimeException("Can not read class file into byte array"); } } }为什么要使用这个模型?
最后来讲讲为什么要使用这个模型?用这个模型有什么好处?
采用双亲委派模式的好处之一是类和它对应的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没有必要再加载一次。
其次是考虑到安全因素,保证java核心api中定义的类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
最新2021整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等
Java中级资料提升以上福利教程领取方式:
1、点赞+评论(勾选"同时转发")
2、关注小编。并私信回复关键字【19】
Linux学习总结4自己学习总结文档,有些乱,勿怪1Linux中的总用量是什么意思linux中,我们经常会用到ll命令(lsl)查看目录信息列表,见下图上图中,总计92是指什么?这里的总计,也可以叫总
我交易亏损50后开始盈利了,过程是这样的四年前,我还是一名本科生,正忙着寻找一份稳定的工作。然而,职场如战场,我并没有找到一份适合自己的工作。于是,我决定成为一名全职交易员。我在为交易努力准备着。我观看了每一个交易视频,
重新认识雪铁龙穿越巴黎去罗马01hr3月13日,雪铁龙途我自在舒适空间(上海站)踏青而来。14日上午9点半,位于上海爱琴海广场的东风雪铁龙舒适空间,迎来这一天的第一位体验者。此时,距离天逸PHEV上市已有半年
复古情怀,当小红点遇到帝国时代II帝国时代II决定版准备篇首先我们聊聊今天要玩的这款帝国时代II决定版。游戏首次发布在1999年,那个奔腾4和迅驰科技的年代,小学生讨论的电脑话题无非是学校的金山打字通作业还有星际争
每年需求200万吨,却被外资掌握命脉20年,现产量5500亿领先国际我国的民众曾享有中国吃货的名号,在国内的众多美食中,有一样食品,我国每年的需求量高达200万吨,却被外资掌握着命脉20年,好在目前已经实现突破。你知道这是哪一样食品吗?随着各国之间
印度准备挑战世界最高铁路桥的记录,这回希望大吗?中国行,我也行!要在基建领域超越中国,而这座大桥开工就是最好的证明!自信的印度人一次次喊口号,这次不同了,似乎真的有了新作品,这座被印度人寄予厚望,能够以此一举超过中国基建领域的大
事关华为5G,加拿大特鲁多突然改口在5G领域,不管是比技术比专利或者比设备,华为都是当之无愧的全球第一,这本该成为中国通信走向世界的一张名片,但却引来了老美的嫉恨和干预,不仅构陷华为设备存在安全后门,而且还实施网络
明明曲面屏是趋势,为什么我用过之后,换手机再也不考虑了?曲面屏是未来旗舰手机发展的趋势,如今这种迹象已经越来越明显了,小米11一加9ProOPPOFindX3系列华为Mate40系列等等,全都采用了曲面屏设计,但笔者去年使用过了一次曲面
OPPO新款5G手机RENO5在RENO4基础上有哪些提升?12月10日,OPPO发布上市新款5G手机RENO5和RENO5PRO,上一篇单独对RENO5和RENO5PRO的硬件参数作了对比分析,今天对RENO5系列在RENO4系列的基础上
关于美索要的芯片机密数据,台积电摊牌了华为在5G领域的强势崛起,让不可一世的美科技界惊出了一身冷汗,更让老美没想到的是,它倾其所有的对华为实施了芯片禁令,非但没有把华为打倒,反而加速了中国芯片产业自给自足的步伐。反观美
搬石自砸?美两大巨头宣布停产!事情越闹越大华为是全球顶尖的科技企业,5G技术芯片设计智能手机等各项业务的背后都着庞大的国际供应链,其地位是不可替代的。然而,老美却不顾半导体巨头们的反对,决然的对华为实施了芯片禁令,虽然这让