Java类的加载机制(JVM)

概述

Class文件由类装载器装载后,在JVM中将形成一个描述其类结构的元信息对象,通过该元信息对象可以得到类的结构信息:比如构造方法、成员变量和其他方法等,Java允许用户借由这个类相关的元信息对象间接调用该类对象的功能。虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

一,什么是类的加载

类的加载 指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区中的方法区内,然后在堆中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。下图简单描述了JVM内存模型。
在这里插入图片描述
类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向我们提供了访问方法区内的数据结构的接口。
在这里插入图片描述
java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。其中类装载器的作用就是类的加载

类加载器什么时候才会启动

类装载器并不需要等到某个类被“首次主动使用(使用new关键字或类名调用静态成员)”时再加载它,JVM规范允许类加载器在预判某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

类加载器从哪里加载.class文件

(1)本地磁盘
(2)在线加载网络上的.class文件(Applet)
(3)本地数据库
(4)压缩文件(ZAR,jar等)
(5)其他诸如JSP应用文件所生成的.class

二,类的加载过程

类从被加载到内存开始,会经历:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、和卸载(Unloading)这7个阶段,其中验证、准备和解析3个统称为连接(Linking)。
在这里插入图片描述

类的加载机制包括:加载、验证、准备、解析、初始化 五个阶段 。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定。因为java语言的动态绑定,在某些情况下可以在初始化阶段之后才开始解析。另外,这里的几个阶段只是按顺序开始,并不一定是按顺序进行或完成的,因为这些阶段通常都是互相交叉地混合进行的。通常是在一个阶段执行的过程中调用或激活另一个阶段。了解 卸载 阶段(GC)可以点: Java垃圾回收机制

1,加载(Loading)

加载即查找并加载类的二进制数据(查找和导入Class文件),是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

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

相对其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

2,验证(Verification)

验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段主要完成4个检验动作:

  1. 文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范(例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型)。并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。

  2. 元数据验证:主要是对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类(除了java.lang.Object之外),类中的字段方法是不是和父类冲突等等。

  3. 字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。

  4. 符号引用验证:验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,它对程序运行期没有影响,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3,准备(Preparation)

准备阶段是 为类变量分配内存设置类变量初始值 的阶段,这些内存都将在方法区中分配。对于该阶段有两点需要注意,也就是类变量初始值两个关键词:

  1. 当前阶段进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  2. 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

假设一个类变量的定义为:public static int value = 1; 那么变量value在准备阶段过后的初始值为0,而不是1,因为这时候尚未开始执行任何Java方法,而把value赋值为1的put static指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为1的动作将在初始化阶段才会执行。

注意: 在上面value是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为static final在编译器就将结果放入调用它的类的常量池中了。

4,解析(Resolution)

解析阶段是虚拟机将常量池内的 符号引用 替换为 直接引用 的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

  1. 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)
  2. 直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。

5,初始化(Initialization)

这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶段,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。

在初始化阶段,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量时指定初始值
  2. 使用静态代码块为类变量指定初始值
5.1,类的初始化时机

类什么时候才被初始化呢?只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  1. 创建类的实例,即使用new关键字创建一个对象。
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值。
  3. 调用类的静态方法。
  4. 反射(Class.forName(“含完整包路径的类名”))。
  5. 初始化一个类的子类(会首先初始化子类的父类)。
  6. JVM启动时标明的启动类(即文件名和类名相同的那个类)直接使用 java.exe命令来运行某个主类
5.2,类的初始化步骤

类的初始化步骤也称作JVM初始化步骤,主要分为三步:

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
  3. 假如类中有初始化语句(如static变量和static块),则系统依次执行这些初始化语句


以上就是类加载机制的整个过程,但是还有一个重要的概念,那就是类加载器

三,类加载器

JVM设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。JVM的类加载是通过ClassLoader及其子类来完成的。Java语言系统自带有三个类加载器:Bootstrap ClassLoader > Extention ClassLoader > Appclass Loader。类的层次关系和加载顺序可以由下图来描述:
在这里插入图片描述

  • Bootstrap ClassLoader 最顶层的启动类加载器,主要加载核心类库,由C++实现,不是ClassLoader的子类。负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。

  • Extension ClassLoader 扩展类加载器,负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。

  • Application ClassLoader 系统类加载器,负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。

  • User Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从自定义ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载,就视为已加载此类,其他加载器不会再次加载他,以保证此类在所有ClassLoader中只被加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

1,双亲委派原则

同一个Class文件如果使用不同的类加载器加载,那么就会加载出不同的类,为了保证一个类的唯一性,Java采用了双亲委派模型

双亲委派原则: 如果一个加载器要加载一个类的时候,它并不会立即自己去加载,而是先向上委托给上层加载器去加载,层层向上,直到最顶层的类加载器,当上层加载器无法完成加载任务时,会逐渐下沉尝试去加载。

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.*,不管是哪个加载器加载这个类,最终都是委托给顶层的引用加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个对象。双亲委派原则归纳一下就是:

  • 可以避免重复加载,父类已经加载了,子类就不需要再次加载
  • 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

2,自定义类加载器

自定义类加载器有两种方式:

  1. 遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
  2. 破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

具体实现步骤:

  1. 创建一个类继承ClassLoader抽象类
  2. 重写findClass()方法
  3. 在findClass()方法中调用defineClass()

四,类的加载方式

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的Java.lang.Class对象,用来封装类在方法区类的对象。虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类。

加载方式主要有以下几种:

  1. 从本地系统直接加载
  2. 通过网络下载.class文件
  3. 从zip,jar等归档文件中加载.class文件
  4. 从专有数据库中提取.class文件
  5. 将Java源文件动态编译为.class文件(服务器)
  6. 命令行启动应用时候由JVM初始化加载
  7. 通过Class.forName()方法动态加载
  8. 通过ClassLoader.loadClass()方法动态加载

类的加载的最终产品是位于堆区中的Class对象。 Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

五,结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  1. 执行了System.exit()方法
  2. 程序正常执行结束
  3. 程序在执行过程中遇到了异常或错误而异常终止
  4. 由于操作系统出现错误而导致Java虚拟机进程终止




参开文档:
愚公要移山:《Java类加载机制,你理解了吗?》
刘镓旗的专栏:《Java虚拟机四:类加载机制》
韦庆明的知乎:《Java类加载机制》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值