惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
博客园 - 聂微东
小众软件
小众软件
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
TaoSecurity Blog
TaoSecurity Blog
博客园 - 司徒正美
罗磊的独立博客
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
S
Security @ Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
月光博客
月光博客
S
Secure Thoughts
P
Proofpoint News Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
W
WeLiveSecurity
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
L
LangChain Blog
T
The Blog of Author Tim Ferriss
NISL@THU
NISL@THU
Google DeepMind News
Google DeepMind News
Cloudbric
Cloudbric
H
Hacker News: Front Page
The Last Watchdog
The Last Watchdog
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cisco Blogs
博客园 - 三生石上(FineUI控件)
博客园_首页
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
Project Zero
Project Zero
SecWiki News
SecWiki News
爱范儿
爱范儿
The Register - Security
The Register - Security
AI
AI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Y
Y Combinator Blog
L
Lohrmann on Cybersecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Privacy International News Feed
J
Java Code Geeks
S
Securelist
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Visual Studio Blog

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
JVM缓存对象对GC的影响与优化方案
无所事事O_o · 2026-05-14 · via 博客园_首页

JVM缓存对象对GC的影响与优化方案

背景

当大量缓存对象长时间驻留堆内存时,JVM 的垃圾回收会被明显拖累。问题不在于对象多,而在于这些对象大多晋升到老年代,并持续引用年轻代对象——这直接破坏了分代 GC 的核心假设。

GC性能问题分析

YGC耗时增长的原因

分代垃圾回收基础原理

JVM 采用分代垃圾回收策略,堆内存分为年轻代(Young Generation)和老年代(Old Generation)。这套设计基于"弱代假说"(Weak Generational Hypothesis):

  • 大多数对象都是短命的
  • 存活时间长的对象引用存活时间短的对象的情况很少

YGC标记过程详解

YGC 阶段,垃圾回收器从 GCRoot(全局引用、栈引用、静态变量等)开始做可达性分析。由于 YGC 只回收年轻代,JVM 在扫描上做了一个优化:

扫描中断机制:

  • 扫描引用链时,一旦遇到老年代对象就中断该路径
  • 老年代对象在 YGC 期间不会被回收,继续往下扫描它引用的年轻代对象没有意义
  • 这个机制缩小了扫描范围,YGC 效率因此提升

![](data:image/svg+xml,%3C%3Fxml%20version='1.0'%20encoding='UTF-8'%3F%3E%3Csvg%20width='1px'%20height='1px'%20viewBox='0%200%201%201'%20version='1.1'%20xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><title></title><g stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'><g transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'><rect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

old-gen scanning 的必要性

但有一种情况需要单独处理:被老年代对象引用的年轻代对象。这些对象:

  • 没有被 GCRoot 直接或间接引用
  • 但被老年代中的对象所引用
  • 常规的 GCRoot 扫描会漏掉它们

为此,JVM 引入了 old-gen scanning,专门扫描老年代中可能引用年轻代对象的区域,保证这些对象不被误判为垃圾。

跨代引用与记忆集(Remembered Set)

为了高效实现 old-gen scanning,JVM 用记忆集(Remembered Set)记录跨代引用,核心组件是 card table

![](data:image/svg+xml,%3C%3Fxml%20version='1.0'%20encoding='UTF-8'%3F%3E%3Csvg%20width='1px'%20height='1px'%20viewBox='0%200%201%201'%20version='1.1'%20xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'><title></title><g stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'><g transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'><rect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

  • 老年代按固定大小切分为 card(通常 512 字节)
  • 每个 card 对应 card table 中的一个字节
  • 老年代对象引用年轻代对象时,对应的 card 被标记为 dirty
  • old-gen scanning 只扫描 dirty card,而不是整个老年代
  • 扫描时间与 dirty card 数量正相关

老年代内存占用越大、跨代引用越多,dirty card 数量也跟着涨,YGC 时间就这样被拉长了。

性能瓶颈分析

old-gen scanning 的瓶颈

old-gen scanning 是 YGC 耗时高的主要原因。

这个阶段老年代被切成若干大小相等的区域(stride),每个工作线程处理其中一部分,负责扫描对应的 card 数组和被标记为 dirty 的老年代空间。

缓存数据大量堆积在老年代时:

  • 跨代引用急剧增加,dirty card 比例上升
  • 分代假设被打破,扫描任务本身变得繁重
  • 调整并发参数只能优化任务分配,解决不了扫描量本身的问题

用 JDK API 诊断 Old Gen 压力

排查 YGC 耗时问题时,用代码直接观测 Old Gen 的占用变化比单看 GC 日志更直接。JDK 通过 java.lang.management 包提供了一组 MXBean 接口,可以在运行时查询各内存池的实时状态:

import java.lang.management.*;
import java.util.List;

List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
for (MemoryPoolMXBean pool : pools) {
    MemoryUsage usage = pool.getUsage();
    System.out.printf("%-30s used=%dMB / max=%dMB%n",
        pool.getName(),
        usage.getUsed() / 1024 / 1024,
        usage.getMax() / 1024 / 1024);
}

典型输出:

PS Eden Space                  used=128MB / max=1024MB
PS Survivor Space              used=32MB  / max=128MB
PS Old Gen                     used=3584MB / max=4096MB

Old Gen 长期处于高水位(如 >80%)时,dirty card 数量通常也很高。配合 GarbageCollectorMXBean 一起看,可以区分是 YGC 频率问题还是单次耗时问题:

List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
    System.out.printf("%s: count=%d, totalTime=%dms%n",
        gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
}

如果 Young GC 的 totalTime / count 持续走高但 count 没有明显增加,往往就是 old-gen scanning 的负担在增加。

优化解决方案

JVM堆内存优化

-XX:ParGCCardsPerStrideChunk

控制 G1 old-gen scanning 的并行粒度,指定每个工作线程每次处理的 card 数量。

默认值 256,对应每个区域大小 = 256 × 512B = 128KB。以 4GB 堆为例,区域数量约 32,768 个,远超工作线程数。调大这个参数可以减少调度开销,降低 GC 暂停时间。

但在缓存量巨大的场景下,调整并行粒度能做的有限。dirty card 数量本身过多时,无论怎么切分任务,总扫描量不变。

-XX:+AlwaysPreTouch

JVM 启动时立即分配并初始化所有堆内存页面,避免运行时因按需分页带来的延迟抖动。适合对启动时间不敏感、对运行时稳定性要求高的应用。

-XX:-UseBiasedLocking

禁用偏向锁,减少锁撤销时的 STW 时间,在多线程竞争激烈的场景下有用。代价是损失一些单线程性能。JDK 15 起偏向锁默认禁用(JEP 374),这个参数在高版本中已无意义。

G1垃圾回收器配置原理

-XX:+UseG1GC

启用 G1 垃圾回收器。

-XX:G1HeapRegionSize=16m

设置 G1 区域大小为 16MB。较大的区域减少内部碎片,但回收粒度也变粗。

-XX:G1ReservePercent=25

预留 25% 的堆空间作为对象复制的 to-space,防止晋升失败触发 Full GC。

-XX:InitiatingHeapOccupancyPercent=30

堆使用率达到 30% 时启动并发标记。提前标记可以避免临近满堆时才触发,内存压力会小很多。

-XX:MaxGCPauseMillis

指定最大 GC 暂停时间目标,G1 会尽量满足但不做硬性保证。

设置过小(如 <50ms)时,G1 会主动压缩年轻代来缩短暂停,结果是年轻代频繁填满、Minor GC 次数激增,整体吞吐反而下降。通常设在 100-200ms,再结合 GC 日志调整。

JDK 11内存优化原理

指针压缩技术
  • -XX:+UseCompressedOops:64 位系统下将对象指针从 64 位压缩为 32 位
  • -XX:+UseCompressedClassPointers:压缩类指针,减少元数据内存占用

缓存场景下对象数量庞大,这两个参数能压缩内存占用,顺带提升 CPU 缓存命中率。

ZGC低延迟回收器

ZGC 是低延迟场景下的可选项,最大暂停时间通常不超过 2ms,适合对响应时间要求苛刻的应用。

版本进度:

  • JDK 11:实验性引入
  • JDK 15:正式生产可用,稳定性大幅提升
  • JDK 16:46 个功能增强 + 25 个 bug 修复
  • JDK 17:成熟稳定,Spring Boot 3.x 最低要求版本
  • JDK 21:引入分代 ZGC,吞吐量大幅提升,容器支持优化

启用方式:-XX:+UseZGC

堆外内存方案

解决缓存 GC 问题的根本思路是把缓存数据从堆内移到堆外,让 GC 不再需要感知这部分数据的存在。缓存的生命周期由应用自行管理,不再委托给 JVM。

堆外内存的监控

迁移到堆外之后,MemoryMXBean.getNonHeapMemoryUsage() 看不到 Direct Buffer 的占用,它只涵盖元空间、代码缓存等 JVM 内部区域。监控堆外缓存要用 BufferPoolMXBean

import com.sun.management.BufferPoolMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;

List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);
for (BufferPoolMXBean pool : pools) {
    System.out.printf("%s: count=%d, used=%dMB, capacity=%dMB%n",
        pool.getName(),
        pool.getCount(),
        pool.getMemoryUsed() / 1024 / 1024,
        pool.getTotalCapacity() / 1024 / 1024);
}
// 典型输出:
// direct: count=1024, used=512MB, capacity=512MB
// mapped:  count=0,    used=0MB,   capacity=0MB

堆外内存的上限由 -XX:MaxDirectMemorySize 控制,默认值等于 -Xmx。超过上限会抛出 OutOfMemoryError: Direct buffer memory,上线前要根据缓存规模显式配置这个参数,并通过 BufferPoolMXBean 持续监控。

JDK 原生堆外内存 API

在引入 OHC、EhCache 这类框架之前,JDK 本身就提供了操作堆外内存的能力。了解底层 API 有助于理解框架的实现原理,也能在简单场景下直接使用。

ByteBuffer.allocateDirect(JDK 1.4+)

最基础的方式是通过 ByteBuffer.allocateDirect() 申请直接内存:

// 申请 64MB 堆外内存
ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024 * 1024);

// 写入数据
buf.putInt(0, 42);
buf.putLong(4, System.currentTimeMillis());

// 读取数据
int val = buf.getInt(0);

allocateDirect 分配的内存不在 JVM 堆内,但对象头本身(DirectByteBuffer 实例)还是在堆里。GC 回收这个对象头时,会通过 Cleaner 机制触发堆外内存的释放。依赖 GC 来驱动释放有不确定性,内存密集的场景下最好显式调用:

// 显式释放(JDK 9+ 可用 sun.nio.ch.DirectBuffer)
((sun.nio.ch.DirectBuffer) buf).cleaner().clean();

ByteBuffer 的 API 是字节级操作,适合简单的二进制数据存取。用它实现一个带 LRU 淘汰、序列化、索引管理的缓存框架,工作量很大,这也是 OHC 存在的原因。

MemorySegment + Arena(JDK 21+)

JDK 21 将 Foreign Function & Memory API(FFM API)转正(JEP 454),提供了更安全的堆外内存管理方式:

import java.lang.foreign.*;

// Arena 管理内存的生命周期,try-with-resources 退出时自动释放
try (Arena arena = Arena.ofConfined()) {
    // 分配 1024 字节的堆外内存
    MemorySegment segment = arena.allocate(1024);

    // 通过 ValueLayout 以类型安全的方式读写
    segment.set(ValueLayout.JAVA_INT, 0, 100);
    segment.set(ValueLayout.JAVA_LONG, 4, System.nanoTime());

    int val = segment.get(ValueLayout.JAVA_INT, 0);
}
// Arena 关闭后内存立即释放,不依赖 GC

相比 ByteBufferMemorySegment 的生命周期更明确,Arena 关闭即释放,不存在"GC 还没跑到、内存还没还"的问题。越界访问会抛出异常,而不是静默读写错误地址。

目前 OHC、EhCache 等框架底层仍使用 ByteBufferUnsafe,FFM API 更多出现在需要与 native 库交互的场景。

OHC堆外缓存框架

OHC(Off-Heap-Cache)最初为 Apache Cassandra 开发,后来独立成为单独类库:

  • 项目地址:https://github.com/snazy/ohc
  • 管理的单机堆外内存可达 10G 左右,缓存条目百万量级
  • get 操作平均耗时约 20 微秒,put 约 100 微秒
  • 可通过 OHC#stats() 获取命中率等指标

Maven依赖配置:

<dependency>
    <groupId>org.caffinitas.ohc</groupId>
    <artifactId>ohc-core</artifactId>
    <version>0.7.0</version>
</dependency>

基本使用示例:

import org.caffinitas.ohc.Eviction;
import org.caffinitas.ohc.OHCache;
import org.caffinitas.ohc.OHCacheBuilder;

OHCache<String, String> ohCache = OHCacheBuilder.<String, String>newBuilder()
        .keySerializer(new StringSerializer())
        .valueSerializer(new StringSerializer())
        .eviction(Eviction.LRU)
        .build();

ohCache.put("hello", "world");
System.out.println(ohCache.get("hello")); // world

OHC 提供两种实现:

  • org.caffinitas.ohc.chunked.OHCacheChunkedImpl:为每个段分配堆外内存,适合小型键值对,目前仍处于实验阶段
  • org.caffinitas.ohc.linked.OHCacheLinkedImpl:为每个键值对单独分配堆外内存,适合中大型键值对,线上推荐使用这个

两种实现都把条目缓存在堆外,堆内只保存指向堆外的地址指针。

EhCache堆外缓存框架

EhCache 是老牌 Java 缓存框架,支持堆外缓存和磁盘持久化。

Maven依赖配置:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.0</version>
</dependency>

使用示例:

CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
        .persistence(new File("/users/kinbug/Desktop", "ehcache-cache"));
PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .with(persistentManagerConfig).build(true);

// disk 第三个参数为 true 表示持久化到磁盘
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder()
        .disk(100, MemoryUnit.MB, true);

CacheConfiguration<String, String> config = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource).build();

Cache<String, String> cache = persistentCacheManager.createCache("userInfo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

cache.put("orderId", "order序列化对象");
System.out.println(cache.get("orderId"));
persistentCacheManager.close(); // 确保数据 dump 到磁盘

EhCache配置选项:

  • ResourcePoolsBuilder.heap(10):缓存最大条目数
  • ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB):缓存最大空间
  • withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10))):空闲过期时间
  • withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))):存活过期时间
  • withSizeOfMaxObjectSize(10, MemoryUnit.KB):限制单个缓存对象大小

存储介质建议

使用堆外内存涉及磁盘存储时,建议配 SSD。SSD 的 IOPS 和带宽比 HDD 高出数量级,能明显降低 IO 延迟。哈希算法推荐 CRC32C,CPU 占用低且分布均匀。

混合存储策略

需要排序功能时,可以把数据和索引分开存:堆外内存存完整数据,堆内只维护用于排序的关键字段(价格、数量等)。old-gen scanning 扫描的对象数量大幅减少,排序功能也不受影响。

总结

把缓存数据迁移到堆外之后,Old Gen 占用降低,dirty card 数量随之减少,YGC 的扫描范围缩小,耗时自然下来。FGC 压力也会跟着减轻,OOM 的风险从 JVM 堆转移到了应用自行管理的堆外内存,更可控。

迁移后记得显式配置 -XX:MaxDirectMemorySize,并通过 BufferPoolMXBean 持续监控直接内存用量,避免直接内存溢出。