一、云时代的JVM

云时代的来临也给 Java 带来了不小的挑战,速度、内存、部署与管理的问题逐渐变成了 Java 的负累。不过它并没有坐以待毙,而是选择积极地适应新时代,引入更多新特性来应对这些挑战。

为了应对运行速度和内存占用问题,Java 持续在 JVM 层面进行优化,比如引入 GraalVM 和 OpenJ9 等新的 JVM 实现,提高了运行速度,减少了内存占用。GC 从 CMS 到 G1 再到 ZGC 的演进,内存空间利用率显著提升。并且,Java 逐渐开始支持更多的云原生特性,比如制作更小的 Docker 镜像,支持 Kubernetes 原生应用和 Serverless 等。

根据 New Relic 2023 年最新的报告统计,过去一年 JDK 17 的使用量增长了 430%,JDK 7 即将退出历史舞台,而 JDK 11 已经成为了使用量最高的 LTS 版本,占比达到了 56.06%。从 Graavlm 到 ZGC、从 AOT 到 Truffle,JVM 正以史无前例的更新速度和更新幅度,以自我颠覆性的方式准备迎接新时代的挑战。

深入学习JVM01_Java

二、JVM的起源

整个计算机系统由层层抽象构建而成,硬件是基础,应用是表层,中间则是操作系统和各种各样的编程语言

深入学习JVM01_Java_02

0/1 机器码是计算机能够直接执行的最底层的指令形式,它由 0 和 1 构成,对应着不同的操作和数据传输。计算机通过将 0/1 机器码加载到存储器中,并由控制器解析、执行这些指令,实现了复杂的计算和数据处理任务。

深入学习JVM01_JVM_03

总的来说就是 Java 虚拟机在计算机硬件和 Java 程序之间建立了一个抽象层,使 Java 程序不必关心底层的硬件和操作系统的情况,从而实现了 Java“一次编写,到处运行”的效果。这也是 Java 语言相比其他语言的一大优势。

三、JVM的特性

平台独立性:Java 代码可以在任何安装了 JVM 的机器上运行,不用为每种平台编写不同的版本。

自动内存管理 :JVM 提供了垃圾收集机制,自动回收不再使用的内存,降低内存泄漏的风险。

高效的即时编译:JVM 运用时即时编译对热点代码进行优化,提升程序执行效率。

强大的监控和调试工具:JVM 为开发者提供了详细的性能数据和调试工具,帮助开发者提升程序性能,解决问题。

四、JVM内部构成

深入学习JVM01_JVM_04

深入学习JVM01_JVM_05

五、云原生时代开发趋势

云原生时代的开发趋势主要集中在无服务器(Serverless)、容器化、Kubernetes、分布式等几个方面

深入学习JVM01_JVM_06

深入学习JVM01_Java_07

六、云时代JVM的挑战

深入学习JVM01_JVM_08

深入学习JVM01_JVM_09

七、JVM的发展趋势

深入学习JVM01_JVM_10

深入学习JVM01_Java_11

深入学习JVM01_JVM_12

深入学习JVM01_JVM_13

深入学习JVM01_JVM_14

深入学习JVM01_JVM_15

深入学习JVM01_JVM_16

深入学习JVM01_JVM_17

八、JVM运行数据区

深入学习JVM01_JVM_18

深入学习JVM01_Java_19

深入学习JVM01_JVM_20

深入学习JVM01_Java_21

栈的生命周期和线程相同,所以它是线程私有的。也就是说,不同线程的栈是相互独立的,不会互相干扰。这种设计能有效地隔离各个线程的运行环境,并且在多线程编程中允许每个线程有它自己的局部变量表、操作数栈等资源,有利于提高程序的并发性能。

Java 虚拟机栈在 Java 运行时内存里占据着重要的地位,主要用来支持 Java 程序的运行。在一个 Java 程序中,每一个线程的运行都对应着一个虚拟机栈。

深入学习JVM01_JVM_22

深入学习JVM01_JVM_23

深入学习JVM01_Java_24

JVM 使用局部变量表来存储方法中的局部变量以及方法参数,这些局部变量表槽位的替代性允许在不同阶段使用相同的槽位来存储不同的变量,从而提高内存的利用效率。在示例中,a 和 b 的槽位在调用 add 方法之前被重用,而 result 的槽位在 add 方法执行完毕后被重新用来存储 sum 的值。这就提醒我们,尽可能地减小每个变量的作用域,这样就为虚拟机提供了更多可能性去优化局部变量表,重用槽位,从而减少了内存占用。

深入学习JVM01_Java_25

深入学习JVM01_Java_26

深入学习JVM01_JVM_27

深入学习JVM01_JVM_28

深入学习JVM01_JVM_29

九、JVM编译器

JVM 的一个重要职责就是把字节码拿到实际运行的物理机上去执行,其中重要的一环,就是根据不同的底层操作系统和 CPU 架构,把字节码转化为实际物理机能够识别的机器码

深入学习JVM01_JVM_30

在 JVM 的演进历程中,字节码到机器码的转化环节共经历了三个发展阶段,分别是解释执行阶段、解释执行 + 编译执行阶段、提前编译阶段

深入学习JVM01_Java_31

深入学习JVM01_JVM_32

深入学习JVM01_Java_33

深入学习JVM01_Java_34

混合模式下 Java 代码执行流程

深入学习JVM01_Java_35

深入学习JVM01_Java_36

深入学习JVM01_JVM_37

深入学习JVM01_Java_38

深入学习JVM01_JVM_39

深入学习JVM01_JVM_40

深入学习JVM01_Java_41

深入学习JVM01_Java_42

深入学习JVM01_Java_43

整理了 JIT 和 AOT 在启动速度、内存占用等方面的区别。通过对比,你会发现 JIT 和 AOT 有各自擅长的领域和应用场景。所以在生产环境中实际选择时,最好是结合应用的特点,充分权衡,选择最适合的解决方案。

十、JVM中的字节码

深入学习JVM01_Java_44

深入学习JVM01_Java_45

深入学习JVM01_JVM_46

深入学习JVM01_Java_47

深入学习JVM01_Java_48

深入学习JVM01_Java_49

深入学习JVM01_Java_50

深入学习JVM01_Java_51

我们来解析一下这些字节码。

* 0、2:`bipush 6` 和 `iconst_2`,把常数6和2分别压入操作数栈, `istore_1` 和 `istore_2` 把这两个常数存储到本地变量表对应的索引位置1和2里,相当于执行代码 `int a = 6; int b = 2`。
* 5、6、7: `iload_1` 和 `iload_2` 将本地变量表中的变量a和b加载到操作数栈,`imul` 操作符执行栈顶两个元素的乘法,`istore_3` 将乘法结果存储到本地变量表索引3的位置,相当于执行代码 `int multiply = a * b`。
* 9、10、11:与乘法操作类似,执行除法后,用 `istore 4` 将结果存储到本地变量表索引4位置,相当于执行代码 `int divide = a / b`。
* 14-32:获取可打印输出的对象,输出乘除运算结果。

### 需要注意的点

写Java代码的时候要避免用大数做除法,比如Integer自动拆装箱导致的NPE问题,在进行除法运算时务必做除数为0的判断。

### 优化点

采用乘以反数的方式替代除法计算可以提高运算效率,例如 `a / 2` 可以优化为 `a * 0.5`。我们对于Java代码需要有深度的理解,更好地利用JVM字节码能够帮助我们快速排查问题,优化程序的性能。像Spring、MyBatis等框架底层也大量使用字节码生成技术来提高代码执行效率。

## 如何优化字节码?

假设我们需要操作一个大的字符串数组,可能会使用下面的模式。

```java
public class StringProcessor {
    public static void main(String[] args) { 
        String[] words = {"big str" ,"big str "} ; //大字符串数组
        for (String word : words) {
            if (word != null && !word.isEmpty()) {
                System.out.println(word.toUpperCase());
            }
        }
    }
}

这种写法的问题在于当数组非常大,而且元素有很多为空或为空串的时候,可能会有性能问题。原因是它仍需要遍历每个单词,检查是否为空或空字符串,这是不必要的,我们可以通过 JVM 字节码动态生成一个新的字符串数组,把空的和空字符串剔除再进行操作。

import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class OptimizedStringProcessor {
    public static void main(String[] args) {
        List<String> words = Stream.of("big str").collect(Collectors.toList()); //
                List<String> filteredWords = (List<String>) Proxy.newProxyInstance(words.getClass().getClassLoader(), new Class[]{List.class}, (proxy, method, methodArgs) -> {
                    if (method.getName().equals("get")) {
                        String word = (String) method.invoke(words, methodArgs);
                        if (word == null || word.isEmpty()) {
                            return null;
                        } else {
                            return word.toUpperCase();
                        }
                    } else {
                        return method.invoke(words, methodArgs);
                    }
                });
 
        for (String word : filteredWords) {
            if (word != null) {
                System.out.println(word);
            }
        }
    }
}

使用这种方式,原始的字符串数组会先经过动态代理的处理,也就是利用动态代理生成类的字节码,重新定义获取元素的方法,剔除空元素,将非空元素转为大写,大大提升了性能,还能使代码更加整洁。在实际项目中,特别是大数据处理场景,了解并恰当应用字节码技术可以带来巨大的性能优势。