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
iload_1
invokevirtual #3
return
方法又是包含了操作数栈和局部变量表两部分:
- 操作数栈用于临时存放数据。
- 局部变量表用于存放方法中的局部变量。
- 在执行到"iconst_0"时,会将0放入操作数栈中。
- 在执行"istore_1"时,会将0从操作数栈中转移到局部变量表的1索引位置。
- 在执行"iload_1"时,会将0从局部变量表中复制一份到操作数栈中。
- 此时操作数栈和局部变量表中都存在0这个值
- 在执行"iinc 1 by 1"时,会在局部变量表执行0+1的操作,此时局部变量表中1索引的值是1。
- 在执行"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
这个案例在企业级项目开发中的意义:排查生产环境的代码是否和本地一致。
-
-
- 运行阶段:对于已经编译后的文件,使用java命令,或使用编译器运行,则会执行其中编写的逻辑。
- 编译阶段:对于源代码阶段的文件,使用javac命令,或使用编译器运行,则会生成一个.class文件(字节码文件),包含了类转化成为的字节码指令。
还没有评论,来说两句吧...