JVM是什么

JVM 是 Java Virtual Machine 的缩写,是 Java 程序的运行环境。它是一个虚拟的计算机,在实际的计算机上运行,它通过将 Java 字节码解释或编译为机器码来执行 Java 程序。

JVM 有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统,它屏蔽了与具体操作系统平台相关的信息。

Java的跨平台实现

Java 语言的跨平台特性就是依靠 JVM 来实现的。就像我们日常所使用的一些应用程序一样,针对不同的操作系统,有不同版本的 JVM ,我们只需要进行相应的安装配置即可

Java 源代码文件通过编译之后,产生的以 .class 结尾的字节码文件可以直接运行在 JVM 上面,从而实现跨平台,同一份字节码文件通过 JVM,可以不加修改就运行在不同操作系统上,也就是所说的 一次编译,处处运行

Java 运行流程

JVM内存结构

JVM 的运行时数据区分为:方法区本地方法栈程序计数器

JVM 内存结构

方法区

方法区只是一个逻辑上的概念,随着 JDK 的更新,具体实现如下,参考:方法区、永久代、元空间的区别

JDK 版本 方法区的实现 运行时常量池的位置
JDK 6 永久代 永久代
JDK 7 永久代
JDK 8+ 元空间

方法区用于存放类似于元数据信息方面的数据的,比如类信息,常量,静态变量,常量池,编译后代码等,类加载器将 .class 文件搬过来就是先丢到这一块上

元空间使用的是本地内存,它不存在于 JVM 当中

主要存放一些数据,例如:对象实例、数组等

堆被划分为了新生代老年代两块区域,内存占比 1:2

新生代又被划分为了 EdenFrom SurvivorTo Survivor 这三块区域,内存占比 8:1:1

新创建的对象一般都存在于新生代当中,如果对象在经历了 15 次垃圾回收之后还处于存活状态,则会转移到老年代当中,或者是创建的较大对象,由于比较占用内存,就会直接放入老年代当中,使用 -XX:PretenureSizeThreshold 可以手动设置大对象大小限制参数

堆内存空间

用来执行方法的内存区域,每一个被调用的方法都是一个栈帧,栈帧包含方法的基本信息,栈顶永远是当前线程正在执行的方法,方法执行完毕后,就会被弹出栈空间。

栈内存空间

本地方法栈

本地方法栈用于执行一些非 java 语言编写的方法,这些方法都用 native 关键字进行修饰

程序计数器

占用极小内存,用于记录当前线程所要执行的下一条指令的地址

是唯一一个在 JVM 规范中没有规定任何内存溢出问题的区域。

内存溢出与栈溢出

栈溢出问题 (StackOverflowError):由于当前栈空间不够用,只压栈不出栈而导致,一般是由递归或是两个方法循环引用所引起

内存溢出问题 (OutOfMemoryError):由于线程太多,占用过多内存,无法创建新的线程所导致

类加载

类的生命周期

从类被加载到虚拟机内存中开始,到释放内存总共有 7 个阶段:加载验证准备解析初始化使用卸载。其中,验证准备解析三个部分统称为链接

加载

在该阶段,Java虚拟机通过类加载器将编译后的 Java 字节码文件加载到内存中,并生成对应的Class对象。Java虚拟机会根据类的全限定名来定位类的字节码文件,并读取该文件的二进制数据。

链接

  1. 验证:在加载完成后,JVM 会对字节码文件进行验证,以确保其符合 Java 虚拟机规范,并且不会对 JVM 和计算机系统造成危害。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
  2. 准备:在准备阶段,JVM 会为类变量(static 变量)分配内存空间,并设置默认初始值。非 final 的 static 变量的初始值通常为 0 或 null。
  3. 解析:解析是将符号引用转换为直接引用的过程。在 Java 中,符号引用指的是用名称来标识一个类、方法、变量等,而直接引用指的是内存地址。Java 程序中的符号引用通常在编译期间生成,而直接引用通常在运行期间生成。解析过程包括将类、方法、变量等的符号引用解析为对应的直接引用,以便于 JVM 执行程序。

初始化

在初始化阶段,JVM 会为类变量设置正确的初始值。如果一个 static 变量在定义时已经指定了初始值,JVM 会使用该初始值来初始化变量。否则,JVM 会使用默认值来初始化变量。在初始化阶段,还会执行静态代码块,以便进行一些初始化操作。类的初始化是在第一次使用该类时完成的。

使用

在类初始化完成后,可以使用该类创建对象或调用该类的方法。此时,JVM 会将对象和实例变量等数据存储在堆内存中,方法和局部变量等数据存储在栈内存中。

卸载

当类不再被引用时,JVM 会卸载该类,并释放相应的内存空间。JVM 会在垃圾回收过程中进行类卸载。如果一个类被加载后,一直没有被使用,则可能会被 JVM 卸载。

类加载器

根加载器 (BootStrap ClassLoader):加载 JAVA_HOME/jre/lib/rt.jar 中存放的 Java 核心类

扩展加载器 (Extension ClassLoader):加载 JAVA_HOME/jre/lib/ext/ 中的扩展 jar 包

应用加载器 (App ClassLoader):加载 classpath 下的 jar 包

自定义的类加载器 (Custom ClassLoader):可以根据需要加载类。继承 ClassLoader 类,重写 findClass() 方法实现自己的类加载逻辑

双亲委派机制

流程:当一个类需要被加载时,首先让其父类加载器尝试加载该类,如果父类加载器可以找到并加载该类,则直接返回父类加载器加载的类。如果不能,则继续向上传递加载请求,直至到达根加载器。如果根加载器无法加载该类,又会层层向下传递加载请求,最终由自己来完成类加载,如果类无法加载,会抛出 ClassNotFoundException

好处:避免重复加载;防止核心类被覆盖,保证安全性

双亲委派机制

垃圾回收(Garbage Collection)机制

垃圾标记

要进行垃圾回收,第一步是要找到垃圾,这里就有两个算法:引用计数法可达分析法

引用计数法(几乎不使用)

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。

缺点:

  • 需要额外的空间来存储引用计数
  • 需要额外的时间来维护引用计数
  • 无法处理循环引用问题

可达分析法

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

GC Roots 一般包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;

  • 方法区中静态属性引用的对象;

  • 方法区中常量引用的对象;

  • 本地方法栈中 JNI(Java Native Interface)引用的对象;

  • 系统类加载器加载的类所引用的对象。

垃圾回收算法

标记 - 清除算法

先遍历堆内存空间,标记出所有可达对象,标记完成后,再次遍历堆内存空间,将未被标记的对象全部清除,释放空间。

缺点:

  1. 效率不高:因为在标记的时候要遍历一次,清除的时候还要遍历一次
  2. 产生内存碎片:回收后,产生不连续的内存块,导致内存不能被合理分配使用

标记 - 清除算法

复制算法

将内存分为两个大小相同的区域,实际上就是新生代的 From Survivor、To Survivor 这两块区域。

将当前内存区域的可达对象复制到另一个空区域中,复制完成后,原来的区域被清空并作为新的空闲区域。每一次垃圾清理都使用这两块区域进行轮流复制。

优点:

  1. 效率高:直接复制,没有标记过程,而且只遍历一次
  2. 不会出现内存碎片

缺点:

  1. 需要更多的内存:始终有一半的闲置内存来用于垃圾回收
  2. 对象复制后,内存地址发生变化,需要额外时间来修改栈帧中的引用地址
  3. 如果可达对象比较多,那么它们都需要复制,效率就会低下

复制算法

标记 - 整理算法

主要用于对老年代等空间较大的区域进行垃圾回收

先遍历堆内存空间,标记出所有可达对象,标记完成后,再次遍历堆内存空间,对标记了的对象进行移动整理,最后对垃圾对象进行回收

优点:

  1. 不会出现内存碎片
  2. 不需要利用额外的内存空间

缺点:

  1. 效率比前面两个算法都低一些,因为它相当于是两者的结合
  2. 同样也需要修改栈帧中的引用地址

标记 - 整理算法

分代收集理论

不同的对象的存活时间不一样,所以针对不同的对象采用不同的垃圾回收算法

新生代中的对象存活时间较短,所以使用 复制算法

老年代中的对象存活时间较长,所以使用 标记 - 清除算法标记 - 整理算法

Young GC 和 Full GC

Young GC 和 Full GC 是 JVM 中两个不同的垃圾回收阶段。Young GC 通常是在年轻代进行的轻量级垃圾回收,它的目标是快速清除不再使用的对象,以便保持年轻代的干净。而 Full GC 则是在整个堆内存上进行的重量级垃圾回收,它的目标是清除整个 Java 堆,包括年轻代和老年代,以释放更多的内存空间。Young GC 和 Full GC 通常是通过不同的垃圾回收器实现的,这些垃圾回收器在不同的垃圾回收算法和策略上有所不同。

需要注意的是,不同的 JVM 实现可能会有不同的 Young GC 和 Full GC 触发机制,具体触发条件可能会因 JVM 版本、堆大小、垃圾回收器类型、堆内存使用情况等因素而异。

Young GC 的触发

Young GC 通常是在年轻代对象数量达到一定阈值时触发的。JVM 会在 Eden 区域被填满之后,将其中存活的对象复制到 Survivor 区域,并清空 Eden 区域。当 Survivor 区域也被填满时,JVM 会将其中存活的对象复制到另一个 Survivor 区域,并清空第一个 Survivor 区域。这个过程可能会多次重复,直到某些对象到达了老年代,或者在 Survivor 区域中存活的时间超过了一定的阈值。当这些条件被满足时,JVM 会触发 Young GC,清除年轻代中不再使用的对象。

Full GC 的触发

Full GC 通常是在老年代对象数量达到一定阈值时触发的,或者是在 Perm 区域或 Metaspace(取决于 JVM 实现)不足时触发的。Full GC 可能需要扫描整个堆内存,因此它通常比 Young GC 更慢,并且可能会导致应用程序中断。因此,Full GC 的触发应该尽可能地减少,以避免影响应用程序的性能。

七大垃圾回收器

参考:如何掌握 Java JVM 垃圾回收器,有哪些基础知识

垃圾回收器 分类 作用位置 算法 特点 使用场景
Serial 串行 新生代 复制算法 响应速度优先 单 CPU 环境下的 Client 模式
Serial Old 串行 老年代 标记 - 整理 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的备选方案
ParNew 并行 新生代 复制算法 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记 - 整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记 - 清除 响应速度优先 集中在互联网站或 B/S 系统服务端上的 Java 应用
G1 并发 新生代和老年代 标记 - 整理和复制算法 响应速度优先 面对服务端应用,将来替换 CMS

到 JDK 8 为止,默认的垃圾收集器是 Parallel Scavenge 和 Parallel Old,从 JDK 9 开始,G1 收集器成为默认的垃圾收集器。