JVMの前置理论篇

1、JVM基本概念

        1.1、入门案例

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

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

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

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

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

        输出.java中的hello world:


        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:

        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++去打开,则会看到:

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

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

GitCode - 开发者的代码家园

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

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

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

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

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

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


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

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

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

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

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

        相关案例:

public class ConstantPoolTest {
    public static final String a1 = "我爱北京天安门";
    public static final String a2 = "我爱北京天安门";

    public static void main(String[] args) {
        ConstantPoolTest constantPoolTest = new ConstantPoolTest();
    }
}

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

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

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

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

        再看一个相关案例:

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

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索引:

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

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

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

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


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

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

        经典问题:

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

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 <java/lang/System.out : Ljava/io/PrintStream;>

iload_1
invokevirtual #3 <java/io/PrintStream.println : (I)V>
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:将局部变量表中对应索引的值复制到操作数栈中

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


        1.5、字节码解析工具

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

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

        部署项目:

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

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

jad com.itheima.springbootclassfile.controller.UserController

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

        

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值