(转)一张图看懂JVM之类装载系统

导读java

 

在以前的文章中,咱们经过一张图的方式(图:point_up_2:)总体上了解了JVM的结构,并重点讲解了JVM的内存结构、内存回收算法及回收器方面的知识。收到了很多读者朋友们的反馈和指正,在这里做者向这些提出中肯建议的读者朋友们表示感谢,谢谢大家的支持。程序员

 

在今天的文章中将主要和你们一块儿探讨关于类装载子系统的内容。咱们知道,Java源代码(.java文件)须要经过编译器编译成字节码文件(.class)后由类装载子系统(ClassLoader)载入运行时数据区(<jdk1.8以前是载入方法区,>=jdk1.8之后是载入元数据区)才能被后续的Java运行程序(线程)正常使用(实例化或引用)。算法

 

那么类装载的具体机制是什么样的呢?下面就让咱们一块儿进一步来了解下吧!编程

 

JVM类装载概述小程序

 

与C/C++那些须要在编译器期进行链接工做的语言不一样,Java类的加载、链接和初始化都是在程序运行时完成的,只有在类被须要的时候才进行动态加载,这种方式被称为“Java语言的运行期类加载机制”数组

 

例如咱们在实际使用Java语言进行编程时一般会编写一个面向接口的应用程序,能够等到运行时再指定其实际的实现类。此外,更高级一点的作法是能够经过自定义的类加载器(关于具体的ClassLoader在后面的内容会提到),让一个本地的应用程序能够在运行时从网络或其余地方加载一个二进制流做为程序代码的一部分(Applet就是这么干的),这种组装应用程序的方式目前已普遍的应用于Java程序之中。安全

 

类(Class)从被加载到虚拟机内存中开始,到卸载出内存为止会经历以下生命周期:网络

 

 

其中验证、准备、解析3个部分又统称为链接(Linking)。在以上过程当中,除解析外,加载、验证、准备、初始化、卸载这5个阶段的顺序都是肯定的,JVM规定类的加载过程必须按照这种顺序循序渐进地开始。而解析阶段则不必定,为了支持Java语言的运行时绑定,解析过程在某些状况下能够在初始化阶段以后再开始。数据结构

 

须要注意的是,这些阶段并非必须等到上一个阶段完成才能开始下一个阶段,这些阶段一般都是互相交叉地混合式进行的,会在一个阶段执行的过程当中就会调用、激活另一个阶段。app

 

聊到这里,咱们大概了解了从一个class字节码文件变成加载到内存中可以被使用的类,按照前后顺序须要通过加载、链接、初始化三大主要步骤。链接过程又须要经历验证、准备、解析三个阶段,完成后类被加载至内存,但此时并不能被使用,还须要通过初始化阶段。

 

那么,在Java中是否全部的类型在类加载的过程当中都须要通过这几个步骤呢?

 

咱们知道Java语言的类型能够分为两大类:基本类型、引用类型。基本类型是由虚拟机预先定义好的,因此不会经历单独的类加载过程。而引用类型又分为四种:类、接口、数组类、泛型参数。因为泛型参数会在编译的过程当中被擦除(关于类型擦除的知识,你们能够查下资料),因此在Java中只有类、接口、数组类三种类型须要经历JVM对其进行链接和初始化的过程。

 

在上述三种类型中数组类是由JVM直接生成的,类和接口则有对应的字节流,字节流最多见的形式就是咱们由编译器生成的class文件,另外的形式也有在前面说到的经过网络加载的二进制流(例如网页中内嵌的小程序 Java applet),这些不一样形式的字节流都会被JVM加载到内存中,成为类或接口。

 

JVM类装载过程

 

那么这些过程具体会干些什么事呢?接下来咱们就详细了解下这些具体步骤的细节。

 

| 加载(Loading)

 

“加载”是“类加载”(Class Loading)过程的一个阶段,是查找字节流并据此建立类的过程。在前面咱们提到过说数组类由于没有对应的字节流因此是由JVM直接生成的,而对于类和接口来讲则须要借助类加载器(后面会讲到)来完成查找字节流的过程。

 

可是咱们在回答上面关于Java中哪些类型须要经历类加载的阶段时,又明确说了数组类型也是须要JVM对其进行链接和初始化的,这是否是有点矛盾呢?事实上,虽然数组类自己并不经过类加载器加载(由虚拟机直接建立),可是数组类与类加载器仍然有很密切的关系,由于数组类的元素类型(Element Type)如对象数组,最终仍是要靠类加载器去建立。

 

关于数组类的加载建立过程是须要遵循以下规范的:

  • 若是数组的元素类型是引用类型的话,那么就会递归采用前面内容中定义的类加载过程去加载这个元素类型,该数组自己将会在加载该元素类型的类加载器的类名称空间上被标识(这一点很是重要,在讲述类加载器的时候会介绍到,一个类必须与类加载器一块儿肯定惟一性)。

     

  • 若是数组的元素类型不是引用类型(例如int[]数组),JVM则会把该int[]数组标记为启动类加载器(Bootstrap Classloader)关联。

 

实际上,在加载阶段虚拟机须要完成如下三件事:

  • 经过一个类的全限定名来获取定义此类的二进制字节流。

     

  • 将这个字节流所表明的静态存储结构转换为方法区(JDK1.8之前)或者元数据(JDK1.8之后)的运行时数据结构。

     

  • 在内存中生成一个表明这个类的java.lang.Class对象,做为方法区或元数据区这个类的各类数据的访问入口。

 

加载阶段完成后,JVM外部的二进制字节流就按照虚拟机所须要的格式存储在了方法区或元数据区的内存中了。

 

| 验证(Verification)

 

验证是链接阶段的第一步,这一阶段的主要目的是为了确保Class文件的字节流中所包含的信息符合当前JVM的要求,而且不会危害JVM自身的安全。虚拟机若是不检查输入的字节流,对其彻底信任的话,极可能会由于载入了有害的字节流而致使系统崩溃,因此验证阶段是很是重要的,这个阶段是否严谨,直接决定了JVM是否能承受恶意代码的攻击。

 

那么验证阶段具体应当检查哪些方面?如何检查?什么时候检查呢?

 

《Java虚拟机规范(Java SE 7版)》中大概是有130页左右的篇幅是来描述验证的过程的,受篇幅所限,咱们没法逐条规则去探讨。但从总体上来看,验证阶段大体会完成以下四个阶段的检验动做:

 

1)、文件格式验证

 

文件格式验证就是要验证字节流是否符合Class文件格式的规范,以及是否能被当前版本的虚拟机处理。例如,常量池的常量中是否有不被支持的常量类型;Class文件中各个部分及文件自己是否有被删除的或附加的其余信息等等。

 

这个阶段验证的主要目的就是为了保证输入的字节流能正确地解析并存储于方法区或元数据区以内,格式上符合描述一个Java类型信息的要求。本阶段的验证是基于二进制字节流进行的,只有经过了这个阶段的验证,字节流才会正常进入内存(方法区/元数据区)中进行存储,因此后面剩下的3个验证阶段所有是基于已经载入内存的存储结构进行的,而不会再直接操做字节流了。

 

2)、元数据验证

 

元数据区验证的主要目的是对类的元数据信息进行语义的校验,以保证描述的信息符合Java语言规范的要求。

 

例如:

 

这个类是否有父类(除了java.lang.Object以外,全部的类都应该有父类)?这个类的父类是否继承了不容许被继承的类(如被final修饰的类)?若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法?类中的字段、方法是否与父类产生矛盾?等等。虽然这些逻辑目前编译器都会在编译字节码文件时加以校验,可是做为JVM类加载自己为了确保自身的安全性,也是须要进行严格校验的。

 

3)、字节码验证

 

字节码验证是一个更加复杂的阶段,主要目的是经过数据流和控制流的分析,肯定程序语义是合法的、符合逻辑的。在元数据验证阶段主要是完成了对元数据信息的类型校验,而这个阶段则是对类的方法体进行校验分析,确保被校验类的方法在运行时不会作出危害虚拟机安全的事件。例如,保证方法体中的类型转换是有效的,能够把一个子类对象赋值给父类的数据类型(上溯造型),可是不能把父类对象赋值给子类数据类型或者把对象赋值给与与它毫无继承关系的类型。

 

4)、符号引用验证

 

符号引用验证能够看作是对类自身之外(主要是常量池中的各类符号引用)的信息进行匹配性校验。目的是确保后面进入解析阶段后,解析动做可以正常执行。若是没法经过符号引用验证,就会抛出如“java.lang.IllegalAccessError”、“java.lang.NoSuchFileIdError”、“java.lang.NoSuchMethodError”等这样的异常信息。

 

对于JVM的类加载机制来讲,验证阶段是一个很是重要,可是不必定必要(对程序的运行期没有影响)的阶段。若是所运行的所有代码,包括本身编写的以及第三方包中的代码都已经被反复使用和验证过,那么就能够考虑使用

-Xverify:none”参数来关闭大部分的验证措施,以缩短虚拟机类加载的时间。

 

| 准备(Preparation)

 

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一块儿分配在Java堆中。

 

另外,上面所说的对类变量进行初始值,一般状况下是初始为零值。如int类型的类变量,初始值就是0。

 

| 解析(Resolution)

 

在class文件被加载至JVM以前,这个类是没法知道其余类及方法、字段所对应的具体地址的,甚至不知道本身方法、字段的内存地址。所以,每当须要引用这些成员时Java编译器会生成一个符号引用。在运行阶段这个符号引用通常都能无歧义地定位在具体目标上。举个例子,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。

 

解析阶段的目的就是将这些符号引用解析成为实际引用。而实际引用就是真正指向内存地址的指针、相对偏移量或能间接定位到目标的句柄。解析动做主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄调用点限定符这7类符号引用进行。

 

在前面咱们提到过,解析阶段并不必定会在链接过程当中完成,由于JVM虚拟机规范并无对此做明确的要求,只是规定了:“若是某些字节码使用了符号引用,那么在执行这些字节码以前,须要完成对这些符号引用的解析”。对于这一点你们不要搞错了。
 

| 初始化(Intialization)

 

类初始化是类加载过程的最后一步,是为标记为常量值的字段赋值,以及执行<clinit>方法的过程。那么什么样的字段才会被标记为常量值呢?<clinit>方法又是什么呢?

 

在Java代码中若是要初始化一个静态字段,咱们能够在声明时直接赋值,也能够在静态代码块中对其赋值。在这里,若是直接赋值的静态字段被 final 所修饰,而且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由Java 虚拟机完成。

 

而除此以外的直接赋值操做,以及全部静态代码块中的代码则都会被Java编译器置于同一方法中,这个方法就是<clinit>方法,也称为类构造器方法。Java 虚拟机会经过加锁来确保类的 <clinit> 只会被执行一次。

 

在咱们讲述JVM类加载过程的时候,并无特别说明什么状况下须要开始类加载过程的第一个阶段:加载?这是由于JVM虚拟机规范并无进行强制约束。可是对于初始化阶段,JVM规范则是严格规定了发生以下状况时必须马上对类进行“初始化”,而加载、验证、准备也天然须要在此以前开始。

 

这几种状况以下:

 

1)、当虚拟机启动时,初始化用户指定的主类(包含main方法的类);

2)、当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

3)、当遇到调用静态方法的指令时,初始化该静态方法所在的类;

4)、当遇到访问静态字段的指令时,初始化该静态字段所在的类;

5)、子类的初始化会先触发父类的初始化(若是父类尚未进行过初始化的话);

6)、若是一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

7)、使用反射 API 对某个类进行反射调用时,初始化这个类;

8)、当初次调用 MethodHandle 实例时(JDK1.7的动态语言支持),初始化该 MethodHandle 指向的方法所在的类;

 

以上基本上就是类加载机制中初始化的大体过程,只有当初始化完成以后,类才能正式成为可执行的状态。

 

类加载器

 

在整个类加载过程当中,除了在加载阶段用户应用程序能够经过自定义类加载器参与以外,其他的动做彻底是由虚拟机主导和控制的。那么什么是类加载器呢?

 

在上述类加载机制的第一个阶段:"加载"中,把“经过一个类的全限定名来获取描述此类的二进制字节流”这个动做由JVM外部实现的代码模块称为“类加载器”

 

从JVM的角度来看,只存在两种不一样的类加载器:启动类加载器(Bootstrap ClassLoader)、其余类的加载器。启动类加载器是由C++语言实现的,属于JVM自身的一部分,而其余的类加载器则都是独立于JVM外部,由Java语言实现的继承java.lang.ClassLoader的类型。

 

而从Java程序员的角度看,类加载器还能够划分得更加细致一些。示意图以下:

 

 

在上图中的类加载器,是有层次关系的,这种关系被称之为类加载器的“双亲委派模式”,它要求除了顶层启动类加载器外,其他全部的类加载器都应当有本身的父类加载器,而且若是一个类加载器在收到类加载的请求以后都要先把这个请求委派给父类加载器去完成(每个层次的类加载器都是如此,所以全部的加载请求最终都应该会传送到顶层的启动类加载器中),只有当父类加载器反馈本身没法完成这个加载请求(在搜索范围没有找到所需的类)时,子加载器才会尝试本身去加载。

 

双亲委派模式不是强制性的约束模型,只是Java设计者推荐给开发者的类加载器的实现方式,可是采用这种模式对于保证Java程序的稳定运做确实很重要的,由于它能够避免Java体系中基础的类型被混乱加载的风险。例如类java.lang.Object,它存放在rt.jar之中,不管那一个类加载器要加载这个类,最终都会委派给启动类加载器,这样Object类在程序的各类类加载器环境中都是一个类,不然就会致使系统中出现多个不一样的Object类,从而连Java类型体系中最基本的行为都没法保证。

 

以上就是关于JVM类加载系统的所有内容了,但愿本文可以对你补充知识盲点起到一点做用!

相关文章
相关标签/搜索