JVM 调优
JVM 调优:Heap 多大合适、GC 怎么选
Elasticsearch 运行在 JVM 上,JVM 参数的配置直接影响集群的稳定性和吞吐量。堆内存配小了频繁 GC,配大了 GC 暂停时间又太长。本文结合 ES 官方建议和生产实践经验,讲清楚 Heap 和 GC 怎么调。
Heap 大小:双上限法则
ES 官方给的 Heap 配置规则很明确:设为物理内存的 50%,但不要超过 31GB(更准确说是 32GB - 1 byte,即 32600MB 左右)。
# config/jvm.options 或 jvm.options.d/heap.options
-Xms16g
-Xmx16g
为什么是 50%?
Lucene 大量使用操作系统的 Page Cache 来缓存磁盘上的索引文件。如果你给 JVM 32GB 堆,Page Cache 就只剩 32GB。ES 的查询性能很大程度上依赖 Page Cache 命中(文件系统级缓存,比堆内存快且不会被 GC 管理),所以必须给 OS 留出足够空间做磁盘缓存。
为什么不能超过 31GB?
JVM 有一个叫 Compressed OOPs(Compressed Ordinary Object Pointers,压缩对象指针)的优化。当堆小于 32GB 时,JVM 可以用 32-bit 指针(实际是 35-bit,通过 8 字节对齐实现)代替 64-bit 指针,节省大量内存。超过 32GB 后,压缩指针失效,所有对象指针变为 64-bit,内存消耗暴增约 40%-50%。
| 堆大小 | 压缩指针 | 实际效果 |
|---|---|---|
| < 32GB(~32600MB) | 开启 | 内存效率高,对象头紧凑 |
| = 32GB ~ 32600MB | 开启(临界值) | 刚好在边界上 |
| > 32GB | 关闭 | 对象指针撑大,同等数据量需要更多堆 |
踩坑案例:某日志集群节点内存 128GB,运维认为 ES 给内存越多越好,设了 -Xmx64g -Xms64g。上线后发现同样数量的索引文档,堆内存使用率比之前(32GB 堆)高了 60%。定位发现是压缩指针关闭导致的,调整到 30GB 后,数据能缓存更多文档,查询反而更快。
GC 选择:G1GC 是默认王者
ES 各版本 GC 变迁
| ES 版本 | 默认 GC | 备注 |
|---|---|---|
| ES 2.x | CMS | 当时主流选择 |
| ES 5.x - 6.x | CMS | 文档推荐 CMS,但 G1 已有不少用户 |
| ES 7.0+ | CMS → G1 过渡 | -XX:+UseG1GC 越来越多 |
| ES 7.10+ | G1GC | 官方推荐 |
| ES 8.x | G1GC(废弃 CMS) | CMS 在 JDK 14+ 已移除 |
G1GC 比 CMS 好的地方:
- 可预测的暂停时间:
-XX:MaxGCPauseMillis=200设定最大暂停目标,G1 会尽量满足(非硬性保证)。 - 不会产生内存碎片:G1GC 以 Region 为单位做整理(compaction),CMS 使用标记清除算法会随运行时间增长产生碎片,最终触发 Full GC。
- 大对象分配更友好:G1 有专门 Humongous 区域处理大对象。
配置建议:
# jvm.options 中已内置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30
G1ReservePercent=25:预留 25% 堆空间,防止并发标记阶段无空间分配导致 evacuation failure。InitiatingHeapOccupancyPercent=30:堆使用率 30% 时启动并发标记周期(并发标记与业务线程并发执行,不影响暂停时间)。ES 默认设为 30 而非 JVM 默认 45,是因为 ES 的对象分配速率高,需要更早开始 GC。
GC 日志解读
ES 默认开启 GC 日志,位置在 logs/gc.log。关键行解读:
[2026-05-21T10:30:15.123+0800][info][gc] GC(1234) Pause Young (G1 Evacuation Pause) 12M->8M(1024M) 15.23ms
[2026-05-21T10:30:18.456+0800][info][gc] GC(1235) Pause Initial Mark (G1 Evacuation Pause) 145M->140M(1024M) 28.45ms
[2026-05-21T10:30:22.789+0800][info][gc] GC(1236) Pause Mixed (G1 Evacuation Pause) 230M->120M(1024M) 95.67ms
我该关注什么:
| 日志特征 | 含义 | 应对 |
|---|---|---|
频繁 Young GC |
对象分配快、生命周期短 | 正常现象,暂停 < 50ms 无需担心 |
Mixed GC 耗时 > 1s |
Old 区回收压力大 | 检查 heap 使用率,是否该扩堆 |
出现 Full GC |
灾难性事件!G1 的 Full GC 是串行单线程的 | 立刻排查:是否内存泄漏、堆是否不够 |
Humongous Allocation |
分配了超过 Region 一半大小的对象 | 检查是否有超大 aggregation bucket 或大请求 |
To-space exhausted / Evacuation Failure |
G1 来不及回收,触发退化 | 增大 G1ReservePercent 或调大堆 |
bootstrap.memory_lock: true 防止 swap
ES 的 bootstrap.memory_lock 将 JVM 堆内存锁定在物理内存中,禁止 OS 将堆 swap 到磁盘:
# elasticsearch.yml
bootstrap.memory_lock: true
为什么重要:一旦 JVM 堆被 swap 到磁盘,一次 GC 可能从几十毫秒变成几十秒(需要先从磁盘读回内存),直接导致节点掉出集群,触发不必要的分片迁移。
注意:开启后需要配置 OS 的 memlock 限制:
# /etc/security/limits.conf
elasticsearch - memlock unlimited
# 或 systemd
# /etc/systemd/system/elasticsearch.service.d/override.conf
[Service]
LimitMEMLOCK=infinity
监控 heap_used_percent
在 ES 监控指标中,JVM 堆使用率是最关键的告警项:
| 告警级别 | heap_used_percent | 动作 |
|---|---|---|
| 正常 | < 75% | 无需操作 |
| 预警 | 75% - 85% | 关注趋势,分析是否有数据增长或查询变化 |
| 警告 | 85% - 95% | 可能频繁 GC,检查 GC 日志、考虑扩堆或加节点 |
| 紧急 | > 95% | 即将 OOM,立刻排查:是否有大查询、聚合爆桶 |
查看方式:
GET /_nodes/stats/jvm
返回的 jvm.mem.heap_used_percent 即当前堆使用率。
补充监控:
jvm.mem.pools.old.used/jvm.mem.pools.old.max— Old 区使用情况。Old 区持续增长是内存泄漏的典型信号。jvm.gc.collectors.old.collection_count/collection_time— 老年代 GC 次数和累计耗时。累计耗时增长过快说明 GC 压力大。jvm.threads.count— 线程数。突然飙升可能是有查询风暴。
小结
ES 的 JVM 调优可以浓缩为三句话:Heap 设物理内存 50% 且 ≤ 31GB,用 G1GC 并关注 Mixed GC 频率,开启 memory_lock 并监控 heap_used_percent。JVM 参数调优并非越复杂越好,ES 默认的 jvm.options 已经包含了社区最佳实践,大多数情况下只需要动 Heap 大小即可。