前言

前文已经讲了虚拟机将java文件编译成class文件后的格式:JVM虚拟机Class类文件研究分析

java文件经过编译,形成class文件,那么虚拟机如何将这些Class文件读取到内存中呢?

加载的时机

JVM 会在程序第一次主动引用类的时候加载该类,被动引用时并不会引发类加载的操作。也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

一个类的生命周期如图所示:

上图中的加载、验证、准备、初始化、卸载这几个步骤是相对固定的,但是初始化这一步不一定,他在某些情况下可以是再初始化之后执行。

加载

加载是类加载的第一阶段,虚拟机此时主要做以下三件事情:

1.通过类的全限定名来获取定义这个类的二进制字节流。

2.将字节流的静态存储结构转化为运行时的数据结构;

3.在内存中生成该类的 java.lang.Class 对象,作为方法区这个类各种数据访问入口。

主动引用一定会加载,但是被动引用则不一定

主动引用

  1. 遇到 new、getstatic、putstatic、invokestatic 字节码指令,例如:

    使用 new 实例化对象;

    读取或设置一个类的 static 字段(被 final 修饰的除外);

    调用类的静态方法。

  2. 对类进行反射调用;

  3. 初始化一个类时,其父类还没初始化(需先初始化父类);

    这点类与接口具有不同的表现,接口初始化时,不要求其父接口完成初始化,只有真正使用父接口时才初始化,如引用父接口中定义的常量。

  4. 虚拟机启动,先初始化包含 main() 函数的主类;

  5. JDK 1.7 动态语言支持:一个 java.lang.invoke.MethodHandle 的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic。

被动引用

  1. 通过子类引用父类静态字段,不会导致子类初始化;

  2. Array[] arr = new Array[10]; 不会触发 Array 类初始化;

  3. static final VAR 在编译阶段会存入调用类的常量池,通过 ClassName.VAR 引用不会触发 ClassName 初始化。

也就是说,只有发生主动引用所列出的 5 种情况,一个类才会被加载到内存中,也就是说类的加载是 lazy-load 的,不到必要时刻是不会提前加载的,毕竟如果将程序运行中永远用不到的类加载进内存,会占用方法区中的内存,浪费系统资源。

验证

目的: 确保 .class 文件中的字节流信息符合虚拟机的要求。

4 个验证过程:

文件格式验证:是否符合 Class 文件格式规范,验证文件开头 4 个字节是不是 “魔数” 0xCAFEBABE

元数据验证:保证字节码描述信息符号 Java 规范(语义分析)

字节码验证:程序语义、逻辑是否正确(通过数据流、控制流分析)

符号引用验证:对类自身以外的信息(常量池中的符号引用)进行匹配性校验

这个操作虽然重要,但不是必要的,可以通过 -Xverify:none 关掉。

准备

  • 描述: 为 static 变量(类变量,非实例变量)在方法区分配内存。

  • static 变量准备后的初始值:

    当static变量未被final修饰时:

    public static int value = 123;

    准备后为 0,value 的赋值指令 putstatic 会被放在 () 方法中,()方法会在初始化时执行,也就是说,value 变量只有在初始化后才等于 123。

    当static变量被final修饰时:

    public static final int value = 123;

    准备后为 123,因为被 static final 赋值之后 value 就不能再修改了,所以在这里进行了赋值之后,之后不可能再出现赋值操作,所以可以直接在准备阶段就把 value 的值初始化好。

解析

描述:将常量池中的 “符号引用” 替换为 “直接引用”,也就是说将引用指向内存。

符号引用,比如com.courage.People引用了com.courage.Man,这时候Man并不在内存中

但是直接饮用则是引用Man所在的内存地址。

在此之前,常量池中的引用是不一定存在的,解析过之后,可以保证常量池中的引用在内存中一定存在。

什么是 “符号引用” 和 “直接引用” ?

  • 符号引用:以一组符号描述所引用的对象(如对象的全类名),引用的目标不一定存在于内存中。
  • 直接引用:直接指向被引用目标在内存中的位置的指针等,也就是说,引用的目标一定存在于内存中。

初始化

描述: 执行类构造器<clinit>()方法的过程。

<clinit>()方法包含的内容:

所有 static 的赋值操作;

static 块中的语句;

<clinit>()方法中的语句顺序:

基本按照语句在源文件中出现的顺序排列;

静态语句块只能访问定义在它前面的变量,定义在它后面的变量,可以赋值,但不能访问。

与实例构造器<init>()不同的地方在于:

不需要显示调用父类的<clinit>()方法;

虚拟机保证在子类的<clinit>()方法执行前,父类的<clinit>()方法一定执行完毕。

也就是说,父类的 static 块和 static 字段的赋值操作是要先于子类的。

接口与类的不同

执行子接口的<clinit>()方法前不需要先执行父接口的<clinit>()方法(除非用到了父接口中定义的 public static final 变量);

执行过程中加锁

同一时刻只能有一个线程在执行<clinit>()方法,因为虚拟机要保证在同一个类加载器下,一个类只被加载一次。

非必要性:

一个类如果没有任何 static 的内容就不需要执行 <clinit>()方法。

注:初始化时,才真正开始执行类中定义的 Java 代码。

虚拟机规范中并没有规定何时加载类,但是以下6种场景,场景必须初始化

类的显式加载和隐式加载

显示加载

  1. 调用 ClassLoader#loadClass(className)Class.forName(className)

  2. 两种显示加载 .class 文件的区别:

    Class.forName(className) 加载 class 的同时会初始化静态域,ClassLoader#loadClass(className) 不会初始化静态域;

    Class.forName 借助当前调用者的 class 的 ClassLoader 完成 class 的加载。

隐式加载

  1. new 类对象;

  2. 使用类的静态域;

  3. 创建子类对象;

  4. 使用子类的静态域;

  5. 其他的隐式加载,在 JVM 启动时

    BootStrapLoader 会加载一些 JVM 自身运行所需的 Class;

    ExtClassLoader 会加载指定目录下一些特殊的 Class;

    AppClassLoader 会加载 classpath 路径下的 Class,以及 main 函数所在的类的 Class 文件。

双亲委派机制

通过一个类的全限定名来获取描述该类的二进制字节流这个动作在Java虚拟机外部实现,这样做的好处是应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

在比较两个类是不是同一个类,只有在同一个类加载器下比较才有意义,对于同一个类用不同的加载器加载内存,两个类是不相等的。

站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现 ,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

为了保证加载应该被加载的类,遵循双亲委派机制,目的是保证安全性,例如自己定义的String类不至于替换掉虚拟机默认的String类,双亲委派机制如图:

需要说明的是,此处的Cache以及仓库是我为了后面说明方便而做的定义,每一个启动器都有自己对应的Class文件存放位置,将这个位置称之为仓库,已经加载进内存的Class存放在内存中,这块内存称之为Cache,对于我们自定义的String类,肯定是放在用户空间的仓库上,如果要加载这个类,会依次往上查找,各级的内存,首先查找用户自定义的ClassLoader,如果已经加载过就直接返回,如果没有加载过就往上一类加载器缓存中查找,如果直到Bootstrap都没有找到的话就会开始查找仓库,查找仓库的顺序与查找缓存相反,先查找Bootstrap的仓库,再查找Extension,找到就加载然后返回Class,也就意味着,自定义的String根本没法被查找到,因为在Bootstrap仓库中已经查找到String并且加载返回了。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。

双亲委派机制被破坏

在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委派模型主要出现过3次较大规模“被破坏”的情况。

第一次被破坏

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法,双亲委派的具体逻辑就实现在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。

第二次被破坏

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。

第三次被破坏

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。说白了就是希望Java应用程序能像我们的电脑外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是大型系统或企业级软件开发者具有很大的吸引力。

JVM类加载与双亲委派机制被打破的更多相关文章

  1. 深入探究JVM之类加载与双亲委派机制

    @ 目录 前言 类的生命周期 加载 验证 准备 解析 初始化 案例一 案例二 案例三 案例四 类加载器 类加载器和双亲委派模型 破坏双亲委派模型 第一次 SPI Tomcat OSGI 总结 前言 前 ...

  2. JVM学习六:JVM之类加载器之双亲委派机制

    前面我们知道类加载有系统自带的3种加载器,也有自定义的加载器,那么这些加载器之间的关系是什么,已经在加载类的时候,谁去加载呢?这节,我们将进行讲解. 一.双亲委派机制 JVM的ClassLoader采 ...

  3. 面试~jvm(JVM内存结构、类加载、双亲委派机制、对象分配,了解垃圾回收)

    一.JVM内存结构 ▷ 谈及内存结构各个部分的数据交互过程:还可以再谈及生命周期.数据共享:是否GC.是否OOM 答:jvm 内存结构包括程序计数器.虚拟机栈.本地方法栈.堆.方法区:它是字节码运行时 ...

  4. 【Java_基础】java类加载过程与双亲委派机制

    1.类的加载.连接和初始化 当程序使用某个类时,如果该类还未被加载到内存中,则系统会通过加载.连接.初始化三个步骤来对类进行初始化.如果没有意外,jvm将会连续完成这三个步骤,有时也把这三个步骤统称为 ...

  5. Java类加载器和双亲委派机制

    前言 之前详细介绍了Java类的整个加载过程(类加载机制详解).虽然,篇幅较长,但是也不要被内容吓到了,其实每个阶段都可以用一句话来概括. 1)加载:查找并加载类的二进制字节流数据. 2)验证:保证被 ...

  6. 【JAVA开发】浅析双亲委派机制

    双亲委派机制存在的意义 双亲委派只是一种说法,个人觉得叫单亲委派更合适,因为向上传递的父类只有一个,估计只是翻译过来的,形成的一种习惯,大家可以当做单亲委派 四种加载器都是用于类的加载,只是加载的对象 ...

  7. 深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

    一.概述   定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的 ...

  8. JVM之类加载器、加载过程及双亲委派机制

    JVM 的生命周期 虚拟机的启动 Java 虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实 ...

  9. JVM 专题四:类加载子系统(二)双亲委派机制

    2. 双亲委派机制 2.1 双亲委派机制工作原理 2.1.1 原理 Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存,生成class对象 ...

随机推荐

  1. Object[] cannot be converted to String[]

    原因: 你应该是想把List数组转 String数组吧! 然后想当然的调用list.toArray()方法. 结果 该方法返回的是Object[]数组,导致类型不匹配! 解决办法: 还在乖乖的用循环吧 ...

  2. Mysql 实战关于date,datetime,timestamp类型使用

    最近在做一个项目 项目中 不同的小伙伴同时在不同的业务模块中用到了date,datetime,timestamp这三个类型 特别是datetime,timestamp这两个 如果不能理解到位  其实很 ...

  3. Linux 下 swap 分区及作用详解

    我们在安装系统的时候已经建立了 swap 分区.swap 分区是 Linux 系统的交换分区,当内存不够用的时候,我们使用 swap 分区存放内存中暂时不用的数据.也就是说,当内存不够用时,我们使用 ...

  4. Nebula Exchange 工具 Hive 数据导入的踩坑之旅

    摘要:本文由社区用户 xrfinbj 贡献,主要介绍 Exchange 工具从 Hive 数仓导入数据到 Nebula Graph 的流程及相关的注意事项. 1 背景 公司内部有使用图数据库的场景,内 ...

  5. php利用腾讯ip分享计划获取地理位置示例分享

    <?php function getIPLoc_QQ($queryIP){ $url = 'http://ip.qq.com/cgi-bin/searchip?searchip1='.$quer ...

  6. OSTU大津法图像分割

    OSTU图像分割 最大类间方差法,也成大津法OSTU,它是按图像的灰度特性,将图像分成背景和目标2部分.背景和目标之间的类间方差越大,说明构成图像的2部分的差别越大,当部分目标错分为背景或部分背景错分 ...

  7. Java通过基姆拉尔森公式判断当前日期是不是工作日

    基姆拉尔森公式 算法如下: 基姆拉尔森计算公式 W= (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400+1)%7 在公式中d表示日期中的日数,m表示月份数,y表示年数. 注意:在公 ...

  8. 基于腾讯云存储网关 CSG 实现视频在线转码分发

    一.背景 随着越来越多的传统业务云化和云端业务发展,数据上云和云端数据处理领域的需求爆发式增长.腾讯云存储网关CSG提供一键部署开箱即用的便捷模式,深度结合COS对象存储生态,为用户提供方便快捷的数据 ...

  9. ajax跨域访问http服务--jsonp

    在前面一篇文章<Spring Cloud 前后端分离后引起的跨域访问解决方案>里我们提到使用ajax跨域请求其他应用的http服务,使用的是后台增加注解@CrossOrigin或者增加Co ...

  10. ES6 自定义一个实现了Iterator接口的对象

    参考资料 var obj = { data: [1,2,3,4,5], // 这里实际上就是去定义如何实现Iterator接口 [Symbol.iterator](){ const that = th ...