引入

  • Java 程序的执行过程

    • 编辑源代码 xxx.java;
    • 编译 xxx.java 文件生成字节码文件 xxx.class;
    • JVM 中的类加载器加载字节码文件;
    • JVM 中的执行引擎找到入口方法 main(),执行其中的方法;

      一、JVM 概览

      1. JVM 物理结构

      JVM 系统学习 - 图4

      2. JVM 生命周期

  • 当启动一个 Java 程序时,一个虚拟机实例诞生;当程序关闭退出,这个虚拟机实例也就随之消亡;

    • JVM 实例的诞生
      • 当启动一个 Java 程序时,一个 JVM 实例就产生了,任何一个拥有 public static void main(String[] args) 函数的 class 都可以作为JVM实例运行的起点;
    • JVM 实例的运行
      • main() 作为该程序初始线程的起点,任何其他线程均由该线程启动,JVM 内部有两种线程:守护线程和非守护线程,main() 属于非守护线程,守护线程通常由 JVM 自己使用,java 程序也可以标明自己创建的线程是守护线程;
    • JVM实例的消亡
      • 当程序中的所有非守护线程都终止时,JVM 才退出;
      • 结束条件
        • 执行了 System.exit() 方法;
        • 程序正常执行结束;
        • 程序在执行过程中遇到了异常或错误而异常终止;
        • 由于操作系统出现错误而导致 Java 虚拟机进程终止;
  • 如果在同一台计算机上同时运行多个 Java 程序,将得到多个 Java 虚拟机实例,每个 Java 程序都运行于它自己的 Java 虚拟机实例中;
  • JVM 实例和 JVM 执行引擎实例
    • JVM实例对应了一个独立运行的java程序——进程级别,JVM执行引擎实例则对应了属于运行程序的线程——线程级别;
  • 主流虚拟机以 HotSpot 为主,其在 Oracle 官网下载的 jdk 里自带(不同Java虚拟机实现上的内存结构不同);

    二、class (字节码)文件

    1. 基础

  • Java 虚拟机实现语言无关性的基石就是Class文件;

JVM 系统学习 - 图5

  • Java 字节码类文件(.class)是 Java 编译器编译 Java 源文件(.java)产生的“目标文件”,是一种 8 位字节的二进制流文件;
  • 任何一个 class 文件都对应着唯一一个类或接口的定义信息,但其并不一定以磁盘文件的形式存在;

    2. JVM 规范中的描述信息规则

  • 背景:为压缩字节码文件的体积;

  • 每个变量/字段都有描述信息,其主要作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值;
    • 基本数据类型和代表无返回值的 void 类型都用一个大写字符来表示;
    • 对象类型使用字符 L 加对象的全限定名称来表示,如 Ljava/lang/String;

JVM 系统学习 - 图6

  • 数组类型:每一个维度使用一个前置的 [ 来表示
    • 如 int[ ] 被记录为 [I,String[ ][ ] 被记录为 [[Ljava/lang/String;
  • 描述方法的顺序

    • 按照先参数列表后返回值的顺序,参数列表按照参数的严格顺序放在一组( )之内,如方法 String getRealnameByIdAndNickname(int id, String name):(I, Ljava/lang/String) Ljava/lang/String;

      3. class 文件结构

      3.1 背景

      3.1.1 字节码的两种数据类型

  • 字节数据直接量:基本数据类型,共细分为 u1、u2、u4、u8 四种,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据;

  • 表(数组):表是由多个基本数据或其他表按照既定顺序组成的大的数据集合,表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的;

    3.1.2 整体结构概览

  • .class 文件本质就是如下一张表:

    JVM 系统学习 - 图7JVM 系统学习 - 图8

    3.1.3 解析工具 javap

    • javap -v -p -s -sysinfo -constants D:\IDEA\JavaCode\testJVM\target\classes\com\cyt\jvm\bytecode\MyTest1.class ```java Classfile /D:/IDEA/JavaCode/testJVM/target/classes/com/cyt/jvm/bytecode/MyTest1.class Last modified 2020-1-30; size 605 bytes MD5 checksum 21df4d82be00cb6b2e84199acd1926a9 Compiled from “MyTest1.java” public class com.cyt.jvm.bytecode.MyTest1 minor version: 0 major version: 50 flags: ACC_PUBLIC, ACC_SUPER Constant pool:

      1 = Methodref #4.#24 // java/lang/Object.”“:()V

      2 = Fieldref #3.#25 // com/cyt/jvm/bytecode/MyTest1.a:I

      3 = Class #26 // com/cyt/jvm/bytecode/MyTest1

      4 = Class #27 // java/lang/Object

      5 = Utf8 a

      6 = Utf8 I

      7 = Utf8

      8 = Utf8 ()V

      9 = Utf8 Code

      10 = Utf8 LineNumberTable

      11 = Utf8 LocalVariableTable

      12 = Utf8 this

      13 = Utf8 Lcom/cyt/jvm/bytecode/MyTest1;

      14 = Utf8 getA

      15 = Utf8 ()I

      16 = Utf8 setA

      17 = Utf8 (I)V

      18 = Utf8 main

      19 = Utf8 ([Ljava/lang/String;)V

      20 = Utf8 args

      21 = Utf8 [Ljava/lang/String;

      22 = Utf8 SourceFile

      23 = Utf8 MyTest1.java

      24 = NameAndType #7:#8 // ““:()V

      25 = NameAndType #5:#6 // a:I

      26 = Utf8 com/cyt/jvm/bytecode/MyTest1

      27 = Utf8 java/lang/Object

      { private int a; descriptor: I flags: ACC_PRIVATE

    public com.cyt.jvm.bytecode.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code:

    1. stack=2, locals=1, args_size=1
    2. 0: aload_0
    3. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4. 4: aload_0
    5. 5: iconst_1
    6. 6: putfield #2 // Field a:I
    7. 9: return
    8. LineNumberTable:
    9. line 3: 0
    10. line 4: 4
    11. LocalVariableTable:
    12. Start Length Slot Name Signature
    13. 0 10 0 this Lcom/cyt/jvm/bytecode/MyTest1;

    public int getA(); descriptor: ()I flags: ACC_PUBLIC Code:

    stack=1, locals=1, args_size=1
       0: aload_0
       1: getfield      #2                  // Field a:I
       4: ireturn
    LineNumberTable:
      line 7: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lcom/cyt/jvm/bytecode/MyTest1;
    

    public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code:

    stack=2, locals=2, args_size=2
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field a:I
       5: return
    LineNumberTable:
      line 11: 0
      line 12: 5
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       6     0  this   Lcom/cyt/jvm/bytecode/MyTest1;
          0       6     1     a   I
    

    public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code:

    stack=0, locals=1, args_size=1
       0: return
    LineNumberTable:
      line 16: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0  args   [Ljava/lang/String;
    

    } SourceFile: “MyTest1.java” ```

    3.2 具体结构解析

  • 魔数:所有的 .class 字节码文件的前 4 个字节都是魔数,0xCAFEBABE (16进制),值固定,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件;

  • 版本信息 :魔数之后的 4 个字节,前两个字节表示 minor version(次版本号),后两个字节表示 major version(主版本号),eg: 00 00 00 34,换算成十进制,表示次版本号为 0,主版本号为 52;

JVM 系统学习 - 图9

  • 常量池(constant pool):紧接主版本号之后,一个 Java 类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作 class 文件的资源仓库,如 类中定义的方法与变量信息都存储在常量池中;
    • 主要存储两类常量:字面量与符号引用;
      • 字面量:如文本字符串,Java 中声明为 final 的常量值等;
      • 符号引用:如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等;
    • 总体结构:主要由常量池数量与常量池数组共同构成
      • 常量池数量紧跟在主版本号之后,占据 2 个字节;
      • 常量池数组紧跟在常量池数量之后,与一般的数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度当然也就不同;
        • 每一种元素的第一个数据都是一个 u1 类型,该字节是个标志位,占据 1 个字节,JVM 在解析常量池时,会根据这个 u1 类型来获取元素的具体类型;
        • 注意:常量池数组中元素的个数 = 常量池数 - 1(其中0暂不使用),目的是满足某些系统常量池索引值的数据在特定情况下需要表达【不引用任何一个常量池】的含义,根本原因在于,索引为 0 也是一个常量(保留常量),只不过它不位于常量表中,对应 null 值,常量池的索引从1而非0开始

JVM 系统学习 - 图10JVM 系统学习 - 图11

  • access_flags 类访问标志:是一系列访问标志的组合,因为有16位所以共有16个标志可以使用,但目前就定义了8个,剩下的估计是给 jdk9 和 10……预留;

       ![](https://cdn.nlark.com/yuque/0/2020/png/611598/1580699278056-a96c457f-2858-47f9-86de-fafe635e7234.png#align=left&display=inline&height=249&originHeight=972&originWidth=1372&size=0&status=done&style=none&width=351)![](https://cdn.nlark.com/yuque/0/2020/jpeg/611598/1580699171071-c0b4fcfb-bb25-4ad6-93e7-c735f6022035.jpeg#align=left&display=inline&height=247&originHeight=952&originWidth=1362&size=0&status=done&style=none&width=353)
    
  • this_cass :保存了当前类的全局限定名在常量池的索引地址;

  • s**uper_class**:若有继承,保存父类全限名在常量池的索引地址;
  • interfaces
    • 被实现的接口将按照 implements 语句后的顺序从左至右排列在接口索引集合中;
    • 接口索引集合分为两部分
      • 第一部分表示接口计数器(interfaces_count),是一个 u2 类型的数据;
      • 第二部分是紧跟在接口计数器之后的接口索引表(接口全限定名的常量池地址),用于表示接口信息;
    • 注:若一个类实现的接口为 0,则接口计数器的值为 0,接口索引表不占用任何字节;
  • fields

    • 保存了当前类的成员列表,包含两部分的内容:fields_count fields[fields_count],与常量池一样,开头两个字节用于表示当前 class 文件中的字段的个数,紧接着是具体的字段数组;
    • fields_count 是类变量和实例变量的字段的数量总和;
    • fileds[ ] 字段表:包含字段详细信息的列表,结构如下;

      Field_Info {
         u2 access_flag; // 字段访问修饰符
         u2 name_index;
         u2 descriptor_index;
         u2 attribute_count;
         attribute_info attributes[attribute_count];
      }
      
      • 字段的访问修饰符:与类的表示方式相似,但是具体的内容不一样; | 标志名称 | 标志值 | 含义 | | :—-: | :—-: | :—-: | | ACC_PUBLIC | 0x0001 | 字段是否为public | | ACC_PRIVATE | 0x0002 | 字段是否为private | | ACC_PROTECTED | 0x0004 | 字段是否为protected | | ACC_STATIC | 0x0008 | 字段是否为static | | ACC_FINAL | 0x0010 | 字段是否为final | | ACC_VOLATILE | 0x0040 | 字段是否为volatile | | ACC_TRANSIENT | 0x0080 | 字段是否为transient | | ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 | | ACC_ENUM | 0x4000 | 字段是否是enum类型 |
  • methods

    • 保存了当前类的方法列表,包含两部分的内容:methods_count methods[methods_count];
    • 与 field_info 基本结构大致相同,仅在 access_flags 和 attribute_info 的可选项中有所区别;
    • methods_count :该类或者接口显示定义的方法的数量;
    • method[]:包含方法信息的一个详细列表,结构如下:

      type MemberInfo struct {
      cp              ConstantPool
      accessFlags     uint16
      nameIndex       uint16
      descriptorIndex uint16
      attributes      []AttributeInfo
      }
      
      • 方法的访问修饰符
        | 标志名称 | 标志值 | 含义 | | :—-: | :—-: | :—-: | | ACC_PUBLIC | 0x0001 | 方法是否为public | | ACC_PRIVATE | 0x0002 | 方法是否为private | | ACC_PROTECTED | 0x0004 | 方法是否为protected | | ACC_STATIC | 0x0008 | 方法是否为static | | ACC_FINAL | 0x0010 | 方法是否为final | | ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized | | ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法 | | ACC_VARARGS | 0x0080 | 方法是否接受不定参数 | | ACC_NAVIVE | 0x0100 | 方法是否为native | | ACC_ABSTRACT | 0x0400 | 方法是否为abstract | | ACC_STRICTFP | 0x0800 | 方法是否为strictfp | | ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
  • attributes: 描述该类或接口所定义的一些属性信息,包含 attributes_count 和 attributes[count] 两部分;

    • attributes_count 指的是 attributes 列表中包含的 attribute_info 的数量;
    • 与其它数据项目要求的顺序、长度、内容不同,属性表集合的要求稍微宽松,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,编译器还可以向属性表中写入自已定义的属性信息,Java虚拟机能忽略掉它不认识的属性;
    • 属性可以出现在 class 文件的很多地方,而不只是出现在 attributes 列表里
      • 如果是 attributes 表里的属性,那么它就是对整个 class 文件所对应的类或者接口的描述;
      • 如果出现在 fileds 的某一项里,那么它就是对该字段额外信息的描述;
      • 如果出现在 methods 的某一项里,那么它就是对该方法额外信息的描述;
    • 属性类型

JVM 系统学习 - 图12
JVM 系统学习 - 图13

  - 对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf_info 类型的常量来表示;
  - 属性值的结构完全自定义,只需要说明属性所占用的长度即可;

JVM 系统学习 - 图14

  • code 属性 详见

    • Java 方法内代码经过 Javac 编译处理后,最终变成字节码指令存储在 code 属性内;
    • code 真正用于存放字节码指令,java 最多可以表达256条指令,目前 java 只有200条左右的指令;
      • JVM 支持的指令大致上可以分成 3 种:没有操作数的、一个操作数的和两个操作数的,因为 JVM 用一个字节来表示指令,所以指令的最多只有 256 个;
      • 指令通用形式

    JVM 系统学习 - 图15

    - [字节码指令大全](https://yq.aliyun.com/articles/7242?spm=5176.100239.blogcont7243.8.3d63c7fjRFN0C)
    
    • 两个子属性 LineNumberTable 和 LocalVariableTable 属于调试信息,非运行时必须;
    • 并非所有方法表都必须存在这个属性,如接口或抽象类的方法,结构如下:

image.png

Code_attribute {
     u2 attribute_name_index; //指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为Code
     u4 attribute_length;
     u2 max_stack;//操作数栈深度的最大值,在方法执行的任意时刻,操作数栈不会大于这个深度
     u2 max_locals;//局部变量所需要的存储空间,单位是slot,slot是虚拟机为局部变量分配内存所使用的最小单位
     u4 code_length;
     u1 code[code_length];//code_length和code用于存储Java源程序编译后生成的字节码指令
     u2 exception_table_length;//异常表【try catch】
     {    //start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
          u2 start_pc;
          u2 end_pc;
          u2 handler_pc;//处理异常的代码的开始处
          u2 catch_type;//会被处理的异常类型,它指向常量池里的一个异常类,当catch_type为0时,表示处理所有的异常,这个可以用来实现finally的功能
     } exception_table[exception_table_length];
     u2 attributes_count;
     attribute_info attributes[attributes_count];
}
  - **子属性 LineNumberTable**:表示 code 数组中的字节码和 java 代码行数之间的关系,可用于在调试的时候定位代码执行行数,非必须属性,如果不生成它,那么产生异常后,堆栈中将不会显示出错的行号,并且在调试时也无法在源码中设置断点,结构如下;
LineNumberTable_attribute {
          u2 attribute_name_index;
          u4 attribute_length;
          u2 line_number_table_length;
          { u2 start_pc;
            u2 line_number;} 
    line_number_table[line_number_table_length];
}
  - **子属性 LocalVariableTable**:描述栈桢中局部变量表中的变量与 Java 源码中定义的变量之间的关系,结构如下:

JVM 系统学习 - 图17JVM 系统学习 - 图18

  • Exceptions 属性:列举出方法中可能抛出的受查异常,也就是 throws 关键字后列表的异常,结构如下:

JVM 系统学习 - 图19

  • SourceClass 属性:用于记录 Class 文件的源码文件名称,结构如下:

JVM 系统学习 - 图20

3.3 静态结构属性关系图

JVM 系统学习 - 图21

三、类加载器机制

  • 在 Java 代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的;
  • 在运行期,一个 Java 类是由该类的完全限定名(二进制名)和用于加载该类的定义类加载器共同决定;
  • 在 JVM 中,从被加载到虚拟机内存中开始,到卸载出内存为止,其整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载 7 个阶段;

    1. 加载

  • 将类的 .class 文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个 java.lang.class 对象(规范并未说明 Class 对象位于哪里,HotSpot 虚拟机将其放在方法区中)用来封装类在方法区内的数据结构;

    • 类的加载的最终产品是位于内存中的 class 对象;
    • class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口;
  • 加载 .class 文件的方式
    • 从本地系统中直接加载;
    • 通过网络下载 .class 文件;
    • 从 zip、jar 等归档文件中加载 .class 文件;
    • 从专有数据库中提取 .class 文件;
    • 将 Java 源文件动态编译为 .class 文件;
  • 类加载器
    • 两种类型的加载器,Java 虚拟机自带的加载器 和 用户自定义类加载器,在此基础上存在定义类加载器(能成功加载 Test 类) 和 初始类加载器(能成功返回 Class 对象引用)两种概念;
      • Java 虚拟机自带的加载器
        • 根(启动)类加载器(Bootstrap)
          • 内建于 JVM 中的启动类加载器会加载 java.lang.ClassLoader 及其它的 Java 平台类,当 JVM 启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(BootStrap);
          • 启动类加载器并不是 Java 类,而其它的加载器则都是 Java 类
          • 启动类加载器是特定于平台的机器指令,负责开启整个加载过程;
          • 启动类加载器也会负责加载供 jre 正常运行所需要的基本操作,这包括 java.util 与 java.lang 包中的类等;
          • 扩展类加载器和系统类加载器也是由启动类加载器所加载的
        • 扩展类加载器(Extension)
        • 系统(应用)类加载器(System、App) ```java public static void testArrayClassLoader(){ // 数组的类加载器在 jvm 运行期间由其子元素的类加载器动态确定 String[] strings = new String[2]; System.out.println(strings.getClass().getClassLoader());//null 根类加载器 System.out.println(“———————————-“); testClassLoader[] testClassLoaders = new testClassLoader[2]; System.out.println(testClassLoaders.getClass().getClassLoader());// sun.misc.Launcher$AppClassLoader@18b4aac2 System.out.println(“———————————-“); int[] ints = new int[2]; System.out.println(ints.getClass().getClassLoader());//null 原生类型不存在类加载器 }

System.out.println(System.getProperty(“sun.boot.class.path”)); 输出: C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\classes

System.out.println(System.getProperty(“java.ext.dirs”)); 输出: C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

System.out.println(System.getProperty(“java.class.path”)); 输出: C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\IDEA\JavaCode\testJVM\target\classes;D:\IDEA\IntelliJ IDEA Community Edition 2019.3\lib\idea_rt.jar


      - **用户自定义的类加载器**:**java.lang.ClassLoader**** 的子类** ,用户可以定制类的加载方式;
```java
package com.cyt.jvm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class Test1 extends ClassLoader{
    private String classLoaderName;
    private final String fileExtension = ".class";
    private String path;

    public Test1(String classLoaderName){
        super(); //将系统类加载器当做该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }
    Test1(ClassLoader parent, String classLoaderName){
        super(parent); // 显示指定该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }
    public void setPath(String path) {
        this.path = path;
    }
    protected Class<?> findClass(String className) throws ClassNotFoundException{
        System.out.println("findClass invoked : " + className);
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] data = this.loadClassData(className);
        return this.defineClass(className, data, 0, data.length);
    }
    private byte[] loadClassData(String name){
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        name = name.replace(".","\\");
        try {
            is = new FileInputStream(new File(this.path + name + this.fileExtension));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
                baos.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return data;
    }
    public String toString(){
        return "[" + this.classLoaderName + "]";
    }

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        Test1 loader = new Test1("my_loader1");
//        loader.setPath("D:\\IDEA\\JavaCode\\testJVM\\target\\classes\\");
        loader.setPath("E:\\");
        Class<?> class1 = loader.loadClass("com.cyt.jvm.Test2");
        System.out.println("class1 : " + class1.hashCode());
        Object obj = class1.newInstance();
        System.out.println(obj);

        Test1 loader2 = new Test1("my_loader2");
        // 若指定其父加载器,则真正加载类的是 loader,两个 class 的 hashCode 一致【双亲委托机制】
//        Test1 loader2 = new Test1(loader, "my_loader2");
        loader2.setPath("E:\\");
        Class<?> class2 = loader2.loadClass("com.cyt.jvm.Test2");
        System.out.println("class2 : " + class2.hashCode());
        Object obj2 = class2.newInstance();
        System.out.println(obj2);


        /** 输出
         * findClass invoked : com.cyt.jvm.Test2
         * class loader name : my_loader1
         * class1 : 21685669
         * com.cyt.jvm.Test2@7f31245a
         * findClass invoked : com.cyt.jvm.Test2
         * class loader name : my_loader2
         * class2 : 1173230247
         * com.cyt.jvm.Test2@330bedb4
         */
        // 说明,如果 Test2 在当前文件路径存在,则调用的是 系统加载器。两个 class 的 hashCode 一致,这是在同一命名空间下,只会同一个类只会加载一次
        // 但若是自定义加载器,则两个 class 的 hashcode 不同,这是因为在不同命名空间下,可能存在不同的两个类
    }
}
  • 获得 ClassLoader 的途径
    • class.getClassLoader();
    • Thread.currentThread().getContextClassLoader();
    • ClassLoader.getSystemClassLoader();
    • DriverManager.getCallerClassLoader();
  • 类加载器并不需要等到某个类被“首次主动使用”时再加载它;
  • JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到 .class 文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报错;
  • 父亲委托机制
    • 类加载器用来把类加载到 JVM 中,类的加载过程采用父亲委托机制来保护 Java 平台的安全 详见;
    • 父亲委托机制:各个加载器按照父子关系形成逻辑上的树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器,类加载器之间的父子关系并不是Java类中的继承关系,同一类型的不同类加载器之间也可是父子关系
    • 类加载器的双亲委托模型的好处
      • 可以确保 Java 核心库的类型安全:所以的 Java 应用都至少会引用 Java.lang.Object 类,也就是说在运行期这个类会被加载到虚拟机中,若这个加载过程由 Java 应用自己的类加载器所完成,那么就很可能会在 JVM 中存在多个版本的 Java.lang.Object 类,而且这些类之间不兼容、不可见(命名空间作用);
      • 可以确保 Java 核心类库所提供的类不会被自定义的类所替代;
      • 不同类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们既可,不同类加载器所加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个又一个相互隔离的 Java 类空间,这类技术在很多框架中都得到了实际应用;
  • 命名空间

    • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成;
    • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;
    • 在不同命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类;
    • 同一个命名空间内的类是相互可见的;
    • 子加载器所加载的类能够访问父加载器所加载的类,而父加载器所加载类无法访问子加载器所加载类;
    • 若两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见;
      public class MyPerson {
      private MyPerson myPerson;
      public void setMyPerson(Object obj){
      this.myPerson = (MyPerson)obj;
      }
      }
      public class Test4_1 {
      public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
      Test1 loader1 = new Test1("loader1");
      Test1 loader2 = new Test1("loader2");
      loader1.setPath("E:\\");//定义在非当前目录下,会调用自定义类加载器
      loader2.setPath("E:\\");
      Class<?> class1 = loader1.loadClass("com.cyt.jvm.MyPerson");
      Class<?> class2 = loader2.loadClass("com.cyt.jvm.MyPerson");
      System.out.println(class1 == class2);//false,命名空间不同
      Object obj1 = class1. newInstance();
      Object obj2 = class2. newInstance();
      Method method = class1.getMethod("setMyPerson",Object.class);
      method.invoke(obj1,obj2);
      // 两个没有父子关系的加载器,其所加载的类相互不可见
      /** 输出
       * findClass invoked : com.cyt.jvm.MyPerson
       * class loader name : loader1
       * findClass invoked : com.cyt.jvm.MyPerson
       * class loader name : loader2
       * false
       * Exception in thread "main" java.lang.reflect.InvocationTargetException
       *     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
       *     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
       *     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
       *     at java.lang.reflect.Method.invoke(Method.java:498)
       *     at com.cyt.jvm.ClassLoader.Test4_1.main(Test4_1.java:23)
       * Caused by: java.lang.ClassCastException: com.cyt.jvm.MyPerson cannot be cast to com.cyt.jvm.MyPerson
       *     at com.cyt.jvm.MyPerson.setMyPerson(MyPerson.java:7)
       *     ... 5 more
       */}}
      
  • 当前类加载器(Current ClassLoader)

    • 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其它类(所依赖的类);
  • 线程上下文类加载器(Context ClassLoader)

    public static void getResource() throws IOException {
       ClassLoader loader = Thread.currentThread().getContextClassLoader();
       String resourceName = "com/cyt/jvm/C.class";
       Enumeration<URL> urls = loader.getResources(resourceName);
       while (urls.hasMoreElements()) {
           URL url = urls.nextElement();
           System.out.println(url);
       }
       //output: file:/D:/IDEA/JavaCode/testJVM/target/classes/com/cyt/jvm/C.class 【磁盘路径】
    }
    
    • 从 jdk 1.2 开始引入,类 Thread 中的 getContextClassLoader() 与 setContextClassLoader(ClassLoader cl) 分别用来获取和设置上下文类加载器;
    • 若没有通过 setContextClassLoader(ClassLoader cl) 进行设置的话,线程将继承其父线程的上下文类加载器,Java 应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源;
    • 重要性

      • SPI(Service Provider Interface) ,如数据库驱动 com.mysql.jdbc.Driver
        • 父 ClassLoader 可以使用当前线程 Thread.currentThread().getContextClassLoader() 所指定的 ClassLoader 加载的类,这就改变了父 ClassLoader 不能使用子 ClassLoader 或者其他没有直接父子关系的 ClassLoader 加载的类的情况,即改变了双亲委托机制;
        • 在双亲委托模型下,类加载器是由下至上的,即下属的类加载器会委托上属进行加载,但对于 SPI 来说,有些接口是 Java 核心库所提供的,而 Java 核心库是由启动类加载器来加载的,而这些接口的实现却来自不同的 jar包(厂商提供),Java 的启动类加载器是不会加载其它来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求,而通过当前线程设置上下文类加载器们就可以由设置的上下文类加载器来实现对于接口实现类的加载; ```java public class Test7 { public static void main(String[] args) { /** 1. 若对上下文类加载器作如下设置,则输出会变成:
      • 当前线程上下文类加载器:sun.misc.Launcher$ExtClassLoader@14ae5a5
      • ServiceLoader 的类加载器 null */ Thread.currentThread().setContextClassLoader(Test7.class.getClassLoader().getParent());

      ServiceLoader loader = ServiceLoader.load(Driver.class); Iterator iterator = loader.iterator();

      while (iterator.hasNext()) {

      Driver driver = iterator.next();
      System.out.println("driver:" + driver.getClass() + ",  loader:" + driver.getClass().getClassLoader());
      

      } System.out.println(“当前线程上下文类加载器:” + Thread.currentThread().getContextClassLoader()); System.out.println(“ServiceLoader 的类加载器” + ServiceLoader.class.getClassLoader()); /** 输出

      • driver:class com.mysql.jdbc.Driver, loader:sun.misc.Launcher$AppClassLoader@18b4aac2
      • 当前线程上下文类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
      • ServiceLoader 的类加载器 : null */ }} ```
    • 线程上下文类加载器就是当前线程的 current ClassLoader;

    • 一般使用模式(获取-使用-还原) ```java ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(targetTool); myMethod(); } finally { Thread.currentThread().setContextClassLoader(classLoader); }

myMethod 内调用了 Thread.currentThread().getContextClassLoader() 获取当前 线程的上下文类加载器做某些事情,如果一个类由类加载器 A 加载,那么这个类的依赖类也是 由相同的类加载器加载的(如果该依赖类之前没有被加载过的话); ContextClassLoader 的作用就是为了破坏 Java 的类加载委托机制; 当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)低层的类时,就必须 要通过线程上下文加载器来帮助高层的 ClassLoader 找到并加载该类; 案例: public class Test6 implements Runnable { private Thread thread; public Test6(){ thread = new Thread(this); thread.start(); } public void run() { ClassLoader loader = this.thread.getContextClassLoader(); this.thread.setContextClassLoader(loader);

    System.out.println("class : " + loader.getClass());
    System.out.println("Parent : " + loader.getParent().getClass());
}

public static void main(String[] args) {
    new Test6();
    /** 输出
     * class : class sun.misc.Launcher$AppClassLoader
     * Parent : class sun.misc.Launcher$ExtClassLoader
     */
}

}

<a name="o6VIp"></a>
## 2. 连接

- 将已读入到内存的类的二进制数据合并到虚拟机的运行时环境中去,分为三个阶段:
   - 验证:确保被加载的类的正确性,验证内容包括:类文件的结构检查、语义检查、字节码验证、二进制兼容性验证等;
      - 结构验证:是否符合class文件规范;
      - 语义验证:检查一个被标记为 final 的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同);
      - 操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等);
   - 准备:为类的静态变量分配内存,并将其初始化为默认值,但在到达初始化之前,类变量都没有初始化为真正的初始值;
   - 解析:把类中的符号引用转换为直接引用
      - 符号引用:以一组符号来描述所引用目标,在 class 文件中以 CONSTANT_CLASS_INFO 等类型常量出现,其与内存布局无关,引用目标不一定加载到内存中;
      - 直接引用:直接指向目标的指针、相对偏移量或能间接定位到目标的句柄;
<a name="gqAGm"></a>
## 3. 初始化

- **所有的 Java 虚拟机实现必须在每个类或接口被 Java 程序“首次主动使用”(7 种方式)时才初始化他们**;
- 把类的静态变量赋予正确的初始值,**静态变量的初始化**
   - 在静态变量的声明处进行初始化;
   - 在静态代码块中进行初始化;
```java
class MyParent1{
    public static String str = "cyt";
    static {
        System.out.println("MyParent static block");
    }
}
class MyChild1 extends MyParent1{
    public static String str2 = "peace";
    static {
        System.out.println("MyChild static block");
    }
}
// 对于静态字段来说,只有定义了该字段的类才会被初始化;
System.out.println(MyChild1.str); 输出: MyParent static block; cyt
// 当一个类在初始化时,要求其父类全部都已初始化完毕;
System.out.println(MyChild1.str2); 输出:MyParent static block;MyChild static block;peace
  • 常量在编译阶段会存入到调用这个常量方法所在的类常量池中,但是對於在編譯期無法確定的常量,則會調用所在類的初始化;

    class MyParent2{
      public static final String str = "final_cyt";
      static {
          System.out.println("MyParent2 static block");
      }
    }
    class MyParent3{
      public static final String str = UUID.randomUUID().toString(); // 在運行期間才能隨機初始一個值
      static {
          System.out.println("MyParent3 static block");
      }
    }
    
  • 数组类型:并不会初始化,对于数组实例来说,其类型是由 JVM 在运行期动态生成的,其父类型是 Object

    class MyParent4{
      static {
          System.out.println("MyParent4 static block");
      }
      {
          System.out.println("MyParent4 normal block");
      }
    }
    MyParent4 myParent4 = new MyParent4(); // output: MyParent4 static block
    MyParent4[] myParent4s = new MyParent4[1]; // 并不会初始化,对于数组实例来说,其类型是由 JVM 在运行期动态生成的,其父类型是 Object
    
  • 类的初始化步骤

    • 若无进行加载和连接,就先进行加载和连接;
    • 若该类存在直接父类,且这个父类还未被初始化,则先初始化直接父类;
    • 若该类中存在初始化语句,则依次执行这些初始化语句;
    • 当 Java 虚拟机初始化一个类时,要求它的所有父类都已被初始化,但此规则并不适用于接口
      • 初始化一个类时,并不会先初始化它所实现的接口;
      • 初始化一个接口时,并不会先初始化它的父接口;
      • 一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致接口的初始化【只有在真正使用到父接口的时候(如引用接口中所定义的常量时),才会初始化】;
        interface MyGrandpa_interface_5{
        public static final Thread thread = new Thread(){
        { System.out.println("MyGrandpa_interface_5 invoked"); }
        };
        }
        interface MyParent_interface_5 extends MyGrandpa_interface_5{
        //父接口会被加载,但不会初始化
        public static final Thread thread = new Thread(){
        { System.out.println("MyParent_interface_5 invoked"); }
        };
        }
        class MyParent_class_5 implements MyGrandpa_interface_5{
        public static final Thread thread = new Thread(){
        { System.out.println("MyParent_class_5 invoked"); }
        };
        }
        class MyChild_class_5 extends MyParent_class_5{
        public static final int c = 5;
        public static int b = 6;
        }
        class MyChild_interface_5 implements MyParent_interface_5{
        public static int b = 6;
        }
        System.out.println(MyChild_interface_5.b); // output:6 【父接口会被加载,但不会初始化】
        System.out.println(MyChild_class_5.b); // output:MyParent_class_5 invoke; 6【父类会被初始化】
        System.out.println(MyChild_class_5.c); // 会直接放入调用类的常量池中,而不会加载子类、父类,更不会初始化父类
        
  • 类的初始化时机

    • 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可认定为是对类或接口的主动使用;
    • 调用 ClassLoader 类的 loadClass 方法加载一个类,并不是对类的主动使用,不会导致类的初始化;

         Class<?> class1 = Class.forName("java.lang.String");
         System.out.println(class1.getClassLoader());// null ,根类加载器
      
         Class<?> class2 = Class.forName("com.cyt.jvm.C");//反射是对类的主动使用
         System.out.println(class2.getClassLoader());//class C static block; sun.misc.Launcher$AppClassLoader@18b4aac2,系统类加载器
      
         // 调用ClassLoader的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
         ClassLoader loader = ClassLoader.getSystemClassLoader();
         Class<?> class3 = loader.loadClass("com.cyt.jvm.C");
         System.out.println(class3.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
      
  • 类的真正初始化与准备阶段初始化的区别

    public static void testSingleton(){
      Singleton singleton = Singleton.getInstance();
      System.out.println("counter1: " + singleton.counter1);
      System.out.println("counter2: " + singleton.counter2);
    }
    class Singleton {
      public static int counter1;
      private static Singleton singleton = new Singleton();
    //    public static int counter2 = 0; // 若 counter2 放在此,则最终输出都为 1;
      private Singleton(){
          counter1++;
          counter2++; // 准备阶段会初始化为默认值
          System.out.println(counter1); //1
          System.out.println(counter2); //1
      }
      public static int counter2 = 0; // 若counter放在这里,则对应输出为0,因为会按照顺序被再次初始化为 0
      public static Singleton getInstance(){
          return singleton;
      }
    }
    

    4. 使用

  • Java 程序对类的使用方式:

    • 主动使用
      - 创建类的实例; 
      - 访问某个类或接口的静态变量,或者对该静态变量赋值(getstate、putstate);
      - 调用类的静态方法(invokestate);
      - 反射(Class.forName("com.xx.xxx"));
      - 初始化一个类的子类;
      - Java 虚拟机启动时被标明为启动类的类(Java Test);
      - Jdk 1.7 开始提供的动态语言支持:java.lang.invoke.MethodHandle 实例的解析结果 REF_getStatic,REF_putStatic,REF_invokeStatic 句柄对应的类没有初始化则初始化;
      
    • 被动使用
      - 除了以上 7 种情况,其他使用 java 类的方法都被看作是对类的被动使用,都不会导致类的初始化;
      

      5. 类的卸载

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

  • 一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期
  • 由 Java 虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载,虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的 Class 对象,因此这些 Class 对象始终是可触及的;
  • 由用户自定义的类加载器所加载的类是可以被卸载的;

    6. VM option

  • -XX:+

  • -XX:-
  • -XX:

    四、执行引擎(execution engine)

    1. 背景

  • 每一个 Java 虚拟机都有一个执行引擎,是虚拟机最核心的组成部分之一,负责执行被加载类中包含的指令;

  • 当 JVM 解析完 class 文件之后就会将其转成运行时结构,并将其存放在方法区中,然后会创建类对象(也就是 Class 对象)提供访问类数据的接口;
  • 虚拟机执行子系统的一个过程,包含了从编译出的 Class文件,到被加载到内存中、验证、初始化等;

    2. 与物理机执行引擎的区别

  • 物理机的执行引擎直接建立在处理器、硬件、指令集和操作系统层面上,而虚拟机的执行引擎由自己实现;

    3. 作用

  • 执行程序员写的业务逻辑,业务逻辑指一些方法,即虚拟机执行引擎就是用来执行各个方法的,而方法的执行用栈帧入栈和出栈来描述,则执行引擎用来执行各个栈帧;

    • 在虚拟机执行时,只有最顶层的栈帧有效,与之关联的方法称为当前方法,并且执行引擎运行的所有字节码都针对当前栈帧;
  • 执行时 JVM 总会先从 main (一开始就会为 main 函数创建一个函数栈帧)方法开始,再从 main 方法的 code attribute 中找到方法体的字节码来调用执行引擎执行,如果要调用方法就创建一个新的函数栈帧,如果函数执行完成就弹出第一个函数栈帧,运行时的栈帧结构如下:

JVM 系统学习 - 图22

五、JVM 运行时的数据区域(内存结构)

  • 程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等,Java 虚拟机将这些信息统统保存在数据区(data areas)中;
  • 根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap);

JVM 系统学习 - 图23

  • 在 JVM 规范中仅抽象地规定了程序在执行期间运行时数据区应该包括哪几部分,具体实现由不同的虚拟机厂商自定义;

    1. 构成

    1.1 程序计数器(Program Counter (PC) Register)

  • 程序计数器,也称为 PC 寄存器,当前线程所执行的字节码的行号指示器;

    • 如果正在执行的是 Native 方法,则此计数器值为空(Undefined);
      • 方法是大多是通过 C 实现,并未编译成需要执行的字节码指令;
      • native 方法是通过调用系统指令来实现多线程,同 C 或 C++ 的实现方式;
  • 背景
    • 在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间
      • 在任一具体时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令;
      • 为保证每个线程在切换后能够恢复在切换之前的程序执行位置,使程序按正常次序执行,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰(私有性);
  • .字节码的执行原理
    • 编译后的字节码在没有经过 JIT(实时编译器)编译前,通过字节码解释器进行解释执行,其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作;
  • 程序计数器的特点

    • 具有线程隔离性;
    • 占用的内存空间非常小,可以忽略不计,且不随程序执行而改变;
    • java 虚拟机规范中唯一一个没有规定任何 OutofMemeryError 的区域;
    • 程序执行的时候,程序计数器是有值的,其记录的是程序正在执行的字节码的地址;
    • 执行native本地方法时,程序计数器的值为空。原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计;

      1.2 JVM 栈 (Java Virtual Machine Stacks)

  • JVM 栈保存基础数据类型的对象和自定义对象的引用,JVM 栈是 Java 方法执行的内存模型;

  • 由于每个线程正在执行的方法可能不同,所有 JVM 栈为线程私有;
  • Java 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息;
    • 局部变量表:用于存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参),其大小在编译器确定,在程序执行期间大小不会改变;
      • 对于基本数据类型的变量,直接存储它的值;
      • 对于引用类型的变量,存的是指向对象的引用;
    • 操作数栈:程序中的所有计算过程都是借助于操作数栈来完成的;
    • 指向运行时常量池的引用:在方法执行的过程中可能需要用到类中的常量,需有一个引用来指向;
      • JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池;
    • 方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方;
  • 当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈,当方法执行完毕之后,便会将栈帧出栈;
    • 线程当前执行的方法所对应的栈帧位于 Java 栈的顶部;
    • 使用递归方法的时候容易导致栈内存溢出;
    • 栈区的空间不用程序员管理;
  • 栈内存的大小设置
    • 固定值
    • 根据线程需要动态增长
  • 栈内存溢出抛出的异常

    • StackOverflowError :程序执行需要的栈内存超过设定的固定值;
    • OutOfMemoryError :出现在栈内存设置成动态增长时,当 JVM 尝试申请的内存大小超过了其可用内存;

      1.3 本地方法栈 (Native Method Stacks)

  • 本地方法是使用非 Java 语言实现的方法,通常指 C 或 C++;

  • 不支持本地方法执行的 JVM 没有必要实现此数据区域;
  • 与 Java 栈的作用和原理非常相似,区别是 Java栈 为执行 Java 方法服务,而本地方法栈则为执行本地方法(Native Method)服务;

    • 在 JVM 规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现,在HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一;

      1.4 堆 (Heap Memory)

  • 堆数据区是用来存放对象和数组(特殊的对象本身),不存放基本类型和对象引用,其随着 JVM 启动而创建;

  • 堆是 Java 垃圾收集器管理的主要区域,因此也称 GC 堆;
  • 在 JVM 中只有一个堆,被多个线程所共享
  • 存放的是对象实例,是数据区中占用空间最大的部分;
  • 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可;
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常;
  • 优缺点
    • 优:可以动态地分配内存大小,生存期也不必事先告诉编译器,在运行时动态分配内存,Java的垃圾收集器会自动收走这些不再使用的数据;
    • 缺:在运行时动态分配内存,存取速度较慢;
  • 堆内存溢出常见原因
    • 内存中加载的数据过多如一次从数据库中取出过多数据;
    • 集合对对象引用过多且使用完后没有清空;
    • 代码中存在死循环或循环产生过多重复对象;
    • 堆内存分配不合理;
    • 网络连接问题、数据库问题等;
  • 堆内空间调整参数

    • -Xms:设置初始分配大小,默认为物理内存的1/64;
    • -Xmx:最大分配内存,默认为物理内存的1/4;
    • -XX:+PrintGCDetails:输出详细的GC处理日志 ;
    • -XX:+PrintGCTimeStamps:输出GC的时间戳信息 ;
    • -XX:+PrintGCDateStamps:输出GC的时间戳信息(以日期的形式);
    • -XX:+PrintHeapAtGC:在GC进行处理的前后打印堆内存信息 ;
    • -Xloggc:(SavePath):设置日志信息保存文件 ;
    • 在堆内存的调整策略中,基本上只要调整两个参数:-Xms和-Xmx ;

      1.5 方法区(Method Area)

  • 方法区存储了被加载类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量及编译后的代码等;

  • 数据进入方法区后并非永久存在,垃圾收集行为在此区域较少出现;
  • 程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须线程安全;
  • 在程序运行时方法区的大小可变,可在运行时扩展,有些 Java 虚拟机的实现也可通过参数来订制方法区的初始大小,最小值和最大值;
  • 在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建出来,但并非 Class 文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如 String的intern方法;
  • 方法区溢出的原因就是没有足够的内存来存放这些数据;
  • 在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory);

    • 元空间的大小仅受本地内存限制,但可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定元空间的大小;

      2. 堆和方法区的划分变化

      JVM 系统学习 - 图24

      3. JVM 运行时数据区域 与 Java 内存模型的区别

  • Java 内存模型是 Java 语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量的多线程操作冲突问题;

    • JVM 规范规定了 Java 虚拟机对多线程内存操作的一些规则,主要集中体现在 volatile 和 synchronized 这两个关键字;
      • volatile 是 JVM 提供的对共享变量在多线程读写时的可见性保证,主要作用是对 volatile 修饰的共享变量禁止被缓存(这里跟CPU的高速缓存和缓存一致性协议有关),不做重排序(重排序:在CPU处理速度远大于内存读写速度的现状下为了提高性能而进行的优化),但是并不保证共享变量操作的原子性;
      • synchronized 锁机制,通过锁的特性和内存屏障保证锁住区域操作的原子性、可见性、有序性;
      • 通过在代码前后加入加载屏障(Load Barrier)和 存储屏障(Store Barrier),能保证锁住代码块或者方法中对共享变量的操作的可见性;
      • 通过在代码前后加入获取屏障(Acquire Barrier)和 释放屏障(Release Barrier),能保证锁住代码块或者方法中对共享变量的操作的有序性;
  • JVM 运行时数据区,是 Java 虚拟机在运行时对该 Java 进程占用的内存进行的一种逻辑上的划分;

    六、垃圾回收器

    1. HotSpot 虚拟机对象

    1.1 对象的创建

    image.png

  • 类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过,若无则必须先执行相应的类加载;

  • 分配内存:类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来;
    • 分配方式
      • “指针碰撞”“空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

JVM 系统学习 - 图26

  • 内存分配并发问题
    • 解决策略一:CAS+失败重试
      • CAS 是乐观锁的一种实现方式,虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;
    • 解决策略二:TLAB
      • TLAB 为每一个线程预先在 Eden 区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配;
        • 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值;
        • 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息;
  • 根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式;
    • 执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, <init> 方法还没有执行,所有的字段都还为零,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来;

      1.2 对象的内存布局

  • 对象在内存中的布局可以分为3块区域:对象头实例数据对齐填充;
  • 对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例;
  • 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容;
  • 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用;

    1.3 对象的访问定位

  • Java 程序通过栈上的 reference 数据来操作堆上的具体对象;

  • 对象的访问方式由虚拟机实现而定,目前主流的访问方式有 使用句柄 直接指针两种;
    • 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
      • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改;

JVM 系统学习 - 图27

  • 直接指针: 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址;
    • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销;

JVM 系统学习 - 图28

2. 垃圾回收机制

  • 当排查各种内存溢出问题或垃圾收集成为系统高并发的瓶颈时,需对自动化的垃圾回收机制进行监控和调节;

    2.1 JVM 内存分配与回收

    JVM 系统学习 - 图29
    注:Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1;

    2.1.1 内存分配策略

    2.1.1.1 对象优先在eden区分配

  • 目前主流的垃圾收集器都会采用分代回收算法,可根据各个年代的特点选择合适的垃圾收集算法;

    • 大多数情况下,对象在新生代中 eden 区分配,当 eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC;

2.1.1.2 大对象直接进入老年代

  • 大对象就是需要大量连续内存空间的对象(比如:字符串、数组);
  • 原因:为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率;

2.1.1.3 长期存活的对象将进入老年代

  • 虚拟机采用分代收集的思想来管理内存,给每个对象一个对象年龄(Age)计数器;
  • 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1,对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中;

2.1.1.4 动态对象年龄判定

  • 如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄;

    2.1.2 回收流程

    JVM 系统学习 - 图30

  • 具体流程

    1. 当现在有一个新的对象产生,JVM需要为该对象进行内存空间的申请;
    2. 先判断 Eden 区是否有内存空间,如果有,直接将新对象保存在 Eden 区;
    3. 如果 Eden 区的内存空间不足,会自动执行一个 Minor GC 操作,将 Eden 区的无用内存空间进行清理 ;
    4. 清理 Eden 区之后继续判断 Eden 区内存空间情况,如果充足,则将新对象直接保存在 Eden 区;
    5. 若执行 Minor GC 后发现 Eden 区内存依然不足,则判断存活区的内存空间,并将 Eden 区的部分活跃对象保存在存活区;
    6. 活跃对象迁移到存活区后,继续判断 Eden 区内存空间情况,如果充足,则将新对象直接保存在 Eden 区 ;
    7. 若存活区也没有空间了,则继续判断老年区,如果老年区充足,则将存活区的部分活跃对象保存在老年区 ;
    8. 存活区的活跃对象迁移到老年区后,则将 Eden 区的部分活跃对象保存在存活区;
    9. 活跃对象迁移到存活区后,继续判断 Eden 区内存空间情况,如果充足,则将新对象直接保存在Eden区;
    10. 如果老年区也满了,这时候产生 Major GC(Full GC)进行老年区的内存清理 ;
    11. 如果老年区执行了 Major GC 之后发现无法进行对象保存,会产生 OutOfMemoryError 异常;

      2.2 如何判断回收对象已死

      2.2.1 堆对象回收_引用计数法**

  • 给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加 1;每当有一个地方不再引用它时,计数器值减 1,这样只要计数器的值不为 0,就说明还有地方引用它,它就不是无用的对象;

    • 缺:当某些对象之间互相引用时,无法判断出这些对象是否已死;

JVM 系统学习 - 图31JVM 系统学习 - 图32

2.2.2 堆对象回收_可达性分析算法**

  • 当一个对象到 GC Roots 没有任何引用链相连(GC Roots 到这个对象不可达)时,就说明此对象是不可用的,是死对象,如下图:object1、object2、object3、object4 和 GC Roots 之间有可达路径,这些对象不会被回收,但 object5、object6、object7 到 GC Roots 之间没有可达路径,这些对象就被判了死刑;

JVM 系统学习 - 图33

  • 被判了死刑的对象并不是必死无疑,还有挽救的余地:

    • 进行可达性分析后对象和 GC Roots 之间没有引用链相连时,对象将会被进行一次标记;
    • 接着会判断如果对象没有覆盖 Object的finalize() 方法或者 finalize() 方法已经被虚拟机调用过,那么它们就会被行刑(清除);
    • 如果对象覆盖了 finalize() 方法且还没有被调用,则会执行 finalize() 方法中的内容,所以在 finalize() 方法中如果重新与 GC Roots 引用链上的对象关联就可以拯救自己;

      2.2.3 方法区回收**

  • 方法区中主要回收的是废弃的常量和无用的类;

2.2.3.1 判断常量是否废弃

  • 判断是否有地方引用这个常量,如果没有引用则为废弃的常量;

2.2.3.2 判断类是否废弃需要同时满足如下条件:

  • 该类所有的实例已经被回收(堆中不存在任何该类的实例);
  • 加载该类的 ClassLoader 已经被回收;
  • 该类对应的 java.lang.Class 对象在任何地方没有被引用(无法通过反射访问该类的方法);

    2.2.4 引用

    2.2.4.1 强引用

  • 类似必不可少的生活用品,垃圾回收器绝不会回收,当内存空间不足,JVM 宁愿抛出 OutOfMemoryError 错误 ,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题;

2.2.4.2 软引用(SoftReference)

  • 类似可有可无的生活用品,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足,就会回收这些对象的内存,软引用可用来实现内存敏感的高速缓存;
  • 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中;

2.2.4.3 弱引用(WeakReference)

  • 类似可有可无的生活用品,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
  • 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中;

2.2.4.4 虚引用(PhantomReference)

  • 即形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收;
  • 虚引用主要用来跟踪对象被垃圾回收的活动
  • 虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用;

注:在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生;

2.3 垃圾收集算法

2.3.1 标记-清除算法

  • 算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,是最基础的收集算法,效率较高,但是会带来明显的效率与空间问题(标记清除后产生大量不连续碎片):

JVM 系统学习 - 图34

2.3.2 复制算法

  • 为解决效率问题,出现“复制”收集算法,可将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次的内存回收都是对内存区间的一半进行回收;

JVM 系统学习 - 图35

2.3.3 标记-整理算法

  • 标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存;
    • 不会产生空间碎片,但是整理会花一定的时间;

JVM 系统学习 - 图36

2.3.4 分代收集算法