JVM类加载机制

前言
  • 什么是类加载机制?
    当我们写完一个类的时候,并不是直接就可以运行的。需要编译成.class文件,然后交给虚拟机进行解释给当前操作系统去执行。这整个过程中,虚拟机如何获取.class文件就是类加载了。总结来说:虚拟机将.class文件从磁盘或者其他地方加载到内存,并同时对文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接识别的类型,这就是类加载机制!

  • 类加载的步骤

    类的生命周期.png

    通过图片可以知道加载、验证、准备、解析、初始化就是我们的类加载过程。而验证、准备、解析又被称为连接阶段。除了解析与使用,其余阶段的顺序都是固定的。

注意:解析阶段不一定会在准备阶段之后就执行,也有可能会在初始化阶段之后,这是为了支持JAVA的动态绑定的特性。
什么是动态绑定呢?相信大家都知道JAVA的4大特性吧:封装、继承、抽象、多态。其实多态就可以理解为动态绑定。多态的实现机制就是:父类或者接口可以创建他们的子类或者实现类的实例对象。简单的来说就是:父类可以new出子类,接口可以new出他的实现类。


步骤解读
  • 加载阶段(装载阶段)
    主要是将.class文件中的二进制字节流读入到JVM中。
    1.通过全限定名(包名+类名)来获取一个类的二进制字节流。
    2.将二进制字节流里代表着静态存储结构转化为方法区里的运行时数据结构。
    3.在java堆内存里生成一个这个类的Java.lang.class对象,通过这个class对象来作为方法区里这个类的数据的访问入口。

而以上相关装载需要用到类加载器ClassLoader
系统提供的加载器类型有三种:
Bootstrap ClassLoader:启动类加载器。负责加载JAVA_HOME/lib/里所有能被虚拟机识别的类(如:rt.jar)。无法被Java程序直接引用,由C++实现,不是ClassLoader子类。

Extension ClassLoader:扩展类加载器。负责加载java平台中扩展功能的一些jar包,包括JAVA_HOME/lib/ext/目录中的或java.ext.dirs系统变量指定目录下的所有类库。是ClassLoad的子类,开发者可以直接使用该加载器。

App ClassLoader:应用程序类加载器。负责加载classpath中指定的jar包及目录中class。getSystemClassLoader()的返回值就是该加载器,开发者可以直接使用该加载器。

如果需要用到我们自定义加载器,则需要实现加载器里的三个方法:loadClass()、findClass()、defineClass()

LoadClass方法是加载目标类的入口方法,在这里面它会首先去找当前加载器以及双亲加载器里是否已经加载过了目标类,如果没有则交给双亲加载器来进行加载,如果加载不了则交给自定义的findClass方法来进行加载目标类。而findClass方法主要是子类加载器自定义实现的,主要是找到目标类的字节码,然后通过调用defineClass方法来将字节码变成Class对象

在上面这段描述中引出了个模型:双亲委派模型

双亲委派模型.png

该模型需要有一个前提条件:除了顶层的类加载器之外,其余的类加载器都应该有自己的父加载器,这里的父加载器指的不是继承,而是组合,即App ClassLoader加载器里面应该要有Extension ClassLoader加载器的引用(为什么用组合而不用继承,大家可以想想其中的利弊)。
  基本工作过程就是:当一个类加载器收到了类加载的请求,他首先不会尝试自己去加载这个类,而是将这次的请求委派给自己的父类加载器去加载。如果父类加载器依然不能加载,则继续用父加载器的父加载器去加载(有点拗口)。层层如此,如果都不能加载,则最终的结果就是到达顶层——启动类加载器Bootstrap ClassLoader。每一层的类加载器都会根据请求所要加载的类去自己应该加载的目录中搜索有没有对应的类和查看该类是否已经被加载。如果有,那么该层加载器加载并返回,如果到达了启动类加载器后还是不能加载,那么就由最初接收到类加载请求的那个类加载器进行加载。

  • 验证阶段
    主要是验证class文件里的二进制字节流里包含的信息符合JVM的规范,不会对JVM造成伤害。如果验证失败会抛出一个java.lang.VerifyError异常或者该子类异常。
    1.文件格式验证:验证字节流文件是否符合class文件格式的规范,并能被JVM正确处理。验证class文件的魔数,主次版本号,常量池是否有不被支持的常量类型等等。只有经过这步验证,字节流才会进入到方法区存储,后面的验证不再接触二进制流,而是基于它在方法区的存储结构进行的。
    2.元数据验证:对字节码描述的信息进行语义分析,保证描述的信息符合java语言规范。比如这个类是否有父类,是否继承了不被允许继承的类,final修饰,是否实现了继承的抽象类中的所有方法。
    3.字节码验证:第二步元数据验证主要是对数据类型进行验证,而该方法主要是对方法体进行验证,防止出现方法体对JVM会造成危害。
    4.符号引用验证:该验证是发生在JVM将符号引用转化为直接引用的时候也就是解析阶段。如果这个阶段验证失败,会抛出java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等异常。可以验证是否可以通过类的全限定名来找到类,符号引用的方法、字段、类的访问权限是否可以被当前类访问到。

  • 准备阶段
    是为类变量(静态变量)在方法区中分配内存空间并设置初始值为0。如果是被final修饰的类变量则直接赋真实值

  • 解析阶段
    是将常量池里的符号引用转换成直接引用。主要是四种类型引用的解析:接口和类的解析、字段属性的解析、方法的解析。

  • 初始化阶段:
    在准备阶段已经对类变量进行了一次初始化了,而在这个阶段是执行类的构造器过程以及静态变量和代码块相关资源执行初始化工作。(包括父类的初始化)
    初始化的时机
    1.遇到了new、getStatic、setStatic、invokeStatic这4条指令,则如果类没有初始化需要立即对类进行初始化操作。
    2.包含了main方法的类需要立即进行初始化。
    3.当用到了反射方法来对类进行调用的时候,需要对类进行初始化。
    4.当子类进行初始化而它的父类还没进行初始化的时候,需要对父类进行初始化。(当初始化一个类的时候并不会要求先初始化它实现的接口,当初始化一个接口的时候并不会要求要初始化它的父接口)
    5.当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokestatic的方法的句柄时,先初始化句柄对应的类
    以下几种情况不会触发类初始化
    1.通过子类调用父类的静态属性,只会触发父类的初始化不会触发子类的初始化
    2.定义对象的数组的时候,不会触发该类的初始化,因为原则上你只是new了个数组而已。
    3.当你调用一个类的常量的时候,不会触发类的初始化,因为常量在编译期间的时候已经被jvm存到调用类的常量池里了。
    4.通过类名来获取Class对象不会触发初始化
    5.通过ClassLoader默认的loadClass方法,也不会触发类的初始化。

  • 使用过程
  • 弃用阶段

推荐阅读更多精彩内容

  • 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编译语言发展的一大步。 虚拟机把描述类的数据从...
    胡二囧阅读 497评论 0 0
  • JVM类加载机制 概述 类加载过程 加载 通过类的全限定名获取类的二进制流 将静态存储结构转化为方法区的运行时数据...
    东溪95阅读 2,623评论 0 15
  • 编译原理请查看之前内容,.java文件编译过程和执行过程分析以及计算机简单认识 需要了解更多Java原创资料,请加...
    技术分享狂人阅读 1,293评论 0 5
  • 面试题 运行的结果: SingletonA 解析过程: SingletonB 解析过程: 含义 类的加载指的是将类...
    一个痴阅读 172评论 0 0
  • 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的...
    丑人林宗己阅读 324评论 0 2