JVM(一)

JVM(一)

码农世界 2024-05-19 后端 62 次浏览 0个评论

1、JVM基本概念

        1.1、入门案例

        在提到JVM的基本概念前,有必要说明下Java类的执行过程:

  • 源代码阶段:当我们用记事本或编译器编写了一个类,但是没有去手动或自动编译的阶段。通常源代码文件以.java结尾。

    JVM(一)

    • 编译阶段:对于源代码阶段的文件,使用javac命令,或使用编译器运行,则会生成一个.class文件(字节码文件),包含了类转化成为的字节码指令。

      JVM(一)

              使用javac命令生成了.class文件,但是目前用记事本打开都是乱码:

      JVM(一)

      • 运行阶段:对于已经编译后的文件,使用java命令,或使用编译器运行,则会执行其中编写的逻辑。

                输出.java中的hello world:

        JVM(一)


                Java虚拟机就是在使用java命令运行.class文件时,对.class文件中的字节码指令进行解释,翻译成让计算机系统能识别的语言。

                1.2、JVM

                相信通过了上面的入门案例,已经对java程序从编写到执行逻辑所经历的过程有了一定的认识,那么接下来简单的介绍一下什么是JVM:

                JVM的英文名称:Java Virtual Machine,简称JVM。是 Java 程序的运行环境,它是一个抽象的计算机,可以在其上运行 Java 字节码,具有以下的特性:

        • 字节码执行:JVM 可以执行 Java 编译器生成的字节码文件,这些字节码文件包含了与特定平台无关的指令集。
        • 内存管理:JVM 负责管理程序运行时所需的内存,包括堆内存、栈内存、方法区等。它通过垃圾回收器来管理堆内存中的对象生命周期。
        • 性能优化:JVM 包含了即时编译器(Just-In-Time Compiler,JIT)和其他优化技术,可以提高 Java 程序的执行效率。
        • 跨平台特性:JVM 的设计使得 Java 程序具有跨平台性,相同的 Java 字节码可以在任何支持 JVM 的平台上运行。
        • 安全性:JVM 提供了安全管理和沙箱机制,可以防止恶意代码对系统造成破坏。        

                  其中字节码执行这一点在上面的小案例中已经有所体现。关于内存管理,是Java语言相对于C,C++进行的优化,可以在适当的时机自动进行垃圾回收,而不需要手动的编写回收逻辑。即时编译技术,简单来说,就是将程序中的一些热点代码保存在内存中,而不需要在用到的时候再反复进行编译,学过Redis等缓存中间件的都知道,从内存中读取的效率较高,即时编译技术也是缓存思想的一种体现。而之所以java需要实时解释,是因为其具有跨平台特性,比如在windows上和linux上的机器码都是不一样的,需要实时进行适配(不知可否认为是适配器思想的体现)。


                  一些常见的JVM:

          JVM(一)

                  1.3、JVM的组成
          • 类加载器(Class Loader):负责将类文件加载到内存中,并生成对应的 Class 对象。类加载器通常按照特定的规则搜索类文件,并将其加载到 JVM 中。

          • 运行时数据区域:包括堆、栈、方法区、程序计数器和本地方法栈等不同的区域,用于存储程序运行时所需的数据和信息。

          • 执行引擎(Execution Engine):负责执行字节码指令,包括解释器和即时编译器(JIT 编译器)。解释器逐条解释执行字节码指令,而即时编译器可以将热点代码编译成本地机器码以提高执行速度。

          • 本地方法接口(Native Interface):允许 Java 程序调用本地方法和库,从而实现与底层系统交互的能力。(在java源码中经常会看到被native关键字修饰的方法。)

          • 安全管理器(Security Manager):负责确保 Java 程序的安全性,限制程序对系统资源的访问。

          • 垃圾回收器(Garbage Collector):负责管理堆内存中的对象,当对象不再被引用时,垃圾回收器会回收这些对象占用的内存空间。

          • 本地方法栈(Native Method Stack):用于执行本地方法,即使用 C 或 C++ 等语言编写的方法。

          • 执行环境接口(Execution Environment Interface):定义了 JVM 与宿主操作系统和硬件交互的接口。

                    1.4、字节码文件的解析

                    在入门案例中演示过,如果直接用windows系统自带的记事本去强行打开.class文件,会出现乱码,无法看到字节码指令。

                    如果使用安装了十六进制插件的nodepad++去打开,则会看到:

            JVM(一)

                    依旧是乱码,但是地址为00000000的十六进制的前四位有些特殊(后面会解释。)

                    为了能查看字节码指令,可以通过jclasslibd的软件进行实现,github地址如下:

            GitCode - 开发者的代码家园

                     安装完成后再去打开案例中的.class文件:

                    一般信息为字节码文件的基本信息的统计,其中主版本号-44即为当前jdk的版本号。

            JVM(一)

                    一般信息中,也包含一个模数,即是用安装了十六进制插件的nodepad++打开的.class文件中,地址为00000000的十六进制的前四位:cafebabe。 

                    这个信息是固定存在的,即只要是经过编译后的.class文件,打开后都会在相同的位置有cafebabe。这样设计的目的,是为了进行文件类型的区分。

                    例如我现在有一张图片,初始是.jpg格式,然后我将其改成了.avi格式,在通过邮件点击文件选择用图片方式打开,也是能打开的,内容也是和.jpg格式是相同的。即只根据文件的后缀无法判断文件类型。

                    软件会通过文件的前几个字节去校验文件类型(虽然文件的格式可以任意更改,但是文件头中的固定字节是无法修改的):

            JVM(一)


                    每个类被加载时都会创建一个常量池(Constant Pool),其中存储着该类中使用的常量信息,包括字面值常量、符号引用等。常量池在类加载时被创建,并且在编译阶段就已经确定其中的内容。

            • 存储字面值常量:常量池存储了类中出现的字面值常量,如字符串、基本数据类型的常量值等。

            • 存储符号引用:常量池还存储了对类、方法、接口等的符号引用,这些引用在运行时将被解析为实际的内存地址或方法入口。

            • 减少重复:常量池中的常量是唯一的,可以避免类文件中出现重复的常量定义,节省内存空间。

            • 动态连接:常量池中存储的符号引用在运行时会被动态解析,可以提高程序的灵活性和效率。

              JVM(一)

                      相关案例:

              public class ConstantPoolTest {
                  public static final String a1 = "我爱北京天安门";
                  public static final String a2 = "我爱北京天安门";
                  public static void main(String[] args) {
                      ConstantPoolTest constantPoolTest = new ConstantPoolTest();
                  }
              }
              

                      在字段标签中虽然有两份,但是常量池中指向的是同一个地址:

              JVM(一)

                      通过常量值索引找到常量池的位置:

              JVM(一)

                      再通过字符串字面量的索引找到对应的值:

              JVM(一)

                      那为什么不能直接通过索引找到字面量的值,还要先通过常量值索引找到常量池?

                      再看一个相关案例:

                      注意看第三个变量,变量名和值是相同的:

              public class ConstantPoolTest2 {
                  public static final String a1 = "abc";
                  public static final String a2 = "abc";
                  public static final String abc = "abc";
                  public static void main(String[] args) {
                      ConstantPoolTest2 constantPoolTest = new ConstantPoolTest2();
                  }
              }
              

                       它们同时指向了常量池中的8索引:

              JVM(一)

                       又通过8索引找到10索引的字面量

              JVM(一)

                      还可以看到,a2的名字指向了常量池中的9索引,它的字面量很显然是变量名

              JVM(一)

                      而abc的名字指向了常量池中的10索引,它的字面量即可能是变量名,也可能是变量的值(在案例中变量名和变量值都是abc):

              JVM(一)

                       为了进行区分,就要先去找到对应变量名在常量池中String类型的引用,然后根据引用去找到值。


                      接口和字段则是该类实现的接口以及成员变量。

                      方法是本类中的所有方法(如果当前类继承了父类,需要显式的重写父类中的方法,否则即使继承了也不会展示父类中的方法),其中init是构造方法的意思(每个类中都有一个默认的无参构造),可以看到方法的字节码指令。

              JVM(一)

                      经典问题:

                      下面这段程序的运行结果是多少?

              public class Demo1 {
                  public static void main(String[] args) {
                      int i=0;
                      i = i++;
                      System.out.println(i);
                  }
              }
              

                      我们以字节码指令的层面去解答这个问题:

              iconst_0

              istore_1

              iload_1

              iinc 1 by 1

              istore_1

              getstatic #2

              iload_1

              invokevirtual #3

              return

                      方法又是包含了操作数栈和局部变量表两部分:

              • 操作数栈用于临时存放数据。
              • 局部变量表用于存放方法中的局部变量。
                1. 在执行到"iconst_0"时,会将0放入操作数栈中。
                2. 在执行"istore_1"时,会将0从操作数栈中转移到局部变量表的1索引位置。
                3. 在执行"iload_1"时,会将0从局部变量表中复制一份到操作数栈中。
                4. 此时操作数栈和局部变量表中都存在0这个值
                5. 在执行"iinc 1 by 1"时,会在局部变量表执行0+1的操作,此时局部变量表中1索引的值是1。
                6. 在执行"istore_1"时,又会将0从操作数栈中转移到局部变量表的1索引位置。(覆盖了局部变量表中上一步得到的1!)

                        结论:上面的代码运行结果是0。

                        通过分析字节码指令,也可以得出一些常用的字节码指令的含义:

                • iconst:将值推送到操作数栈中
                • istore:将值从操作数栈中转移到局部变量表对应的索引上
                • iload:将局部变量表中对应索引的值复制到操作数栈中

                          属性表明了类的属性,包括源码的文件名,有无内部类等信息

                  JVM(一)


                          1.5、字节码解析工具

                          常见的字节码解析工具,除了上面提到过的jclasslib外,还有阿里的arthas工具,由于在Spring底层入门中讲解过安装和使用,在本节中展示一个查看线上程序反编译结果的案例。

                         在linux服务器上,提前准备好了两个jar包,分别是某个项目的jar包和arthas工具的jar包:

                  JVM(一)

                          部署项目:

                  JVM(一)

                          启动arthas工具,并连接上当前java项目的进程:

                  JVM(一)

                          假设我们需要看com.itheima.springbootclassfile.controller下UserController类的反编译结果,只需要使用jad命令即可

                  jad com.itheima.springbootclassfile.controller.UserController

                  JVM(一)

                          这个案例在企业级项目开发中的意义:排查生产环境的代码是否和本地一致。

                          

转载请注明来自码农世界,本文标题:《JVM(一)》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,62人围观)参与讨论

还没有评论,来说两句吧...

Top