• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

JVM类加载与双亲委派机制被打破

其他 等不到的口琴 1200次浏览 0个评论

前言

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

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

加载的时机

JVM 会在程序第一次主动引用类的时候加载该类,被动引用时并不会引发类加载的操作。也就是说,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类,双亲委派机制如图:

JVM类加载与双亲委派机制被打破

需要说明的是,此处的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类加载与双亲委派机制被打破
喜欢 (0)

您必须 登录 才能发表评论!

加载中……