JVM 中篇:字节码与类的加载

JVM 中篇:字节码与类的加载,第1张

JVM 中篇:字节码与类的加载

文章目录
  • 字节码的跨平台性
  • 前端编译器
  • Class 文件结构
    • Class类的本质
    • Class文件格式
    • Class 文件结构
      • Magic Number(魔数)
      • 文件版本号
      • 常量池
        • constant_ pool count(常量池计数器)
        • constant_pool[] 常量池表
          • 字面量和符号引用
          • 全限定名
          • 简单名称
          • 描述符
          • 符号引用和直接引用的区别与关联
        • 小结
      • 访问标识
      • 类索引、父类索引、接口索引集合
      • 字段表集合
      • 方法表集合
      • 属性表集合
      • 解析Class 文件
  • 解析字节码方式
      • javap
  • 字节码指令集
    • 加载与存储指令
      • 局部变量压栈指令
        • 常量入栈指令
        • 出栈装入局部变量表指令
      • 算术指令
      • 类型转换指令
        • 宽化类型转换
        • 窄化类型转换
      • 对象的创建与访问指令
        • 创建指令
        • 字段访问指令
        • 数组 *** 作指令
        • 类型检查指令
      • 方法调用与返回指令
        • 方法调用指令
        • 方法返回指令
      • *** 作数栈管理指令
      • 控制转移指令
        • 条件跳转指令
        • 比较条件跳转指令
      • 多条件分支跳转指令
        • 无条件跳转指令
      • 抛出异常指令
      • 同步控制指令
        • 方法级的同步
        • 方法内指定指令序列的同步
  • 类的生命周期
    • 概述
    • 加载阶段(Loading)
      • 加载完成的 *** 作
      • 二进制流的获取方式
      • 类模型的位置与Class实例的位置
        • 类模型的位置
        • Class实例的位置
      • 数组类的加载
    • 链接化(linking)
      • 验证阶段( Verification)
      • 准备阶段(preparation)
      • 解析阶段(Resolution)
      • 初始化阶段(Initialization)
        • 类的初始化情况
          • 主动使用
          • 被动使用
    • 类的使用(Using)
    • 类的卸载(Unloading)
  • 再谈类的加载器
    • 概述
      • 类加载的加载方式分类
      • 命名空间
    • 类的加载器分类
      • 启动类加载器 Bootstrap ClassLoader
      • 扩展类加载器( ExtensIon ClassLoader)
      • 系统类加载器, AppClassloade
      • 用户自定义类加载器
      • Class.forName() 与 ClassLoader. loadClass()
      • 双亲委派机制
        • 破坏双亲委派机制
        • 热替换
      • 沙箱安全机制
      • 自定义类加载器
      • Java 9 类的加载的新特性

字节码的跨平台性
  1. Java语言:跨平台的语言( write once, run anywhere)
    当Java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译
    这个优势不再那么吸引人了。 Python、PHP、Ruby、Lisp等有强大的解释器
    跨平台似乎已经快成为一门语言必选的特性。
  2. Java虚拟机:跨语言的平台
    Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。无论使用何种语言
    进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的
    Class文件结构,就是Java虚拟机的基石、桥梁。
  3. 想要让一个Java程序正确地运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。
    • 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的宇节码文件。
    • javac是一种能够将Java码编译为字节码的前端编译器。
    • Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码
前端编译器

前端编译器ⅴs后端编译器
Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Jaνa源码编译为字节码,承担这个重要责任的就是配置在
path环境变量中的 Javac编译器。 Javac是一种能够将Java源码编译为字节码的前端编译器。

前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot的JIT编译器负责。

Javac之外,还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse中的ECJ(Eclipse Compiler for java)编译器。和 Javac的全量式编译不同,ECJ是一种增量式编译器

ECJ不仅是Eclipse的默认内置前端编译器,在 Tomcat中同样也是使用ECJ编译器来编译jsp文

默认情况下, IDEA使用Javac编译器。(还可以自己设置为 Aspectj编译器ajc)

Class 文件结构 Class类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流。

Class文件格式

Class的结构不像XML等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限
定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变

Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无号数和表。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个丰节和8个字节的无符号数
    无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值

  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info”结尾。表用于描述有层次
    关系的复合结构的数据,整个Class文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明

Class 文件结构

Class文件的结构并不是一成不变的随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。

  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

Magic Number(魔数)
  • 每个Class文件开头的4个字节的无符号整数称为魔数( Magic Number)

  • 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。

  • 魔数值固定为0xCAFEBABE 。不会改变。

  • 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误

    Error: A JNI error has occurred, please check your installation and try again Exception in thread"main" java. lang Class FormatError: Incompatible magic value 1885430635 in class file StringTest

  • 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动

文件版本号

紧接着魔数的4个字节存储的是Class文件的版本号同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本
号 minor_ version,而第7个和第8个字节就是编译的主版本号 maJor_version

它们共同构成了Class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式
版本号就确定为M.m

版本号和Java编译器的对应关系如下表

  • Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。
  • 不同版本的Java编译器编译的 Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的 Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出 Java.lang.UnsupportedClassVersionError异常(向下兼容)
  • 虚拟机JDK版本为1.k(k>=2)时,对应的Class文件格式版本号的范围为45.0-44+k.0(含两端)
常量池
  • 常量池是Class文件印内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用
  • 随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。
  • 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池项
  • 常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_ pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。
  • 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
constant_ pool count(常量池计数器)
  • 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。

  • 常量池容量计数值(u2类型):从1开始,表示常量池中有多少项常量。即 constant pool count=1表示常量池中有0个常量项

  • 通常我们写代码时都是从开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向
    常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目的含义,这种情况可用索引值θ来表示。

constant_pool[] 常量池表
  • 常量池主要存放两大类常量:字面量( Literal)和符号引用( Symbolic References)
  • 它包含了 Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同
    的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为 tag byte(标记字节、标签字节)

字面量和符号引用

全限定名

com/test/Demo这个就是类的全限定名,仅仅是把包名的替换成"/",为了使连续的多个全限定名之间不产生混淆,在
使用时最后一般会加入一个“;”表示全限定名结束

简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。

描述符

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型
(byte、char、doub1e、 float、int、long、 short、 boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示:


虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字宇段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

符号引用和直接引用的区别与关联
  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位至目标即可。符号引用与虛拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
  • 直接引用:直接引用可以是直接指向目标的措针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定己经存在于内存之中了。
小结
  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

  • 在常量池列表中, CONS TANTυtf8info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息

  • 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有 CONSTANT_f8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定 编译后,通过utf-8编码,就可以知道其长度

  • 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一

  • 常最池中为什么要包含这些内容?

    Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用, 再在类创建时或运行时解析、翻译到具体的内存地址之中。

访问标识

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为public类型;是否定义为 abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示

  • 类的访问权限通常为ACC开头的常量。
  • 每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是 public final的类,则该标记为
    ACC_PUBLIC ACC_FINAL。
  • 使用ACC_ SUPER可以让类更准确地定位到父类的方法 super.method(),现代编译器都会设置并且使用这个标记。
  • 带有 ACC_INTERFACE标志的Class文件表示的是接口而不是类,反之则表示的是类而不是接口。
    • 如果一个class文件被设置了ACC_INTERFACE标志,那么同时也得设置 ACC_ABSTRACT标志。同时它不能再设置 ACC FINAL、
      ACC_SUPER或 ACC_ ENUM标志
    • 如果没有设置 ACC_INTERFACE标志,那么这个class文件可以具有上表中除 ACC_ANNOTATION外的其他所有标志。当然, ACC_ FINAL和 ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
  • ACC_ SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虛拟机指令集的编译器
    都应当设置这个标志。对于Java SE8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本
    号是多少,Java虚拟机都认为每个class文件均设置了 ACC_SUPER标志
    • ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的 ACC_SUPER标志在由JDK1.2之前的
      编译器所生成的 access_Hags中是没有确定含义的,如果设置了该标志,那么 Oracle的Java虚拟机实现会将其忽略
  • ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
  • 注解类型必须设置 ACC_ANNOTATION标志。如果设置了 ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志
  • ACC_ENUM标志表明该类或其父类为枚举类型。
  • 表中没有使用的 access_flags标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为,Java虚拟机实现也应该忽略它们。
类索引、父类索引、接口索引集合

在访问标识会指定该类的类别、父类类别以及实现的接口:

这三项数据来确定这个类的继承关系

  • 类索引用于确定这个类的全限定名
  • 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了
    java.lang.object之外,所有的Java类都有父类,因此除了java.lang.object外,所有Java类的父类索引都不
    为0
  • 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。
  1. this_class(类索引)

    2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/ atguigu/java1/Demo。this_ class的值必须是对常量池表中某 项的一个有效索引值。常量池在这个索引处的成员必须为 CONSTANT_Class_info类型结构体,该结构体表示这个class文件所定义的类或接口

  2. super class(父类索引)

    2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/ Object类。同时,由于Java不支持多继承,所以其父类只有一个 ;superclass指向的父类不能是final。

  3. interfaces
    指向常量池索引集合,它提供了一个符号引用到所有己实现的接口
    由于一个类可以实现多个回,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的
    CONSTANT_ Class(当然这里就必须是接口,而不是类)。

  4. interfaces_count(接口计数器)
    interfaces_count项的值表示当前类或接口的直接超接口数量

  5. interfaces [] (接口索引集合)
    interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为 interfaces_ count。每个成员
    interfaces[i]必须为 CONSTANT_ Class_info结构,其中8<= i< interfaces_ count。在 interfaces[]中,各成员
    所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[]对应的是源代码中最左边的接口。

字段表集合

fields

  • 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、 private或 protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。
  • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
  1. fields count(字段计数器)
    fields count的值表示当前class文件 fields表的成员个数。使用两个字节来表示
    fields表中每个成员都是一个 field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明
    的变量,也不包括从父类或父接口继承的那些字段。
  2. fields [] (字段表)
    fields表中的每个成员都必须是一个 fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
    一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。

作用域(public、 private、 protected修饰符)
是实例变量还是类变量( static修饰符)
可变性(final)
并发可见性( volatile修饰符,是否强制从主内存读写)
可否序列化( transient修饰符)
字段数据类型(基本数据类型、对象、数组)
字段名称
字段表结构

字段表作为一个表,同样有他自己的结构:

  • 字段名索引:根据字段名索引的值,查询常量池中的指定索引项即可。

  • 描述符索引:

    描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据
    类型(byte,char, double, float,int,long, short, boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符
    象的全限定名来表示

  • 属性表集合 :一个字段还可能拥些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute count中,属性具体内容存放在 attributes数组中。

方法表集合

methods:指向常量池索引集,它完整描述了每个方法的签名。

  • 在字节码文件中,每一个 method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(pub1ic
    private或 protected),方法的返回值类型以及方法的参数信息等
  • 如果这个方法不是抽象的或者不是 native的,那么字节码中会体现出来
  • 一个方面, methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面, methods表有可
    会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类(接口)初始化方法()和实例初
    始化方法()。
  • 只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是
    以合法共存于同一个class文件中
  • 尽管Java语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法,但是和Java语法规范相反,字节码
    文件中却恰恰允许存放多个方法签名相同的方法,唯一的条件就是这些方法之间的返回值不能相同
  1. methods count(方法计数器)
    methods count的值表示当前class文件 methods表的成员个数。使用两个字节来表示
    methods表中每个成员都是一个 method info结构

  2. methods[] (方法表)
    methods表中的每个成员都必须是一个 method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个
    method_info结构的 access_flags项既没有设置 ACC_NATIVE标志也没有设置 ACC_ABSTRACT标志,那么该结构中也应
    包含实现这个方法所用的Java虚拟机指令。
    method_info结构可以表示类和接口中定义的所有方法,包活实例方法、类方法、实例初始化方法和类或接口初始化方法
    方法表的结构实际跟字段表是一样的,方法表结构如下

    方法表访问标志
    跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下

属性表集合

方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有
RetentionPolicy.CLASS或者 RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及
Java程序的调试,一般无须深入了解

此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息
属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器.都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

  1. attributes_count(属性计数器)

    attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个 attribute_info结构。

  2. attributes

    属性表的每个项的值必须是 attribute info结构。属性表的结构比较灵活,各种不同的属性只要满足以下结构即可。

    • 属性的通用格式: 只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义
    • 属性类型:属性表实际上可以有很多类型,上面看到的code属性只是其中一种,Java8里面定义了23种属性



      3.code属性列表
      code属性就是存放方法体里面的代码。但是,并非所有方法表都有code属性。像接口或者抽象方法,他们没有具体的方法体.因此也就不会有code属性了。

      可以看到:code属性表的前两项跟属性表是一致的,即code属性表遵循属性表的结构,后面那些则是他自定义的结构
解析Class 文件

解析字节码方式

通过反编译生成的字节码文件,我们可以深入的了解java代码的工作机制。但是,自己分析类文件结构太麻烦了!除使用第三方的jclasslib工具之外,oracle官方也提供了工具: Javap
Javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析岀当前类对应的code区(字节码指令)
局部变量表、异常表和代码行偏移量映射表、常量池等信息
通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信息

javap

Javap的用法格式:
Javap

其中,classes就是你要反编译的class文件。
在命令行中直接输入 Javap或 Jaap-help可以看到 Java的 options有如下选项

1、通过 Javap命令可以查看一个java类反汇编得到的Class文件版本号、常量池、防问标调、变量表、指令代码行号表等等信息。不显示类索引、父类索引、接口索引集合、()、()等结构
2、通过对前面例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的 *** 作

(1) java栈中:局部变量表、 *** 作数栈
(2) java堆。通过对象的地址引用去 *** 作。
(3) 常量池。
(4) 其他如帧数据区、方法区的剩余部分等情况。

字节码指令集

Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令

Java虚拟机的指令由一个字节长度的、代表着某种特定 *** 作含义的数字(称为 *** 作码, Opcode)以及跟随其后零至多个代表此 *** 作所需参数(称为 *** 作数, Operands)而构成。由于Java虚拟机采用面向 *** 作数栈而不是寄存器的结构,所以大多数的指令都不包含 *** 作数,只有一个 *** 作码。

由于限制了Java虚拟机 *** 作码的长度为一个字节(即0~255),这意味着指令集的 *** 作码总数不可能超过 256条

在Java虚拟机的指令集中,大多数的指令都包含了其 *** 作所对应的数据类型信息。例如,iload指令用于从局部变量表
中加载int型的数据到 *** 作数栈中,而fload指令加载的则是 float类型的数据。

对于大部分与数据类型相关的字节码指令,它们的 *** 作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

  • i代表对int类型的数据 *** 作
  • l代表long
  • s代表 short
  • b代表byte
  • c代表char
  • f代表float
  • d代表 double

也有一些指令的助记符中没有明确地指明 *** 作类型的字母,如 arraylength指令,它没有代表数据类型的特殊字符,但
*** 作数永远只能是一个数组类型的对象。
还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。

在处理boolean、byte、short和char类型的数组时,会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于 boolean、byte short和char类型数据的 *** 作, 实际上都是伸用相应的int类型作为运算类型

字节码指令集按用途大致分成9类

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • *** 作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值可能是对象的引用)被压入 *** 作数栈。
一个指令,也可以从 *** 作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等 *** 作

加载与存储指令
  1. 作用
    加载和存储指令用于将数据从栈帧的局部变量表和 *** 作数栈之间来回传递。
  2. 常用指令

    对于这若干组特殊指令来说,它们表面上没有 *** 作数,不需要进行取 *** 作数的动作,但 *** 作数都隐含在指令中。

比如:
iload_0:将局部变量表中索引为0位置上的数据压入 *** 作数栈 通常角标为0-3,如果超过则使用如:load_4;将局部变量表中索引为4位上的数据压作数栈中

局部变量压栈指令

局部变量压栈指令将给定的局部变量表中的数据压入 *** 作数栈。

这类指令大体可以分为:
xload_n(x为i、1、f、d、a,n为0到3)
xload(x为i、1、f、d、a )

说明:在这里,x的取值表示数据类型。
指令 xload_n表示将第n个局部变量压入 *** 作数栈,比如i1oad1、 fload0、 aload0等指令。其中 aload_n表示将
个对象引用压栈。
指令xload通过指定参数的形式,把局部变量压入 *** 作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个
比如指令iload、fload等。

常量入栈指令

常量入栈指令的功能是将常数压入 *** 作数栈,根据数据类型和入栈内容的不同,又可以分为 const系列、push系列和
ldc指令。

const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: iconst_i (i从-1到5)
lconst_l(l从0到1)、 fconst_f(f从0到2)、 dconst_d (d从0到1)、 aconst_null。
比如
iconst_m1将-1压入 *** 作数栈;
iconst_x(x为0到5)将x压入栈:
fconst_0、 fconst_1、 fconst_2分别将浮点数0、1、2压入栈;

从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数, f表示浮
点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含 *** 作的参数,会以下划线形式给出。

push系列:主要包括 bipush和 sipush。它们的区别在于接收数据类型的不同, bipush接收8位整数作为参数 sipush接收16位整数,它们都将参数压入栈。

ldc系列:如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该专数指向常量池中的int、float或者 String的索引,将指定的内容压入堆栈。

类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。
如果要压入的元素是long或者 double类型的,则使用ldc2_w指令,使用方式都是类似的

出栈装入局部变量表指令

出栈装入局部变量表指令用于将 *** 作数栈中栈顶元素d出后,装入局部变量表的指定位置,用于给局部变量赋值。
这类指令主要以 store的形式存在,比如 xstore(x为 i、l、f、d、a)、 xstore_n(x为i、l、f、d、a,n为 0至3)

  • 其中,指令 istore_n将从 *** 作数栈中d出一个整数,并把它赋值给局部变量索引n位置。
  • 指令 xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置。

说明:
类似像 store这样的命令需要带一个参数,用来指明将d出的元素放在局部变量表的第几个位置。但是,为了尽可能压缩指令大小,使用专门的 istore_1指令表示将d出的元素放置在局部变量表第1个位置。

类似的还有
istore_0、 istore_2、 istore_3,它们分别表示从 *** 作数栈顶d出一个元素,存放在局部变量表第0、2、3个位置。
由于局部变量表前几个位置总是非常常用,因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积
如果局部变量表很大,需要存储的槽位大于3,那么可以使用 istore指令,外加一个参数,用来表示需要存放的槽位位置

算术指令

算术指令用于对两 *** 作数栈上的值进行某种特定运算,并把结果重新压入 *** 作数栈。

大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。

在每一大类中,都有针对ava虚拟机具体数据类型的专用算术指令。但没有直接支持byte、 short、char和 boolean类
型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理 boolean、byte、 short和char类


数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢岀的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当岀现除数为0时会导致虚拟机抛出异常 ArithmeticException

运算模式

  • 向最接近数舍入模式:JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的
  • 向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果

NaN值使用
当一个 *** 作产生溢出时,将会使用有符号的无穷大表示,如果某个 *** 作结果没有明确的数学定义的话,将会使用NaN值
来表示。而且所有使用NaN值作为 *** 作数的算术 *** 作,结果都会返回NaN;

所有的算术指令包括:

  • 加法指令:iadd、ladd、fadd、dadd
  • 减法指令:isub、lsub、fsub、dsub
  • 乘法指令:imul、lmul、fmul、dmul
  • 除法指令:idiv、ldiv、fdiv、ddiv
  • 求余指令irem,lrem、frem,drem / /remainder:余数
  • 取反指令:ineg、lneg、fneg、dneg //negation:取反
  • 自增指令:iinc
  • 位运算指令
    • 位移指令:ishl、ishr、 iushr.、lshl、lshr、lushr
    • 按位或指令:ior、lor
    • 按位与指令:iand、land
    • 按位异或指令:ixor、lxor
  • 比较指令: dcmpg、dcmpl、 fcmpg、fcmpl、lcmp

比较指令的说明

比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈。
比较指令有: dcmpg,dcmpl、 fcmpg、fcmpl、lcmp。

对于 double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有 fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同

指令dcmpl和 dcmpg也是类似的,根据其命名可以推测其含义
指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。

举例:
指令 fcmpg和fcmpl都从栈中d出两个 *** 作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1>V2则压入1:若v1 两个指令的不同之处在于,如果遇到NaN值, fcmpg会压入1,而fcmpl会压入-1。

类型转换指令

①类型转换指令可以将两种不同的数值类型进行相互转换。
②这些转换 *** 作一般用于实现用户代码中的显式类型转换 *** 作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

宽化类型转换

转换规则:
Java虚拟机直接支持以下数值的宽化类型转换( widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:

  • 从int类型到long、float或者 double类型。对应的指令为:i2l、i2f、i2d
  • 从long类型到float、 double类型。对应的指令为:l2f、l2d
  • 从float类型到 double类型。对应的指令为:f2d

简化为:int–>long–>float–>double

精度损失

  • 宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到long,或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的。
  • 从int、long类型数值转换到float,或者long类型数值转换到 double时,将可能发生精度丢失—可能丢失掉
    几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。
  • 尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常
窄化类型转换

转换规则

Java虚拟机也直接支持以下窄化类型转换

  • 从int类型至byte、 short或者char类型。对应的指令有:i2b、i2s、i2c
  • 从long类型到int类型。对应的指令有:l2i
  • 从float类型到int或者long类型。对应的指令有:f2i、f2l
  • 从double类型到int、long或者float类型。对应的指令有:d2i、d2l、d2f

精度损失问题

窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。尽管数据类型窄化转换可能会发生上限溢出、下限溢岀和精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常

说明

  • 如果浮点值是NaN,那转换结果就是int或long类型的。
  • 如果浮点值不是无穷大的话,浮点值使用IEEE754的向零舍入模式取整,获得整数值v,如果v在目标类型T(int或long)的表示范围之内,那转换结果就是v。否则,将根据v的符号,转换为T所能表示的最大或者最小正数
  • 如果转换结果的绝对值太小而无法使用float来表示,将返回float类型的正负零。
  • 如果转换结果的绝对值太大而无法使用float来表示,将返回float类型的正负无穷大。
  • 对于 double类型的NaN值将按规定转换为float类型的NaN值。
对象的创建与访问指令

Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象 *** 作可进一步细分为创建指令、字段访问指令、 数组 *** 作指令、类型检验指令

创建指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与 *** 作使用了不同的字节码指令

  1. 创建类实例的指令:
    1. 创建类实例的指令
      它接收一个 *** 作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
  2. 创建数组的指令:
    1. 创建数组的指令: newarray、 anewarray、 multianewarray。
      1. newarray:创建基本类型数组
      2. anewarray:创建引用类型数组
      3. multianewarray:创建多维数组

上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。

字段访问指令

对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。

  • 访问类字段( static字段,或者称为类变量)的指令: getstatic、 putstatic
  • 访问类实例字段(非 static字段,或者称为实例变量)的指令: getfield、 putfield
数组 *** 作指令

数组 *** 作指令主要有xastore和xaload指令。具体为:

  • 把一个数组元素加载到 *** 作数栈的指令: baload、caload、 saload、 iaload、laload、faload
    daload、 aaload
  • 将一个 *** 作数栈的值存储到数组元素中的指令: bastore、 castore、 sastore、 iastore、lastore、 fastore、 dastore、 aastore
  • 取数组长度的指令: arraylength
    该指令d出栈顶的数组元素,获取数组的长度,将长度压入栈

说明

  • 指令xaload表示将数组的元素压栈,比如 saload、caload分别表示压入 short数组和char数组。指令xaload在执行时,要求 *** 作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会d出栈顶这两个元素,并将a[i]重新压入堆栈。
  • xastore则专门针对数组 *** 作,以 iastore为例,它用于给一个int数组的给定索引赋值。在 iastore执行前, *** 作数栈顶需要以此准备3个元素:值、索引、数组引用, iastore会d出这3个值,并将值赋给数组索引的位置
类型检查指令

检查类实例或数组类型的指令: instanceof、 checkcast

  • 指令 checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么 checkcast指令不会改变 *** 作数栈否则它会抛出ClassCastException异常
  • 指令 instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入 *** 作数栈
方法调用与返回指令 方法调用指令

方法调用指令: invokevirtual、 invokeinterface、 invokespecial、 invokestatic、 invokedynamic

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。
  • invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。
  • invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。
  • invokedynamic:调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法返回指令

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

  • 包括 ireturn(当返回值是 boolean、byte、char、 short和int类型时使用)、 lreturn、 freturn、 dreturn和 areturn
  • 另外还有一条 return指令供声为void的方法、实例初始化方法以及类和接口的类初始化方法使用
  • 如果当前返回的是 synchronized方法,那么还会执行一个隐含的 monitorexit指令,退出临界区。 最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者
*** 作数栈管理指令

同 *** 作一个普通数据结构中的堆栈那样,JVM提供的 *** 作数栈管理指令,可以用于直接 *** 作 *** 作数栈的指令

这类指令包括如下内容:

  • 将一个或两个元素从栈顶d出,并且直接废弃:pop,pop2
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup,dup2,dup_x1, dup2_1,dup_x2,dup2_x2
  • 将栈最顶端的两个Slot数值位置交换:swap。Java虚拟机没有提供交换两个64位数据类型(long、 double)数值的指令
  • 指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等

这些指令属于通用型,对栈的压入或者d出无需指明数据类型

说明

  • 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2 dup的系数代表要复制的 Slot个数
    • dup开头的指令用于复制1个Slot的数据。例如1个int或1个 reference类型数据
    • dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个 float类型数据
  • 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1,dup2_x1 ,dup_x2,dup2_x2.对于带x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
    • dup_x1插入位置:1+1=2,即栈顶2个Slot下面
    • dup_x2插入位置:1+2=3,即栈顶3个Slot下面
    • dup2_x1插入位置:2+1=3,即栈顶3个Slot下面
    • dup2_x2插入位置:2+2=4,即栈顶4个Slot下面
  • pop将栈顶的1个Slot数值出栈。例如1个 short类型数值
  • pop2:将栈顶的2个Slot数值出栈。例如1个 double类型数值,或者2个int类型数值
控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为
1)比较指令、2)条件跳转指令、3)比较条件跳转指令、4)多条件分支跳转指令、5)无条件跳转指令等。

条件跳转指令

条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转
条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull, ifnonnull。这些指令都接收两个字节的 *** 作数用于计算跳转的位置(16位符号整数作为当前位置的 offset)。

它们的统一含义为:d出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。

  • 对于 boolean 、byte、char、 short类型的条件分支比较 *** 作,都是使用int类型的比较指令完成
  • 对于long、 float、 double类型的条件分支比较 *** 作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到 *** 作数栈中,随后再执行int类型的条件分支比较 *** 作来完成整个分支跳转
比较条件跳转指令

比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为 一
这类指令有:if_ icmpeq、if_ icmpne、if_icmplt、 if_icmpgt、 if_icmple、if_ icmpge、if_ acmpeq和 if_acmpne。
其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数 *** 作(也包括 short和byte类型),以字符“a”开头的指令表示对象引用的比较。


这些指令都接收两个字节的 *** 作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较。
指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续行下一条指令

多条件分支跳转指令

多条件分支跳转指令是专为 switch-case语句设计的,主要有 tableswitch和lookupswitch

  • ableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的 *** 作数 index,可以立即定位到跳转偏移量位置,因此效率比较高。
  • lookupswitch内部存放着各个离散的case- offset对,每次执行都要搜索全部的case- offset对,找到匹配的case值,并根据对应的 offset计算跳转地址,因此效率较低。
    • lookupswitch处理的是离散的case,但是出于效率考虑,将case-offset对按照case值大小排序,给定 index时需要査找与 index相等的case,获得其 offset,如果找不到则跳转到 default。
无条件跳转指令

目前主要的无条件跳转指令为goto,指令goto接收两个字节的 *** 作数,共同组成一个带符号的整数,用于指定指令的偏移量指令执行的目的就是跳转到偏移量给定的位置处
如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_w,它和goto有相同的作用,但是它接收4个字节的 *** 作数,可以表示更大的地址范围

指令jsr、jsr_w、ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃

抛出异常指令

(1) athrow指令
在Java程序中显示(手动)抛出异常的 *** 作( throw语句)都是由 athrow指令来实现
除了使用 throw语句显示抛岀异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常

注意
正常情况下, *** 作数栈的压入d出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除 *** 作数栈上的所有内容,而后将异常实例压入调用者 *** 作数栈上。

(2) 处理异常与异常表

  1. 在Java虚拟机中,处理异常( catch语句)不是由字节码指令来实建的(早期使用jsr、ret指令),而是采用异常表来完成 的
  2. 异常表
    如果一个方法定义了一个try- catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者 finally块的信息。异常表保存了每个异常处理信息。比如:
  • 起始位置

  • 结束位置

  • 程序计数器记录的代码处理的偏移地址

  • 被捕获的异常类在常量池中的索引

当一个异常被抛岀时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并d出当前栈帧 并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧d出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。 不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在 return前,它直接跳到finally块来完成目标

同步控制指令

Java虚拟机支持两种同步结构:法级的同步和方法内部一段指令序列的同步,这两种同步都是使用 monitor来支持的。

方法级的同步

是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回 *** 作之中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法;

当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED访问标志是否设置。

  • 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。
  • 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

说明
对于同步方法而言,当虚拟机通过方法的防问标示与判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是有异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言, monitorenter和 monitorexit指令是隐式存在的,并未直接出现在字节码中。

方法内指定指令序列的同步

同步一段指令集序列:通常是由java中的 synchronized语句块来表示的。JVM的指令集有 monitorenter和 monitorexit 两条指令来支持 synchronized关键字的语义。

当一个线程进入同步代码块时,它使用 monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入
若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为,才会被允许进入同步块。

当线程退岀同步块时,需要使用 monitorexit声明退岀。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。

指令 monitorenter和 monitorexit在执行时,都需要在 *** 作数栈顶压入对象,之后 monitorenter和 monitorexit的锁定和释放都是针对这个对象的监视器进行的

类的生命周期 概述

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载

按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:

加载阶段(Loading)

所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型一一类模板对象。所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析岀的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。

反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。

加载完成的 *** 作

加载阶段,简言之,查找并加载类的二进制数据,生成Class的实例。
在加载类时,Java虚拟机必须完成以下3件事情:

  • 通过类的全名,获取类的二进制数据流。
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
二进制流的获取方式

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(要所读取的字节码符合JVM规范即可)

  • 虚拟机可能通过文件系统读入一个class后缀的文件(最常见)
  • 读入jar、zip等归档数据包,提取类文件。
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于HTTP之类的协议通过网络进行加载
  • 在运行时生成一段Class的二进制信息等

在获取到类的二进制信息后,Java虚拟机就会处理这些数据,并最终转为一个java.lang. Class的实例。如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。

类模型的位置与Class实例的位置 类模型的位置

加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后:元空间)。

Class实例的位置

类将,class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象

外部可以通过访问代表 Order类的class对象来获取 Order的类数据结构。

Class的构造方法是私有的,只有JVM能够创建。
java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过 Class类提供的接口,可以获得目标类所关联的. class文件中具体的数据结构:方法、字段等信息

数组类的加载

创建数组类的情况稍微有些特殊,因为数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的但数组的元素类型仍然需要依靠类加载器去创建。创建数组类(下述简称A)的过程:

  1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组A的元素类型;
  2. JVM使用指定的元素类型和数组维度来创建新的数组类。

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public

链接化(linking) 验证阶段( Verification)

当类加载到系统后,就开始链接 *** 作,验证是链接 *** 作的第一步
它的目的是保证加载的字节码是合法、合理并符合规范的

验证的步骤比较复杂,实际要验证的项目也很繁多,大体上Java虚拟机需要做以下检査

验证的内容则涵盖了类数据信息的格式验证、语义检查、宇节码验证,以及符号引用验证等。

  • 其中格式验会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中
  • 格式验证之外的验证 *** 作将会在方法区中进行
准备阶段(preparation)

准备阶段( Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值
Java虚拟机为各类型变量默认的初始值如表所示。

注意:

  • Java并不支持 boolean类型,对于 boolean类型,内部实现是int,由于int的默认值是0,故对应的, boolean的默认值就是 false
  • 这里不包含基本数据类型的字段用 static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值
  • 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
  • 对于基本数据类型:
    • final修饰的变量,在准备环节进行默认初始化赋值。
    • final修饰以后,在准备环节直接进行显示赋值
    • 如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显示赋值。
解析阶段(Resolution)

解析阶段( Resolution),简言之,将类、接口、字段和方法的符号引转为直接引用

符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在class类文件中通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println()方法被调用时,系统需要明确知道该方法的位置。

所谓的解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在那么可以肯定系统中存在该类、方法或者字段。但只存在能确定系统中一定存在该结构。

不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在 HotSpot JVM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析 *** 作往往会伴随着JⅥM在执行完初始化之后再执行。

初始化阶段(Initialization)

初始化阶段,简言之,为类的静态变量赋予正确的初始值。

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会初始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的Java程序代码。)
初始化阶段的重要工作是执行类的初始化方法:()方法。

  • 该方法仅能由Java编译器生成并由JVM调用,程序开发者无法自定义一个同名的方法,更无法直接在Java程序中调
    用该方法,虽然该方法也是由字节码指令所组成。
  • 它是由类静态成员的赋值语句以及 static语句块合并产生

说明

  1. 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的()总是在子类()之前被调用。
    也就是说,父类的 static块优先级高于子类
  2. Java编译器并不会为所有的类都产生()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含()方法
  • 对应非静态的字段,不管是否进行了显式赋值,都不会生成 ()
  • 一个类中声明类变量,静态的字段,没有显式的赋值 不会生成 ()
  • 对于声明为 static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成 ()方法

使用 static+ final修饰的字段的显式赋值的 *** 作,到底是在哪个阶段进行的赋值?

  • 情况1:在链接阶段的准备环节赋值
  • 情况2:在初始化阶段()中赋值

**对于基本数据类型如果如果使用 static final修饰 则在链接化的准备阶段显示赋值 (直接赋值常量,而非调用方法 ) **,没有final修饰的static则在初始化阶段显示赋值

对于String 类型来说如果字面量方式赋值使用static final 则在链接化的准备阶段赋值,其他引用类型无论是否加final该静态变量都是在初始化阶段()显示赋值;

小结

使用 static+ final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或 String类型的显式赋值,是在链接阶段的准备环节进行

对于()方法的调用 也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性。 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。

正是因为函数()带锁线程安全的,因此,如果在一个类的()方法中有耗时很长的 *** 作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。

如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行()方法了。那么,当需要使用这个类时虚拟机会直接返回给它己经准备好的信息。

类的初始化情况

Java程序对类的使用分为两种:主动使用(调用())和被动使用(不会调用())。

主动使用

Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件地装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的的使用是指主动使用,主动使用只有下列几种情况:(即:如果出现如下的情况,则会对类进行初始化 *** 作。而初始化 *** 作之前的加载、验证、准备已经完成)

  1. 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化

  2. 当调用类的静态方法时,即当使用了字节码 invokestatic指令

  3. 当使用类、接口的静态字段时(final修饰特殊考虑),比如,使用 getstatic或者 putstatic指令。(对应访问变量赋值变量 *** 作)

  4. 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class. forName(“com. java.Test”)

  5. 当初始化子类时如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

    当Java虚拟杋初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口

    在初始化一个类时,并不会先初始化它所实现的接回
    在初始化一个接口时,并不会先初始化它的父接口

    因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化。

  6. 如果 一个接口定义了 default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。

  7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

  8. 当初次调用 MethodHandle实例时,初始化该 MethodHandle指向的方法所在的类。(涉及解析 REF_getstatic、 REF_putstatic、 REF_invokestatic 方法句柄对应的类)

被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会引起类的初始化
也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化
    • 当通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就己经被显式赋值了
  4. 调用ClassLoader类的LoadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化
类的使用(Using)

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了。

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例

类的卸载(Unloading)

类、类的加载器、类的实例之间的引用关系
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载

调用Class对象的getClassloader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系

一个类的实例总是引用代表这个类的Class对象。在 Object:类中定义了getclass()方法,这个方法返回代表对象所属的Class对象的引用。此外,所有的Java类都有一个静态属性Class,它引用代表这个类的Class对象。

当类被加载、链接和初始化后,它的生命周期就开始了。当代表该类的Class对象不再被引用,即不可触及时, Class对象就会结束生命固期, 该类在方法区内的数据也会被卸载,从而结束类的生命周期。

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

loader1变量和obj变量间接应用代表 Fample类的Class对象,而objclass变量则直接引用它
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时 Sample对象结束生命周期,MyClassloader对象结束生命周期,代表Sample类的class对象也结束生命周期, Sample类在方法区内的二进制数据被卸载

  1. 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
  2. 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到 unreachable的可能性极小
  3. 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文坏境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)

综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在应该对虚拟机的类型系统中的特定功能

再谈类的加载器 概述

类加载器是JVM执行类加载机制的前提。

ClassLoader的作用:

ClassLoader是Java的核心组件,所有的Class都是由Classloader进行加载的, ClassLoader负责通过各种方式将
Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等 *** 作。因此, ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由 Execution Engine决定。

类加载的加载方式分类

类的加载分类:显式加载vs隐式加载

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。

  • 显式加载指的是在代码中通过调用Classloader加载class对象,如直接使用 Class. forName(name)或
    this.getclass().getclassloader().loadclass()加载class对象
  • 隐式加载则是不直接在代码中调用classloader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中

在日常开发以上两种方式一般会混合使用

命名空间

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则即使这两个类源自同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
  • 在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。
类的加载器分类

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器
  • 不同类加载器看似是继承( Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用
启动类加载器 Bootstrap ClassLoader
  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  • 它用来加载ava的核心库( JAVA_HOME/jre/lib/rt.jar或sun.boo.classpath路径下的内容)。用于提供JVM自身需要的类
  • 并不继承自java.lang.Classloader,没有父加载器
  • Bootstrap启动类加载器只加载包名为java、 javax、sun等开头的类
扩展类加载器( ExtensIon ClassLoader)
  • Java语言编写,由sun.misc. Launcher$ExtclassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
系统类加载器, AppClassloade
  • java语言编写,由sun.misc.Laurcher$AppClassLbader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量 classpath或系统属性java.class.path指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过classloader的 getsystemclassLoader()方法可以获取到该类加载器
用户自定义类加载器
  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定
    义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载
    ,加载源可以是本地的JAR包,也可以是网络上的远程资源
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如
    Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程
    序就能实现。
  • 同时,自定义加载器能够实现应用隔离,例如 Tomcat, Spring等中间件和组件框架都在内部实现了自定义的加载
    器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新
    增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于Classloader
Class.forName() 与 ClassLoader. loadClass()
  • Class.forName():是一个静态方法,最常用的是Class. forName( String className);根据传入的类的全限定名返回一个Class对象。该方法在将class文件加载到内存的同时,会执行类的初始化。如 Class.forName(“java HelloWorld”)
  • Classloader. loadclass():这是一个实例方法,需要一个Classloader对象来调用该方法。该方法将class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到个Classloader对象,所以可以根据需要指定使用哪个类加载器.如: ClassLoader cl= cl.loadClass("java Helloworld ")
双亲委派机制

类加载器用来把类加载到Java虚拟机中。JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证 Java平台的安全。

类加载器用来把类加载到Java虚拟机中。JDK1.2版本开始,类的加载过程采用双亲委派机制,这种机制能更好地保证 Java平台的安全。

1.定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

2.本质

规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

3 优势

  • 避兔类的重复加载,确保一类的全局唯一性

    Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子Classloader再加载一次

  • 保护程序安全,防止核心API被随意篡改

4 原理

  1. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
  2. 判断当前加载器的父加载器是否为字,如果不为空,则调用 parent.loadclass(name,false)接口进行加载
  3. 反之,如果当前加载器的父类加载器为空,则调用 findBootstrapClassOrNull(name)接口,让引导类加载器进行加载。
  4. 如果通过以上3条路径都没能成功加载,则调用 findclass(name)接口进行加载。该接口最终会调用java.lang.Classloader接口的 definedclass系列的 native接口加载目标Java类

5 双亲委托模式的弊端

检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个Classloader的职责非常明确,但是同时会带来一个问题,即顶层的 ClassLoader无法访问底层的Classloader所加载的类。

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会岀现问题。比如在系统类中提供了一个接口接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题

6 结论

由于Java虛拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已

比如在 Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法

破坏双亲委派机制

第一次破坏双亲委派机制:

由于双亲委派模型在JDK1.2之后才被引入,但是类加载器的概念和抽象类java.langClassloade则在Java的第 一个版本中就己经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadclass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.Classloader中添加一个新的 protected方法 findclass(),并引导用户编写的类加载逻辑时尽可能去重 写这个方法,而不是在loadclass()中编写代码。上节我们己经分析过loadclass()方法,双亲委派的具体逻辑就在这里面,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的 findclass()方法来完成加载,这既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的

第二次破坏双亲委派机制

线程上下文类加载器

这个类加载器可以通过java.lang.Thread类的 setContextclassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK6时,JDK提供了 Java.util.Serviceloader类,以meta-INF/ services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供

第三次破坏双亲委派机制

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:代码热替换( Hot Swap)、模块热部署( Hot Deployment)

热替换

替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务能中断,修改必须立即表现正在运行的系统之中。基本上大部分脚本语言都是天生支持热替换的,比如:PHP,只要替换了PHP源文件,这种改动就会立即生效,而无需重启web服务器。

但对Java来说,热替换并非天生就支持,如果一个类已经加载到系统中,通过修改类文件,并无法让来加载并重定义这个类。因此,在Java中实现这一功能的一个可行的方法就是灵活运用 ClassLoader

沙箱安全机制

沙箱安全机制

  • 保证程序安全
  • 保护Java原生的JDK代码

Java安全模型的核心就是Java沙箱( sandbox)。什么是沙箱?沙箱是一个限制程序运行的环境。

沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏

沙箱主要限制系统资源访问,那系统资源包括什么?cpυ、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略

自定义类加载器

为什么要自定义类加载器?

  • 隔离加载类
    在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确
    保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如: Tomcat这类Web应用服务器,内部自定义了好几
    种类加载器,用于隔离同一个Web应用服务器上的不同应用程序
  • 修改类加载的方式
    类的加载模型并非强制,除 Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源
    比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄漏
    Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码

常见的场景

  • 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是 Java EE和0SGI、JPMS等框架
  • 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己 *** 纵字节码,动态修改或者生成类型

实现

Java提供了抽象类java.lang. Classloader,所有用户自定义的类加载器都应该继承ClassLoader类在自定义ClassLoader的子类时候,我们常见的会有两种做法:

  • 方式一:重写loadclass()方法
  • 方式二:重写 findclass()方法:推荐

这两种方法本质上差不多,毕竞loadclass()也会调用 findclass(),但是从逻辑上讲我们最好不要直接修改loadclass()的内部逻辑。建议的做法是只在 findclass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用

  • lσadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写 loadclass()方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择
  • 当编写好自定义类加载器后,便可以在程序中调用1oadc1ass()方法来实现类加载 *** 作。
Java 9 类的加载的新特性

为了保证兼容性,JDK9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动

  • 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器( platform classloader)。可以通过 ClassLoader的新方法getplatformclassloader()来获取。
  • JDK9时基于模块化进行构建(原来的rt.jar和 tools.jar被拆分成数十个JMOD文件)
    其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留< JAVA_HOME> libext目录,此前使用这个
    录或者java.ext.dirs系统变量来扩展JDK功能的机制己经没有继续存在的价值了
  • 平台类加载器和应用程序类加载器都不再继承自java.net.URLClassloader
    现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader
  • 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是c++实现),但为了与之前代码兼容.在获取启动类加载器的场景中仍然会返回null而不会得到 Bootclassloader实例。
  • 类加载的委派关系也发生了变动。
    当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/4965515.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-14
下一篇 2022-11-13

发表评论

登录后才能评论

评论列表(0条)

保存