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.0 | 00 00 00 34 |
1.7.0 | 00 00 00 33 |
1.6.0 | 00 00 00 32 |
1.5.0 | 00 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_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final 类型 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespcial 字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为抽象类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码生成 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
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:以上内容来自对慕课教程的学习与总结。