JVM

0 引言

什么是 JVM ?

定义:

Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处:

一次编写,到处运行

自动内存管理,垃圾回收功能

数组下标越界检查

多态

比较:

jvm jre jdk

常见的 JVM

image-20230320105212686

路线

image-20230320105426694

1 内存结构

1.1 程序计数器

  • Program Counter Register 程序计数器(寄存器)
  • 作用:是记住下一条jvm指令的执行地址
  • 特点
    • 是线程私有的
    • 不会存在内存溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0: getstatic #20                       // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return

1.2 虚拟机栈

  • Java Virtual Machine Stacks (Java 虚拟机栈)

    • 每个线程运行时所需要的内存,称为虚拟机栈
    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  • 栈内存溢出

    • 栈帧过多导致栈内存溢出
    • 栈帧过大导致栈内存溢出
  • 线程诊断

1.3 本地方法栈

1.4 堆

  • Heap 堆
    • 通过 new 关键字,创建对象都会使用堆内存
  • 特点
    • 它是线程共享的,堆中对象都需要考虑线程安全的问题
    • 有垃圾回收机制
  • 堆内存溢出
  • 堆内存诊断
    • jps 工具 查看当前系统中有哪些 java 进程
    • jmap 工具 查看堆内存占用情况 jmap - heap 进程id (某一时刻,要连续得使用jconsole)
    • jconsole 工具 图形界面的,多功能的监测工具,可以连续监测

1.5 方法区

  • 方法区内存溢出
    • 1.8以前会导致永久代内存溢出
    • 1.8 之后会导致元空间内存溢出
  • 运行时常量池
    • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息
    • 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
  • StringTable
    • 特性
      • 常量池中的字符串仅是符号,第一次用到时才变为对象
      • 利用串池的机制,来避免重复创建字符串对象
      • 字符串变量拼接的原理是 StringBuilder (1.8)
      • 字符串常量拼接的原理是编译期优化
      • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
        • 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        • 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
    • 性能调优
      • 调整 -XX:StringTableSize=桶个数
        • 字符常量种类多则调整大一些
      • 考虑将字符串对象是否入池

1.6 直接内存

  • Direct Memory
    • 常见于 NIO 操作时,用于数据缓冲区
    • 分配回收成本较高,但读写性能高
    • 不受 JVM 内存回收管理
  • 分配和回收原理
    • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
    • ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存

2 垃圾回收

2.1 如何判断对象可以回收

  • 引用计数法

    • A引用B B引用A会造成循环引用,出问题(故Java不使用)
    • 可达性分析算法
      • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
      • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
      • 哪些对象可以作为 GC Root ?
  • 四种引用

    • 强引用
      • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
    • 软引用(SoftReference)
      • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
      • 可以配合引用队列来释放软引用自身
    • 弱引用(WeakReference)
      • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
      • 可以配合引用队列来释放弱引用自身
    • 虚引用(PhantomReference)
      • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存
    • 终结器引用(FinalReference)
      • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

    image-20230320190113297

2.2 垃圾回收算法

  • 标记清除算法
    • 速度快但是会造成内存碎片
    • image-20230321143736610
  • 标记整理算法
    • 没有内存碎片但是速度慢
    • image-20230321143833384
  • 复制算法
    • 不会有内存碎片但是需要双倍的空间
    • image-20230321143908721

2.3 分代垃圾回收

image-20230321151452214

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
表头 表头
堆初始大小 -Xms
堆最大大小 Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

2.4 垃圾回收器

  • 串行 -XX:+UseSerialGC = Serial + SerialOld

    • 单线程
    • 堆内存较小,适合个人电脑
    • image-20230321154846388
  • 吞吐量优先

    • 多线程

    • 堆内存较大,多核 cpu

    • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

    • ```java
      -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
      -XX:+UseAdaptiveSizePolicy
      -XX:GCTimeRatio=ratio
      -XX:MaxGCPauseMillis=ms
      -XX:ParallelGCThreads=n

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      - ![image-20230321154900987](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230321154900987.png)

      - 响应时间优先

      - 多线程

      - 堆内存较大,多核 cpu

      - 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.

      - ```java
      -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
      -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
      -XX:CMSInitiatingOccupancyFraction=percent
      -XX:+CMSScavengeBeforeRemark
    • image-20230321154909859

2.5 G1垃圾回收器

  • 定义:Garbage First

    • 2004 论文发布
    • 2009 JDK 6u14 体验
    • 2012 JDK 7u4 官方支持
    • 2017 JDK 9 默认
  • 适用场景

    • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
    • 超大堆内存,会将堆划分为多个大小相等的 Region
    • 整体上是 标记+整理 算法,两个区域之间是 复制 算法
  • 相关 JVM 参数

    • -XX:+UseG1GC
    • -XX:G1HeapRegionSize=size
    • -XX:MaxGCPauseMillis=time
  • 垃圾回收阶段

    • image-20230321160301405

    • Young Collection

      • 会stw(stop the work)

        • image-20230321160440625

          新生代

        • image-20230321160447582

          新生代到幸存区

        • image-20230321160534802

          进入老年代

    • Young Collection + CM

      • 在 Young GC 时会进行 GC Root 的初始标记
      • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
        • -XX:InitiatingHeapOccupancyPercent=percent (默认45%)
    • Mixed Collection(优先收集垃圾最多的以达到暂停时间最短的)

      • 会对 E、S、O 进行全面垃圾回收

        • 最终标记(Remark)会 STW
        • 拷贝存活(Evacuation)会 STW
      • -XX:MaxGCPauseMillis=ms

        image-20230321160831105

2.6 垃圾回收调优

  • 调优领域
    • 内存
    • 锁竞争
    • cpu
    • 占用 io
  • 确定目标
    • 【低延迟】还是【高吞吐量】,选择合适的回收器
    • CMS,G1,ZGC
    • ParallelGC
    • Zing
  • 最快的GC
    • 查看 FullGC 前后的内存占用,考虑下面几个问题
      • 数据是不是太多?
        • resultSet = statement.executeQuery(“select * from 大表 limit n”)
      • 数据表示是否太臃肿?
        • 对象图
        • 对象大小 16 Integer 24 int 4
      • 是否存在内存泄漏?
        • static Map map =
        • 第三方缓存实现
  • 新生代调优
    • 新生代的特点
      • 所有的 new 操作的内存分配非常廉价
        • TLAB thread-local allocation buffer
      • 死亡对象的回收代价是零
      • 大部分对象用过即死
      • Minor GC 的时间远远低于 Full GC
  • 老年代调优
    • CMS 的老年代内存越大越好
    • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
    • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
      • -XX:CMSInitiatingOccupancyFraction=percent

3 类加载与字节码技术

image-20230322101115226

3.1 类文件结构

  • 根据jvm规范,类文件结构

  • ```java
    ClassFile {

    u4 magic;//前四个字节表示:魔数(ca fe ba be)
    u2 minor_version;//小版本
    u2 major_version;//大版本
    u2 constant_pool_count;//常量池(占主要)
    cp_info constant_pool[constant_pool_count-1];//访问标识与继承信息
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;// Field 信息,表示成员变量的数量
    field_info fields[fields_count];
    u2 methods_count;//方法信息
    method_info methods[methods_count];
    u2 attributes_count;//附加属性
    attribute_info attributes[attributes_count];
    

    }

    1
    2
    3
    4
    5

    #### 3.2 字节码指令

    - javap工具

    [root@localhost ~]# javap -v HelloWorld.class
    Classfile /root/HelloWorld.class

    Last modified Jul 7, 2019; size 597 bytes
    MD5 checksum 361dca1c3f4ae38644a9cd5060ac6dbc
    Compiled from "HelloWorld.java"
    

    public class cn.itcast.jvm.t5.HelloWorld

    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_SUPER
    

    Constant pool:

    #1 = Methodref #6.#21 // java/lang/Object."<init>":()V
    #2 = Fieldref #22.#23 //
    

    java/lang/System.out:Ljava/io/PrintStream;

    #3 = String #24 // hello world
    #4 = Methodref #25.#26 // java/io/PrintStream.println:
    

    (Ljava/lang/String;)V

    #5 = Class #27 // cn/itcast/jvm/t5/HelloWorld
    #6 = Class #28 // java/lang/Object
    #7 = Utf8 <init>
    #8 = Utf8 ()V
    #9 = Utf8 Code
    #10 = Utf8 LineNumberTable
    #11 = Utf8 LocalVariableTable
    #12 = Utf8 this
    #13 = Utf8 Lcn/itcast/jvm/t5/HelloWorld;
    #14 = Utf8 main
    #15 = Utf8 ([Ljava/lang/String;)V
    #16 = Utf8 args
    #17 = Utf8 [Ljava/lang/String;
    #18 = Utf8 MethodParameters
    #19 = Utf8 SourceFile
    #20 = Utf8 HelloWorld.java
    #21 = NameAndType #7:#8 // "<init>":()V
    #22 = Class #29 // java/lang/System
    #23 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
    #24 = Utf8 hello world
    #25 = Class #32 // java/io/PrintStream
    #26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
    #27 = Utf8 cn/itcast/jvm/t5/HelloWorld
    #28 = Utf8 java/lang/Object
    #29 = Utf8 java/lang/System
    #30 = Utf8 out
    #31 = Utf8 Ljava/io/PrintStream;
    #32 = Utf8 java/io/PrintStream
    #33 = Utf8 println
    #34 = Utf8 (Ljava/lang/String;)V
    

    {

    public cn.itcast.jvm.t5.HelloWorld();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
            stack=1, locals=1, args_size=1
                0: aload_0
                1: invokespecial #1 // Method java/lang/Object."
    

    “:()V

                4: return
            LineNumberTable:
                line 4: 0
            LocalVariableTable:
                Start Length Slot Name Signature
                    0 5 0 this Lcn/itcast/jvm/t5/HelloWorld;
    
    public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
            stack=2, locals=1, args_size=1
                0: getstatic #2 // Field
    java/lang/System.out:Ljava/io/PrintStream;
                3: ldc #3 // String hello world
                5: invokevirtual #4 // Method
    java/io/PrintStream.println:(Ljava/lang/String;)V
                8: return
            LineNumberTable:
                line 6: 0
                line 7: 8
            LocalVariableTable:
                Start Length Slot Name Signature
                    0 9 0 args [Ljava/lang/String;
        MethodParameters:
            Name Flags
            args
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

    - 过程

    - 原始java代码

    - 编译后的字节码文件

    - 常量池载入运行时常量池

    ![image-20230322104419079](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104419079.png)

    - 方法字节码载入方法区

    ![image-20230322104440580](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104440580.png)

    - main线程开始运行,分配栈帧内存

    ![image-20230322104528061](https://myl-mdimg.oss-cn-beijing.aliyuncs.com/TyporaImg/JVM.assets/image-20230322104528061.png)

    - 执行引擎开始执行字节码

    #### 3.3 编译期处理

    - 所谓的 `语法糖` ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利

    - 默认的构造器

    - ```java
    public class Candy1 {
    }
    • 编译成.class后的代码

    • ```java
      public class Candy1 {

      // 这个无参构造是编译器帮助我们加上的
      public Candy1() {
          super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."
          <init>":()V
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      - 自动拆装箱

      - JDK5以后引用

      - ```java
      public class Candy2 {
      public static void main(String[] args) {
      Integer x = 1;
      int y = x;
      }
      }
    • ```java
      public class Candy2 {

      public static void main(String[] args) {
          Integer x = Integer.valueOf(1);
          int y = x.intValue();
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      - 泛型集合取值

      - 泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息
      在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理

      - ```java
      public class Candy3 {
      public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();
      list.add(10); // 实际调用的是 List.add(Object e)
      Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
      }
      }
    • 所以取值的时候,编译器真正生成的字节码中,还要额外做一个类型转换的操作

    • ```java
      // 需要将 Object 转为 Integer
      Integer x = (Integer)list.get(0);
      // 需要将 Object 转为 Integer, 并执行拆箱操作
      int x = ((Integer)list.get(0)).intValue();

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      - 可变参数

      - JDK5引入新特性

      - ```java
      public class Candy4 {
      public static void foo(String... args) {
      String[] array = args; // 直接赋值
      System.out.println(array);
      }
      public static void main(String[] args) {
      foo("hello", "world");
      }
      }
    • 转为了

    • ```java
      public class Candy4 {

      public static void foo(String[] args) {
          String[] array = args; // 直接赋值
          System.out.println(array);
      }
      public static void main(String[] args) {
          foo(new String[]{"hello", "world"});
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      - foreach循环

      - jdk5引入

      - ```java
      public class Candy5_1 {
      public static void main(String[] args) {
      int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖哦
      for (int e : array) {
      System.out.println(e);
      }
      }
      }

    • ```java
      public class Candy5_1 {

      public Candy5_1() {
      }
      public static void main(String[] args) {
          int[] array = new int[]{1, 2, 3, 4, 5};
          for(int i = 0; i < array.length; ++i) {
                int e = array[i];
              System.out.println(e);
          }
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      - switch字符串

      - jdk7开始switch 可以作用于字符串和枚举类,这个功能其实也是语法糖

      - ```java
      public class Candy6_1 {
      public static void choose(String str) {
      switch (str) {
      case "hello": {
      System.out.println("h");
      break;
      }
      case "world": {
      System.out.println("w");
      break;
      }
      }
      }
      }
    • 变编译器转换为

    • ```java
      public class Candy6_1 {

      public Candy6_1() {
      }
      public static void choose(String str) {
          byte x = -1;
          switch(str.hashCode()) {
          case 99162322: // hello 的 hashCode
              if (str.equals("hello")) {
                  x = 0;
              }
              break;
          case 113318802: // world 的 hashCode
              if (str.equals("world")) {
                  x = 1;
              }
          }
          switch(x) {
          case 0:
              System.out.println("h");
              break;
          case 1:
              System.out.println("w");
          }
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - switch枚举

      - 枚举类

      - jdk7引入

      - ```java
      enum Sex {
      MALE, FEMALE
      }
    • ```java
      public final class Sex extends Enum {

      public static final Sex MALE;
      public static final Sex FEMALE;
      private static final Sex[] $VALUES;
      static {
          MALE = new Sex("MALE", 0);
          FEMALE = new Sex("FEMALE", 1);
          $VALUES = new Sex[]{MALE, FEMALE};
      }
      /**
      * Sole constructor. Programmers cannot invoke this constructor.
      * It is for use by code emitted by the compiler in response to
      * enum type declarations.
      *
      * @param name - The name of this enum constant, which is the identifier
      * used to declare it.
      * @param ordinal - The ordinal of this enumeration constant (its position
      * in the enum declaration, where the initial constant is
      assigned
      */
      private Sex(String name, int ordinal) {
          super(name, ordinal);
      }
      public static Sex[] values() {
          return $VALUES.clone();
      }
      public static Sex valueOf(String name) {
          return Enum.valueOf(Sex.class, name);
      }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - try-with-resources

      - JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`

      - ```java
      try(资源变量 = 创建资源对象){

      } catch( ) {

      }
  • 方法重写时的桥接方法

    • ```java
      class A {
      public Number m() {
          return 1;
      }
      
      }
      class B extends A {
      @Override
      // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
      public Integer m() {
          return 2;
      }
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      - ```java
      class B extends A {
      public Integer m() {
      return 2;
      }
      // 此方法才是真正重写了父类 public Number m() 方法
      public synthetic bridge Number m() {
      // 调用 public Integer m()
      return m();
      }
      }
  • 匿名内部类

3.4 类加载阶段

  • 加载
    • 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
      • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露给 java 使用
      • _super 即父类
      • _fields 即成员变量
      • _methods 即方法
      • _constants 即常量池
      • _class_loader 即类加载器
      • _vtable 虚方法表
      • _itable 接口方法表
    • 如果这个类还有父类没有加载,先加载父类
    • 加载和链接可能是交替运行的
  • 链接
    • 验证:验证类是否符合JVM规范,安全性检查
    • 准备:为 static 变量分配空间,设置默认值
    • 解析:将常量池中的符号引用解析为直接引用
  • 初始化
    • 初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

3.5 类加载器

image-20230322153720740

  • 启动类加载器

    • 用 Bootstrap 类加载器加载类:

    • ```java
      package cn.itcast.jvm.t3.load;

      public class F {
          static {
              System.out.println("bootstrap F init");
          }
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      - 执行

      - ```java
      package cn.itcast.jvm.t3.load;
      public class Load5_1 {
      public static void main(String[] args) throws ClassNotFoundException {
      Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
      System.out.println(aClass.getClassLoader());
      }
      }
    • 输出

    • E:/git/jvm/out/production/jvm>java -Xbootclasspath/a:.
      cn.itcast.jvm.t3.load.Load5
      bootstrap F init
      null
      
      • -Xbootclasspath 表示设置 bootclasspath
      • 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
      • 可以用这个办法替换核心类
        • java -Xbootclasspath:<new bootclasspath>
        • java -Xbootclasspath/a:<追加路径>
        • java -Xbootclasspath/p:<追加路径>
  • 扩展类加载器

  • 双亲委派模式

    • 所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
  • 线程上下文类加载器

  • 自定义类加载器

3.6 运行期优化