08-深入拆解Java虚拟机-08-深入拆解Java虚拟机
- 书名: 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