JVM 虚拟机(一)导学与字节码文件组成

2023-12-07 06:04

本文主要是介绍JVM 虚拟机(一)导学与字节码文件组成,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、实战 JVM - 基础篇

初识 JVM

什么是 JVM?

Java Virtual Machine(JVM),中文翻译为 Java 虚拟机

image.png

JVM 的功能

  1. 解释和运行:对字节码文件中的指令进行实施的解释成机器码,让计算机执行。
  2. 自动为对象和方法分配内存:自动的垃圾回收机制,不用自己编写代码进行垃圾回收。
  3. 即时编译:对热点代码进行优化,提升执行的效率。
即时编译

image.png

因为 Java 虚拟机比起诸如 C 或 C ++ 多了一个**解释**功能,这个功能可以支持 Java 语言的**跨平台**的特性,可以将语言转化为对应的系统的机械码,对应的,比起 C 和 C++ 性能要差一些。

跨平台性:
如果一个编程语言没有跨平台的特性,需要在每个不同的操作系统上进行适配才能让程序在该系统上运行。这意味着针对每个操作系统,需要重新编写或修改程序的部分代码,以使其能够与该操作系统兼容。
举例来说,假设某个编程语言的代码只能在特定操作系统(比如只能在 Windows 上运行)上执行,那么如果希望这段代码能在其他操作系统(如 macOS、Linux 等)上运行,就需要对其进行适配或重写,以使其能够兼容其他操作系统的特性和功能。
这种情况下,开发者需要针对不同的操作系统编写特定版本的代码或者使用特定的工具来进行跨平台适配,以确保程序能够在不同的操作系统上运行。这也是为什么具备跨平台能力的编程语言和工具(比如 Java、Python 等)受到青睐,因为它们可以使开发者编写一次代码,然后在不同操作系统上运行而无需针对每个操作系统进行修改。

image.png
当 Java 虚拟机发现某一段代码是热点代码,即这段代码会在后面的程序中多次使用的时候,JVM 会将这个代码解释并优化成机械码,然后将其保存在内存中,方便之后的调用。
上述的操作使得 Java 虚拟机实现了即时编译的功能(JIT),能保证性能接近 C 和 C++ 甚至在特定的情况下超越。

常见的 JVM

image.png

Java 虚拟机规范
  • 《 Java 虚拟机规范 》由 Oracle 指定,内容包含了虚拟机在设计和实现的时候需要遵守的规范,主要包含 class 字节码文件的定义、类和接口的加载和初始化、指令集等内容。
  • 这个规范是对虚拟机的要求,但不一定是 Java 虚拟机,其他语言生成的 class 字节码文件之上也可以运行虚拟机。
HotSpot 发展史

image.png

字节码文件详解

Java 虚拟机的组成

image.png
JVM(Java Virtual Machine,Java 虚拟机)是运行 Java 字节码的虚拟计算机,它由多个组成部分构成,包括以下主要组件:

  1. 类加载器(Class Loader):负责将类的字节码加载到 JVM 中。它将类文件加载到内存,并生成相应的 Class 对象,用于在运行时创建实例、访问字段和调用方法。
  2. 运行时数据区域(Runtime Data Areas):包括方法区、堆、虚拟机栈、本地方法栈和程序计数器。
    1. 方法区(Method Area):存储类信息、常量、静态变量等数据。
    2. 堆(Heap):存放对象实例。
    3. 虚拟机栈(Java Virtual Machine Stacks):存储方法调用的局部变量、部分结果和返回值。
    4. 本地方法栈(Native Method Stack):为 Java 方法调用 Native 方法(非 Java 语言编写的方法)服务。
    5. 程序计数器(Program Counter):记录当前线程执行的字节码指令地址。
  3. 执行引擎(Execution Engine):负责执行编译后的字节码。包括解释器和 JIT 编译器。解释器逐行解释字节码,而 JIT 编译器将热点代码(频繁执行的代码)编译为本地机器代码以提高执行效率。
  4. 本地方法接口(Native Interface):允许 Java 代码调用本地库中的方法。
  5. 本地库接口(Native Libraries):Java 虚拟机使用的本地库,提供与操作系统交互的能力。

其中的本地方法指的是使用本地语言(如 C 或 C++)编写的方法,通过这些方法,虚拟机可以实现与操作系统或者硬件之间的交互,通过本地方法,虚拟机可以实现如下的功能:

  1. 操作系统交互:Java 中的一些功能需要直接与操作系统进行交互,比如文件操作、网络通信、图形界面等。为了实现这些功能,Java 使用本地方法调用操作系统提供的相关功能。
  2. 性能优化:某些对性能要求极高的任务可能通过本地方法来实现,因为本地语言(如C或C++)可以更接近底层硬件,可以更高效地执行某些计算密集型操作。
  3. 特定硬件功能:有时,需要访问特定硬件设备或执行底层操作,这时候本地方法可以提供直接的接口,以便与硬件交互。

字节码文件

以正确的方式打开文件

字节码文件保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读

jclasslib:https://github.com/ingokegel/jclasslib/releases
同样的,可以选择 IDEA 插件,也叫做 jclasslib,后期主要使用这个插件进行操作
字节码文件的五个组成部分

  1. 基础信息:魔数、字节码文件对应的 Java 版本号,访问表示(public、final 等等)父类和接口
  2. 常量池:保存了字符串常量、类或接口名、字段名主要在字节码指令中使用
  3. 字段:当前类或者接口声明的字段信息
  4. 方法:当前类或者接口声明的方法信息的字节码指令
  5. 属性:类的属性,比如源码的文件名、内部类的列表等等

详解字节码文件的组成

image.png
这里我们主要说基本信息、常量池和方法

基本信息

魔数、字节码文件对应的 Java 版本号,访问表示(public、final 等等)父类和接口

  1. 魔数

魔数通常用于识别特定格式的文件,比如图像文件(如JPEG、PNG)、可执行文件(如ELF、PE)或归档文件(如ZIP、RAR)。

  • 文件时无法通过文件的拓展名来确定文件的类型的,文件的拓展名可以随意的修改,不影响文件的内容
  • 软件使用文件的头几个字节(文件头)去验证文件的类型,如果文件不符合着各种类型就会报错
  • Java 字节码文件中,将文件头称为 magic 魔数

我们通过查看文件的十六进制编码就可以在开头看到这些标识。
image.png

  1. 主副版本号

要了解主副版本号,我们首先来回顾一下 JDK(Java Development Environment)
JDK 主要包含了下面这些内容

  • JRE(Java Runtime Environment): JDK 中包含了 JRE 的所有内容,因此 JDK 也可以用作运行 Java 程序的环境。JRE 包括 Java 虚拟机(JVM)和运行 Java 应用程序所需的核心类库
  • Java 命令行工具: JDK 提供了一系列的命令行工具,用于编译、调试、执行和分析 Java 应用程序。比如 java(运行 Java 程序)、javac(编译 Java 程序)、javadoc(生成 API 文档)、jdb(Java 调试器)等。
  • Java API 和类库: JDK 包含了大量的 Java API 和类库,提供了丰富的功能和工具,用于开发各种类型的应用程序。这些类库包括 Java 核心类库、I/O、网络、集合框架、GUI(Swing、JavaFX)、安全性等各个领域的支持。
  • 开发工具(IDE)支持: JDK 可与各种 Java 开发工具(如 Eclipse、NetBeans、IntelliJ IDEA 等)集成使用,提供开发和调试 Java 应用程序的环境
  • 调试器和性能分析工具: JDK 提供了调试器(jdb)和一系列性能分析工具,用于调试和分析 Java 应用程序的性能问题,帮助开发人员诊断和解决代码中的错误和性能瓶颈。
  • JavaFX 和其他扩展: JDK 中还包括 JavaFX 等扩展技术,用于开发富客户端应用程序。此外,JDK 也支持其他扩展和工具,如 Java Mission Control(JMC)等。

我们编写的代码会通过 JDK 中的编译工具编译成字节码文件之后交给虚拟机运行,这个编译过程会首先检查我们编写的代码和当前 JDK 的版本是否兼容。

  • 判断当前字节码文件的版本和运行时的 JDK 是否兼容,

image.png
我们在开发中可能会遇到下面的报错:
比如我们引用别人的库的时候,调用其中的 class 文件如果和我们当前运行的环境不匹配的
image.png
通过上面的 主版本号 - 44 的公式,我们可以看出这个需要的是 JDK 8 但我们的环境是 JDK 6,这时候我们有两种解决方法:

  1. 升级 JDK 版本
  2. 降低我们依赖的版本号或者更换依赖

显然后一种最稳健,不会对项目中的其他代码产生影响。

常量池

为了避免相同的内容的重复定义,节省空间
Java中的常量池主要分为两种:

  1. 编译期常量池(Compile-Time Constant Pool): 这是在编译期间确定的常量池。它包含类文件中的常量池表(Constant Pool Table),存储着类、方法、接口等的符号引用、字面量常量等。
  2. 运行时常量池(Runtime Constant Pool): 这是JVM在运行时动态生成的常量池。它是方法区的一部分,用于存放在编译期无法确定的常量,比如使用String类的intern()方法在运行时将字符串对象添加到常量池中。

Java常量池的使用方式和注意事项:

  • 字符串常量池:Java中的字符串常量池是String类中特有的,它存储着所有的字符串字面量。当创建字符串常量时,如果常量池中已经存在相同内容的字符串,则不会再创建新的对象,而是直接引用已存在的字符串对象。
  • 使用final关键字:通过使用final关键字定义的常量会被优化,并在编译时被放入常量池中。这些值在运行时无法修改。
  • 自动装箱:在使用自动装箱时,如果值在-128到127之间,Java会将其缓存起来,使得对象引用指向同一个常量。
  • 字符串的intern()方法:调用字符串的intern()方法会将字符串对象添加到常量池中(如果池中已经存在相同内容的字符串,则返回池中的对象引用)。
  • 编译器优化:编译器会对一些简单的表达式进行计算,并在编译期间将计算结果存入常量池中。

比如看下面这个例子,我们如果写了很多相同的字符串,如果字节码文件采取右边的存储方法,光是存储这些字符串就占了大量的内存,为了节省内存的开支,JVM 虚拟机有一块单独的内存,用于存储被编译器或运行时系统使用的常量和字面量。它包含了编译时生成的各种字面量和符号引用。
image.png
下面我们来利用 jclasslib 来观察一下常量池的机制,先编写一个简单代码示例

package com.kq.Basic;/*** 演示常量池的特点*/
public class ConstantPools {String a = "abc";String b = "abc";
}

image.png
编译后使用 jclasslib 打开
image.png
image.png
我们打开就可以发现一个指向的是字符串池子中的对象,一个是字符串常量池中的具体元素
image.png
这时候查看一下这段程序的执行流程,在方法区域,观察字节码文件

0 aload_0        // 将当前对象的引用加载到操作数栈上
1 invokespecial #1 // 调用超类构造方法4 aload_0        // 将当前对象的引用加载到操作数栈上
5 ldc #7 <abc>   // 将字符串常量"abc"加载到操作数栈上
7 putfield #9    // 将操作数栈上的"abc"赋值给当前对象的字段// (此处 #9 是字段的索引或标识)10 aload_0       // 将当前对象的引用加载到操作数栈上
11 ldc #7 <abc>  // 将字符串常量"abc"加载到操作数栈上
13 putfield #15  // 将操作数栈上的"abc"赋值给当前对象的字段// (此处 #15 是字段的索引或标识)16 return        // 返回

注意看这一步 将操作数栈上的"abc"赋值给当前对象的字段 我们通过地址就会发现是我们上面提到的 #7 对象,这时候 a 指向的就是字符串常量池中的对象。

image.png
我们顺着这个地址过去,发现是指向的这个对象,这个对象又指向了字符串常量池的另一个对象
image.png,那为什么不直接使得指向的这个对象就是字符串呢,还要多一个中间的步骤?

字符串常量池是为了节省内存并提高性能而设计的特殊机制,主要原因如下:

  • 字符串频繁被使用:在许多应用中,字符串是经常使用的数据类型之一。由于字符串的不变性质,它们很容易被共享和重复使用,因此将字符串放入共享的常量池中可以减少内存占用。
  • 字符串不变性:字符串在 Java 中是不可变的,即一旦创建就不能被修改。如果将字符串直接放入常量池中,可以保证字符串的不可变性,避免了在运行时更改字符串的可能性。
  • 字符串比较效率:使用字符串常量池可以加快字符串的比较速度。由于字符串常量池中的字符串是唯一的,可以通过比较引用地址来进行比较,提高了比较的效率。
  • 其他类型的常量(例如整数、浮点数、布尔值等)在 Java 中也有常量池,但并不像字符串常量池那样明确,因为它们通常不会像字符串那样频繁地被使用。字符串常量池的设计是为了针对字符串这种特殊的不变性和频繁使用性质而提出的一种优化策略。
方法

当前类或者接口声明的方法信息,这个部分放在下个博客中详细说明。

这篇关于JVM 虚拟机(一)导学与字节码文件组成的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/464748

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入

Springboot项目启动失败提示找不到dao类的解决

《Springboot项目启动失败提示找不到dao类的解决》SpringBoot启动失败,因ProductServiceImpl未正确注入ProductDao,原因:Dao未注册为Bean,解决:在启... 目录错误描述原因解决方法总结***************************APPLICA编

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security

SpringBoot多环境配置数据读取方式

《SpringBoot多环境配置数据读取方式》SpringBoot通过环境隔离机制,支持properties/yaml/yml多格式配置,结合@Value、Environment和@Configura... 目录一、多环境配置的核心思路二、3种配置文件格式详解2.1 properties格式(传统格式)1.

Apache Ignite 与 Spring Boot 集成详细指南

《ApacheIgnite与SpringBoot集成详细指南》ApacheIgnite官方指南详解如何通过SpringBootStarter扩展实现自动配置,支持厚/轻客户端模式,简化Ign... 目录 一、背景:为什么需要这个集成? 二、两种集成方式(对应两种客户端模型) 三、方式一:自动配置 Thick

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与

Java.lang.InterruptedException被中止异常的原因及解决方案

《Java.lang.InterruptedException被中止异常的原因及解决方案》Java.lang.InterruptedException是线程被中断时抛出的异常,用于协作停止执行,常见于... 目录报错问题报错原因解决方法Java.lang.InterruptedException 是 Jav