跳转至

08-深入拆解Java虚拟机-08-深入拆解Java虚拟机

  •  08-深入拆解Java虚拟机|200
  • 书名: 08-深入拆解Java虚拟机
  • 作者: 08-深入拆解Java虚拟机
  • 简介:
  • 出版时间
  • ISBN:
  • 分类:
  • 出版社:

高亮划线

08-深入拆解Java虚拟机

开篇词 | 为什么我们要学习Java虚拟机?

开篇词 | 为什么我们要学习Java虚拟机?_1

01 | Java代码是怎么运行的?

01 | Java代码是怎么运行的?_1

  • 📌 加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。 ^5-3431-3485

    • ⏱ 2024-05-29 18:21:53
  • 📌 HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。 ^5-4425-4499

    • ⏱ 2024-05-29 18:26:06
  • 📌 之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。 ^5-5087-5270

    • ⏱ 2024-05-29 18:28:49
  • 📌 从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。 ^5-5278-5347

    • ⏱ 2024-05-29 18:29:22
  • 📌 为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的编译线程中进行的。HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。 ^5-5355-5524

    • ⏱ 2024-05-29 18:30:17

02 | Java的基本类型

02 | Java的基本类型_1

  • 📌 也就是说,boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的,和引用类型也是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用 4 个字节;而在 64 位的 HotSpot 中,他们将占 8 个字节。 ^7-5177-5310

    • ⏱ 2024-05-29 18:58:15
  • 📌 当然,这种情况仅存在于局部变量,而并不会出现在存储于堆中的字段或者数组元素上。对于 byte、char 以及 short 这三种类型的字段或者数组单元,它们在堆上占用的空间分别为一字节、两字节,以及两字节,也就是说,跟这些类型的值域相吻合。 ^7-5318-5438

    • ⏱ 2024-05-29 19:03:27

03 | Java虚拟机是如何加载Java类的?

03 | Java虚拟机是如何加载Java类的?_1

  • 📌 准备阶段的目的,则是为被加载类的静态字段分配内存 ^9-3058-3082

    • ⏱ 2024-05-30 06:30:44
  • 📌 Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。 ^9-3672-3747

    • ⏱ 2024-05-30 06:31:27
  • 📌 如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。 ^9-3825-4101

    • ⏱ 2024-05-30 06:32:53
  • 📌 那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况:当虚拟机启动时,初始化用户指定的主类;当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;当遇到调用静态方法的指令时,初始化该静态方法所在的类;当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;使用反射 API 对某个类进行反射调用时,初始化这个类;当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。 ^9-4178-4554

    • ⏱ 2024-05-30 06:34:28
  • 📌 由于类初始化是线程安全的,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个 Singleton 实例。 ^9-5140-5196

    • ⏱ 2024-05-30 06:35:36

04 | JVM是如何执行方法调用的?(上)

04 | JVM是如何执行方法调用的?(上)_1

  • 📌 重载的方法在编译过程中即可完成识别。具体到每一个方法调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段:在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。 ^11-1655-1931

    • ⏱ 2024-05-30 07:02:13
  • 📌 如果 Java 编译器在同一个阶段中找到了多个适配的方法,那么它会在其中选择一个最为贴切的,而决定贴切程度的一个关键就是形式参数类型的继承关系。在开头的例子中,当传入 null 时,它既可以匹配第一个方法中声明为 Object 的形式参数,也可以匹配第二个方法中声明为 String 的形式参数。由于 String 是 Object 的子类,因此 Java 编译器会认为第二个方法更为贴切。 ^11-1945-2147

    • ⏱ 2024-05-30 07:04:29
  • 📌 除了同一个类中的方法,重载也可以作用于这个类所继承而来的方法。也就是说,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型不同,那么在子类中,这两个方法同样构成了重载。那么,如果子类定义了与父类中非私有方法同名的方法,而且这两个方法的参数类型相同,那么这两个方法之间又是什么关系呢?如果这两个方法都是静态的,那么子类中的方法隐藏了父类中的方法。如果这两个方法都不是静态的,且都不是私有的,那么子类的方法重写了父类中的方法。 ^11-2154-2388

    • ⏱ 2024-05-30 07:04:14

05 | JVM是如何执行方法调用的?(下)

05 | JVM是如何执行方法调用的?(下)_1

06 | JVM是如何处理异常的?

06 | JVM是如何处理异常的?_1

07 | JVM是如何实现反射的?

07 | JVM是如何实现反射的?_1

08 | JVM是怎么实现invokedynamic的?(上)

08 | JVM是怎么实现invokedynamic的?(上)_1

09 | JVM是怎么实现invokedynamic的?(下)

09 | JVM是怎么实现invokedynamic的?(下)_1

10 | Java对象的内存布局

10 | Java对象的内存布局_1

  • 📌 在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位。也就是说,每一个 Java 对象在内存中的额外开销就是 16 个字节。以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。 ^23-1816-1996

    • ⏱ 2024-05-30 07:50:59
  • 📌 为了尽量较少对象的内存使用量,64 位 Java 虚拟机引入了压缩指针 [1] 的概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本 64 位的 Java 对象指针压缩成 32 位的。 ^23-2004-2116

    • ⏱ 2024-05-30 07:51:17

11 | 垃圾回收(上)

11 | 垃圾回收(上)_1

  • 📌 为了防止在标记过程中堆栈的状态发生改变,Java 虚拟机采取安全点机制来实现Stop-the-world 操作,暂停其他非垃圾回收线程。 ^25-5210-5279
    • ⏱ 2024-05-30 17:48:59

12 | 垃圾回收(下)

12 | 垃圾回收(下)_1

  • 📌 答案是:再申请多个停车位便可以了。这项技术被称之为 TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。 ^27-1939-2028

    • ⏱ 2024-05-30 17:53:44
  • 📌 具体来说,每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB。 ^27-2036-2091

    • ⏱ 2024-05-30 17:53:39
  • 📌 接下来的 new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。我猜测会有留言问为什么不把 bump the pointer 翻译成指针碰撞。这里先解释一下,在英语中我们通常省略了 bump up the pointer 中的 up。在这个上下文中bump 的含义应为“提高”。另外一个例子是当我们发布软件的新版本时,也会说bump the version number。 ^27-2179-2448

    • ⏱ 2024-05-30 17:54:40
  • 📌 Minor GC 的另外一个好处是不用对整个堆进行垃圾回收。但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。 ^27-3249-3378

    • ⏱ 2024-05-31 16:07:36
  • 📌 这样一来,岂不是又做了一次全堆扫描呢? ^27-3386-3405

    • ⏱ 2024-05-31 16:13:15
  • 📌 HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。 ^27-3425-3561

    • ⏱ 2024-05-31 16:13:10
  • 📌 针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New。这三个采用的都是标记 - 复制算法。其中,Serial 是一个单线程的,Parallel New 可以看成 Serial 的多线程版本。Parallel Scavenge 和 Parallel New 类似,但更加注重吞吐率。此外,Parallel Scavenge 不能与 CMS 一起使用。针对老年代的垃圾回收器也有三个:刚刚提到的 Serial Old 和 Parallel Old,以及CMS。Serial Old 和 Parallel Old 都是标记 - 压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。 ^27-6981-7312

    • ⏱ 2024-05-31 16:18:10
  • 📌 CMS 采用的是标记 - 清除算法,并且是并发的。除了少数几个操作需要 Stop-the-world 之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java 虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于 G1 的出现,CMS 在 Java 9 中已被废弃 [3]。G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。G1 能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是 G1 名字的由来。 ^27-7320-7705

    • ⏱ 2024-05-31 16:18:16
  • 📌 即将到来的 Java 11 引入了 ZGC,宣称暂停时间不超过 10ms。如果你感兴趣的话,可参考 R 大的这篇文章 [4]。 ^27-7713-7776

    • ⏱ 2024-05-31 16:18:20

13 | Java内存模型

13 | Java内存模型_1

  • 📌 happens-before 关系是用来描述两个操作的内存可见性的。如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。 ^29-2427-2504

    • ⏱ 2024-05-31 16:30:13
  • 📌 由此可以看出,解决这种数据竞争问题的关键在于构造一个跨线程的 happens-before 关系 :操作 X happens-before 操作 Y,使得操作 X 之前的字节码的结果对操作 Y 之后的字节码可见。 ^29-4497-4603

    • ⏱ 2024-05-31 16:36:20

14 | Java虚拟机是怎么实现synchronized的?

14 | Java虚拟机是怎么实现synchronized的?_1

15 | Java语法糖与Java编译器

15 | Java语法糖与Java编译器_1

16 | 即时编译(上)

16 | 即时编译(上)_1

17 | 即时编译(下)

17 | 即时编译(下)_1

18 | 即时编译器的中间表达形式

18 | 即时编译器的中间表达形式_1

19 | Java字节码(基础篇)

19 | Java字节码(基础篇)_1

20 | 方法内联(上)

20 | 方法内联(上)_1

21 | 方法内联(下)

21 | 方法内联(下)_1

22 | HotSpot虚拟机的intrinsic

22 | HotSpot虚拟机的intrinsic_1

23 | 逃逸分析

23 | 逃逸分析_1

【工具篇】 常用工具介绍

【工具篇】 常用工具介绍_1

24 | 字段访问相关优化

24 | 字段访问相关优化_1

25 | 循环优化

25 | 循环优化_1

26 | 向量化

26 | 向量化_1

27 | 注解处理器

27 | 注解处理器_1

28 | 基准测试框架JMH(上)

28 | 基准测试框架JMH(上)_1

29 | 基准测试框架JMH(下)

29 | 基准测试框架JMH(下)_1

30 | Java虚拟机的监控及诊断工具(命令行篇)

30 | Java虚拟机的监控及诊断工具(命令行篇)_1

31 | Java虚拟机的监控及诊断工具(GUI篇)

31 | Java虚拟机的监控及诊断工具(GUI篇)_1

32 | JNI的运行机制

32 | JNI的运行机制_1

33 | Java Agent与字节码注入

33 | Java Agent与字节码注入_1

34 | Graal:用Java编译Java

34 | Graal:用Java编译Java_1

35 | Truffle:语言实现框架

35 | Truffle:语言实现框架_1

36 | SubstrateVM:AOT编译框架

36 | SubstrateVM:AOT编译框架_1

尾声 | 道阻且长,努力加餐

尾声 | 道阻且长,努力加餐_1

读书笔记

本书评论