JVM指南

本文详细介绍了JVM的定义、作用、JVM、JRE与JDK的关系,以及JVM的整体架构,包括类加载子系统、运行时数据区、执行引擎。此外,还探讨了JVM常用参数配置,如跟踪垃圾回收、类加载与卸载,以及堆和栈空间的配置。最后,简要介绍了Class文件的数据类型和结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. JVM概述

1.1 定义

  JVM (Java Virtual Machine 简称 JVM),即 Java 虚拟机,是运行所有 Java 程序的抽象计算机。
  Java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java 虚拟机屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码,即.class文件),就可以在多种平台上运行。

1.2 作用

  跨平台:Java 语言之所以有跨平台的优点,完全是 JVM 的功劳,跨平台性是 JVM 存在的最大亮点。例如:无论是Windows、Linux还是Unix操作系统,安装上 JVM 之后,就都可以支持 Java 程序的运行。
  垃圾回收: Java 语言的诞生,极大的降低了软件开发人员的学习难度,除了 Java 面向对象编程的特性能够降低学习难度以外,还有一个比较重要的点,就是在进行 Java 编程的时候,可以更少的去考虑垃圾回收机制。在C 语言编程过程中,要通过代码手动实现内存垃圾的回收与空间释放,这提升了编程的难度,因为考虑内存空间释放,更多的会涉及到底层的知识,这是非常高的一个门槛。而JVM 拥有自己的垃圾回收机制,为开发人员分担了部分工作。
  Tips:JVM 在 Java 语言中占据了非常重要的地位,学习 JVM 是 Java 技术人员必须要做的事情,目前企业对于 Java 从业者对 JVM 的掌握程度要求非常高,是重点学习内容。

1.3 JVM、JRE 、JDK 的联系

  JDK:全称 Java Development Kit ,开发工具包,是 java 的核心。JDK 包含了JRE,一堆工具类(javac、java)以及 Java 的基础类库(Object,string);
  JRE:全称 java runtime environment。包含了JVM 实现和需要的类库。JRE 是一个运行环境,并非开发工具;
  JVM:它是一个虚拟计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
在这里插入图片描述
  总体来说,我们利用 JDK 开发程序,通过 JDK 的 javac 工具包进行编译,将 Java 文件编译为. class 文件(字节码文件),在 JRE 上运行这些文件的时候,JVM将字节码文件翻译给操作系统,映射到 CPU 指令集或者是操作系统调用,最终完成程序的运行。

2. JVM 整体架构

2.1 结构组成

  JVM 结构主要分为以下几个模块:
在这里插入图片描述

  Class 文件:主要指编译成字节码的 Java 文件,Class 文件才是 JVM 可以识别的文件,所以 Java 文件需要先进行编译才可进入 JVM 执行;
  类加载子系统:类的加载,主要负责从文件系统,或者网络中加载 Class 信息,并与运行时数据区进行交互;
  运行时数据区:主要包括五个小模块,Java 堆, Java 栈,本地方法栈,方法区,寄存器。
  执行引擎:分配给运行时数据区的字节码将由执行引擎执行,执行引擎读取字节码并逐个执行。垃圾回收器就是执行引擎的一部分;
  本地方法接口:与本机方法库进行交互,提供执行引擎所需的本机库;
  本地方法库:它是执行引擎所需的本机库的集合。

  下面通过 6 个步骤来简单描述一个 Java 文件在 JVM 中的流转过程:
在这里插入图片描述

  步骤 1 : 我们的 Demo.java 文件,通过 JDK 的 javac 命令被编译为Demo.class 文件;
  步骤 2 :JVM 有自己的类加载器,将编译好的 Demo.class文件进行了加载;
  步骤 3 :类加载器将加载的 Demo.class文件投放到运行时数据区,供程序执行使用;
  步骤 4 :运行时数据区将字节码文件,交给执行引擎执行;
  步骤 5 :执行引擎执行完毕,会对运行时数据区的数据进行操作,比如说垃圾回收;
  步骤 R :图中有很多步骤 R ,指的是随机发生的步骤,只要我们的程序在运行过程中需要调用本地方法,那么步骤R就会发生。

2.2 类加载子系统

  Java 的动态类加载功能由类加载器子系统处理,处理过程包括加载、链接和初始化,如下图所示。
在这里插入图片描述

  加载:通过三种不同的类加载器对 Class 文件进行加载,我们也可以自定义类加载器。
  链接:对加载好的 Class 文件进行字节码、静态变量、方法引用等进行验证和解析,为初始化做准备。
  初始化:类加载的最后阶段,对类进行初始化。

2.3 运行时数据区

  运行时数据区共包含如下 5 个模块:方法区,Java 栈,本地方法栈,堆和程序计数器。
在这里插入图片描述

  方法区(Method Area):所有的类级数据将存储在这里,包括静态变量。每个 JVM 只有一个方法区,它是共享资源;
  堆区(Heap Area):所有对象及其对应的实例变量和数组将存储在这里。每个 JVM 也只有一个堆区域。由于方法和堆区域共享多个线程的内存,所存储的数据不是线程安全的;
  栈区(Stack Area):对于每个线程,将创建单独的运行时栈。对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。所有局部变量将在栈内存中创建。栈区域是线程安全的,因为它不共享资源;
  PC寄存器(PC Registers):也称作程序计数器。每个线程都有单独的 PC 寄存器,用于保存当前执行指令的地址。一旦执行指令,PC 寄存器将被下一条指令更新;
  本地方法栈(Native Method stacks):保存本地方法信息。对于每个线程,将创建一个单独的本地方法栈。

  Tips:方法区和堆为共享内存区域,多线程环境下共享这两块内存区域。 Java 栈,本地方法栈和程序计数器为线程私有部分,私有数据对其他线程不可见。

2.4 执行引擎

  执行引擎包含三个模块:解释器,JIT 编译器和垃圾回收器。
在这里插入图片描述

  解释器:解释器是作用于字节码的解释。解释器的缺点是当一个方法被调用多次时,每次都需要一个新的解释;
  JIT 编译器:JIT 编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助,但是当它发现重复的代码时,将使用 JIT 编译器,这提高了系统的性能;
  垃圾回收器(Garbage Collector):收集和删除未引用的对象。可以通过调用 System.gc() 触发垃圾收集。

3. JVM常用参数配置

3.1 IntelliJ IDEA添加JVM运行参数

  打开 “Run->Edit Configurations”菜单,然后在VM Options中添加相应的 JVM 参数。
在这里插入图片描述

3.2 JVM常用参数

3.2.1 跟踪垃圾回收

在这里插入图片描述

-XX:+PrintGC 参数
  参数作用:垃圾回收跟踪中的常用参数。使用这个参数启动 Java 虚拟机后,只要遇到 GC,就会打印日志。
  示例

[GC (System.gc())  3933K->792K(251392K), 0.0054898 secs]
[Full GC (System.gc())  792K->730K(251392K), 0.0290579 secs]

  结果分析
  GC 与 Full GC:代表垃圾回收的类型;
  System.gc():代表引发方式,是通过调用 gc 方法进行的垃圾回收;
  3933K->792K(251392K):代表之前使用了 3933k 的空间,回收之后使用 792k 空间,言外之意这次垃圾回收节省了 3933k - 792k = 3141k 的容量。
  251392K 代表总容量;
  0.0054898 secs:代表了垃圾回收的执行时间,以秒为单位。

-XX:+PrintGCDetails 参数
  参数作用:垃圾回收跟踪中十分常用的参数,打印比-XX:+PrintGC 参数更详细的日志。
  示例

[GC (System.gc()) [PSYoungGen: 3933K->792K(76288K)] 3933K->800K(251392K), 0.0034601 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 792K->0K(76288K)] [ParOldGen: 8K->730K(175104K)] 800K->730K(251392K), [Metaspace: 3435K->3435K(1056768K)], 0.0217628 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

  结果分析
  PSYoungGen:代表「年轻代」的回收;
  ParOldGen:代表「老年代」的回收;
  Metaspace:代表「元空间」的回收,JDK 的低版本也称之为永久代。

-XX:+PrintHeapAtGC 参数
  参数作用:对堆空间进行跟踪时十分常用的参数,可以在每次 GC 前后分别打印堆的信息。注意,是 GC 前后均打印,打印两次。
  示例

{Heap before GC invocations=2 (full 1):
 PSYoungGen      total 76288K, used 792K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000)
  eden space 65536K, 0% used [0x000000076b400000,0x000000076b400000,0x000000076f400000)
  from space 10752K, 7% used [0x000000076f400000,0x000000076f4c6030,0x000000076fe80000)
  to   space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000)
 ParOldGen       total 175104K, used 0K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
  object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1c00000,0x00000006cc700000)
 Metaspace       used 3420K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 371K, capacity 388K, committed 512K, reserved 1048576K
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 76288K, used 0K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000)
  eden space 65536K, 0% used [0x000000076b400000,0x000000076b400000,0x000000076f400000)
  from space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x000000076fe80000)
  to   space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000)
 ParOldGen       total 175104K, used 705K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
  object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1cb07a0,0x00000006cc700000)
 Metaspace       used 3420K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 371K, capacity 388K, committed 512K, reserved 1048576K
}

  结果分析:从结果来看,在 GC 前后,打印了两次堆空间信息,并且将 PSYoungGen 以及 ParOldGen 进行了更加详细的日志打印。

-XX:+PrintGCTimeStamps 参数
  参数作用:在每次 GC 发生时,额外输出 GC 发生的时间,该输出时间为虚拟机启动后的时间偏移量。需要与 -XX:+PrintGC 或 -XX:+PrintGCDetails 配合使用,单独使用 -XX:+PrintGCTimeStamps 参数是没有效果的。
  示例

0.247: [GC (System.gc())  3933K->760K(251392K), 0.0114098 secs]
0.259: [Full GC (System.gc())  760K->685K(251392K), 0.0079185 secs]

  结果分析:可以看到,与 -XX:+PrintGC 参数打印的结果相比,唯一的区别就是日志开头的 0.247 与 0.259。此处 0.247 与 0.259 表示, JVM开始运行 0.247 秒后发生了 GC,开始运行 0.259 秒后,发生了 Full GC。

3.2.2 跟踪类的加载与卸载

在这里插入图片描述

-XX:+TraceClassLoading 参数
  参数作用:跟踪类的加载。
  示例

[Opened C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.lang.Object from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.util.ArrayList$SubList from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.util.ListIterator from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.util.ArrayList$SubList$1 from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded DemoMain.TracingClassParamsDemo from file:/D:/GIT-Repositories/GitLab/Demo/out/production/Demo/]
[Loaded java.lang.Class$MethodArray from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar]

  结果分析
  第一行:Opened rt.jar。打开 rt.jar,rt.jar 全称是 Runtime,该 jar 包含了所有支持 Java 运行的核心类库,是类加载的第一步;
  第二行:加载 java.lang.Object。Object 是所有对象的父类,是首要加载的类;
  第三、四、五行:加载了 ArrayList 的相关类,示例代码中使用到了 ArrayList,因此需要对该类进行加载;
  第六行:加载测试类 TracingClassParamsDemo ;
  第七行:加载 java.lang.Class 类,并加载类方法 MethodArray;
  第八行:加载 java.lang.Void 类,因为main 函数是 void 的返回值类型,所以需要加载此类;
  第九、十行:加载 java.lang.Shutdown 类, JVM 结束运行后,关闭 JVM 虚拟机。
  从以上对日志的分析来看,JVM 对类的加载,不仅仅加载我们代码中使用的类,还需要加载各种支持 Java 运行的核心类。类加载的日志量非常庞大,此处仅对重点类的加载进行日志的解读。

-XX:+TraceClassUnloading 参数
  参数作用:跟踪类的卸载。由于系统类加载器加载的类不会被卸载,并且只加载一次,所以普通项目很难获取到类卸载的日志。

-XX:+PrintClassHistogram 参数
  参数作用:打印、查看系统中类的分布情况。
示例:
在这里插入图片描述

  结果分析
  num:自增的序列号,只是为了标注行数,没有特殊的意义;
  instances:实例数量,即类的数量;
  bytes:实例所占子节数,即占据的内存空间大小;
  class name:具体的实例。
  取第 3 条日志进行解析:系统当前状态下,java.lang.String 类型的实例共有 2700 个,共占用空间大小为 64800 bytes。

3.2.3 配置堆空间与栈空间

在这里插入图片描述

-Xms 和 -Xmx 参数
  参数作用
  -Xms:设置堆的初始空间大小;
  -Xmx:设置堆的最大空间大小。
  Tips:多数情况下,这两个参数是配合使用的,设置完初始空间大小后,为了对堆空间的最大值有一个把控,还需要设置堆空间的最大值。
  示例
  设置堆的初始空间大小为 10 M,设置堆的最大空间大小为 20 M。(此处设置的空间大小为实验数据,具体值的设置,需要根据不同项目的实际情况而定。)
  在 VM Options 中配置参数 -Xms10m -Xmx20m。

-Xmn 参数
  参数作用:专门设置年轻代 PSYoungGen 大小的参数。
  示例
  设置堆的初始空间大小为 10 M,最大空间大小为 20 M,单独设置年轻代 PSYoungGen 的大小为 5m。
  在 VM Options 中配置参数-Xms10m -Xmx20m -Xmn5m。
  Tips堆空间大小 = 年轻代空间大小 + 老年代空间大小,此处设置堆空间初始大小为 10m,年轻代大小为 5m, 那么老年代的空间大小为 10m - 5m = 5m。

-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数
  Tips:在 JDK 1.8 之前,所有加载的类信息都放在永久代中。但在 JDK1.8 时,永久代被移除,取而代之的是元空间(Metaspace)。
  参数作用
  -XX:MetaspaceSize :元空间发生 GC 的初始阈值;
  -XX:MaxMetaspaceSize :设置元空间的最大空间大小,如果不手动设置,默认基本是机器的物理内存大小。
  Tips:-XX:MetaspaceSize 这个参数并非设置元空间初始大小,而是设置的发生 GC 的初始阈值。举例来说,如果设置 -XX:MetaspaceSize 为 10m,那么当元空间的数据存储量到达 10m 时,就会发生 GC。
  示例
  设置元空间发生 GC 的初始阈值的大小为 10 M,设置元空间的最大空间大小为 20 M。
  在VM Options中配置参数 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=20m

-Xss 参数
  参数作用:设置单个线程栈大小,一般默认 512 - 1024kb。
  Tips:由于单个线程栈大小跟操作系统和 JDK 版本都有关系,因此其默认大小是一个范围值, 512 - 1024kb。在平时工作中,-Xss 参数使用到的场景是非常少的,因为单个线程的栈空间大小使用默认的 512 - 1024kb 就能够满足需求。
  如果在某些场景下,单个线程的栈空间发生内存溢出,多数情况是由于迭代的深度达到了栈的最大深度,导致内存溢出。这种异常情况,多数会选择优化方法,并不是立刻提升栈空间大小,因为盲目提升栈空间大小,是一种资源浪费。

4. Class文件

4.1 Class文件数据类型

  根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
  无符号数:有u1、u2、u4、u8四种类型, 分别代表修饰的对象大小为 1 个字节、2 个字节、4 个字节和 8 个字节;这就像Java的整数类型有byte、short、int、long,无符号数则有u1、u2、u4、u8。
  :表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表名都以“info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。

4.2 Class 文件结构

  Class 文件是一组以(8位bit的)byte 字节为基础单位的二进制流。下图为 Class 文件的字节码示意图:
在这里插入图片描述

  其中绿色框圈起来的为标准的 Class 文件的样子。左侧为软件本身提供的辅助信息,记录当前行前面总共有多少个 byte (或者说多少个 u1 ),用于快速定位数据(通过数据偏移量的方式。右侧为直接以编辑器打开 Class 文件的样子,显示为乱码。

4.2.1 魔数(Magic Number)

  定义:每个 Class 文件的头 4 个字节(u4)称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。所有 Class 文件,魔数均为 0xCAFEBABE。

4.2.2 次版本号与主版本号

  定义:魔数后边紧跟是 u2 的次版本号,次版本号后面是 u2 的主版本号,次版本号与主版本号共同标识了我们所使用的的 JDK 版本,如 JDK 1.8.0 版本的次版本号为 u2 大小,用字节码表示为 00 00,主版本号也是 u2 大小,用字节码表示为 00 34。
  Tips:如果 Class 文件的开头 8 个字节分别为 CA FE BA BE 00 00 00 34,那么我们可以确定,这是一个 JVM 可识别的 Class 文件,且使用的 JDK 1.8.0的版本进行的编译,因为前4个字节魔数为 CA FE BA BE 符合标准,后4 个字节 00 00 00 34 为 JDK 1.8.0的版本。
  版本号对照表:

JDK 版本16进制字节码
1.8.000 00 00 34
1.7.000 00 00 33
1.6.000 00 00 32
1.5.000 00 00 31

4.2.3 常量池计数器与常量池

  定义
  常量池计数器:记录常量池中的常量的数量。由于常量池中的常数的数量是不固定的,所以在常量池的入口放置了一个 u2 类型的数据,来代表常量池容器记数值(constant_pool_count)。常量池计数器也是无符号数类型数据。
  常量池:Class 文件中的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最多的数据项目之一,同时它还是 Class 文件中第一个出现的表类型数据项目。
  常量池中存储的数据:常量池中主要存放着两种常量,字面量(Literal)和符号引用(Synbolic References)。
  字面量包括:文本字符、声明为 final 的常量值、基础数据类型的值等;
  符号引用包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

4.2.4 访问标志(access_flags)

  定义:在常量池结束之后,紧接着的 2 个字节(u2类型)代表访问标志,用于识别一些类或接口层次的访问信息。
  访问标志类型对应表

标志类型对应标志值标志意义
ACC_PUBLIC0x0001是否为 public 类型
ACC_FINAL0x0010是否被声明为 final 类型
ACC_SUPER0x0020是否允许使用 invokespcial 字节码指令的新语义
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为抽象类型
ACC_SYNTHETIC0x1000标识这个类并非由用户代码生成
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

4.2.5 类索引与父类索引

  定义:类索引(this_class)和父类索引(super_class)都是一个 u2 大小的数据。
  类索引:确定当前类的全限定名。
  父类索引:确定当前类的父类的全限定名。

4.2.6 字段表计数器和字段表

  定义:接口索引集合后边紧跟的是u2的字段表计数器,字段表计数器后边紧跟的是字段表。
  字段表计数器(fields_count):记录字段表中字段的数量,为无符号数类型。
  字段表(fields):字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量(即静态变量)以及实例变量(即:非静态变量),但不包括在方法内部声明的局部变量。字段表为表类型结构。

4.2.7 方法表计数器与方法表

  定义:字段表后边紧跟的是方法表计数器,方法表计数器后边紧跟的是方法表。
  方法表计数器(methods_count):记录方法表中字段的数量,为u2的无符号数类型。
  方法表(methods):存储了当前类或者当前接口中的 public 方法,protected 方法,default 方法,private 方法等。方法表为表结构类型。
  Tips:Class文件是通过Java文件编译而来的,如果文件中有方法,就会将方法的信息存储到方法表,并通过方法表计数器进行方法的计数。

4.2.8 属性表计数器与属性表

  定义:方法表后边紧跟的是属性表计数器,属性表计数器后边紧跟的结构为属性表。至此,Class 文件的全部结构就讲解完了。回顾之前的知识,Class 文件结构以魔数开头,以属性表结尾。
  属性表计数器(attributes_count):记录属性表中属性的数量,为u2的无符号数类型。
  属性表(attributes):属性表与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不能识别的属性。

  这一篇主要讲了JVM的概念和整体结构,详细的模块将在后面的文章中介绍。
  ps:以上内容来自对慕课教程的学习与总结。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值