JVM类加载机制

Posted jiangxiaoju

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM类加载机制相关的知识,希望对你有一定的参考价值。

原创不易,未经允许,请勿转载。

博客主页:https://xiaojujiang.blog.csdn.net/

Java虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。这个过程被称作虚拟机的类加载机制。与那些在编译时需要进行连接的语言不同,Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时额外增加一些性能开销,但是却为Java应用提供了极高的拓展性和灵活性,Java天生可以动态拓展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

一、 类加载时机

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。过程如下图所示。

加载、验证、准备、初始化、卸载这五个过程顺序是确定,类型的加载过程必须按照这个顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定特性(也成为动态绑定或晚期绑定)。但是按部就班开始不代表按部就班的完成。这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

关于什么情况下需要开始类加载过程的第一个阶段”加载“,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行”初始化“(而加载、验证、准备自然需要在此之前开始):

  1. 遇到newgetstaticputstatic、或invokestatic这四个字节码指令时,如果类型没有进行初始化过,则需要先触发器初始化阶段。能够生成这四条指令的典型Java代码场景有:
    • 使用new关键字实例化对象的时候。
    • 读取或设置一个类型的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候。
    • 调用一个类型的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父泪的初始化。
  4. 当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个类。
  5. 点使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化过,则需要先触发其初始化。
  6. 当一个接口中定义了JKD8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的时下类发现了初始化、那该接口要在其之前被初始化。

二、 类加载过程

2.1 加载

加载阶段是整个类加载过程的一个阶段。在加载阶段,Java虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生曾一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

但是《Java虚拟机规范》对这三点要求并没有很具体。与虚拟机的具体有关。例如第一点,并没有指明二进制流的来源。这使得我们在加载阶段可以非常的灵活。例如可以从ZIP压缩包读取,到后来的JAR、EAR、WAR等。也可以从网络中获取、通过运行时计算生成(在动态代理技术中用的最多),也可以由其他文件生成(例如JSP技术)等等。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能以及开始,但这些加载加载阶段中进行的动作,仍然属于连接阶段的一部分,两个阶段的开始时间仍然保持着固定的顺序。

2.2 验证

验证是连接的第一步,这一阶段的目的是确保Class文件的字节流包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

Java语言本身是相对安全的编程语言,使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个类型转为它并未实现的类型、跳转到不存在的代码行之类的事情。如果尝试去做了,编译器会毫不留情地抛出异常、拒绝编译。但Class文件不一定只能由Java源码编译而来,它可以使用包括0和1直接在二进制编辑器中敲出CLas文件在内的任何途径产生。上述在Java代码无法做到的事情在字节码层面都是可以实现的。

验证阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从代码量和耗费的执行性能角度上将,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。

验证阶段大致会完成下面四个阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。验证点包括以下这一些:

    • 是否已魔数0xCAFEBABE开头。
    • 主、次版本号是否在当前Java虚拟机接受范围内。
    • 常量池的常量中是否有不被支持的常量类型。(检查常量tag标记)。
    • Class文件中各个部分以及文件本身是否又被删除的或者附加的其他信息。
    • ………………
  2. 元数据验证:这个阶段是对字节码描述的信息进行予以分析,以保证其描述的信息符合《Java语言规范》的要求,这个阶段可能包括的验证点如下:

    • 这个类是否包含父类(除了java.lang.Object之外,所有的类都应当由父泪)。
    • 这个类是否继承了不允许被继承的类(被final修饰的类)。
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
    • ………………
  3. 字节码验证:这个是验证过程最复杂的一个阶段,主要目的是通过对数据流分析和控制数据流分析,确定程序语义是合法的、符合逻辑的的。在元数据验证后,这个阶段就要对类的方法体进行校验分析,保证被校验的类方法在运行时不会做出危害虚拟机安全的行为,例如:

    • 保证任意时刻操作数栈的数据类型与指令代码序列都能找到匹配的工作,例如不会出现类似于”在操作站位置放了一个int类型的数据,使用时却按long类型来加载入本地变量表中“这种的情况。
    • 保证任何跳转指令都不会跳转到方法体意外的字节码指令上。
    • ………………
  4. 符号引用验证:这个校验行为发生在虚拟机将符号引用转为直接引用的时候。符号引用验证可以看作是对类自身以外的各类信息进行匹配性校验,通俗来说就是该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。验证内容通常需要以下几点:

    • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
    • 在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
    • …………

    符号引用验证的主要目的是确保解析行为能够正常执行,如果无法通过符号引用验证,Java虚拟机会爆出一个java.lang.IncopatibleClassChangeError的子类异常,典型的如:java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError等。

2.3 准备

准备阶段是证实为类中的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的状态,从概念上讲,这个变量所使用的内存都应当在方法区中进行分配。

2.4 解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用在Class文件中以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等类型的常量出现。

  • 符号引用:符号引用以一组符号来描述引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义地定位到目标即可。
  • 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能简介定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。

2.5 初始化

初始化阶段是类加载过程的最后一个步骤,在之前的几个类加载的动作里,除了在加载阶段用户应用程序可以直接自定i类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。知道初始化阶段,Java虚拟机才真正开始执行类中编写的java程序代码,将主导权移交给应用程序。

三、类加载器

3.1 类与类加载器

类加载器虽然只能用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。换句话说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载他们的类加载器不同,那么这两个类必定不相等。

3.2 双亲委派模型

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

站在Java开发人员的角度来看,类加载器应该划分为更细致一些。自JDK1.2依赖,Java一直保持着三层类加载器、双亲委派的类加载架构。虽然在一些情况下会有所改变,但依然没有改变其主体结构。

三层类加载器如下图所示:

  • 启动类加载器(Bootstrap Class Loader):这个类加载器负责加载存放在<JAVA_HOME>\\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar,tools.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载委派启动类加载器去处理,拿直接使用null替代即可。
  • 拓展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式存在。它负责加载<JAVA_HOME>\\ext目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器(Application Class Loader):这个类加载器是在类sun.misc.Launcher$AppClassLoader来实现。它负责加载用户类路径上的所有类库。

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

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,二十把这个加载器请求委派给父类加载器去完成,每一个层次的类加载器都是如痴,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈给自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。

四、Tomcat类加载器架构

首先来分析一下,一个功能健全的Web服务器,需要解决哪些问题:

  1. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求每个类库在服务器中只能有一份,服务器应该能够保证两个独立应用程序的类库可以相互独立使用。
  2. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现互相共享。这个需求与前面一点正好相反,但是也很常见。例如用户可能有10个使用spring组织的应用程序部署在同一台服务器上,如果把10分Spring分别放在各个应用程序的隔离目录中,将会是很大的资源浪费——这倒不是浪费磁盘空间的问题,二十至类库在使用时都要被加载到内存中,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  3. 服务器需要尽可能地保证自身的安全不受部署的web应用程序影响。目前,有许多主流的Java Web服务器自身也是使用Java语言来实现的。因此服务器本身也有类库依赖问题,一般来叔,基于安全问题考虑,服务器所使用的类库应与应用程序的类库互相独立。
  4. 支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能。我们知道JSP文件最重要被编译成Class文件才能放到虚拟机中执行,但是JSP文件由于纯文本存储的特性,被运行时修改的概率远大于第三方类库或程序自己的Class文件。而且ASP、php和JSP这些网页应用也把修改后无需重启作为一个很大的”优势“来看待,因此”主流“的Web服务器都会支持JSP生成类的热替换,当然也有”非主流”的,如运行在生产模式下的WebLogic服务器默认就不会处理JSP文件的变化。

在Tomcat目录结构中,可以设置3组目录(/common/*、/server/*和/Shared/*,但默认不一定是开放的,可能只有/lib/*目录存在)用户存放Java类库,另外还应该在Web应用程序自身的/WEB-INF/*目录,一共四组。把Java类库放置在这四组目录中,每一组都有独立的意义,分别是:

  • 放置在/common目录中。类可以被Tomcat和所有的Web应用程序共同使用。
  • 放置在/server目录中。类库可以被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在/shared目录中。类库可被所有的Web应用程序使用,对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序不可见。

拒绝白嫖从一键三连开始!

原创不易,未经允许,请勿转载。

博客主页:https://xiaojujiang.blog.csdn.net/

本文参考《深入理解Java虚拟机》整理。

以上是关于JVM类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

JVM笔记二双亲委派机制

图解JVM类加载机制和双亲委派模型

入门篇JVM类加载机制

JVM 类加载机制与加载过程

JVM类加载机制详解JVM类加载过程

JVM类加载机制详解JVM类加载过程