JVM 概述和参数配置
浅谈 CPU 和线程
线程与进程
操作系统:实现管理进程、存储、设备、文件、交互等功能;说白了就是使硬件协调工作并能输入、处理、输出。
进程: 进程是操作系统为应用程序分配的资源;进程之间的资源相互独立;一个应用程序至少有一个进程;可理解为程序要跑起来所需要的内存空间;程序运行中处理某些指令会影响到占用空间,所以进程大小会变。
线程:每个进程至少有一个线程,同一进程中各线程资源共享;一个线程可能包含多条指令,CPU 执行的一段指令就是线程,CPU 根本不理解自己执行的指令属于哪个线程,CPU 也不需要理解这些,CPU 需要做的事情就是根据 PC 寄存器中的地址从内存中取出后执行,其它没了。Java 虚拟机会为每一个 Java 线程创建 PC 寄存器,在任意时刻,一个 Java 线程总是在执行一个方法。
线程数和 CPU 核心数
前面说到线程就相当于一段指令,单个核心上可以跑任意多个线程,只要内存够就行;线程数和 CPU 核心数没有直接关联。
BIO、NIO、AIO
BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理;Java 的 NIO 库允许 Java 程序使用直接内存(Java 堆外,直接向系统申请的内存)。
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。
Java 虚拟机的基本结构
虚拟机发展史
- 1996 年 1 月,JDK1.0 发布,使用一款叫做 Classic 的虚拟机解释执行 Java 字节码,此虚拟机边解释边运行代码,并非像现在先编译、后执行,速度上肯定快不起来,只能说够用。
- 1997 年 2 月,JDK1.1 发布,同年 Sun 公司通过收购途径获得了 Hotspot 虚拟机。
- 1998 年,JDK1.2 发布,Classic 依然作为默认的 Java 虚拟机。
- 2000 年,JDK1.3 发布,Hotspot 虚拟机成为默认的 Java 虚拟机,直到现在,依然是我们最常用的虚拟机。
- 2014 年,JDK 8 发布,移除永久代、引入 Lambda 与 Stream,至今仍大量存留于生产环境。
- 2017 年,JDK 9 发布,模块化落地、G1 成为默认收集器;此后进入半年一版的节奏,JDK 17、21、25 是近年的 LTS 里程碑。
Java 虚拟机架构
Java 虚拟机是一台执行 Java 字节码的虚拟计算机,它拥有独立运行的机制,其运行的 Java 字节码可以由 Java、Groovy、Scala 等语言编译而成。
- 基本结构

- 结构说明

总结
数据结构中的栈是一块私有的内存空间,每一次方法调用相当于开辟了一块新的栈空间,每一次方法调用结束都有相应的栈被弹出;Java 栈和栈有着相似的含义,只是 Java 栈中主要内容是栈帧,而每个栈帧对应着一个方法。
常用虚拟机参数
堆的参数配置
几乎所有对象都存放在堆中,并且 Java 堆通过垃圾回收机制可完全自动化管理,不需要显式释放对象。
根据垃圾回收机制的不同,Java 堆可能拥有不同的结构。最为常见的是将整个 Java 堆分为新生代和老年代。
最大堆和初始堆
Java 虚拟机会尽量使堆空间趋近于 Xms(初始堆空间),堆空间不足时会自动扩容,但最大容量不会超过 Xmx(最大堆空间);实际工作中可以将 Xms 和 Xmx 的值设为相等,可以减少程序运行时进行的垃圾回收次数,从而提高程序的性能。
Java 堆中存放着对象实例,垃圾对象会被垃圾回收机制自动处理;堆空间不足对应的异常为:java.lang.OutOfMemoryError: Java heap space。
程序中可通过 Runtime.getRuntime().maxMemory() 等方法获取堆信息。
通常建议 -Xmx 不要超过物理内存的 80%。
-Xms1024m -Xmx1024m新生代、老年代配置
- -Xmn 用于配置新生代的大小;设置新生代大小会影响到老年代的大小,这个参数对性能和 GC 有很大影响,一般设置为整个堆空间的 1/3 到 1/4;
-Xmn340m- -XX:SurvivorRatio=Eden/Survivor 用于设置新生代中 Eden 区和 from/to 区(统称为 Survivor 区)的比例(from 和 to 区大小相等、可互换角色);
-XX:SurvivorRatio=2- -XX:NewRatio=老年代/新生代 用于设置老年代和新生代的比例;
# 除了 -Xmn 指定新生代绝对大小,另一种指定新生代大小的方式 -XX:NewRatio=2几个重要的堆分配参数含义

图中的持久区是 JDK 7 及以前的布局;JDK 8 起持久区被移除,改用本地内存中的元数据区。
堆溢出处理
-XX:HeapDumpOnOutOfMemoryError 开启在内存溢出时导出整个堆信息【实测导出文件名为
java_pid<进程号>.hprof,可用 MAT 工具打开查看】;-XX:HeapDumpPath=保存目录 指定堆信息导出路径,和导出堆信息配合使用;
非堆内存的参数配置
方法区配置
方法区和堆一样,所有线程共享;方法区用于保存类信息,如字段、方法、常量池等。
# 永久区/永久代是方法区的主要实现,空间有限
# 大量生成类(代理、动态编译等)导致溢出时,抛 OutOfMemoryError: PermGen space
-XX:PermSize=64m
-XX:MaxPermSize=128m# 永久代被移除,取而代之的是元数据区(使用本地内存)
# 不指定时默认无上限,只受系统内存约束;溢出时抛 OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=256m实测提示:JDK 8 传 -XX:MaxPermSize 只会被警告忽略(ignoring option MaxPermSize=128m; support was removed in 8.0),JDK 21 上该参数已彻底删除,传入直接拒绝启动。
栈配置
-Xss 栈空间较小,栈分配速度快,出栈不会触发 GC;虚拟机可能将局部对象实例存入栈中,以提高效率;
栈空间直接决定了函数调用的最大深度;
每次函数调用都会生成对应的栈帧,从而占用一定的栈空间,当入栈的空间大于最大栈空间,就会抛出 java.lang.StackOverflowError 栈溢出错误;例如递归调用方法可能导致栈溢出。
直接内存配置
-XX:MaxDirectMemorySize 直接内存默认值为最大堆空间 Xmx 的值;直接内存适合申请内存空间次数少,读写频繁的场景,如频繁的 IO 操作、网络连接等。直接内存不一定能触发 GC(除非达到了 MaxDirectMemorySize 的设置值),分配不合理会使系统内存耗尽,导致内存溢出异常
Client 和 Server
Java 虚拟机曾支持 Client 和 Server 两种运行模式:Client 模式启动快,适合桌面与开发机;Server 模式启动慢,但会做更充分的即时优化,执行速度远快于 Client 模式,长期运行的服务首选 Server。
# 32 位系统上两种 VM 并存,用 -client / -server 切换,-version 可查看当前模式
-client# 32 位 Client VM 已随 JDK 9 移除,所有平台只提供 Server VM
# -client 仍可传入,但实测等价于 Server VM,无需再设置
-client其他参数配置
GC 日志参数
查看每次 GC 运行前后堆的大小变化;只有发生了 GC 才会打印,包含 GC 前堆空间、GC 后堆空间、可用堆空间、GC 耗时相关数据;详情版输出更细粒度的各代变化。
# 概要日志
-XX:+PrintGC
# 详细日志
-XX:+PrintGCDetails
# 输出样例(概要)
[GC (Allocation Failure) 16384K->824K(62976K), 0.0008705 secs]# 统一日志::gc 概要,:gc* 更详细
-Xlog:gc
-Xlog:gc*
# 输出样例
[0.071s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 23M->0M(64M) 1.044ms旧参数并未立刻作废,实测 JDK 21 传 -XX:+PrintGC 仍可运行,只是警告已弃用并自动改用 -Xlog:gc。
-XX:+PrintClassHistogram
加上此启动参数,程序启动后,在 Java 的控制台中按下 Ctrl+Break 组合键,就会显示部分当前系统中占用空间最大的对象类名、以及它的实例数量和所占空间大小。Ctrl+Break 是 Windows 控制台的操作方式;JDK 9+ 更推荐用
jcmd <pid> GC.class_histogram达到同样效果。-XX:+PrintVMOptions
在程序运行时打印当前系统的实际运行显式参数;显式传递给虚拟机的参数是人为设置的,虚拟机运行自行设置的参数为隐式参数。
-XX:+PrintCommandLineFlags
在程序运行时打印传递给虚拟机的显式和隐式参数。
-XX:+PrintFlagsFinal
显示所有 Java 虚拟机参数及其当前取值;虚拟机参数众多,开启后有数百行输出(实测 JDK 8 约 730 行,JDK 21 移除了大量废弃参数后反而精简到约 530 行),有兴趣可以玩玩儿。
垃圾回收算法
引用计数法
引用计数是最古老最经典的一种垃圾回收算法;对于一个对象 A,只要有任何对象引用了 A,则 A 的引用计数器(整型)就加一;当引用失效就减一;计数器为 0 时,对象被清除;
缺点:无法解决循环引用(如 A 引用 B,B 引用 A),计数器会消耗性能;Java 的垃圾收集器没有使用这种算法
标记清除法
分为两个阶段,标记阶段:可理解为标记有引用关系的对象(不包含循环引用);清除阶段:清除所有未标记的对象;
缺点:未标记对象可能地址不连续,会影响效率
复制算法
将内存分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中;
优点:若垃圾对象多,则相应需要复制的存活对象就少,复制算法会很高效;这种情况在新生代经常发生;
缺点:要将系统内存折半;
Java 的新生代串行垃圾回收器中,使用了复制算法的思想;其中 from 和 to 区,他们地位、大小相等,用于存放未被回收的对象(幸存空间),因此他们也叫做 survivor 区;
间述:在进行垃圾回收时,eden 区的存活对象会被复制到 from 区(假设 from 区未使用),to 区(假设使用中)的年轻对象也会被复制到 from 区(大对象或老年对象会直接进入老年代,若 from 区满了,也直接进入老年代),此时 eden 区和 to 区的剩余对象就被当做垃圾对象进行回收;
标记压缩法
复制算法适用于新生代,但是在老年代,大部分对象都是存活对象,用复制算法显然复制成本会很高;标记压缩就是适用于老年代的一种算法,和标记清除法一样,只是多了一个压缩再清理的过程,它将所有存活对象压缩到内存一端,然后清理边界外所有内存,解决了标记清除法清理后,地址不连续的问题
分代算法
分代更像一种思想,在前面的算法中,没有一种完美的算法能适用于各种环境,分代算法将内存区间根据对象的特点分成几块,每块根据其特点使用不同的算法;
分区算法
将整个堆空间分成连续的不同小区间,每个小区间独立使用,独立回收,有点类似于并行的意思,分工处理以减少 GC 产生的停顿时间
对象的引用方式
Java 中提供了 4 个级别的引用:强引用、软引用、弱引用、虚引用;强引用相当于一般的对象赋值,其他三个引用可手动实现,对应类型的类可在 java.lang.ref 包中找到;这 4 个引用类型强度逐级减弱
强引用
程序中的一般引用类型;强引用可以直接访问目标对象、在任何时候都不会被系统回收、可能导致内存泄漏;
// 局部变量a被分配在栈上,指向A实例所在的堆空间
// 通过变量a可直接操作该实例,a就是A实例的强引用
A a = new A();
// 此时,A实例有两个引用( a 和 b ),他们都是强引用
A b = a;软引用
如果一个对象只持有软引用,当堆空间不足时会被回收;
使用参数 -Xmx10m 运行下面代码(A 实例自身持有 4MB 数组;JDK 8 与 JDK 21 实测输出一致):
// A 实例自身占用约 4MB 堆空间
static class A {
byte[] data = new byte[1024 * 1024 * 4];
}
A a = new A();
SoftReference<A> softRef = new SoftReference<>(a);
a = null;
System.gc();
// 堆空间充裕,软引用不会被清理,打印 false;softRef.get() 可拿到对象 A 的实例
System.out.println(softRef.get() == null);
// 堆中剩余空间放不下这个 6MB 数组,只有清掉软引用才能分配成功,打印 true
byte[] b = new byte[1024 * 1024 * 6];
System.out.println(softRef.get() == null);弱引用
发现即回收;不管堆空间怎样,只要被 GC 发现就清理;若没有及时发现,它可能存在较长时间;
示例代码:
A a = new A();
WeakReference<A> weakRef = new WeakReference<>(a);
a = null;
// 强引用已断开,但对象尚未被回收,打印 false
System.out.println(weakRef.get() == null);
System.gc();
// 发现即回收,打印 true
System.out.println(weakRef.get() == null);小结:软引用、弱引用都非常适合保存那些可有可无的缓存数据,可提高效率;
虚引用
一个对象若只剩虚引用,和没有引用几乎是一样的;虚引用的作用在于跟踪垃圾回收过程,且必须配合 ReferenceQueue 使用,否则没有意义。
垃圾收集器和内存分配
HotSpot 虚拟机实现了很多不同的收集器,因为直到现在为止还没有万能的收集器,根据场景应用最合适的收集器才是最好的选择。
垃圾回收器的种类
串行回收器 Serial GC
最古老的一种回收器,仅仅使用单线程进行垃圾回收,且回收时线程阻塞;优点:久经考验,无线程交互开销,单核环境下简单高效
- 新生代串行回收器 使用复制算法实现
- 老年代串行回收器 使用标记压缩法实现
# 新生代、老年代都使用串行回收器 -XX:+UseSerialGC新生代并行回收器 Parallel GC
在串行回收器基础上改进,以并行方式减少垃圾回收时间,使用复制算法实现。
Parallel 收集器更关注吞吐量(CPU 运行用户代码时间与 CPU 总消耗时比值),如虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
# 新生代使用 ParallelGC 回收器,老年代使用串行回收器 # JDK 14 起弃用该组合,建议直接使用默认的 G1 回收器 -XX:+UseParallelGC以下参数面向 ParallelGC 回收器(其中 MaxGCPauseMillis 对 G1 同样是核心参数):
- 垃圾停顿时间 -XX:MaxGCPauseMillis 其值是一个大于 0 的整数;值越小停顿越短,但回收会变得更频繁
- 垃圾耗时比例 -XX:GCTimeRatio 垃圾回收耗时占比目标为 1/(1+N);JDK 8 默认 99,即目标是 GC 耗时不超过总时间的 1%,实测 JDK 21 默认已改为 12,对应约 7.7%
- 自适应 GC 调节 -XX:+UseAdaptiveSizePolicy 默认开启;手动设置比较困难的场景下,此参数可自动调整新生代大小、Eden 和 Survivor 区的比例等
新生代 ParNew 回收器(JDK 9 弃用、JDK 14 移除)
ParNew 回收器工作在新生代,相当于是 Serial 收集器的多线程版本,使用的是复制算法实现
ParNew 和 Parallel 的区别是 ParNew 能与老年代收集器 CMS 配合工作。
# 新生代使用 ParNew 回收器,老年代使用 CMS 回收器 # JDK 9 弃用,JDK 14 移除;实测 JDK 21 传参报 Unrecognized VM option 拒绝启动 -XX:+UseConcMarkSweepGC # 新生代使用 ParNew 回收器,老年代使用串行 Serial 回收器 # 同样止步于 JDK 14,高版本建议直接使用默认的 G1 回收器 -XX:+UseParNewGC老年代 ParallelOldGC 回收器
和 ParallelGC 回收器类似,也是多线程并行回收器,关注吞吐量;但它应用于老年代;使用标记压缩法实现。
# 新生代使用 Parallel GC,老年代使用 ParallelOld GC # JDK 14 弃用、JDK 15 移除;此后 -XX:+UseParallelGC 一并启用新生代与老年代的并行回收 -XX:+UseParallelOldGCCMS 回收器(JDK 9 弃用、JDK 14 移除)
CMS 是 concurrent mark sweep 的缩写,意为并发标记清除;这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS 收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。
# 新生代使用 ParNew 回收器,老年代使用 CMS 回收器 # JDK 9 弃用(JEP 291),JDK 14 移除(JEP 363) -XX:+UseConcMarkSweepGC因为 CMS 基于标记清除算法实现,这意味着收集结束时会有大量空间碎片产生,会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配比较大的对象,导致提前触发 Full GC(针对整个堆内存的回收算法,速度比较慢,尽量减少 Full GC 的次数,以保证系统的性能)。
G1 回收器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,JDK 9 之后的默认回收器;使用了分区算法,将堆分为一个个区域,每次回收只回收其中几个。
# JDK 9+ 默认收集器,无需显式开启 -XX:+UseG1GC # 目标停顿时间(默认 200ms),G1 据此规划每次回收的区域数量 -XX:MaxGCPauseMillis=200对象超过分区大小一半时称为大对象(Humongous Object),直接进入老年代的 Humongous 分区。
ZGC 回收器(JDK 11 引入,JDK 15 转正)
面向超大堆的低延迟收集器,停顿亚毫秒级且不随堆大小增长;JDK 21 引入分代模式,JDK 23 起分代成为默认。
# 实测 Temurin JDK 21 开启成功(日志输出 Using The Z Garbage Collector) -XX:+UseZGCShenandoah 回收器(JDK 12 引入,JDK 15 转正)
与 ZGC 定位类似的低延迟收集器;Oracle JDK 不包含它,需使用 Temurin、Corretto 等 OpenJDK 发行版。
# 实测 Temurin JDK 21 开启成功(日志输出 Using Shenandoah) -XX:+UseShenandoahGC
回收器相关的其他参数
# 设置垃圾回收器线程数量,最好和 CPU 数量一致
-XX:ParallelGCThreads=16内存分配
垃圾收集器用于回收分配给对象的内存,对于给对象分配内存,往大方向讲,就是在堆上分配。对象主要分配在新生代的 Eden 区上,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次新生代 GC(Minor GC,发生在新生代的垃圾收集动作)。
大对象直接进入老年代、长期存活的对象将进入老年代;
Eden 区对象经过一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,对象在 Survivor 区中每“熬过”一次 Minor GC,年龄(相当于计数器)就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代;
对象年龄不一定达到阈值才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代;
# 若对象大小大于 1M,直接在老年代分配(仅 Serial/ParNew 生效,Parallel/G1 不理会该阈值)
-XX:PretenureSizeThreshold=1048576
# 设置对象晋升老年代年龄阈值(默认 15,实测 JDK 8 与 JDK 21 一致)
-XX:MaxTenuringThreshold=10生产环境通用参数配置
以下配置以 4 核 8G 独占应用服务器(只跑一个 Java 进程)为基准开发,整段复制、把 xxx.jar 换成实际应用包即可启动;换服务器规格时先按内存账重算堆大小,再对照下表逐项核对。两份的差别只在回收器与 GC 日志写法,内存参数一致。注意 bash 的续行符中间不能插入注释行,否则命令会被截断,所以注释按分组写在命令上方。
| 服务器资源 | 关联参数 | 取值依据 |
|---|---|---|
| 内存 8G | Xms / Xmx = 4g | 独占机器给堆 50%~70%(8G 取 4G),其余留给元空间、直接内存、线程栈和系统页缓存;与其他进程混部时按占比下调 |
| CPU 4 核 | GC 线程数 | ParallelGCThreads 默认取核数,4 核机器无需显式设置 |
| 磁盘 | GC 日志、堆转储路径 | 指向空间充足的目录;堆转储文件大小约等于堆(4G 堆出约 4G 的 .hprof),必须留足 |
| 延迟要求 | 收集器、MaxGCPauseMillis | 接口 SLA 高选 G1/ZGC 并调小停顿目标,吞吐优先用 Parallel |
| 容器限额 | 显式 Xmx | JDK 8u191+ / JDK 10+ 才感知容器内存限额,容器内必须显式设堆,不能依赖默认值 |
# 基准:4 核 8G 独占服务器(仅一个 Java 进程),其他规格按上文表格换算
# 堆给 8G 的一半,Xms=Xmx 避免扩缩容抖动;元空间、直接内存封顶,防止吃穿系统内存
# G1 是 JDK 9+ 默认回收器,停顿目标 200ms 对齐接口 SLA;GC 日志滚动保留 5 个 x 50M
# OOM 先导出堆转储供 MAT 分析再快速退出;屏蔽代码里的 System.gc();时区编码显式声明
java \
-Xms4g -Xmx4g \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=1g \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
"-Xlog:gc*:file=/data/app/logs/gc.log:time,uptime:filecount=5,filesize=50m" \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/app/logs/ \
-XX:+ExitOnOutOfMemoryError \
-XX:+DisableExplicitGC \
-Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8 \
-jar xxx.jar# 基准:4 核 8G 独占服务器(仅一个 Java 进程),其他规格按上文表格换算
# 堆给 8G 的一半,Xms=Xmx 避免扩缩容抖动;元空间、直接内存封顶,防止吃穿系统内存
# JDK 8 默认 Parallel(吞吐优先),延迟敏感或更大堆改 -XX:+UseG1GC(需 8u40+)
# GC 日志滚动保留 5 个 x 50M;OOM 先转储再退出(ExitOnOutOfMemoryError 需 8u92+);屏蔽 System.gc()
java \
-Xms4g -Xmx4g \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=1g \
-XX:+UseParallelGC \
-XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:/data/app/logs/gc.log \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/app/logs/ \
-XX:+ExitOnOutOfMemoryError \
-XX:+DisableExplicitGC \
-Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8 \
-jar xxx.jar
