栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JVM指南

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JVM指南

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 FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.lang.Object from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.util.ArrayList$SubList from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.util.ListIterator from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.util.ArrayList$SubList$1 from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded DemoMain.TracingClassParamsDemo from file:/D:/GIT-Repositories/GitLab/Demo/out/production/Demo/]
[Loaded java.lang.Class$MethodArray from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.lang.Void from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.lang.Shutdown from C:Program FilesJavajdk1.8.0_152jrelibrt.jar]
[Loaded java.lang.Shutdown$Lock from C:Program FilesJavajdk1.8.0_152jrelibrt.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:以上内容来自对慕课教程的学习与总结。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/754834.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号