《深入理解Java虚拟机》读书笔记:第7章 虚拟机类加载机制

1、概述

我们知道,Java文件编译后会生成后缀名为“.class”的字节码文件,虚拟机把这些”.class“文件中类的描述信息加载到虚拟机并经过校验、转换解析和初始化最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

与C、C++等需要在编译时期进行连接的语言不同,在Java语言里,类的加载、连接和初始化过程都是在程序运行期间完成的。这种策略会牺牲一定的新能,但是带来了非常大的灵活性。我们既可以使用Java预定义的加载器,也可以使用自定义的加载器;可以在运行时加载网络上、本地磁盘其他目录下的Java字节码;甚至可以加载程序运行时动态生成的字节流等等。在Android里典型的应用就是插件化和热修复了。

2、类的加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Anitiatization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这 7 个阶段的发生顺序如图 7-1 所示。

在这里插入图片描述

其中加载、验证、准备、初始化和卸载5个阶段顺序是确定的,解析则有可能会在初始化之后开始。

Java虚拟机规范没有明确类的加载过程要在什么情况下开始,但是在以下5中情况下必须立即对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic字节码指令的时候,如果类没有初始化过,则初始化。

  2. 使用反射调用类的时候,如果类没有初始化过,则初始化。

  3. 初始化一个类的时候,如果它的父类没有初始化,先初始化父类。

  4. 虚拟机启动时,初始化包含main方法的那个类;

  5. 当使用JDK的动态语言支持时,如果一个 javalanginvoke.Methodtandle 实例最后的解析结果 REF_getStatic、REF_puStatic、REF_jnvokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

3、类加载的过程

3.1 加载

加载阶段的工作:

  1. 通过类的全限定名来获取定义此类的二进制字节流。

  2. 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构。

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

开发人员可以通过自定义加载器并重写loadClass方法控制字节流的获取方式,网络、磁盘、运算时生成等。动态代理就是基于运算时生成字节流的方式。

3.2 验证

验证的目的是微克确保字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机本身的安全。它包括以下几个方面的验证:

  1. 文件格式验证,验证字节流是否符合Class文件规范:

    • 是否以魔数0xCAFEBABE开头。
    • 主次版本号是否在本虚拟机处理范围内。
    • 常量池是否有不被支持的常量类型(检查常量tag标志)。
      。。。。
  2. 元数据验证,对字节码描述的信息进行语义分析以保证其符合Java语言规范的要求:

    • 这个类是否有父类,除了Object类外都应有父类。
    • 是否继承了不允许被继承的类。
    • 非抽象类是否实现类抽象类或者接口要求实现的方法。
      。。。。
  3. 字节码验证,通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的:

    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现在操作栈中放了一个int类型的数据却按long类型加载入本地变量表中。
    • 保证跳转指令不会跳转到方法体外的字节码指令上。
    • 保证方法体中的类型转换是有效的。
      。。。。
  4. 符号引用准备,这个校验发生在虚拟机将符号引用转化为直接引用的时候,它是对类自身意外(常量池中各种符号引用)的信息进行匹配性校验:

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

3.3 准备

准备阶段是正式为类变量分贝内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配。这时分配内存的仅包括类变量也就是static变量,不包括实例变量。变量的“初始值”通常是“零值”,除非字段存在ConstantValue属性。

public static staticVal = 123; // 初始化阶段后,staticVal是0,还没有进行赋值
public static final constVal = 123; // 初始化阶段后,constVal是123,因为有ConstantValue属性

3.4 解析

解析阶段是虚拟机将常量池内的符号引用转替换成直接引用的过程。

  • 符号引用;符号引用以一组符号来描述引用的目标,与虚拟机内存布局无关,引用的目标不一定已经加载到内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄,与虚拟机的内存布局有关。同一个符号引用在不同虚拟机上翻译出来的直接引用一般不同。

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

3.5 初始化

初始化阶段才真正开始执行类中定义的Java代码。在准备阶段,变量已经赋过一次初始值,但未必是程序员指定的初始值,而初始化阶段则是按照程序员的意愿去初始化类变量和其他资源。

初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是编译器自动收集类中的所有类变量赋值动作和静态代码块合并产生的,收集的顺序取决于源文件中出现的顺序。

<clinit>()方法和实例构造器方法<init>()不同,它不需要显式地调用父类<clinit>()方法,虚拟机会保证子类<clinit>()方法执行前父类的<clinit>()方法已经执行完毕。

如果接口或者类没有静态代码块也没有变量赋值操作,那么不会生成<clinit>()方法。

虚拟机会保证多线程情况下只有一个线程会初始化同一个类,而其他线程会阻塞等待。

4、类的加载器

4.1 类与类加载器

类加载器虽然只用于实现类的加载,但是对于任意一个类都需要加载它的加载器和这个类本身一同确定其在虚拟机里的唯一性。也就是说,对于不同加载器加载的两个类进行比较的化,两个类肯定是不“相等”的。

4.2 双亲委派模型

从Java虚拟机的角度来说,只有两种类加载器:一种是启动类加载器,是虚拟机自身的一部分;另一种就是所有的其他类加载器,独立于虚拟机并继承自抽象类java.lang.ClassLoader。

从Java程序员的角度来看,绝大多数Java程序都会用到以下3种类加载器

  1. 启动类加载器:负责加载放在 <JAVA_HOME>\lib 目录中的或者被 -Xbootclasspath 参数执行的路径中的并且是被虚拟机是别的类。

  2. 扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载 <JAVA_HOME>\lib 目录中的或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用此加载器。

  3. 应用程序加载器:由sun.misc.Launcher$AppClassLoader实现,负责加载用户路径上所指定的类库。如果应用程序没有自定义过加载器,一般情况下这就是默认的类加载器。

我们应用程序都是由这3种类加载器相互配合加载的,必要的情况下还可以加入自定义的类加载器。这些类加载器之间的关系一般如图所示:

在这里插入图片描述

这种类加载器之间的层次关系成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其他加载器都应当有父类加载器。

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载,而是把这个请求委派给父类加载器去完成,父类加载器没法完成的时候才尝试自己去加载。

4.3 破坏双亲委派

双亲委派不总是适用的,有时候为了实现某些功能比如热部署等,我们会破坏双亲委派模型。

5、参考书籍

《深入理解Java虚拟机》 周志明著

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值