[{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/201/","section":"Tags","summary":"","title":"201","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/2026/","section":"Tags","summary":"","title":"2026","type":"tags"},{"content":" 2026 年重回 Java：一条面向有经验工程师的现代化路径 # 如果你离开 Java 几年，或仍把「Java」等同于 Java 8 时代的写法，真正卡住你的往往不是语法，而是选型面、发布节奏与工具链同时变了。许多团队仍停留在「能编译就行」的升级策略上，却在生产环境遭遇 GC 默认值变化、安全 API 收紧、测试框架断言风格不一致等隐性成本。下文按工程决策组织：每一节先说明动机，再交代机制与约束，给出可落地的最小示例，并列出常见误区。文中对无法从官方文档逐句核实的比较性判断，会标注为演讲者观点；对幻灯片与官方文档不一致的配置键，以后者为准并说明差异。\n图：How to (Re)start Your Java Journey in 2026 — 回归 Java 的现代化全景主题。\n构件坐标与可审计交付 # 为什么 # 企业系统长期依赖第三方库。没有统一、可检索的构件仓库，团队会在「同名不同产物」、缺失校验和、难以审计的私服之间消耗精力。\n机制与约束 # Maven Central 由 Sonatype 运营，发布要求明确 groupId、artifactId、version 三元组，并对发布物要求 GPG 签名与 checksum（见 发布坐标要求）。同一 GAV 在 Central 上应可重复解析——本次未在 Central 文档中检索到「禁止覆盖同版本」的逐字表述；供应链治理还需结合组织内的依赖锁定与 SBOM 实践。\n怎么做 # \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Gradle 侧等价声明见 依赖管理基础。\n常见误区 # 把「Central 上能下到包」等同于「构建完全可复现」——还需锁定插件版本、记录 mvnw/gradlew 与 CI 镜像。 将「比其他生态更成熟」当作平台事实——演讲者观点；官方文档讨论的是坐标与签名，而非跨语言生态排名。 多元生态下的选型负担 # 为什么 # Java 的优势之一是标准与实现并存；代价是回归者要在构建工具、云原生框架、Jakarta EE 实现之间做一连串决定，决策成本被戏称为 the agony of choice。\n图：the agony of choice — 选型多元带来创新，也抬高决策成本。\n机制与约束 # 构建：Maven、Gradle、Bazel Java Rules 均可作为主干；团队通常沿用既有仓库约定，而非「选最强的」。 云原生框架：Helidon、Micronaut、Spring Boot、Quarkus、Open Liberty 等并列存在——演讲者观点（清单），无单一规范页一次性枚举。 企业标准层：Jakarta EE 定义 API；WildFly、Payara、Open Liberty、各国商业中间件等为实现（幻灯片以 Logo 墙展示多样性）。 怎么做 # 新仓库优先对齐团队已有栈；若必须评估，用同一业务场景（CRUD + DB + 观测）做两周 spike，而不是读排行榜。\n常见误区 # 把「框架多」等同于「必须全学」——多数团队只需精通一个运行时 + 了解迁移路径。 忽视 Jakarta 命名空间迁移（javax.* → jakarta.*）对依赖树的连锁影响。 图：Jakarta EE 实现与中间件 Logo 墙 — 标准统一，厂商实现分散。\nJDK 发行版、JVM 与 Native Image # 为什么 # 「装一个 Java」在 2026 年仍不够：HotSpot 与 Eclipse OpenJ9 行为不同，云厂商与社区也提供各自构建；部分工作负载还需要 AOT 原生镜像以降低冷启动。\n机制与约束 # 语言特性通过 JEP 流程进入 OpenJDK。 GraalVM Native Image 文档写明：\u0026ldquo;Native Image is a technology to compile Java code ahead-of-time to a binary\u0026rdquo;，CLI 为 native-image。 发行版选型可参考 whichjdk.com（幻灯标注来源）。 图：Azul、Temurin、Oracle、IBM、AWS、Microsoft、GraalVM、Dragonwell 等发行版一览，角标 source: whichjdk.com。\n图：OpenJDK 与多家厂商 JDK 标识并列 — 先定支持策略再定发行版。\n怎么做 # # 常规 JAR java -jar target/app.jar # Native Image（需 GraalVM 与可达性配置） native-image -jar target/app.jar -o target/app ./target/app 常见误区 # 默认把所有服务都原生编译——反射、动态代理、类路径扫描会显著增加构建与排错成本。 忽略 LTS 与支持合约，仅因「版本号最大」选 JDK（见下节）。 六个月特性列车与两年 LTS # 为什么 # 固定节奏让特性可预期，但也要求团队区分「尝鲜」与「生产基线」，并理解预览 API 的生命周期。\n机制与约束 # OpenJDK JDK 项目 写明：\u0026ldquo;The Project ships a feature release every six months\u0026rdquo;。Oracle Java SE 支持路线图 指出 LTS 约每两年一次，并列举 Java SE 8, 11, 17, 21, and 25 are LTS releases；非 LTS 版本会被后续版本取代。预览特性默认关闭，需 --enable-preview（各版本 JEP 示例略有差异）。\n图：The evolution of Java — 从经典 Duke 到现代品牌标识的演进。\n图：JDK 8 至 JDK 25 发布节奏柱状图，来源 blogs.oracle.com/java。\n怎么做 # # 试用预览 API（版本号随目标 JDK 调整） javac --release 25 --enable-preview Hello.java java --enable-preview Hello 生产环境优先 LTS + 明确支持窗口；非 LTS（例如口播中的 Java 26）适合实验与 CI 矩阵，不宜默认当作全公司基线——Java 26 未列入上述 LTS 句。若你维护长期运行的服务端，建议把「JDK 升级」与「依赖升级」拆成两条流水线：前者验证字节码与模块系统，后者验证框架 Recipe 与集成测试，避免在一次发布里同时踩两类回归。\n常见误区 # 跳过多个 LTS 直接追最新 feature release，导致依赖与字节码工具链同时失效。 在预览 API 上构建核心业务且未计划迁移路径。 简化入口：脚本化与教学场景 # 为什么 # 小型自动化、样例与培训不应被 public static void main 与双步编译吓退；语言在通过 JEP 降低仪式化代码量。\n机制与约束 # 演进链条（不宜写死为单一 JDK）：\nJEP 版本 要点 JEP 445 21 Preview 未命名类、void main()、源文件启动 JEP 477 23 隐式声明类、java.io.IO JEP 495 24 Simple Source Files 术语延续 JEP 477 示例使用 import static java.io.IO.* 后的 println(...)；幻灯片写作 IO.println(...) 与限定名 java.io.IO.println 语义等价，字面与 JEP 排版略有差别。\n图：上框传统 public static void main，下框 void main() 与 IO.println 对比。\n怎么做 # import static java.io.IO.*; void main() { println(\u0026#34;Hello World\u0026#34;); } java --enable-preview Hello.java # 以目标 JDK 的 JEP/发行说明为准 常见误区 # 把简化入口当作 Spring Boot 服务的默认结构——企业代码仍需模块化与显式边界。 复制 --release 25 示例却运行在 JDK 21 上，忽略预览开关与 API 差异。 虚拟线程：在阻塞式模型里扩展并发 # 为什么 # I/O 密集服务若用平台线程池，线程数与内存会成为瓶颈；全面改写为反应式栈则抬高认知与调试成本。JEP 444（Java 21，已交付）在保留命令式代码的前提下提供轻量线程抽象。\n机制与约束 # 虚拟线程在阻塞 java.* I/O 时，运行时发起非阻塞系统调用并 挂起（suspend） 虚拟线程，使其从载体线程 unmount（JEP 444 用语）。与既有 Thread API 兼容；纯 CPU 饱和负载未必提升吞吐（JEP 动机段）。\n图：Virtual Threads make it easy to write high-performance apps。\n图：Benefits of Virtual Threads — Compatible with existing Thread API 等要点。\nSpring Boot 可通过 spring.threads.virtual.enabled 启用（默认 false）。\n怎么做 # try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -\u0026gt; { // 阻塞 HTTP / JDBC }); } spring.threads.virtual.enabled=true 压测侧可并行发起请求观察线程栈与延迟（示例）：\nseq 1 100 | xargs -n1 -P20 curl -s -o /dev/null http://localhost:8080/ping 常见误区 # 在 synchronized 块内长时间阻塞载体线程导致 pinning（JEP 444 讨论过此类场景，需读当前版本说明）。 把虚拟线程当作「免费无限并发」，忽视连接池、下游限流与堆内存。 跨版本差异：用检索工具代替记忆 # 为什么 # 从 Java 8/11/17 升级时，真正耗时的是 API 增删、弃用与行为变化，而非语法糖本身。\n机制与约束 # 社区与官方维护的对照站点（均可 HTTP 访问，具体表头以 live 站点为准）：\nJava Version Almanac — 版本间 API diff Java Evolved dev.java — 官方学习路径 Inside Java — OpenJDK 团队资讯 图：javaalmanac.io 展示 New APIs in Java 25，Java 25 与 Java 21 的 java.io 包对比。\n图：dev.java 首页 The Destination for Java Developers，含 Virtual Threads 等深度学习入口。\n怎么做 # # CI 守门：编译期暴露弃用与可疑用法 javac --release 25 -Xlint:all $(find src -name \u0026#39;*.java\u0026#39;) 在 Almanac 选择 from / to 版本，过滤关心的包（如 java.base），将 diff 条目纳入升级 checklist。\n常见误区 # 只升级 JDK 不改依赖——许多破坏来自传递依赖的字节码版本。 忽略 SecurityException 等新抛出行（例如 Almanac 中 ObjectInputStream 相关变更）。 OpenRewrite：用 LST 做确定性批量迁移 # 为什么 # JUnit 4→5、包名迁移、框架升级若靠手工 diff，容易漏改且难以在数百仓库复现。\n机制与约束 # OpenRewrite 使用 Lossless Semantic Tree (LST)：相对 AST 强调类型归因与格式保留（见 LST 概念）。迁移通过 Recipe 描述；Maven 集成 rewrite-maven-plugin。\n图：JUnit 4 to 5 migration — @Test(expected=…) 迁入 assertThrows 方法体，来源 Moderne OpenRewrite。\n图：Framework Migrations — Quarkus、Micronaut、Spring Boot、Helidon、Open Liberty 与 JUnit 标识。\n怎么做 # mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \\ -Drewrite.activeRecipes=org.openrewrite.java.testing.junit5.JUnit4to5Migration 迁移后测试形态示例（摘自 Recipe 文档 Before/After 语义）：\n@Test void schedulerAbsent() { assertThrows(NoSuchBeanDefinitionException.class, () -\u0026gt; { context.getBean(Scheduler.class); }); } 跨框架 Recipe 应在 Recipe catalog 按名检索，勿臆造 ID。\n常见误区 # 运行 Recipe 后不跑测试——LST 保留格式不等于语义全覆盖。 把「幻灯片上的框架 Logo」理解为「一键互迁」——多数迁移需分阶段与定制 Recipe。 在 Java 栈里接本地 LLM # 为什么 # 实验性 AI 功能若绑死某云 API，本地开发与离线演示会受阻；Ollama 提供本地 HTTP 服务，Java 生态通过 Spring AI 与 Quarkus LangChain4j 统一配置模型端点。\n机制与约束 # Spring AI（已核对属性表）默认：\n属性 默认 spring.ai.ollama.base-url http://localhost:11434 spring.ai.ollama.chat.options.model 文档示例含 mistral，可改为 llama3.2:1b Quarkus LangChain4j 的 @ConfigMapping(prefix = \u0026quot;quarkus.langchain4j.ollama\u0026quot;) 中，base-url 在 ollama 根下，非 chat-model 子级；模型键推荐 chat-model.model-id，model-name 为兼容别名（配置源码）。\n图：Spring AI 与 Quarkus LangChain4j 的 Ollama 配置对照（幻灯键名与官方略有差异，见正文）。\n幻灯草稿使用 quarkus.langchain4j.ollama.chat-model.base-url — 与官方 quarkus.langchain4j.ollama.base-url 不一致，下文以官方为准。\n怎么做 # curl -s http://localhost:11434/api/tags | head spring.ai.ollama.base-url=http://localhost:11434 spring.ai.ollama.chat.options.model=llama3.2:1b spring.ai.ollama.chat.options.temperature=0.0 quarkus.langchain4j.ollama.base-url=http://localhost:11434 quarkus.langchain4j.ollama.chat-model.model-id=llama3.2:1b @RestController record ChatApi(org.springframework.ai.chat.client.ChatClient chatClient) { @GetMapping(\u0026#34;/ai\u0026#34;) String ask(@RequestParam String q) { return chatClient.prompt().user(q).call().content(); } } 口述另提及 EclipseStore、Deep Netts — 本次未读项目当前文档，unable to verify。\n常见误区 # 照抄幻灯 Quarkus 键名导致启动期配置不生效。 未设超时与并发限制，本地小模型在高并发下拖垮开发机。 缩短反馈回路：Dev Mode 与 Leyden 方向 # 为什么 # 现代化不止是新语法；团队留存率也与「改一行多久能看见效果」相关。\n机制与约束 # Quarkus dev mode：\u0026ldquo;Dev mode enables hot deployment with background compilation\u0026rdquo;，入口 ./mvnw quarkus:dev。 Spring Boot DevTools 通过 spring.devtools.restart.enabled 等属性控制重启类加载。 Project Leyden 关注启动、预热与足迹；例如 JEP 516 Ahead-of-Time Object Caching 面向特定 JDK 交付 — 不宜写成「所有应用默认已启用 AOT 缓存」。 JBR「增强类重定义」— 本次未抓到 JetBrains Runtime 一手逐字定义，unable to verify。 图：Stay in the Flow — Dev Mode、Vaadin、Quarkus / Spring Boot 缩短反馈回路。\n怎么做 # ./mvnw quarkus:dev spring.devtools.restart.enabled=true 常见误区 # 把 DevTools 热替换当作生产特性——仅用于开发 classpath。 将 Leyden 预览能力与 GraalVM Native Image 混为一谈。 在容器里仍用「全量重启」代替分层镜像与类数据共享调优——Dev Mode 解决的是编码阶段，不是集群发布阶段。 组织侧：现代化是人与流程问题 # 为什么 # 工具齐全仍可能失败：资深开发者担心既有经验贬值，团队缺少可试错环境与文档契约。\n机制与约束 # 演讲者观点：需要 safe space to experiment、README/AGENTS.md 记录构建约定、结对（buddy）与更快的本地 Maven 构建。技术债常来自「心理安全」而非缺少 JEP 链接。\n图：Software (Stack) Modernization — To developers, this can feel like 集体迁徙。\n图：JUGs around the world — https://dev.java/community/jugs/ 全球用户组地图。\n怎么做 # ## Build ./mvnw -q verify ## Run ./mvnw spring-boot:run git clone \u0026lt;repo\u0026gt; \u0026amp;\u0026amp; cd \u0026lt;repo\u0026gt; \u0026amp;\u0026amp; ./mvnw -q verify 问答中关于「AI 期望管理」「系统十年寿命」— 演讲者观点，无官方规范页。\n常见误区 # 只买工具不做结对与 JUG 交流——社区是隐性文档。 引用外部 MCP 基准称「Java 表现最佳」— unable to verify（无方法论与复现链接）。 参考与延伸阅读 # Maven Central 文档门户 Maven 发布坐标要求（groupId / artifactId / version） Jakarta EE 官网 OpenJDK JEP 索引 OpenJDK JDK 项目（六个月发布节奏） Oracle Java SE 支持路线图（LTS / 非 LTS） JEP 12：预览特性 JEP 444：虚拟线程 JEP 445 / 477 / 495：简化源文件与实例 main GraalVM Native Image 手册 whichjdk.com — JDK 选型参考 Java Version Almanac dev.java 开发者门户 Inside Java OpenRewrite 文档与 LST 概念 JUnit4to5Migration Recipe Spring AI Ollama Chat 配置 Quarkus LangChain4j 扩展文档 Quarkus Maven 开发与 dev mode Spring Boot DevTools Project Leyden 与 JEP 516 dev.java Java User Groups 地图 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-how-to-re-start-your-java-journey-in-2026/","section":"文章","summary":"2026 年重回 Java：一条面向有经验工程师的现代化路径","title":"2026 年重回 Java：一条面向有经验工程师的现代化路径","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/agent/","section":"Tags","summary":"","title":"Agent","type":"tags"},{"content":" Agent 监督栈：从静态评测到轨迹级可观测性 # 多 Agent 系统把工程重心从「单次生成质量」推向「长轨迹是否可信」。固定 benchmark 仍有用，但开放任务里路径空间爆炸，让人工逐条读 trace 的成本与模型能力增长不同步。Patronus AI 联合创始人 Anand Kannappan 在围绕 Percival 的公开叙事里，把问题框成：评估（evaluation）→ 可观测性（observability）→ 监督（oversight） 的递进；本文按机制拆解该链条，并标明文献、官方文档与「演讲者观点」的边界——不假装各方已达成单一结论。\n问题空间：四段技术栈与三类张力 # 常见演进路径（演讲者观点）可概括为：孤立 LLM → 含 RAG 的复合系统 → 单 Agent 工具循环 → 多 Agent 互调。第三段之后，访谈里反复出现三类结构性张力，其中定量说法（如「数千万 token 级上下文」）未给出可复现 benchmark，宜作方向判断而非规模事实：\n张力 工程后果 上下文爆炸 长轨迹超出单次窗口，动作顺序依赖强 领域自适应 通用 rubric 难以覆盖垂直合规 多 Agent 编排 静态用例难以枚举联合失败模式 画面左侧可见 Weaviate podcast 标识，右侧为嘉宾出镜——本期以口述架构为主，无连续技术幻灯片。\n静态评估并未作废，但边界在变窄 # 为什么 # 订票、固定写博客、schema 校验等步骤少、目标清晰的流程，仍适合固定数据集与 outcome 检查（演讲者观点）。行业实践也长期用 HaluBench 一类基准做回归。\n机制与约束 # 开放营销类 Agent、路径不可预知的自治体，预设用例覆盖率会快速衰减；此时「过程奖励」若要用于优化，往往需运行时评判每一步是否合规，而非仅靠离线标签（演讲者观点）。这与 LMUnit 强调的 per-instance 自然语言单元测试在理念上相邻：准则可在运行时生成，但 Percival 与 LMUnit 无公开实现对比。\n怎么做（最小示例） # 对固定 RAG 流水线，保留三元组 (context, question, answer) 的回归集，并用专用 judge 做二元幻觉检测：\n# 概念示意：Lynx 类 judge 输出 grounded / not grounded verdict = lynx_judge(context=ctx, answer=ans) # 见 HaluBench 协议 assert verdict.label in {\u0026#34;grounded\u0026#34;, \u0026#34;hallucination\u0026#34;} 常见误区 # 因 Agent 流行而废弃全部静态集；高确定性子任务仍应保留快照测试。 把访谈中的「静态 eval 不可能」推广到所有系统——该说法针对高开放度多 Agent，无法核实为普适定律。 生产 trace 优先：Percival 与失败分类学 # 为什么 # Patronus 调试文档 将 Percival 定位为分析 agentic traces、检测错误并给出优化建议；首版叙事强调解读已运行 trace（开发/测试/生产均可），而非替代整条静态 eval 流水线（演讲者观点）。客户轶事称：人工读懂少量 trace 可耗「数小时」，再抽一条变成 eval 案例约「+1 小时/trace」——无样本量与统计协议，仅作成本量级参考。\n机制与约束 # 公开 Error Taxonomy 写明 20+ failure modes，分 Reasoning / System Execution / Planning and Coordination 等大类；支持 Custom error taxonomies 扩展。访谈曾称 60 类——与官网 20+ 不一致，可能含子类型或内测枚举；截至公开文档，无完整 60 项列表。\n演示个案（不可当作产品平均性能）对单次运行打出 1–5 分多维分数：文档明确 security、reliability 及 other dimensions；UI 中可见 Plan Optimality、Instruction Adherence 等扩展维。\nInsights from Percival：Reliability 2.5/5、Plan Optimality 2/5、Instruction Adherence 3/5、Security 5/5、Overall 3.12/5；侧栏含 Context Handling Failures、Tool Selection Errors、Incorrect Memory Usage、Configuration Errors。\nOCR 与文档一致片段包括 Context Handling Failures、Tool Selection Errors；Prematurely calling the \u0026lsquo;final_answer\u0026rsquo; tool 出现在演示 UI，未在公开 base taxonomy 表中找到同名条目。Trace 树显示 ToolCallingAgent.run、span 约 578.818s，说明监督对象已是长时、多步工具调用链，而非单次 completion。\n怎么做 # 将现有 OpenTelemetry / 框架 trace 导入 Patronus 或同类平台。 从高频 failure mode 反推 rubric，再决定是否写入自定义 taxonomy。 采用 Suggested Prompt Fix（文档：Suggests concrete prompt improvements）作人工评审输入，避免未验证的一键自动改 prompt（产品策略，演讲者观点）。 常见误区 # 用单次演示 Overall 3.12/5 代表 SLA。 假设 taxonomy 已覆盖「过早 final_answer」——需自定义或等待产品收录。 忽视 建议性 修复与自动补丁的界限，引发错误自动修补循环（演讲者观点）。 LLM-as-a-judge：开发期评测与实时护栏的两条轨 # 为什么 # 同一「模型评模型」范式，在不同延迟预算下服务不同目标：离线对比 prompt/权重 vs 在线拦截高风险输出。\n机制与约束 # Lynx（PatronusAI/Llama-3-Lynx-70B-Instruct）面向 RAG reference-free 幻觉检测；在 HaluBench（约 15k 样本，含 finance、medicine 等）上报告 Accuracy，论文称优于 GPT-4o、Claude-3-Sonnet 等同设定模型。训练使用 GPT-4o 生成的 CoT 推理轨迹做 instruction tuning——文献支持该做法；访谈「首次用 CoT 训练 evaluation model」过度宣称，不宜写死「全球首次」。\nGlider（GLIDER 3.8B，基于 Phi-3.5-mini-instruct）支持 0–1 / 1–3 / 1–5 Likert、rubric、multi-metric；论文在 FLASK 等设置用 Pearson correlation 对标人类评分，在 LiveBench 部分子任务报告 F1（例如 GLIDER 0.654 vs GPT-4o-mini 0.481）。访谈「首个 SLM 对标 GPT-4o-mini」应限定为特定 benchmark，非全任务 SOTA。\n产品 典型场景 主指标（论文） Lynx RAG groundedness、离线回归 HaluBench Accuracy Glider Guardrails、低延迟多指标 Pearson / F1（任务相关） 辩论式 judge（多模型互辩）在访谈中被判为几乎无生产落地；同预训练分布的 foundation model 互辩会产生同向偏置，需分布不重叠的专用 judge（演讲者观点，无普查数据）。\n怎么做 # # Glider：按 rubric 返回多指标（0–1 或 Likert） scores = glider.score( interaction=log, rubric={\u0026#34;helpfulness\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;safety\u0026#34;: \u0026#34;...\u0026#34;}, ) if scores[\u0026#34;safety\u0026#34;] \u0026lt; threshold: block_or_escalate() 常见误区 # 用 Lynx 的 Accuracy 类比搜索场景的 NDCG——主持人曾作类比，属访谈类比，非论文指标。 在生产默认路径堆叠高延迟辩论式 judge。 忽略 Glider 与 Lynx 基准不同（FLASK vs HaluBench），直接横向比「谁更强」。 嘉宾侧出镜讨论评估模型路线；画面无 Lynx 论文截图，技术细节以 arXiv:2407.08488 为准。\n动态评估与可扩展监督（scalable oversight） # 为什么 # 当人类无法在规模上审完 AI 产出时，需要「能力相当的系统」在运行时评判被测 Agent（演讲者观点）。这与 Bowman et al., 2022 提出的 scalable oversight 在概念层相邻——该文不证明 Percival 有效，不可将文献结论直接嫁接为产品实证。\n机制与约束 # 动态评估 ≠ 放弃基准，而是对不可预知轨迹补充过程级裁判。Percival 被定位为 agentic supervision 的产品化路径之一；与 OpenTelemetry 等行业「supervision」术语访谈未精确定义一一对应，写作时应分域使用。\n反直觉一点（演讲者观点，无定量调研）：用户更在意工作流是否完成，而非秒级响应；为监督增加额外推理延迟在部分场景可接受。这与「测试面扩大必然拖垮 UX」的直觉并不完全一致——压力更多在算力与人力解读 trace，而非单纯 latency。\n怎么做 # 常见误区 # 把「AI oversee AI」理解为取代 human-in-the-loop；访谈强调的是协作接口（如 Chat、inbox 式提问），非全自动放行。 将 scalable oversight 论文结果当作 Percival 的 A/B 测试结论。 记忆层：episodic 与 semantic 的分工 # 为什么 # 长轨迹监督需要跨 run 积累：「上次同类失败如何修」与「领域基线事实」是不同信息类型。\n机制与约束 # 调试文档 区分 episodic（历史 trace 中的工具调用等）与 semantic（人类对 agent 的反馈类语义记忆），用于持续改进分析。访谈观点称 Percival 核心依赖 Weaviate、collection、text2vec 向量化、按 run 近邻检索；在 Patronus 公开 docs 的 Percival 章节 检索未见 Weaviate 字样（核实日 2026-05）。Weaviate 的 multi-tenancy、hybrid search 仅说明向量库能力，不能反向证明 Percival schema。\n怎么做（概念） # episodic：按 trace_id / run_id 存 span 摘要，检索「相似失败修复史」。 semantic：存稳定领域约束（合规条款、API 契约），与 episodic 分 collection，避免噪声污染基线。 常见误区 # 把 Podcast 合作关系等同于已公开的 Percival 存储实现。 用单一向量库混合所有记忆类型，导致检索混淆工具错误与领域事实。 画面 OCR 碎片含 Weaviate 品牌字样，不足以支撑 memory schema 细节，实现以 Patronus 后续技术博文为准。\n过程奖励、结果奖励与 Prompt / 权重编辑 # 为什么 # 复合 AI 系统常被拆成多步 inference，每步可有静态 eval；开放自治体更依赖 outcome + 环境反馈（主持与嘉宾共识，细节因系统而异）。\n机制与约束 # Process rewards：逐步检查轨迹内行为（工具选择、是否过早 final_answer）。 Outcome rewards：最终是否达成业务目标（如订票成功）。 Prompt 仍是多数客户「改一处、全局抖动」的主战场（演讲者观点）。Model editing 与 sparse autoencoder、mechanistic interpretability 相邻，嘉宾称对 guardrail / eval 模型（如 Lynx）做 SAE 几乎空白——未验证「行业无人做过」。\n下一代 eval 数据将更多由 Agent 拉工具/RAG/内部文档生成领域样本，并辅以 adversarial dataset generation 压测（演讲者观点）；无 Patronus 公开复现流水线。\n常见误区 # 只优化 outcome 而忽略过程中「过早结束」类逻辑错误（见 Percival 演示）。 期待 Percival 自动整段替换 prompt；首版刻意只给建议（演讲者观点）。 左侧 Weaviate podcast 标识清晰可见，讨论进入多 Agent 与评估范式分支。\n双嘉宾远程连线画面，无产品 UI，架构论点需结合上文文档与论文。\n约 10 分钟处画面：主持人侧 Weaviate podcast 背景，嘉宾侧居家办公场景。\n字幕 cue 附近帧 OCR 含 Self 等碎片，无法与 Lynx 论文标题对齐，不作为产品证据。\nOCR 含 Weaviate 与 ELITE 等碎片，仅作品牌语境，不推导 API。\n未收敛的分歧（刻意并列） # 主题 常见实践 嘉宾/产品叙事 证据限度 静态 eval 回归集 + CI 高开放 Agent 需动态评判 分场景成立；「不可能」仅访谈定性 Failure 规模 文档 20+ 访谈 60 以 taxonomy 文档 为准，60 标演讲者观点 Judge「首次」 多家 SLM judge Lynx/Glider 首创表述 论文仅 claim 特定 benchmark Weaviate 集成 向量库通用能力 Percival 记忆后端 记忆类型有 docs；Weaviate 实现为访谈观点 延迟 vs 监督 越低越好 完成率优先 演讲者观点，无调研引用 若你要落地 # 分层评测：确定性子链路保留静态集 + outcome；开放 Agent 上对生产 trace 做过程级 taxonomy 与抽样人工复核。 先接 trace，再扩 benchmark：用 Percival Debugging 类能力把「读 trace 数小时」压缩为「看 failure mode + 建议 prompt」，再反哺 eval 用例。 Judge 选型：RAG 离线回归用 Lynx/HaluBench 协议；在线护栏用 Glider 类 SLM + 明确 rubric，并记录 Accuracy vs Pearson/F1 不同指标，避免混榜。 记忆分库：episodic（轨迹摘要）与 semantic（稳定约束）分 collection；Weaviate 配置在 Patronus 公开 schema 前标为待核实。 人工闸门：Suggested Prompt Fix 走 PR 评审，不把未验证建议自动写入生产 prompt。 参考与延伸阅读 # Patronus AI 官网 Percival — Agentic Supervision 产品页 Percival Debugging Overview（官方文档） Percival Error Taxonomy（官方文档） Lynx 论文（arXiv:2407.08488） Lynx 开源仓库 HaluBench 数据集 Glider 论文（arXiv:2412.14140） Glider 模型（HuggingFace） LMUnit 论文（arXiv:2412.13091） LMUnit 介绍（Contextual AI） Scalable Oversight（Bowman et al., 2022） Weaviate 开发者文档 Weaviate Multi-tenancy Weaviate Hybrid search OpenAI Models 文档（上下文与 GPT-4o mini） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-patronus-ai-with-anand-kannappan-weaviate-podcast-122/","section":"文章","summary":"Agent 监督栈：从静态评测到轨迹级可观测性","title":"Agent 监督栈：从静态评测到轨迹级可观测性","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/agentic/","section":"Tags","summary":"","title":"Agentic","type":"tags"},{"content":" Agentic RAG：当检索管线长出「规划与工具环」 # 工程团队把 RAG 从 demo 推到生产时，常卡在三个张力：延迟（用户要秒级答案）、可控性（失败要能定位）、覆盖（单一向量库装不下 Slack、Web、数仓）。2024 年前后，Weaviate 等厂商用 Agentic RAG 命名一类方案：在检索之上叠加 LLM 的 plan → act → observe 环，用 function calling 选工具、决定是否继续取数。本文把公开文档、论文与一线访谈中的可核对部分和仍属架构猜想的部分分开写——不给出「一种架构统治一切」的结论。\n片头：纽约天际线航拍，与本期 NYC 线下活动语境呼应（画面无技术 API 文案）。\n双主持人分屏：左侧主持、右侧嘉宾 Erika Cardenas，典型远程播客录制画面。\n问题空间：Agent、Compound AI 与 RAG 的交界 # 为什么要在 RAG 旁再谈 Agent # Weaviate 官方博客 将 agent 描述为：具备角色与任务、使用 tools 与 短/长期 memory、能 plan, act, and adapt 的 LLM 系统。这与 Lewis 等人定义的 RAG 模型结构（parametric + non-parametric memory）不是同一层抽象：前者是编排语义，后者是生成架构。混用名词时，团队容易在评审里争论「算不算 agent」，却忽略真正要定的：终止条件、工具边界、观测粒度。\n机制与约束：开放环 vs 固定流水线 # 一种实现是 function calling loop：模型读工具返回 → 再决定任务是否完成（访谈中常称 agent）。另一种是 compound AI system：prompt 与工具顺序写死在 pipeline 里。二者名称在业界可互换，边界仍模糊（演讲者观点）。选型上，固定流水线通常更易测延迟与回归；开放环更灵活，但失败模式是「多轮空转」或「过早停止」。\n怎么做（最小示例） # 在 OpenAI 兼容栈上，把向量检索注册为一个 tool，由模型决定是否调用——与博客所述 \u0026ldquo;tool use, multi-step retrieval, and validation\u0026rdquo; 方向一致：\ntools = [{\u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;search_weaviate\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Hybrid search over internal docs\u0026#34;, \u0026#34;parameters\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {\u0026#34;query\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;}}}, }}] # 循环：completion → 若有 tool_calls → 执行 → 把结果塞回 messages 常见误区 # 把「能调 API」等同于「有自主性」——未注册的数据源（如 Slack）不会被模型自动访问；博客 明确需 \u0026ldquo;use an API to retrieve … from Slack channels\u0026rdquo;。 用名词争论代替 SLO：应先定 p95 延迟与最大 tool 轮次。 约 1:43：嘉宾开口阐述 agent 四要素时的分屏画面。\nVanilla RAG 与 Agentic RAG：单次检索 vs 多步验证 # 为什么单次管线会不够用 # Weaviate FAQ（页面 JSON-LD）写明：\u0026ldquo;Vanilla RAG is typically one-shot retrieval with limited tool access\u0026rdquo;；正文对比 agentic 侧具备 \u0026ldquo;retrieve, evaluate, re-retrieve, and validate\u0026rdquo;。常见痛点：一次语义检索拿不到带时间/对象约束的片段（例如「2024-01-10 与某人的会话」），或库内过时需补 Web。\n维度 Vanilla（文档/访谈归纳） Agentic（文档/访谈归纳） 检索次数 通常一次 可多步、可重检索 工具 有限 Web、Slack API、数仓等需显式注册 查询 难自动 metadata filter NL → filter + semantic（agent 行为为访谈观点） 机制：文档支持什么、访谈补充什么 # 已核实：外部工具含 \u0026ldquo;web searches\u0026rdquo;；agent 可 \u0026ldquo;routing/looping logic\u0026rdquo;。未在博客字面出现：「向量库不够才触发 Web」的条件分支（演讲者观点）。\n怎么做 # Weaviate Filters 支持按属性包含/排除对象；Python v4 示例：\nfilters = Filter.byProperty(\u0026#34;round\u0026#34;).equal(\u0026#34;Double Jeopardy!\u0026#34;) 由 agent 把自然语言时间戳解析为 Filter.byProperty(\u0026quot;timestamp\u0026quot;).greater_than(...) 是合理产品路径，但具体 NL→filter 行为未见于 Agentic RAG 博文正文——部署前应在你方 schema 上写集成测试。\n常见误区 # 把 Lewis et al. 的 RAG 模型等同于「retrieve-augment-generate 三步管线」——论文讨论的是密集检索 + 生成器，非产品管线术语。 以为 hybrid search 问法只会命中博客 chunk（访谈演示路径，非文档保证）。 约 10 分钟：讨论 ReAct 与 CoT 差异时的双人对谈画面。\n约 8:47：双方微笑交谈，属对话节奏帧而非架构幻灯。\n规划范式：CoT、ReAct 与 Tree of Thoughts # 为什么规划方式决定成本曲线 # ReAct 论文 摘要指出：模型以 interleaved 方式生成 reasoning traces 与 task-specific actions，并把推理与行动从「分话题研究」拉到一起。相对地，Tree of Thoughts 强调 \u0026ldquo;considering multiple different reasoning paths\u0026rdquo; 与 backtracking——在 Game of 24 等任务上相对 CoT 有显著增益（论文报告 CoT 4% vs ToT 74%，任务域特定）。\n机制：文献原词 vs 口语二分 # 主张 证据 ReAct 交错推理与行动 ReAct 摘要 已核实 CoT 易幻觉、ReAct 可借环境反馈纠错 ReAct 摘要 已核实 「CoT = 初始拆解，ReAct = 迭代 loop」 访谈归纳，论文未用该标签 ToT 在线 RAG 过重 演讲者观点；无 RAG 延迟基准 主持人在 function schema 强制 thought 字段 ≈ ReAct 无法核实（无公开 schema） 两篇工作作者列表均含 Shunyu Yao（arXiv 元数据可核对）。嘉宾推测 OpenAI o1 的中间步骤展开「像 ReAct」，并提到 ReAct 作者流向 OpenAI——嘉宾自述 \u0026ldquo;it could be a theory\u0026rdquo;（未证实）。\n怎么做 # ReAct 提示轨迹常见 Thought / Action / Observation 三段；生产上也可在每次 tool call 前要求 rationale 字段——与 ReAct 精神相近，但不等于论文算法复现。\n常见误区 # 在 RAG 场景默认上 ToT/MCTS：搜索树前期成本高；访谈倾向 ReAct「错了再改」 更轻（无 benchmark 数字）。 把 RL 类比（action = function call，next state = 工具返回）当成已验证的 RAG 训练方案——目前多是启发式架构讨论（演讲者观点）。 约 14:00：嘉宾手势说明多 agent 编排时的分屏帧。\n约 16 分钟：嘉宾面向镜头发言，书架侧可见《A Thousand Brains》书脊。\n多 Agent：编排、并行与异构模型 # 为什么单 Agent 扛不住多域知识 # Weaviate 博客 设 Multi-agent RAG Systems 专节，指出单 agent 局限。访谈中的主流形态（演讲者观点）：顶层 orchestrator 并行调度子 agent，按 collection/主题/工具切分；子 agent 可绑更小 LLM（如 Llama 3 8B 管单库，GPT-4o/o1 做综合）。博客 HTML 未出现 “parallel” 字样，仅 \u0026ldquo;orchestrate its components\u0026rdquo;——并行调度属访谈架构建议。\n与 DSPy 的类比：多角色类似 signature 限定「只做这一块」（非形式等价）。点名 OpenAI Swarm 时需注意：README 写明项目 已由 OpenAI Agents SDK 取代，属 experimental/educational——2024-11 录制语境可能仍讨论 Swarm。\n怎么做 # 逻辑上三层即可落地验证：\nRouter agent：分解子问题（可参考 LlamaIndex Sub Question Query Engine——文档写 \u0026ldquo;breaks down the complex query into sub questions\u0026rdquo;）。 Worker agents：各绑定一工具集 + 一 collection。 Synthesizer：合并子答案并做一致性检查。 常见误区 # 多 agent = 多份完整系统提示词堆叠 → 成本与冲突指数上升，需共享 trace id。 把 CrewAI/AutoGen 点名当作 Weaviate 官方推荐栈（本期无实测数据）。 记忆：Letta、向量库与「记忆算工具还是本体」 # 为什么 RAG 不够装下「身份与职位变更」 # Letta（前身 MemGPT）面向 hierarchical memory 与 \u0026ldquo;virtual context management\u0026rdquo;，支持跨会话 remember/reflect/evolve。访谈区分：短程在 prompt（含 tool 返回）；长期由 Letta 在对话中更新（「身份/职位变更」场景为演讲者观点）。相对 DSPy 优化 prompt/权重，Letta 更偏运行时 记忆块 演化（Memory 概念文档 有 Archival memory、Context hierarchy 等目录——具体写入机制需读专文）。\n未决架构（演讲者观点，2024-11 语境）：向量库作 长期记忆存储 还是 被 agent 调用的 tool？主持人倾向对 agent 内部记忆再做 Agentic RAG；Weaviate × Letta 集成仍待完善。\n常见误区 # 把所有历史消息塞进向量库就算「长期记忆」——缺少分层与淘汰策略会拖垮检索质量。 假设 Letta 与 Weaviate 已一键打通（产品集成未完成）。 约 22:57：评测与可靠性话题中的轻松对谈瞬间。\n约 24 分钟：双方微笑，讨论观测与 LLM-as-judge 时的画面。\n可观测性与评测：Trace 先于「感觉还行」 # 为什么 Agent 系统必须先可追踪 # Agent 链路慢、步骤多；没有 \u0026ldquo;still running\u0026rdquo; 信号，运维与用户都会误判卡死。Arize Phoenix 文档写明通过 OpenTelemetry 接收 OTLP trace，并支持 LLM-as-a-judge evaluators。Google Vertex AI Agent Builder 文档含 Observability、Unified Trace Viewer 等方向——与播客提到的「分解步骤 UX」部分核实（具体 UI 未逐帧对照）。\n机制：观测量的最小集合 # 建议每条用户请求至少记录：plan_id、tool_name、latency_ms、retrieved_ids、token_usage、final_stop_reason。这与博客强调的 \u0026ldquo;validation\u0026rdquo; 闭环一致——否则无法回答「错在检索还是生成」。\n常见误区 # 只盯答案 BLEU/ROUGE，不看 哪一步 tool 返回为空。 把 Demo 里的步骤条当成生产级 trace（访谈 UX 观点）。 LLM-as-Judge 与「More Agents」：论文能支持什么 # 为什么法官模型不可靠（访谈侧） # 嘉宾认为业界 高估 LLM-as-judge：temperature 常未配置、同 prompt 多次漂移（工程观察，无一手论文）。主持人转述 Nils Reimers 观点：用 OpenAI 评 Cohere 会有 模型偏见；GPT-4o 能嗅出同族文风（二手引述，未复现）。\nMore Agents Is All You Need（arXiv:2402.05120） # 已核实：提出 sampling-and-voting（AgentForest 代码库），任务含 GSM、MATH、MMLU、Chess、HumanEval 等；性能与 task difficulty（固有难度、推理步数、先验概率等维）相关。\n无法核实 / 与访谈不一致：\n访谈表述 论文实际 ~25 次采样 分析中出现 sampling 40 times 等，无通用「25」推荐 easy/hard 化学/物理题 全文 无 chemistry/physics 关键词 难题上多采样对 judge 无效 论文 非 LLM-as-judge 设定 judge 场景 ~20 次更稳 访谈观点 引用该文时勿写成化学/物理 easy-hard，除非另找来源。嘉宾认为 agent 数量 边际递减（演讲者观点；论文讨论 scaling，非 judge 专用曲线）。\n常见误区 # 把「多次采样判分」等同于 HumanEval 的 pass@k 而不读任务定义。 用单一法官模型评跨厂商栈却不做 对照人工标注。 长任务、合成数据与 Generative Feedback Loop # 为什么有些 workload 宁可等数小时 # 问答型 hybrid search 适合 秒级 RAG；STORM（\u0026ldquo;researches a topic and generates a full-length report with citations\u0026rdquo;）与多层 DSPy 流水线（访谈归纳为 research→outline→content→title）属于 慢路径——多步工具、自检、引用。访谈中的 GFL（generative feedback loop）：agent 在 Web + Weaviate 间循环产 合成数据/标注，且 autonomy 可能优于「只做 RAG 生成」（无对照实验；Weaviate Agentic RAG 博客无 GFL 字样）。\n主持人提出「套娃 GFL」：写论文式任务需把代码/实验结果写回库，agent 自身再依赖 GFL 管理中间态（架构提案，非产品承诺）。嘉宾强调 human-in-the-loop、可「拔电源」——呼应 2023 Auto-GPT 文化下的抵触（演讲者观点）。\n常见误区 # 用 chatbot 延迟预期衡量 STORM 类报告系统。 把四层 DSPy 当作论文规定阶段（访谈演示归纳；DSPy 论文强调 compiling declarative LM calls 与 self-improving pipelines，未规定该四段博客程序）。 片尾活动卡：Techniques for Building Better Agent Systems、evaluate_agents(、type=\u0026quot;AI\u0026quot;、scope=\u0026quot;Agent Systems\u0026quot;、Erika Cardenas、Arize / LlamaIndex / AutoGen 等标识（非本期中段架构幻灯）。\n约 33:34：收尾对谈分屏，临近 NYC 线下活动提及。\n若你要落地 # 先钉工具清单与隐私边界：只注册允许访问的数据源；用 Filters 写清 schema，再让 planner 生成 filter——并为 NL→filter 写回归用例（博客未保证 agent 自动解析）。 设定 loop 预算：最大 tool 轮次、p95 延迟、停止条件（空结果、重复查询、置信度阈值）；在 Phoenix 或 OTLP 后端落 trace。 分层选型：秒级问答走 one-shot / 轻 hybrid；多步调研走 ReAct + 验证，慎上 ToT 除非你有离线算力与评测集证明收益。 评测分开两层：检索（MRR、nDCG）与端到端（人工 + 抽样）；若用 LLM-as-judge，固定 temperature、记录法官模型版本，勿照搬 More Agents 的化学/物理叙事。 多 agent 先串行验证再谈并行：博客未承诺 parallel；异构小模型子 agent 是成本优化方向，需用你自己的 QPS 与质量曲线证明。 参考与延伸阅读 # Weaviate — What Is Agentic RAG? — Agentic vs vanilla、multi-agent、tool loop 主锚 Weaviate — 查询 Filters — 属性过滤 API 与示例 Lewis et al. — Retrieval-Augmented Generation (arXiv:2005.11401) — RAG 奠基工作（模型层定义） ReAct — Synergizing Reasoning and Acting (arXiv:2210.03629) — 交错推理与行动 Tree of Thoughts (arXiv:2305.10601) — 多路径与回溯 LlamaIndex — Sub Question Query Engine — 子问题分解检索 OpenAI — Function calling 指南 — tool schema 与循环集成 DSPy — Compiling Declarative LM Calls (arXiv:2310.03714) — 声明式流水线与优化 MemGPT — arXiv:2310.08560 — 分层记忆与虚拟上下文 Letta — GitHub — MemGPT 后继项目 Letta — Memory 概念 — Memory blocks / Archival memory STORM — stanford-oval/storm — 带引用的长报告生成 More Agents Is All You Need — arXiv:2402.05120 — sampling-and-voting / Agent Forest Agent Forest — GitHub — 论文参考实现 Arize Phoenix — LLM Traces — OTLP 与 OpenTelemetry Google Cloud — Vertex AI Agent Builder 概览 — Agent 平台与可观测性 产品能力与论文结论随版本变化；访谈中的并行编排、NL→filter 触发链、GFL 与 o1–ReAct 推测等，部署前请在自有数据与 trace 上复测。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-agentic-rag-with-erika-cardenas-weaviate-podcast-109/","section":"文章","summary":"Agentic RAG：当检索管线长出「规划与工具环」","title":"Agentic RAG：当检索管线长出「规划与工具环」","type":"posts"},{"content":" Agentic 主题建模：嵌入管线、LLM 与人在环内的工程权衡 # 大规模语料库上的「主题」早已不是 LDA 词袋时代的专属问题。向量检索、RAG 与 Agent 工具链把文档级 embedding推上主舞台，但运营侧仍需要可浏览、可过滤、可迭代的主题层元数据——既能支撑分析师读报告，也能在百万文档场景里充当粗筛索引。近期讨论里常把 agentic topic modeling 与 TopicGPT、BERTopic 并置；若剥离播客叙事，核心张力其实是三条路线的取舍：静态嵌入聚类、LLM 维护主题表并分派，以及人在环内用自然语言 steer 粒度。下文按工程主题组织，证据以官方文档与论文为准；未在一手来源出现的数值与产品吞吐，标注为演讲者/主持人观点。\n问题空间：主题在 RAG 栈里扮演什么角色 # 为什么：纯文档 embedding 检索在语义上已经很强，但用户往往还要回答「这批投诉主要在抱怨什么」「过去一季度新增了哪些议题」——这需要介于全文与单句之间的中层结构。主题层若做得好，可作为向量库上的元数据分区或两阶段检索的第一跳；若做得差，则引入错误先验，比不用更糟。\n机制/约束：「主题」本身没有唯一形式——可以是 BERTopic 的 c-TF-IDF 关键词列表，也可以是 TopicGPT 式的自然语言标签与描述。演讲者观点：呈现形式高度主观，不存在放之四海而皆准的「正确主题」。\n怎么做：在架构上把主题产出与下游消费解耦——先明确消费者（分析师 UI、检索 router、评估流水线），再选表示（词权重 vs LLM 摘要 vs 层次树）。\n常见误区：把「主题」等同于「LLM 生成的一句标题」；缺少可比分数时，人眼会过度信任流畅表述（见后文评估一节）。\n模块化嵌入管线：可替换的 building blocks # 为什么：Embedding 模型、聚类器与降维算法迭代很快；若主题建模绑死单一栈，每次换 SentenceTransformer 或换密度聚类器都要重写流水线。\n机制/约束：BERTopic 算法页 将流程表述为 five steps：Embeddings → Dimensionality Reduction → Clustering → Tokenizer → c-TF-IDF（class-based TF-IDF）→ 可选 Representation。各步可替换；arXiv:2203.05794 摘要亦写明：对文档嵌入聚类，再以 class-based TF-IDF 生成主题表示。\n图：幻灯片「Build Your Topic Model」— 自下而上为 Convert document into embeddings（Transformers / SBERT / SpaCy）、Reduce embedding dimensionality（TruncatedSVD / PCA / UMAP）、Cluster reduced embeddings（HDBSCAN / BIRCH / k-Means）、Tokenize documents。\n怎么做（最小示例，与官方默认一致）：\nfrom bertopic import BERTopic from umap import UMAP import hdbscan umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric=\u0026#34;cosine\u0026#34;) hdbscan_model = hdbscan.HDBSCAN(min_cluster_size=15) topic_model = BERTopic(umap_model=umap_model, hdbscan_model=hdbscan_model) topics, probs = topic_model.fit_transform(docs) 常见误区：把展示用 2D 散点图当成聚类真实空间。官方示例使用 n_components=5；5–10 维为嘉宾经验区间，非 UMAP 文档规定的默认值（UMAP parameters）。2D DataMapPlot 适合叙事，解释簇组成时应回到高维空间、词权重与样本文档。\nLLM-native 主题发现：TopicGPT 与 TnT-LLM # 为什么：聚类 + c-TF-IDF 的主题词偏「统计显著」，不一定符合业务话术；LLM 可直接产出可读标签与层级 taxonomy。\n机制/约束：\nTopicGPT（arXiv:2311.01449）：prompt 框架，用 LLM 发现 latent topics（自然语言标签 + 描述），README 含 generate_topic_lvl1、refine_topics、assign_topics 等，generation 与 assignment 分离。 TnT-LLM（arXiv:2403.12173）：two-phase — 阶段一生成/refine label taxonomy，阶段二用 LLM 标注训练轻量分类器做规模化 assignment。 演讲者观点：对全库逐文档反复调用 LLM 浪费；更稳妥的是 embedding 预聚类 + LLM 做引导或顶层摘要。若推理成本趋近零，全库 LLM inspector 在逻辑上说得通——届时 embedding 可能退居可视化与层次分析（条件性前瞻，未有 benchmark）。\n怎么做：将 LLM 主题表导出为标签向量 y，接入 BERTopic 监督/半监督路径（见下节），而非推倒重来。\n常见误区：认为 TopicGPT 与 BERTopic 结构不可对接；嘉宾主张可把 TopicGPT 主题扔回 BERTopic 做推断与可视化（访谈工作流，能力上由 Supervised / Semi-supervised 支持）。\n图：分屏访谈画面，左侧背景可见 Weaviate podcast 标识与波形装饰（约 10 分钟段，讨论 TopicGPT 类路线时的典型镜头）。\nAssignment 与 Generation 解耦 # 为什么：全库 LLM 重写表示层成本高；而「把 10 万文档分成 200 簇」与「给 200 簇各写一句人话标题」的计算形态不同。\n机制/约束：BERTopic 先 assignment（聚类），再 generation（c-TF-IDF 与可选 Representation models）。TopicGPT / TnT-LLM 亦显式区分 topic generation 与 assignment。演讲者观点：表示层迭代只触及主题数级体量，相对全库 LLM 便宜——「~1% 数据量」为量级直觉，无一手公式，不宜写成论文数据。\n怎么做：固定 assignment 后，仅替换 representation_model 或重跑 c-TF-IDF，避免重算 UMAP/HDBSCAN。\n常见误区：端到端一次 prompt 搞定分派与命名，导致无法单独评估「分派对不对」与「名字好不好听」。\n「Agentic」：工具调用、人在环内与粒度 # 为什么：企业场景里最大摩擦往往不是算法，而是粒度——太粗漏掉子议题，太细无法对接检索 Agent。\n机制/约束：本集语境下的 agentic topic modeling ≈ 工具调用 + human-in-the-loop + 自然语言 steer，非通用 Agent 框架的形式化定义（访谈观点）。演讲者观点：流程仍是 user-driven；再强的模型也不会自动猜中你想要的粗细。主持人补充：选择自由度太高反而 choice overload——人更擅长在有限选项里选（例如「合并 Topic 3 与 7」「只要 12 个顶层主题」）。\n怎么做：把 Agent 接口设计成提议-确认循环：系统输出候选主题与样本文档 → 用户用结构化指令合并/拆分/重命名 → 再触发半监督重聚类或仅重跑表示层。\n常见误区：把「能调 API 的 LLM」等同于「会自动收敛到业务满意主题」；忽略层级主题与单层检索 Agent 的对接成本——演讲者观点：检索场景可忽略层次，用细粒度主题元数据即可；主持则认为层次会增加与查询 Agent 的接口复杂度（双方均未给出统一结论）。\n图：访谈分屏，嘉宾侧讨论粒度与人工反馈时的画面（Weaviate podcast 品牌可见）。\n聚类选择：HDBSCAN、k-means 与参数试错 # 为什么：密度聚类能发现任意形状并标噪声，但超参不透明；k-means 固定 K，便于与 LLM 对比邻簇。\n机制/约束：HDBSCAN min_cluster_size 表示「仍视为簇的最小 grouping 规模」，增大倾向更大、更少的簇，但簇总数仍需实验——不是直接的「我要 K 个主题」旋钮。演讲者观点：min_cluster_size 难先验，是企业部署主要摩擦；有人改用 k-means + 事后密度过滤（偏好陈述，HDBSCAN 文档未讨论）。\n怎么做：\nimport hdbscan clusterer = hdbscan.HDBSCAN(min_cluster_size=30, min_samples=10) labels = clusterer.fit_predict(reduced_embeddings) 对 k-means，可让 LLM 阅读相邻簇关键词差异以辅助选 K（访谈思路，未核实 k-LLM-means 等方法细节）。\n常见误区：递归对 HDBSCAN 子簇再聚类即可得到稳定层次——嘉宾认为子簇参数敏感，实践中难维护（演讲者观点）。\nc-TF-IDF：簇对比度与可读分数 # 为什么：LLM 单行标签缺少跨主题可比分数，分析师难以判断「这个词有多属于本主题」。\n机制/约束：ClassTfidfTransformer 将每个簇视为一个「类文档」，做 l1 归一化词频 × IDF，突出本簇相对其他簇的独特词；论文称 class-based variation of TF-IDF（arXiv:2203.05794）。演讲者观点：可类比简易「对比学习」；反对词云——字号误导人类对权重的感知，故 BERTopic 可视化不提供 word cloud（产品行为与口述一致，官方页未逐字写「反对词云」）。\n怎么做：向 UI 暴露 term score 排序列表，而非仅用面积编码的词云。\n常见误区：见到流畅 LLM 主题名即停止阅读代表文档。\n评估：自动化 coherence 与「用户想要的主题」 # 为什么：需要离线指标筛模型，但神经/嵌入主题模型上经典 topic coherence 可能与人类判断脱节。\n机制/约束：\nAre Neural Topic Models Broken?（arXiv:2210.16162）：神经主题模型在稳定性、与人类类别对齐上可能逊于经典 LDA。 Is Automated Topic Model Evaluation Broken?（arXiv:2107.02173)：自动化 coherence 与 topic rating / word intrusion 不一致。 Revisiting Automated Topic Model Evaluation with LLMs（arXiv:2305.12152)：LLM 评判与人工相关性更强于部分传统自动指标。 播客口述论文题 \u0026ldquo;are neural topic modeling evaluation metrics broken?\u0026rdquo; 未命中上述精确标题，写作应引用 arXiv 题名。画面叠印 ProxAnn: Use-Oriented Evaluations… 方向与「面向用途」评估一致；与 O\u0026rsquo;Reilly Hands-On Large Language Models（Jay Alammar \u0026amp; Maarten Grootendorst）为不同出版物。\n图：O\u0026rsquo;REILLY《Hands-On Large Language Models》封面（Jay Alammar \u0026amp; Maarten Grootendorst），叠印论文题 Use-Oriented Evaluation of Topic Models and Document Clustering（Alexander Hoyle 等）。\n演讲者观点：coherent + diverse 仍可能不是业务关心的维度；须叠加用户约束（最少主题数、必须覆盖某类、禁用某些词）。\n常见误区：单一 NPMI/coherence 排行榜选型；不做人工读文档与任务导向评测（use-oriented）。\n图：访谈中段分屏（约 24 分钟），讨论评估指标与神经主题模型时的典型画面。\n检索加速：两阶段与 IVF 类比 # 为什么：百万文档 × 全库向量 ANN 成本高；若文档已带主题 ID，可先缩小候选集。\n机制/约束：演讲者观点：先对主题 embedding 检索 Top-K 主题，再在相关主题内做文档级检索；语义信息可能与纯 embedding 重叠，但速度是明确收益。主持人口头估算约 5% 文档进入二阶段——无 benchmark，无法核实。与 FAISS IndexIVF 的粗量化类似；Weaviate 向量索引 文档列的是 HNSW / flat 等，非以 IVF 为主表述——类比对象需写明。\n怎么做：为主题标签/描述单独建 embedding 索引；文档对象写入 topic_id 元数据；二阶段 recall 用 held-out 查询集测。\n常见误区：未测二阶段召回损失即上线；把 5% 当作普适加速比。\n图：访谈后期分屏（约 49 分钟），讨论检索与向量库时的画面。\n混合管线：Supervised 与 Semi-supervised BERTopic # 为什么：已有 TopicGPT 标签或业务 taxonomy 时，不必再让 HDBSCAN 「重新发现」一遍。\n机制/约束：\nSupervised BERTopic：skip 降维，用分类器替代聚类，再对标签跑 c-TF-IDF。演讲者观点：称「hack」，但简单有效。 Semi-supervised：用部分标签做 semi-supervised UMAP，再 HDBSCAN；fit(docs, y=...)，未标注可用 -1。 Serialization：safetensors 不保存聚类/降维权重，推理可基于 topic embeddings；Dynamic Topic Modeling 可在不重跑聚类下对各时间片滑 c-TF-IDF。 怎么做：\n# 已有外部 labels（如 TopicGPT 输出） topic_model = BERTopic(umap_model=None, hdbscan_model=your_classifier) topic_model.fit(docs, y=labels) topic_model.save(\u0026#34;model_dir\u0026#34;, serialization=\u0026#34;safetensors\u0026#34;) 半监督：对已知主题文档传入 y，让 UMAP supervised 引导流形，再聚类。\n常见误区：以为 supervised 会「修正错误标签」——它假定 assignment 已给定，只重建表示与推断接口。\n图：访谈末段分屏（约 40 分钟），讨论监督式与动态主题时的画面。\n图：画面角标 Weaviate podcast（OCR 锚定：Weaviate podcast so）。\n未收敛的分歧（不必强行统一） # 轴 常见做法 嘉宾/讨论中的另一极 证据状态 主路线 Embed → UMAP → HDBSCAN → c-TF-IDF 全库 LLM TopicGPT 式分派 嵌入路线有 官方文档；全库 LLM 成本论点为访谈 层次 vs 单层 层次 BERTopic / TopicGPT lvl2 单层 + 细粒度元数据服务检索 双方观点，无实验对比 评估 NPMI / coherence LLM-as-judge + 人工 见 2107.02173、2305.12152 Agent 定义 固定 DAG workflow Tool use + human-in-the-loop 访谈观点 图：画面角标 m@\u0026amp; Weaviate（OCR 锚定）。\n若你要落地 # 先定消费者：分析师读报告、检索 router、还是离线评估——再选「词权重主题」还是「LLM 标签主题」，避免端到端黑箱。 默认走模块化 BERTopic：n_components=5 起步（官方示例），2D 图只作叙事；用 c-TF-IDF 分数 + 样本文档做 QA，而非只看 LLM 标题。 LLM 放在 generation 或引导：TopicGPT/TnT-LLM 产出 y 后，用 Supervised/Semi-supervised 接入；全库反复 assign 前先做成本估算。 评估叠加任务约束：在 coherence 批判 与 LLM 评判 之外，增加「必须覆盖类」「最少主题数」等业务规则；引用论文用精确 arXiv 题名。 检索二阶段先测 recall：主题 Top-K 与文档 ANN 的级联需 held-out 查询验证；勿采用未核实的 5% 文档比例作为容量规划依据。 参考与延伸阅读 # BERTopic 官方文档 — 模块化主题建模入口 BERTopic 算法总览 — 五步管线与默认组件 BERTopic 论文 arXiv:2203.05794 — class-based TF-IDF 与聚类流程 TopicGPT arXiv:2311.01449 — Prompt 式主题发现与分派 TnT-LLM arXiv:2403.12173 — 两阶段 taxonomy 与轻量分类器 Supervised BERTopic — 外部标签跳过聚类 Semi-supervised BERTopic — 标签引导 UMAP 再聚类 BERTopic safetensors 序列化 — 推理不保存降维/聚类权重 UMAP 监督降维 — y 与 target_metric HDBSCAN 参数选择 — min_cluster_size 语义 Are Neural Topic Models Broken? arXiv:2210.16162 — 神经主题模型稳定性与对齐 Is Automated Topic Model Evaluation Broken? arXiv:2107.02173 — coherence 与人工评判不一致 Revisiting Topic Model Evaluation with LLMs arXiv:2305.12152 — LLM-as-judge 与人工相关性 ProxAnn arXiv:2507.00828 — 面向用途的主题模型评估 Hands-On Large Language Models（O\u0026rsquo;Reilly） — Jay Alammar \u0026amp; Maarten Grootendorst 合著 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-agentic-topic-modeling-with-maarten-grootendorst-weaviate-podcast-126/","section":"文章","summary":"Agentic 主题建模：嵌入管线、LLM 与人在环内的工程权衡","title":"Agentic 主题建模：嵌入管线、LLM 与人在环内的工程权衡","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/agents/","section":"Tags","summary":"","title":"Agents","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/ai/","section":"Categories","summary":"","title":"AI","type":"categories"},{"content":" AI 驱动搜索：当 RAG、Agent 与经典 IR 重新接线 # 把 RAG 接到生产系统的人，迟早会撞上同一组张力：检索质量与 Agent 循环谁该背锅；长上下文与 可检索历史谁更便宜；MTEB 榜单模型与 本域 corpus 谁才算「上线答案」。Doug Turnbull（Relevant Search 合著者）与 Trey Grainger（Searchkernel；与 Doug 合授 AI-Powered Search）在一场约 54 分钟的对话里，从不同工程立场拆解这些张力——没有统一结论，只有可落地的分界与待验证边界。\n本场画面以三人连麦为主，未见可 OCR 的架构幻灯片；下文技术论断绑定公开文献/产品文档，嘉宾原话标为演讲者观点。\n问题空间：搜索仍在，只是接口变了 # 电商、论坛类场景里，用户仍习惯「搜索框 + 快速点击」——precision 与低延迟优先。法律发现、医疗文献、招聘筛选等则要求 高 recall、可审计的多轮穷尽（演讲者观点）。同一套向量栈无法同时优化这两种目标函数；NDCG@k、Recall@k、MRR 等指标也随任务类型在 MTEB 中并非单一总分可比。\nAgent 把「查索引」变成工具调用，把「拼上下文」变成 prompt 工程——Trey 将之与经典 relevance engineering 并置：内容、领域、用户三桶上下文，与 Agent 侧 prompt 管理同构（演讲者观点）。差别在于：搜索系统几十年沉淀的 索引接地（index grounding） 与 行为监督（revealed preferences） 是否被搬进 Agent 环。\n约 10 分钟：三人连麦；左下可见 Weaviate podcast 角标，右侧屏幕为 SearchKernel「Building the next generation of Search」。\n开场段画面：si} Weaviate - @ podcast 角标与 SearchKernel 品牌同屏。\nAgent 能否替代强检索器？ # 常见做法 # 行业叙事常暗示：Agent 可多轮发 query、自纠错，弱 retriever 也能「够用」；编码 Agent 里 grep / keyword 的成功案例又被拿来类比搜索。\n嘉宾分歧 # 立场 要点 迭代可补偿弱检索（主持方追问） 大量 query、自调整；是否不必继续卷 embedding benchmark？ 仍须强检索与可解释匹配（Doug） 对「good enough 就不训嵌入」：「you absolutely have to」 继续改进（演讲者观点）；grep/BM25 有效 partly 因为 输入—输出可预测，便于 Agent 推理。 BM25 + Agent 曾可行（Doug，~08:47） 给 Agent 简单 BM25 工具，靠 query expansion、多策略查索引、聚合，可能逼近语义检索——无一手 benchmark 证明等价于 dense retrieval（核实边界）。 机制/约束：Agent 环解决的是 策略搜索（试哪条查询、何时停）；Okapi BM25 与 dense retriever 解决的是 单次检索的匹配函数。二者正交。Doug 同时强调 harness：可 start/stop、重置 context、避免重复探索已搜区域，有时 dumber model + good harness 足够（演讲者观点）——这与「百万 token 单窗」路线不同。\n最小示例（概念）：\nloop: q = agent.expand(previous_hits, user_goal) hits = bm25(index, q, k=50) if agent.should_stop(hits): break # 评测：Recall@100 on legal discovery，而非单次 MRR@10 常见误区：用 coding-agent 的 lexical 成功 直接推出 产品搜索可停训 embedder；在 recall 优先 场景下，弱检索 + 多轮 Agent 仍可能漏掉关键文档（待任务评测）。\nDoug 还提到：纯 similarity ranking 不做 filtering——向量近邻能找「像 Purple 的床垫」，却分不清用户要的是 品牌 Purple 还是 紫色；这类歧义要靠索引挖掘、facet 或分类器补齐（演讲者观点），不能指望单次 cosine 排序自愈。\n约 20 分钟：讨论 agentic search 与 embedding 投入；右侧背景 SearchKernel 字样，非技术幻灯片。\n同段连麦画面：al NG Ue, aN eA (@\u0026amp; Weaviate —— @ podca i 角标叠字。\n上下文：压缩还是可搜索的历史？ # 为什么 # 长对话 Agent 为省 token 常做 compaction——删掉早期 tool 输出与用户澄清，只留摘要。\n机制/约束 # Trey 与 Doug（~11:46）主张：不应把 context 扔掉；应 log 到侧，需要时再 scan，按当前 prompt 生成更好上下文；当前 compaction 浪费数据，本质是 search problem（演讲者观点）。这与 RAG 的非参数记忆思想一致：外部可检索存储 + 按需拉取，而非在窗口内硬删。\n怎么做：为会话维护可全文检索的侧存储（同一 倒排索引 或向量索引均可）；每步用 检索 + 摘要 填充窗口，而非单向丢弃。\n常见误区：把 compaction 等同于「高效记忆」；丢失的 tool 轨迹往往是 免费监督信号（演讲者观点），后续无法做 LTR 或失败分析。\n上下文讨论时段连麦：(@® Weaviate _ @f podcast 角标。\n多阶段 RAG：先理解 query，再喂给生成 # 为什么 # 单阶段「embed query → top‑k → LLM」在实体稀疏、字段结构重的 catalog 上，常把 理解错误 与 检索错误 混在同一跳里，难以调试。\n机制/约束 # Trey（~17:35）描述 multi-stage RAG：第一阶段 query interpretation——在索引里匹配 query 片段、实体、术语；第二阶段才把 已接地 的检索结果交给下游 RAG（演讲者观点）。这与 Lewis et al. 原文中「整段 conditioning vs per-token passages」的两种 formulation 不同——后者是论文内消融，前者是 索引挖掘式 工程模式（核实：RAG 定义有文献支持，阶段划分属访谈）。\n怎么做 # stage1: spans, facets = interpret(query, index_stats) stage2: hits = hybrid(spans, filters=facets) stage3: answer = llm.generate(hits, query) 常见误区 # 把 Weaviate Query Agent 的过滤能力当成「已解决 query understanding」——能力存在，但 业务域 taxonomy 与评测集 仍需自建；访谈中的 filter inspector 命名未在公开文档对齐。\nQuery 理解：LLM 分类 vs 索引接地 # 常见做法 # 用 LLM 直接做域分类（如 NAICS 产业码、商品 taxonomy），再写过滤条件或生成答案。\n嘉宾主张 # Doug：LLM 可借助训练数据里的公开 taxonomy 快速分类，但易 hallucinate 错误码（演讲者观点）。Trey：Index-as-LM——从 query 抽片段 → 在 索引字段/术语/聚合 里核对 → 再 faceting、写检索 query；「almost like RAG for the LLM」 用于理解，而非仅生成最终段落（演讲者观点）。\n机制/约束：NAICS 等码表有官方层级；LLM 输出必须在 corpus 统计 上可验证（某 facet 是否存在、共现是否支持）。Weaviate Query Agent 文档描述 semantic search with optional filters 与聚合；访谈中的 filter inspector node 命名 未在公开文档中出现（部分核实）。\n最小示例：\nspans = llm.extract_entities(query) for s in spans: assert index.term_stats(s) # 接地：术语必须出现在索引或可聚合字段 facets = index.top_facets(matched_field) query = build_lexical_or_hybrid(spans, facets) 常见误区：跳过索引统计，直接让 LLM 写 filter JSON——在长尾实体、品牌/颜色歧义（如「Purple mattress」）上易错（演讲者举例）。\nHybrid 融合 vs Wormhole：两条正交路线 # Hybrid（经典工业路径） # [Trey 归纳 ~31:42]：BM25 与 dense 各跑一路，再 merge/boost——Weaviate hybrid search 文档 已核实：\u0026ldquo;runs both search types in parallel and combines their scores\u0026rdquo;，支持 relativeScoreFusion / rankedFusion 与 alpha 权重。\n为什么：lexical 擅 精确标识符（SKU、产品 ID）；dense 擅 语义近邻；并行融合缓解单路盲区。\n常见误区：把 fusion 当成 wormhole 的「更强版 merge」——二者目标不同（见下）。\nWormhole vectors（课程/访谈概念） # Trey（~26:17–35:58）：把 query 视为 文档集合的语义表示，在 sparse/lexical、dense、behavioral（如 矩阵分解推荐 隐因子）空间间 跳转，而非仅并排融合列表（演讲者观点）。\n访谈机制示例：lexical 命中集 → aggregate（如平均） 文档向量 → 在 dense 空间找邻域；可反向 dense top‑k → 生成 lexical/SKG 查询。社区实现 wormhole-vectors README 写的是 significant_terms 聚合，未写「向量平均」——与字幕 \u0026ldquo;average\u0026rdquo; 表述 不一致（核实边界：平均向量算子标为访谈观点）。\n向量 算术 直觉来自 Word2Vec 类比；Trey 的 Darth Vader + puppy → cosplay puppy 为口语例，强依赖嵌入模型与域（演讲者观点）。倒排索引可教学上视为极高维 稀疏 term 向量（IR 教科书 级表述，非严格 one-hot）。\n怎么做（伪代码，访谈版）：\ndocs = bm25.search(q, k=100) centroid = mean(embed(d) for d in docs) neighbors = dense.knn(centroid, k=20) 常见误区：未建 behavioral/CF 空间却宣称三空间 wormhole；在未 A/B 的情况下用 MTEB 榜首 embedder 代替 域内微调。\n约 26 分钟：Trey 阐述 wormhole；右侧 SearchKernel 屏显，无公式幻灯片。\n约 35:47 连麦帧：(@ Weaviate Sf podcast [exery 角标噪声，不足以单独支撑公式级论断。\n约 32 分钟：讨论 BM25 与 dense 分工；三人连麦 + SearchKernel 背景。\n榜单嵌入、域内因子与行为信号 # 常见做法 # 选 MTEB 榜首文本嵌入，接 Weaviate / Elasticsearch 即上线。\n嘉宾主张 # Trey（~39:06）：榜单模型是 起点；「secret unlock」 是把 query 接到 本 corpus、本 domain、本用户（演讲者观点）——与 MTEB 论文 \u0026ldquo;no particular text embedding method dominates across all tasks\u0026rdquo; 方向一致（文献支持）。Doug（~41:51）：数十年 beat BM25 靠的是 domain ranking factors，非仅 hybrid；embedding 质量 必要但不充分（演讲者观点）。\n行为 \u0026gt; 复杂重排（Trey，~44:17）：head query 上对 用户已点击/购买 项做 pop-boost，常优于纠结 BM25 位次——与 Learning to Rank 从点击推断标签的工业实践 方向一致；「常优于」无本场定量 A/B（访谈观点）。\n训练目标分裂（Doug，~25:15）：为 ranking/embedding 优化 vs 为 点击/购买等 revealed preferences 优化，是不同 objective 的灰区——与 LLM 时代前的搜索/推荐一脉（演讲者观点）。\n常见误区：用 Claude 等生成 ranking function 却不隔离评测集——Doug 警示可能 记忆训练集（演讲者观点，与一般 ML 泄漏常识一致）。\n约 38 分钟：IR leaderboard 与域适配；SearchKernel 屏显「next generation of Search」。\n连麦角标帧：(@) Weaviate Sef podcast ——\n交付形态：Workflow 优先于工具堆 # 常见做法 # 给 Agent 一堆 tools，指望模型自行组合成产品。\n嘉宾主张 # Trey（~53:46）：用 Agent 设计 workflow 与 tools，再 wire 成可重复、可上线的 workflow；atomic agents 在范围极窄时可由 小模型确定性 输出（演讲者观点）。这与「开放式 tool 环」相对——生产搜索更偏向 编排 + 原语质量。\nDoug 侧补充：搜索框在 高 recall 流程 中仍会存在，与大众 冲动点击 行为并存（演讲者观点）。片尾预告 ColBERT 等 late interaction 作为下阶段课程主题——已核实论文存在，与本场 wormhole/hybrid 讨论互补。\nTrey 在开场还提到 Google 「Generative UIs」 方向：LLM 产出 HTML/JS/CSS 生成交互界面（演讲者观点）。本环境 未 锁定与字幕完全同名的一手论文或博客 URL（无法核实），不宜把该点写进架构决策的硬依赖——仅作「生成式 UI + 搜索」的联想边界。\nworkflow 讨论邻近帧：(@\u0026amp; Weaviate podcast Sey 角标。\n约 52 分钟：workflow vs agent 堆；Weaviate podcast 标牌清晰可见。\n收尾段：@@ Weaviate ¢ podcast 角标帧。\n若你要落地 # 先定目标函数：precision@10 的导购搜索 vs recall@1000 的合规/法律发现——再决定是否上 Agent 外环，以及评测用 MRR 还是 Recall。 默认 hybrid，再评估 wormhole：用 官方 hybrid API 做 BM25+dense 并行基线；若跨空间跳转，对照 wormhole-vectors 的 significant_terms 实现与访谈中的向量平均是否一致，勿混为一谈。 Query 理解必须 index-grounded：LLM 抽实体后，用 facet/term 统计过滤幻觉；taxonomy（NAICS）对照官方码表。 会话记忆用检索侧存储，避免不可逆 compaction；把历史 tool 输出当可搜 corpora。 行为信号接入 LTR 或浅层 boost，与嵌入训练分开看 objective；任何 LLM 生成的 ranker 特征要做 hold-out 泄漏检查。 参考与延伸阅读 # Retrieval-Augmented Generation（Lewis et al., 2020） — RAG 参数/非参数记忆定义 Massive Text Embedding Benchmark（Muennighoff et al., 2022） — 无单一嵌入统治全任务 MTEB Leaderboard — 公开榜单与任务分解 ColBERT: Efficient and Effective Retrieval via Late Interaction（Khattab \u0026amp; Zaharia, 2020） — 片尾预告主题 Efficient Estimation of Word Representations（Mikolov et al., 2013） — 向量类比与组合性讨论起点 Introduction to Information Retrieval（Manning, Raghavan \u0026amp; Schütze） — 倒排、BM25、评测基础 Okapi BM25（Wikipedia + Robertson 综述链） — lexical 基线 From RankNet to LambdaMART（Microsoft Research） — LTR 与点击监督 Matrix Factorization Techniques for Recommender Systems（Koren et al. PDF） — behavioral 隐因子 Weaviate Hybrid Search 概念文档 — 并行 BM25+vector 与 fusion Weaviate Query Agent 介绍 — 语义检索 + 可选过滤/聚合 wormhole-vectors（社区 OpenSearch 概念实现） — dense↔sparse 桥接（机制与访谈需对照） AI-Powered Search（Grainger \u0026amp; Turnbull, Manning） — 课程配套教材 Relevant Search（Turnbull 等, Manning） — 相关性工程背景 U.S. Census Bureau — NAICS — 产业分类官方入口（本环境曾抓取失败，链接仍为标准入口） 证据说明：Weaviate hybrid、ColBERT、RAG、MTEB、BM25/LTR/Word2Vec 等见上链文档；wormhole 平均向量、BM25+agent≈语义、pop-boost 优于重排、workflow-first、compaction=搜索问题等标为演讲者观点或部分核实；Google「Generative UIs」论文题名本场未锁定一手出处（无法核实）。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-doug-turnbull-and-trey-grainger-on-ai-powered-search-weaviate-podcast-13/","section":"文章","summary":"AI 驱动搜索：当 RAG、Agent 与经典 IR 重新接线","title":"AI 驱动搜索：当 RAG、Agent 与经典 IR 重新接线","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/amber/","section":"Tags","summary":"","title":"Amber","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/ann/","section":"Tags","summary":"","title":"Ann","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/apache/","section":"Tags","summary":"","title":"Apache","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/arctic/","section":"Tags","summary":"","title":"Arctic","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/att/","section":"Tags","summary":"","title":"Att","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/avatar/","section":"Tags","summary":"","title":"Avatar","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/babylon/","section":"Tags","summary":"","title":"Babylon","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/bean/","section":"Tags","summary":"","title":"Bean","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/benchmark/","section":"Tags","summary":"","title":"Benchmark","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/bhis/","section":"Categories","summary":"","title":"BHIS","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/bhis-2026/","section":"Tags","summary":"","title":"Bhis-2026","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/boot/","section":"Tags","summary":"","title":"Boot","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/chamfer/","section":"Tags","summary":"","title":"Chamfer","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/cilium/","section":"Categories","summary":"","title":"Cilium","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/cilium/","section":"Tags","summary":"","title":"Cilium","type":"tags"},{"content":" Cilium 1.19：升级前该核对什么 # 若你正规划从 1.18 升到 1.19.0（2026-02-04），这版 minor 的「含金量」主要在运维契约变化：Multi-Pool IPAM 从 Beta 转正、数据面 IPsec 与 BPF 主机路由可组合、Ztunnel 进入 Beta、网络策略与 Cluster Mesh 默认更严、BGP v1 Peering Policy 被移除。对平台工程师而言，值得保留的不是幻灯片逐条朗读，而是一张「升级前检查单」：策略是否跨集群、DNS 是否用 **.、BGP CRD 是否仍 v1、IPAM 是否要多池、可观测是否要开 Option 136。本期 eCHO 为 GitHub Release 走读，无现场集群 demo；下文按主题归纳可落地项，命令与 Helm 字段以 v1.19 文档 为准，口述性能数字单独标注。\n升级与发布全貌 # Release 载明约 2934 commits、1010+ 贡献者。官方明确：使用 Network Policies、Cluster Mesh、LoadBalancer IPAM 或 BGP 时，升级前应阅读 1.19 Upgrade Notes，否则可能在策略语义或 BGP CRD 上踩坑。\n图：GitHub Release —「We are excited to announce the Cilium 1.19.0 release!」及「You may need to take action during upgrade… Network Policies, Cluster Mesh, LoadBalancer IPAM or BGP」提示。\nSelect Clusters Explicitly（#40609）：策略 selector 未写 cluster 时，默认只允许本集群流量。以前依赖「隐式全集群」的 Cluster Mesh 规则需要显式补 cluster 名，否则跨集群服务会突然不通 — 这是 1.19 最值得优先 diff 的 breaking 行为之一。\nActively Deny Connections（#41406）：egress 被 Network Policy 拒绝时，可回 ICMPv4 Destination unreachable，让客户端尽快失败，而不是黑洞式丢包。能力为 experimental，且仅 IPv4 egress。\n# Helm（v1.19，默认 none） policyDenyResponse: icmp Agent 等价：--policy-deny-response=icmp。见 Policy intro — Deny Response。\n图：Release 条目 —「Actively Deny Connections… ICMPv4 Destination unreachable」「Select Clusters Explicitly… default to only allowing the local cluster」。\nDNS 多级通配：matchPattern 支持 **.example.com 前缀，可匹配 foo.bar.example.com 等多级子域，但 不匹配 父域 example.com 本身；若策略同时要管 apex，需另写 matchName（策略语言 — DNS）。升级说明指出：既有 **. 规则语义可能变化，上线前应用 staging 集群回放 DNS 策略。\n弃用与引擎重构：Kafka 协议匹配（beta）、ToRequires / FromRequires 字段弃用；策略引擎内部重构为后续 minor 铺路（#39906 等）— 用户面主要是弃用字段迁移。\n数据面：IPsec、Multi-Pool IPAM、Gateway # BPF Host Routing + IPsec（#41997）：在 kube-proxy replacement、BPF masquerade 与 IPsec 同时开启时，转发可走 eBPF 主机路由，减少传统路由查找开销。Upgrade Notes 写明会自动启用 eBPF Host Routing；节点内核需包含 CVE-2025-37959 修复。演讲者提到短连接 CRR 约 30% 提升 — 未出现在 Release 或文档，勿写入容量规划。\nMulti-Pool IPAM（Stable，#40460）：Helm 设 ipam.mode=multi-pool；CiliumPodIPPool（cilium.io/v2alpha1）新增 spec.podSelector，用 Pod 标签选池而无需改 Pod spec。文档要求：每个 IP family 必须且仅能匹配一个 pool，否则分配失败。Release 写明可与 IPsec + direct routing 联用，适合按池划分地址段并加密跨子网流量（multi-pool 概念）。池级注解仍包括 ipam.cilium.io/ip-pool 等。\nGateway API：Cilium 在 GAMMA 场景下支持 GRPCRoute（#41936），与既有 HTTPRoute 能力对齐；gRPC 负载均衡在 Cilium 数据面已久，1.19 主要是 API 面对齐。Release 写依赖 Gateway API v1.4；gateway-api 文档 页脚仍写 v1.3.0 — 以 Release 与 go.mod 为准。\nEncryption strict mode：IPsec / WireGuard 均可开 strict，未加密节点间流量直接丢弃（#39239 等）— Release 有述，本期口述较简。若你已在用 WireGuard 或 IPsec 透明加密，升级时建议对照 Upgrade Notes 检查是否与 kube-proxy replacement、masquerade 模式冲突。\nZtunnel Beta（namespace 级纳管） # Ztunnel 标为 Beta（#42766 等）：Cilium 作控制面负责 workload 发现、证书签发；Pod netns 内通过 iptables 将 TCP 重定向到本地 ztunnel，实现透明加密。不支持 Pod 级开关，仅 namespace 标签；与 Cluster Mesh 不兼容（ztunnel 文档）。\nencryption: enabled: true type: ztunnel # 先按文档生成 secrets，再纳管 namespace kubectl label namespace \u0026lt;ns\u0026gt; io.cilium/mtls-enabled=true Helm 等价：--set encryption.enabled=true --set encryption.type=ztunnel。Mutual Authentication（out-of-band）在 1.19 默认关闭（#42665）；若目标是 workload mTLS，文档倾向先试 Ztunnel。Beta 阶段应预留回滚路径，并在非生产集群验证与现有 CiliumNetworkPolicy 的交互。\nLoadBalancer 与 port-forward：前者由云或 Cilium BGP/LB 数据面暴露 VIP；后者经 kube-apiserver 代理，适合调试。二者可并存访问同一 Service，但路径与可观测性完全不同 — 见 K8s port-forward 与 LoadBalancer Service（Cilium 上 LB 路径为演讲者归纳）。\nHost Firewall：VRRP / IGMP # keepalived VIP、IGMP 组播等流量没有 TCP/UDP 端口，旧版 host firewall 常因未知 L4 被丢弃。1.19 在 host 规则中支持 VRRP / IGMP（#39872、#41949），需集群开启 --enable-extended-ip-protocols。\n# v1.19.0 examples/policies/host/allow-extended-protocols.yaml toPorts: - protocol: VRRP # 或 IGMP 图：Release —「Match New Protocols: … VRRP and IGMP protocols in host firewall rules」及相邻 Network Policy 条目。\n口述中的 port: \u0026quot;0\u0026quot; 未出现在官方示例 — 部署前用 cilium policy validate 或集群实测确认 CRD 接受形态。\n可观测性：IP 追踪与 Hubble 过滤 # IPv4 Option 136（Stream ID）在路径上标记流，Hubble 用同一 ID 串联（#41306）：\nhelm install cilium oci://quay.io/cilium/charts/cilium --version 1.19.0 \\ --namespace kube-system \\ --set bpf.monitorTraceIPOption=136 hubble observe --ip-trace-id \u0026lt;id\u0026gt; hubble observe --encrypted # #43096 hubble observe --unencrypted 安装字段为 bpf.monitorTraceIPOption（非口述的 bpfMonitor.trace）。CLI 上 IP 追踪用 --ip-trace-id，--trace-id 是另一类过滤器（Hubble CLI）。「经 NAT 仍可追踪」为演讲者口述，官方教程未写死该场景。\nFlowLog 聚合（#42011）：在 dynamic export 上配置 fieldAggregate、aggregationInterval、fieldMask 等，向 SIEM 发送聚合事件，Hubble 仍保留完整流 — 功能在 CHANGELOG/Helm 注释中成立，尚无与 Release highlights 同级的独立教程。典型动机是降低 Elasticsearch/Splunk 写入量，同时用 includeFilters 只导出关心的 verdict 或 namespace；aggregationInterval 需与 fieldAggregate 同时非空且大于 0 才生效（见 Helm hubble.export 与仓库内 valid-flowlogs-config.yaml）。\nPLPMTUD、Helm OCI、BGP 与其它网络项 # 图：Release Networking —「Packetization-Layer Path MTU Discovery」「IPv6 Underlay」「Multi-Pool IPAM is ready for wider use」及 Services/BGP 摘要。\nPLPMTUD（#42012）：用 TCP 在 endpoint netns 探测路径 MTU；Helm 键 pmtuDiscovery.packetizationLayerPMTUDMode 等。统一最低内核版本表 Release 未给出，需查 PR 或在目标节点实测。\nHelm 分发：除 helm repo add cilium https://helm.cilium.io 外，可用 OCI：\nhelm install cilium oci://quay.io/cilium/charts/cilium --version 1.19.0 \\ --namespace kube-system 见 Helm 安装 — 两种源并存，文档未宣布淘汰经典仓库。\nBGP breaking change：CiliumBGPPeeringPolicy（v1）已移除，需迁移到 cilium.io/v2 的 CiliumBGPClusterConfig、CiliumBGPPeerConfig、CiliumBGPAdvertisement 等（Upgrade Notes）。1.19 还增加 Interface 类型通告、可覆盖 BGP session 源地址、无 endpoint 时撤回路由等运维向能力（Release BGP 小节）— 但若仍引用 v1 Peering Policy YAML，升级会直接失败，应优先改 GitOps 清单。同页还可关注：BIG TCP in tunnels（#43416）、IPv6 underlay（#40324）、L2 Announcements IPv6 ND、IPv6 Service loopback — 均以 Release 为准。\n未闭合边界 # 断言 状态 BPF+IPsec「~30% TCP CRR」 演讲者口述，无官方 benchmark hubble observe --trace-id 做 IP 追踪 应为 --ip-trace-id Host policy port: \u0026quot;0\u0026quot; 部分核验，以示例 YAML + 实测为准 PLPMTUD / BIG TCP 内核门槛 需按节点查 PR/能力 Release 将 v1.19 与 Cilium 十周年并列；贡献入口 slack.cilium.io、Contributing。KubeCon 阿姆斯特丹议程见 Cilium at KubeCon EU 2026。书籍发行日、下期 1.20 嘉宾等为口述，不作技术契约。\n延伸阅读：v1.19.0 Release · 文档首页 · Upgrade Notes · IP 包追踪教程\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-echo-ebpf-cilium-office-hours-echo-episode-202-exploring-new-features-in-cilium-1-19/","section":"文章","summary":"Cilium 1.19：升级前该核对什么","title":"Cilium 1.19：升级前该核对什么","type":"posts"},{"content":" Cilium 十年：社区规模、调研信号与 1.19 技术脉络 # 本集值得保留什么 # 这不是功能发布会，而是一场创始人圆桌 + 社区调研揭晓 + AMA。若你关心 Cilium 为何从 2015 年一个 .gitignore 起步走到今日 CNCF 头部项目，或想对照口述路线图与已发布的 1.19 文档，这集仍有归档价值：屏上的里程碑数字、LinkedIn 调研 Top 答案、以及 Daniel Borkmann 对 Big TCP / BBR 与 Thomas Graf 对 ztunnel、L7、2.0 的表述，适合作为「社区叙事」与「官方文档」交叉阅读的入口。穿插的 NGINX 策略 typo、用 Quake 3 测 NodePort 等轶事无法独立核验，但有助于理解维护者如何看数据平面演进。本场无现场终端演示；下文命令来自官方文档，可在自己集群复现。\n十年刻度：从首 commit 到 CNCF 体量 # 2015 年 12 月 16 日，Thomas Graf 在 GitHub 上提交 Cilium 初始 commit（7fa3c60，变更含 .gitignore 与 LICENSE）。CNCF 项目页记载 Cilium 于 2021-10-13 以 Incubating 项目纳入，2023-10-11 毕业。\n节目中屏显里程碑（时点快照，非实时 API）包括：1000+ 贡献者（幻灯片曾显示 1,012）、CNCF 按总贡献量排名第二、45k+ GitHub stars、加入 CNCF 后总贡献 546,000+（较 2021 年增 142%）、全仓库 50,000+ PR、贡献者遍布 82 国。主持 Liz Rice 亦提醒部分数字可能已过时——与 GitHub star、贡献者口径随时间漂移一致；撰写引用宜注明统计日期。「CNCF 贡献第二」「546k/82 国/50k PR」等未在本次独立导出 DevStats 报表中复核，若需严谨引用请查 Cilium DevStats。\n图：幻灯片 Milestones — 首 commit 2015 年 12 月、1000+ contributors、CNCF 贡献量第二、45k+ GitHub stars（屏上文字）。\n图：幻灯片 Milestones (cont\u0026rsquo;d) — Total Contributions 546,000+、Total PRs 50,000+、Global Reach 82 countries（屏上文字）。\n资源\nCilium 初始提交 CNCF 项目页 — Cilium cilium/cilium 仓库 社区调研在说什么（竞猜结果，非官方采用率） # 节目用「Survey says…」形式揭晓事先在 LinkedIn/社交媒体收集的答案。下列排名属社区调研宣读，不可写成官方产品统计或采用率。\n问题 屏上揭晓的 Top 方向 用 Cilium 替换过什么 其他 CNI → kube-proxy → Service mesh / Ingress 生产中最常用特性 Networking（泛化 CNI）→ Network policy → Hubble UI → kube-proxy replacement 解决的「最大问题」 Network policies that work → Observability → Performance → Service mesh complexity 如何首次听说 Blog/article → Conference → … → YouTube and eCHO 企业名猜题中屏显 Adobe、Microsoft、OpenAI；USERS.md 可核对自述用户（含 Adobe、Microsoft 等），OpenAI 未出现在该列表（截至公开 USERS.md）。轶事层面，创始人提到用 Cilium 替换 Calico、kube-proxy、Ingress、独立 Service mesh 等——与调研方向一致，但仍属演讲者/社区叙述。\n图：Survey says — Network policies that work、Observability/visibility、Performance issues、Service mesh complexity（屏上文字）。\n资源\nCilium 用户自述列表 USERS.md Big TCP、eBPF 路径与 1.19 已落地项 # AMA 中 @eBPFCilium 提问 Big TCP 里 eBPF 的角色；Daniel Borkmann 解释：Big TCP 依赖内核聚合更大报文；若流量仍走传统 host 栈，socket 关联可能被「孤立」，TCP 背压失效——需 eBPF Host-Routing 等路径才能拿到文档所述的性能收益。他口述 Cilium 侧报文规模约 256K；当前 stable 文档写 GSO/GRO 上限为 192k，正文应以文档为准，256K 记为口述、未与文档对齐。\nCilium 1.19（口述「约 2 月初」）已与 v1.19.0 发布（2026-02-04）对齐，要点包括：Ztunnel Beta、Gateway API 改进、Helm chart 经 OCI 发布。口述「增强 mutual TLS」在发布说明中对应 ztunnel 新路径；旧 Mutual Authentication 默认关闭，迁移时需读 release note，不宜简单理解为「旧 SPIRE 流加强版」。\n图：观众提问 — Big TCP 中 eBPF 扮演什么角色、用到哪些 eBPF program（聊天区文字）。\n启用 Big TCP 与 Host-Routing 时，文档要求（Cilium 1.19.x stable）内核：IPv6 BIG TCP ≥ 5.19，IPv4 BIG TCP ≥ 6.3。\n# Helm 示例（摘自性能调优文档思路；按环境改 values） helm upgrade cilium oci://quay.io/cilium/charts/cilium \\ --namespace kube-system \\ --reuse-values \\ --set kubeProxyReplacement=true \\ --set bpf.masquerade=true \\ --set routingMode=native \\ --set enableIPv4BIGTCP=true \\ --set enableIPv6BIGTCP=true kubectl -n kube-system exec ds/cilium -- cilium-dbg status # 关注 Host Routing: BPF、IPv4/IPv6 BIG TCP: enabled 等行 资源\n性能调优 — BIG TCP / eBPF Host-Routing 系统要求 — 内核版本 v1.19.0 Release Notes Helm OCI 安装 ztunnel、加密与 IPv6 设计史 # Thomas Graf 描述 ztunnel（encryption.type=ztunnel）：节点侧透明 L4 mTLS，控制面由 agent 与本地 ztunnel 协作；不宜在同一集群混用 Cilium ztunnel 节点与 Istio Ambient ztunnel 节点——工程判断，公开文档未逐字写「禁止混用」，但 Istio 集成 强调避免冲突配置。重要冲突：他提到通过 Cluster Mesh 传播 ztunnel 身份；而 ztunnel 文档 写明 Cluster Mesh 与 ztunnel 不兼容（安装前勿启用 Cluster Mesh）。正文若以文档操作为准，应视为互斥。\nhelm install cilium oci://quay.io/cilium/charts/cilium \\ --namespace kube-system \\ --set encryption.enabled=true \\ --set encryption.type=ztunnel kubectl label namespace \u0026lt;your-ns\u0026gt; io.cilium/mtls-enabled=true IPv6 方面，演讲者观点：早期版本曾偏 IPv6-only（容器规模与地址规划动机），后因用户压力强化 IPv4；「用 IPv6 寻址内存」更像愿景，Daniel Borkmann 称今日更常见 DMA 等路径，无已知生产级「IPv6 寻址内存」方案可核验。\n资源\nZtunnel 透明加密（Beta） Mutual Authentication（SPIRE/mTLS） Gateway API BBR、BGP 与实现选型 # BBR：Daniel 提到修复 pod→host 路径上 TCP 时间戳丢失问题；Bandwidth Manager 记载旧内核在 netns 切换时的同类问题，并写明需 eBPF Host-Routing，bandwidthManager.bbr=true。KubeCon 上 Cubic vs BBR 流媒体对比属演示轶事。\nkubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i bandwidth BGP：聊天区 Tony Norlin 描述——Cilium BGP 使 CNI VXLAN 外组件（如外部 k8s control plane）可通信；BGP Control Plane 支持向路由器通告 Pod/Service 路由，具体拓扑为用户场景、本场无 demo。\n# Helm values 片段 bgpControlPlane: enabled: true Go：Thomas、André 解释当年 K8s 生态客户端以 Go 为主、内核开发者上手快；agent 内存占用难精细控制、早期 CI 用 bash——均为演讲者观点。\n资源\nBandwidth Manager（含 BBR） BGP Control Plane kube-proxy 无替代模式 L7 能力边界与「2.0」何时出现 # Thomas Graf 区分两条线：开源 L7 网络策略经节点侧 Envoy 代理（见 Layer 7 Policies）；Isovalent Enterprise 的 eBPF L7 解析器（HTTP/DNS 等被动可观测、低开销）不能替代完整 proxy（无 retry、L7 LB 等需终止/流控的能力）——Enterprise 产品线在 docs.cilium.io 无与「L7 eBPF parser」逐字对应的开源页，功能边界标演讲者观点。数据平面成熟度（如超大规模才触发的 race）亦为维护者经验，无独立 CVE 条目。\n关于 Cilium 2.0：无时间表；动机包括长期「无缝升级」累积的技术债、以及 Kubernetes 用于虚拟化的新需求；不会为营销单独打 2.0/10.0——演讲者观点。若你跟进特性，Liz 预告后续 eCHO 可能有 1.19 特性专场与云原生展望，属节目安排而非发布承诺。\n资源\nLayer 7 Policies（Envoy） TLS 连接检查 Cilium 文档首页 未核验边界（阅读时留意） # 社区调研全部排名、屏显企业名单与真实采用率 P02 中 546k 贡献、82 国、50k PR、CNCF「第二」的精确口径 Big TCP 256K vs 文档 192k；ztunnel + Cluster Mesh 口述 vs 文档互斥 Enterprise L7 eBPF parser、Cilium 2.0 时间与动机、Quake3/Datadog/Android 等轶事 IPv6-only 首发完整时间线 若你只关心可动手验证的路径：OCI 装 1.19 → cilium-dbg status 看 Host Routing / BIG TCP / BandwidthManager → 按需启用 ztunnel 或 BGP，并对照上表区分「文档」与「本场口述」即可。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-echo-ebpf-cilium-office-hours-echo-episode-200-celebrating-a-decade-of-cilium/","section":"文章","summary":"Cilium 十年：社区规模、调研信号与 1.19 技术脉络","title":"Cilium 十年：社区规模、调研信号与 1.19 技术脉络","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/cli/","section":"Tags","summary":"","title":"Cli","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/cloudnative/","section":"Categories","summary":"","title":"CloudNative","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/compound/","section":"Tags","summary":"","title":"Compound","type":"tags"},{"content":" Compound AI：当「一次 LLM 调用」不够用时 # 生产里的 AI 应用很少停在 chat.completions 单轮调用上。Berkeley BAIR 对 Compound AI System 的定义把重点放在多个可交互组件——多次模型调用、检索器、外部工具、业务逻辑——共同完成一项任务；文中引用的企业调研还提到约 60% 的 LLM 应用含 RAG、约 30% 含多步链。与此同时，「Agent」产品形态、DSPy 式 workflow、o1 带动的逐步采样，都在争夺同一套工程预算。\nBaseten 开发者关系负责人 Philip Kiely 在一场约一小时的访谈里，从结构化输出与推理栈讲到多模型编排与部署粒度，并反复把 compound AI 描述成「建系统的方式」，而非某个 SKU。下文按工程主题整理可落地的机制与边界；凡未在官方文档或论文中核对的数字，均标明来源。\n开场分屏：左侧主持人背景可见 Florida Atlantic University 文凭；右侧嘉宾 Polo 衫印有 baseten 标识，身后书架陈列 Brandon Sanderson 系列\n问题空间：从 RAG 到 Agent，争的是同一条流水线 # 常见做法：用向量库 + 生成模型做 RAG，或用带 function schema 的 ReAct 循环做 Agent；评测分别指向检索质量（MRR、nDCG）或开放任务（如 WebArena）。\n另一种视角（演讲者观点）：compound AI 是节点与边组成的图——模型、API wrapper、人工审核（HITL）、普通代码都是节点；「Agent」只是用这种图做出来的一种产品形态，与固定步骤的 workflow 并列，并不自动减少底层部署单元。\n证据与边界：BAIR 文支持「多组件系统」框架，但未在术语层面区分 Agent / Workflow。主持人提到的 Alto（面向 compound 查询 的流式与并行编排）与 Agent Workflow Memory（用 workflow 轨迹做 in-context 学习）在公开摘要里与「分组件扩展」方向一致，但访谈未展开实现细节，正文不引用其具体指标。\n结构化输出：约束解码 vs 事后清洗 # 为什么 # 多模型流水线里，上下游 schema 不一致会直接变成集成债务：ASR 文本格式、工具 JSON、向量库写回字段，任一环节输出非法结构都会触发重试、降级或静默丢数据。\n机制与约束 # Outlines 将生成建模为有限状态机上的转移；arXiv:2307.09702 通过词表 index 与 allowed_tokens 在每一步屏蔽非法 token，文档称结构保证发生在 during generation 且 adds little overhead。outlines-core 的 Index::new 对应「由 schema/regex 编译一次、多次复用」。\n演讲者观点：Baseten 在 model server 层集成 Outlines，稳态可达接近 100% schema 合法且 tokens/s 与无约束接近；首次为复杂 schema 编译 state machine 可能需 10–20+ 秒（更复杂则更久），适合 schema 固定的批量场景（如向量库 generative feedback loops），不适合每条请求动态换 schema。\n部分可核对：论文支持 FSM 与低开销方向，不等于吞吐持平或 10–20s 冷启动——后者未见于公开 README，实施前需自测。\n怎么做（minimal example） # # 概念示意：固定 schema 时编译 Index，推理阶段逐步 mask from outlines import models, generate model = models.transformers(\u0026#34;meta-llama/Llama-3.2-3B-Instruct\u0026#34;) generator = generate.json(model, YourPydanticModel) # 编译一次 out = generator(\u0026#34;Extract entities from: ...\u0026#34;) # 多次复用 备选路径：自然语言生成 → 第二次 LLM 清洗；或 OPRO 类 prompt 搜索。主持人认为 constrained generation「在赢」；演讲者观点提醒可能影响 reasoning 等能力，需 A/B。\n常见误区 # 把「解析失败再 prompt 一次」当成免费重试——延迟与成本会线性叠加。 在每条请求不同 schema 的场景硬上编译缓存，冷启动会主导 P99。 忽视 DSPy Assertions 一类运行时约束（文档注明 Assert 已 deprecated，宜转向 Refine / Suggest）与解码约束的分工：前者偏程序语义，后者偏 token 级保证。 分屏访谈画面：OCR 可见 NOONWEE、NOOGNVas、Atlante 等书架字样；画面无技术幻灯片，讨论点依赖音频上下文\n约 4 分钟处：嘉宾衬衫 baseten 标识与 Tress of the Emerald Sea 书脊同框，对应结构化输出与 model server 讨论段\n语音与多模态：专家模型链 vs 单一「全能模型」 # 为什么 # GPT-4o 类内置多模态把 ASR、推理、TTS 收进一个接口，部署故事简单；但在实时语音、区域延迟敏感的场景，流水线 specialist 仍常见。\n机制与约束 # Whisper 的 transcribe() 使用 sliding 30-second window（文档支持，verified）。长音频必须分块、并行转写、再拼接——这是模型/产品边界，不是可选优化。\nREADME 模型表列 turbo 相对 large 约 ~8× 速度，脚注写明在 A100 上测英语转写，real-world speed may vary。演讲者观点将其表述为 real-time factor 并提及 T4/A10/L4——GPU 清单与 RTF 口径未在 README 中给出，写作时应写「相对 large 的 README 相对速度」，勿直接等同 RTF。\n典型链：Whisper → 流式 LLM → TTS（Piper、Coqui、Rime 等）。演讲者观点：同区域部署、避免公网 hairpin、DNS/集群内路由，可把端到端压在亚秒级；编排不当则轻易到 3–4 秒。\n怎么做 # 音频流 → chunk@30s → Whisper Turbo (并行) → 拼接文本 → LLM (stream) → TTS → 音频流 ↑ 同 VPC / 同 AZ，避免跨区回环 常见误区 # 假设「一个 Realtime API」一定比三模型链贵或慢——取决于流量形态、区域与是否愿意投入编排。 忽视 ColPali 一类「向量库直接检索 PDF 图像」与「图→文本再 RAG」的路线分歧；访谈未给出定量对比，属工程选择。 Whisper / 语音流水线讨论时段：书脊可见 TRESS、WALL STREET 等字样，嘉宾 Polo 衫 baseten\nEMERALD SEA、BRANDON SANDERSON 书脊与分屏构图，用于标记多模态 compound 讨论语境\n推理栈：prefill/decode、MoE、TensorRT-LLM 与 Truss # 为什么 # Compound 系统的瓶颈往往在最慢节点的 GPU 利用率，而不是 API 层 QPS。\n机制与约束 # Prefill vs decode（部分 verified）：NVIDIA 推理优化文 将 prefill 描述为可饱和 GPU 利用的并行阶段，decode 为 memory-bound、受 HBM 带宽主导。演讲者观点进一步把 prefill 归为 compute（FLOPs）主导——比官方表述更细，长 context 时两者可能同时吃紧。\nMoE（部分 verified）：Switch Transformers 强调每样本 sparsely-activated。演讲者观点：batch 推理时不同请求可能激活不同 expert 子集，batch 内往往趋向激活全部 expert，MoE 相对 dense 的带宽优势会缩水——属工程归纳，缺论文逐字定理。\nTensorRT-LLM（部分 verified）：Core Concepts 说明通过 build_engine 编译，TensorRT 会为 available GPU 选择 kernel；in-flight batching 即 continuous / iteration-level batching。演讲者观点：Baseten 内测部分场景优于 vLLM——无公开表格，不可当作通用结论。换 GPU 通常应 rebuild engine（强推断，文档未写死一句 \u0026ldquo;must rebuild\u0026rdquo;）。\nTruss（演讲者观点）：Truss 用 Python + YAML 打包模型服务，对标 Ray Serve、BentoML；Philip 自述非 Docker 专家，靠 Truss 降低 serving 门槛。快速原型仍可用 vLLM 搭 OpenAI-compatible 端点。\n怎么做 # # truss 极简示意（结构因版本而异） model_name: my-llama python_version: py311 resources: accelerator: A10G 常见误区 # 在 batch 流量下按 MoE「单请求稀疏」估算成本。 未为 TensorRT engine 规划 GPU 型号锁定 的 CI/CD。 把 Agent「一个入口」等同于「一个 autoscaler」——见下一节。 约 20 分钟：TensorRT-LLM、vLLM、batching 讨论时段的分屏画面，右侧 baseten 标识清晰\n约 20:39：嘉宾讲解推理栈时张口发言，背景 Oathbringer、Tress 等书脊\nTruss / Ray 话题附近：TRESSS、WALLSTREET 书脊 OCR 片段与 baseten 衬衫\nESSorme、BRANDON TRESS、WALL STREET 等书架 OCR 与推理性能讨论时段对齐\nAgent、工具调用与评测：图相同，产品不同 # 为什么 # WebArena 类 benchmark 奖励开放循环（「任务完成了吗？」）；固定五步写博客流水线则更像 workflow，可显式并行，类似写 forward pass（主持人归纳）。\n机制与约束 # Function calling 下沉：Llama 3.2 MODEL_CARD 给出 BFCL V2 Tool Use：1B acc 25.7、3B acc 67.0（bf16 行），3B 接近 Llama 3.1 8B 的 67.1。表头为 Tool Use 而非 exact 短语 \u0026ldquo;function calling\u0026rdquo;，正文宜写 BFCL 工具调用能力。\n工具选择与执行分离（演讲者观点）：倾向 function selection——LLM 只选工具与填参，执行在 compound 图外层。主持人补充 Gorilla 与 GraphQL/text-to-SQL 场景下参数格式化仍是难点。\n搜索式 ensemble：主持人问 o1 式「每步采样 N 次再集成」是否成主流；演讲者观点野外更常见的是多模型拼多模态，而非大规模 repeat inference ensemble。\nMeta Agent Search：Automated Design of Agentic Systems 用搜索发现 agent 设计；演讲者观点客户项目多为能力驱动（加长音频、加模态），架构常由模型能力边界倒推，再对弱项加校验步骤，而非先搜索拓扑。\n怎么做 # 对固定流程：用显式 DAG + 无依赖步骤并行；有依赖或超长步骤再用队列 + async/webhook（Baseten async inference）。Weaviate generative feedback loops（对集合批量跑 LLM 写回属性）适合 schema 固定 + 批量；与 Philip 强调的「无依赖子任务不要串行队列」一致。\n常见误区 # 用 WebArena 分数指导确定性流水线的产品决策。 以为单个 Agent 服务能减少 Whisper / LLM / TTS 的独立 endpoint 数量（演讲者观点：仍须分别部署与扩缩，例如 2× Whisper + 3× Llama + 1× TTS）。 把模型弱项（拼写、计数、数学）全塞回微调；BAIR 强调 system design 常比单纯 scale 更快迭代，演讲者观点 外围 regex/检索/算术代码往往更省。 约 30 分钟：Agent、compound 范式讨论时段，右侧 Taj Mahal 模型与 Sanderson 书架\n约 12 分钟：Whisper Turbo、多模型 autoscale 讨论时嘉宾开口讲解\n可靠性：校验、队列与人在回路 # 为什么 # Compound 系统的失败往往是级联：检索漏了、JSON 歪了、工具超时了，最终都表现为「模型胡说」。\n机制与约束 # 外围校验（部分 verified）：BAIR 列举 filtering outputs、verify facts 等控制手段。DSPy 的 Assert/Suggest 在失败时可 backtrack（Assert 已 deprecated）。\n演讲者观点：若目标是 100% 格式合规，有时 regex 包裹 比继续堆 RL 更省；「verifying is easier」在访谈中为口头概括，未定位到单一论文标题。\n队列 vs 并行：有依赖 → task queue + webhook；无依赖 → 并行。HITL 可接 Slack + HumanLayer 等（主持人举例）。\n常见误区 # 把所有步骤塞进 FIFO 队列「求稳」，拖慢可并行的段落。 只做生成端约束，不做业务事实校验（RAG 仍须 citation / 二次检索验证）。 约 10 分钟分屏：左侧 Florida Atlantic University 文凭，右侧 baseten 标识与 Sanderson 书架，对应可靠性/编排讨论语境\n未收敛的分歧（刻意并存） # 主题 常见实践 访谈中的另一强调 你怎么选 系统 vs 产品 买一个 Agent 平台 Compound 是构图方式；Agent 是产物之一 先画数据流与 SLA，再贴标签 多模态 单一 frontier 多模态 API 同区域 specialist 链 量延迟方差与$/分钟 输出约束 Instructor / 二次 LLM Outlines 类解码约束 schema 是否固定、能否摊销编译 部署 单 K8s Deployment 每模型独立 GPU 与 autoscaler 看流量比是否随时间漂移 弱项 继续 SFT / RLHF 外围代码与 regex 失败是否可形式化 若你要落地 # 先画 compound 图：标出每个节点的输入/输出 schema、超时、重试策略；再决定叫 Agent 还是 workflow。 固定 schema 的批量写回（向量库 generative、ETL）：评估 Outlines 或同类 constrained generation，并为 schema 编译做冷启动预算（自测 P99，勿照搬 10–20s）。 语音/多模态：按 Whisper 30s 窗口 设计分块；README 的 turbo ~8× 仅作相对 large、A100 的参考，上线前在你的 GPU 与语种上复测。 推理：原型用 vLLM；极致吞吐再评估 TensorRT-LLM engine + in-flight batching，并把 GPU 型号 锁进构建流水线。 扩缩：按节点流量比配置独立副本（Whisper vs LLM vs TTS），避免 monolith autoscaler；无依赖步骤默认并行，队列只保护有状态或顺序敏感的边。 参考与延伸阅读 # BAIR — Compound AI Systems（定义与 RAG/多步链统计） arXiv:2307.09702 — Efficient Guided Generation for LLMs（Outlines 论文） Outlines — 生成期结构保证 outlines-core — Index 与 FSA API OpenAI Whisper — 30 秒滑动窗口与 turbo 速度表 NVIDIA — LLM Inference Optimization（prefill/decode） TensorRT-LLM — Core Concepts TensorRT-LLM — In-flight Batching vLLM 项目 Baseten Truss — 模型打包与部署 Llama 3.2 MODEL_CARD — BFCL V2 Tool Use arXiv:2307.13854 — WebArena arXiv:2305.15334 — Gorilla arXiv:2403.04311 — Alto arXiv:2408.08435 — Automated Design of Agentic Systems ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-compound-ai-systems-with-philip-kiely-weaviate-podcast-105/","section":"文章","summary":"Compound AI：当「一次 LLM 调用」不够用时","title":"Compound AI：当「一次 LLM 调用」不够用时","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/compute/","section":"Tags","summary":"","title":"Compute","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/conference/","section":"Categories","summary":"","title":"Conference","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/conference/","section":"Tags","summary":"","title":"Conference","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/consumer/","section":"Tags","summary":"","title":"Consumer","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/copilot/","section":"Tags","summary":"","title":"Copilot","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/core/","section":"Tags","summary":"","title":"Core","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/data/","section":"Tags","summary":"","title":"Data","type":"tags"},{"content":" Data Agent：当「会写代码的模型」撞上真实数据栈 # 企业里最常见的数据问题，早已不是「模型会不会 SQL」。同一笔业务可能散落在 Snowflake 仓库、MySQL 应用库、Mongo 文档、Salesforce（SOQL）和 Slack 线程里；join key 可能是 bid_* 与 bref_* 的前缀游戏，而不是干净的 id = id。在这种栈上，Retrieval-Augmented Generation (RAG) 的「单一大索引」愿景与 Kaggle 式「千张表各自为政」之间，夹着一类更难评测的系统：data agent——跨源取数、清洗、推理并给出可核对答案的自主（或半自主）执行体。\n下文把 UC Berkeley 方向研究者 Shreya Shankar 在公开 benchmark、声明式文档系统与「部落知识」记忆上的工作，与 Weaviate 侧对 query/transformation agent 的产品映射并置；不强行收束为单一结论。可核对处对齐论文与仓库；口述数字与机制转述处会标明边界。\n「Data agent」到底指什么 # 为什么争论定义本身很重要 # 评测、产品路线图和招聘 JD 里的「data agent」若不对齐，团队会在 text-to-SQL leaderboard 高分与「跨库答不出题」之间产生虚假安全感。定义分裂会直接决定：你是优化单一 vector DB 上的 NL 检索，还是建设跨 DBMS 的工具链与失败模式库。\n机制与约束 # 常见有三层张力（演讲者观点与产品表述并存）：\n视角 核心主张 可核对边界 宽定义 跨库、跨文件、跨协作工具取齐信息，再计算与推理 难点在 data thinking 与工具熟练度，而非单点 NL→SQL（演讲者观点） 产品映射 读路径（NL→检索/聚合）+ 写路径（分类、填属性）拼成完整 agent Weaviate Query Agent / Transformation Agent 为演讲者观点下的读/写两半 评测定义 多 DBMS、畸形 join、SQL+Python 工具轨迹 以 Data Agent Benchmark (DAB) 的 workload 属性为准 怎么做（最小落地视角） # 先把「一题」拆成可观测子能力，再选执行形态，而不是先选「是否叫 agent」：\n用户问题 → schema/样本探测 → 清洗与 join 计划 → 执行（SQL / Python / 声明式算子）→ 可自动对标的答案 常见误区 # 把 pass@1 在单库 text-to-SQL 上的分数等同于 data agent 能力。 默认「上 RAG」就能替代跨库 join 与清洗；结构化可表达时，嘉宾倾向 能 SQL 就 SQL（演讲者观点），embedding 留给模糊语义。 公开 benchmark 说了什么、没说什么 # 为什么需要新的 DAB # Can AI Agents Answer Your Data Questions? 指出：既有工作多覆盖 text-to-SQL 片段、小表 in-context 或单管道能力，缺少对跨多 DBMS 完整流水线的系统评估。嘉宾称见过至少 25 个相关 benchmark（演讲者观点，未见可核对清单）；「25+」不宜写成论文定理。\nDAB 规模（论文）：54 道查询、12 个数据集、9 个领域、4 种 DBMS；与 Hasura PromptQL 的行业访谈共同塑造 workload（含 ill-formatted key joins、多库集成）。\n机制与约束 # 指标（论文，2026-05 核实）：\n最佳前沿模型 Gemini-3-Pro：pass@1 ≈ 38%（播客口述约 37%，属同一量级）。 每题 50 次独立 trial；同一设置下 pass@50 最高约 69%——说明「多试几次」与「一次配对」差距极大。 pass@k 沿用代码生成文献惯例：k 次独立运行中任一次正确即计该题成功。 刻意未覆盖（设计与演讲者观点一致）：semantic-operator 系统（如 LOTUS、Palimpzest）的对照轨；逼真且可自动评的 写操作；可靠的多轮「模拟业务用户」。每题仍多为单一标答，与真实分析的多解性存在张力。\n怎么做 # 复现或对标 leaderboard 时，必须对齐：模型名、trials/query、是否 hints。DataAgentBench 仓库 与 leaderboard 行字段缺一不可。\n常见误区 # 把第三方提交的 5 trials/query、60%+ 与论文主实验的 50 trials、38% 直接比较。 认为 benchmark 高分等于生产可部署（写路径、权限、成本未测）。 多库集成：schema 不够，必须「看数据」 # 为什么 # DAB 将 multi-database integration 与 ill-formatted key joins 列为核心属性：例如 PostgreSQL 与 SQLite 并存时，id 与 bid_* / bref_* 前缀不一致，迫使 agent 解析而非假设等值 join。同一公司也可能同时跑多个 SQL 引擎（仓库 + 业务 MySQL），算子相似仍无法一条 SQL 联邦（演讲者观点）。\n机制与约束 # metadata API 能列出 schema，但对 ill-formed join key（文本、字符串数组等）往往不够；轨迹级失败类型含 Incorrect Regular Expression (FM4) 等（论文附录）。嘉宾强调：需看数据再定清洗（如多正则 union 后再 join），不能只靠「脑中假设」（演讲者观点，与论文方向一致）。\n怎么做 # -- 探测性采样（论文轨迹中常见；非生产最佳实践） SELECT * FROM some_table LIMIT 5; -- 随后：根据样本设计清洗，再 join（逻辑示意） 生产侧更宜：受控采样 API、列级 profiling、把清洗固化为可版本化的 transform，而非留给 agent 临场 regex。\n常见误区 # 以为 Spider 2.0（企业级 text-to-SQL workflow）与 DAB 测的是同一类「跨库 agent」能力——二者相关但 benchmark 不同。 仅增加 prompt 里的 schema 文本，而不给可重复的样本访问策略。 典型失败模式：会采样，但不会「像 DBA 一样想」 # 为什么 # 工具面允许「SQL 落盘 → Python」时，agent 会走阻力最小的路；论文明确提供 execute_python（Pandas/Pyarrow），因此 SELECT * → 平面文件 → Pandas 是合法路径，也是扩展性陷阱。\n机制与约束 # 论文可核对模式：SELECT * LIMIT 5 后即停；catalog 探测；普遍用 regex 做文本抽取；FM1–FM4 类错误（浅采样、错误计划、错列、错 regex）。\n演讲者观点（论文未写死阈值）：约 10 万行即抱怨「记录太多」；而 DBA 心理尺度常在千万、亿级——尺度错位会导致拒全扫与过度依赖 Pandas。\n怎么做 # 在 agent 外层加策略护栏（非替代模型）：\n# 示意：限制无脑 LIMIT 5 就结束 if step == \u0026#34;sample\u0026#34; and rows \u0026lt; min_profile_rows: require(\u0026#34;distribution_stats or explicit justification\u0026#34;) 对语义过滤类题（例：sports article 描述最长），DAB 题面需要语义理解；评估对象仍是 ReAct+SQL/Python，不是 LOTUS 式 sem_filter（论文设计选择）。\n常见误区 # 把 regex 失败简单归因于「模型不够大」，而不改 工具反馈与中间结果校验。 禁止 Python 工具——论文表明 Python 有时是必要路径；关键是避免 本可单条 SQL 却全盘导出。 执行路径分裂：SQL、Python、语义算子与声明式重写 # 为什么 # 多库、多方言、非 SQL 源并存时，不存在单一「银弹」执行引擎。嘉宾与主持的分歧，本质是 自主探索 vs 声明式优化器 谁更适合数据工作负载。\n机制与约束 # 路径 优势 局限（节目与文献） 多方言 SQL + skills 语义清晰、可审计 无法单 SQL 联邦；Mongo 等方言 agent 常弱（演讲者观点） Python/Pandas agent 当前最会 高延迟、难扩展；嘉宾称整体仍「pretty bad」（演讲者观点） Semantic operators NL 版 map/filter/join；可 per-row LLM 或合成 regex DAB 未测 该类系统；agent 仍偏爱 regex（论文 takeaway 支持倾向） 声明式 + rewrite 搜索 DocETL 的 rewrite directives + 计划评估 偏文档/非结构化管线；与端到端 Codex 式探索形成张力 DocETL（论文核实）：用户写声明式 pipeline；系统用 agent 提出 rewrite strategies（chunking、多级聚合、gleaning 等），在 rewrite × prompt × model 空间搜索；四任务上较强 baseline 高约 25%–80%（任务依赖）。\nPalimpzest（播客口误 Palimpsest）：声明式 AI 负载优化，强调 cost optimization；嘉宾以 Cascades 式类比为方向性说法，不宜写成论文正式术语。\nLOTUS：semantic operators + model cascade（小模型+置信度阈值再升 oracle）降成本。\n怎么做 # CUAD 例（510 份合同、41 类条款；嘉宾口述 512 为四舍五入）：朴素计划「每文档抽 41 属性」→ 可 rewrite 为 8×5 分组 等；由 DocETL agent 在样本上搜索 chunking/分组（使用示例，非 CUAD 论文强制设定）。\n# 概念示意：声明式算子链（非真实 DocETL 语法） pipeline: - map: \u0026#34;extract clause types from chunk\u0026#34; - reduce: \u0026#34;merge by contract_id\u0026#34; 常见误区 # 用 DSPy 优化出的超长 BM25 字符串 代表「可维护 skill」（嘉宾批评性观点，未绑定期刊版本）。 认为 persistence 差异等于 read/write 语义差异——嘉宾澄清：DocETL 与 LOTUS/Palimpzest 均可持久化；差在 优化/rewrite 哲学（演讲者观点）。 Agent-first 数据库：分支、推测与 pass@k # 为什么 # 当 LLM agent 成为主要查询者，传统「一次提交、一次结果」的事务叙事不够：agent 需要并行试错、保留一次成功、丢弃失败探索（演讲者观点，与 Supporting Our AI Overlords: Redesigning Data Systems to be Agent-First 主题一致——正式标题非字面 overlords）。\n机制与约束 # CIDR 方向论文提出 agentic speculation、branched updates：在 Neon 等 copy-on-write branching 上 fork 状态、推测性写入、失败分支回滚。pass@50 与 Neon 的逐字绑定未见于已抓取论文 HTML；pass@k 来自 DAB 协议，与 CIDR 文为不同来源。嘉宾对 Neon 实现细节自认不熟（演讲者观点）。\n怎么做 # 将「评测时的 50 trials」与「生产分支策略」分开设计：评测要独立随机种子；生产用短生命周期 branch + 合并策略，避免 agent 写脏主库。\n常见误区 # 把 branching 当成唯一解法，而忽略 权限模型与成本（每次 fork 的存储与计算）。 假设所有探索都可自动 merge——业务约束常要求人工确认。 Tribal knowledge：不是「数据维基」，而是纠偏记忆 # 为什么 # 跨库失败后，团队常想建「知识库」让 agent 记住 Nike 拼写、列别名习惯；若离线洗入模型无关真理，可能错配执行器行为（regex vs LIKE vs 列名 brand）。\n机制与约束 # Arming Data Agents with Tribal Knowledge（Tk-Boost；嘉宾转述同事工作，无 Shankar 署名）定义：tribal knowledge 是纠正 agent misconception 的可复用 NL 陈述，而非复述库表事实。存储为 TK Store，每条带 applicability conditions（SQL 结构特征：用到的列、clause 等）；流程为 先 first-attempt SQL → 按适用性检索 → 子查询级修正。在 BIRD、Spider 2.0 上相对各 baseline 有约 +13.7% / +16.9% 量级提升（相对增幅，非绝对准确率）。\n嘉宾口述的「按 SQL keyword 列 + table/column 列」与论文 schema 有出入；写作应以论文为准。核心精神一致：记忆常绑定特定模型的失败模式（演讲者转述），离线泛化可能是 premature optimization。\n怎么做 # 1. 记录失败轨迹中的「误解」而非仅记录事实 2. 为每条知识写 applicability（哪些查询形状会触发） 3. 评测时固定 agent 版本，再测记忆迁移 常见误区 # 把 tribal knowledge 当成 RAG 文档库全文灌入。 不区分 database facts vs correction statements（论文明确区分）。 检索与 RAG：一个索引 vs 千张表 # 为什么 # 主持提出两极：互联网级单索引 vs 数据集搜索式的海量表；嘉宾更愿意说 retrieval 而非 RAG（对 generation 部分存疑，演讲者观点）。数据 agent 场景下，agentic grep、工具调用链难以嵌入经典 IR/DB 框架——需要混合检索策略。\n机制与约束 # 结构化可表达时优先 SQL（演讲者观点）；embedding 用于模糊匹配。Weaviate 产品侧的 hybrid search 与 agent 编排是另一套工程路径，不与 DAB 分数自动等价。\n怎么做 # 为每类源选默认检索器：表/列 → SQL + 统计；文本 → 向量 + 关键词；日志 → 时间范围 + 正则（并审计）。\n常见误区 # 用单一 vector collection 假装覆盖了 warehouse 里的规范化表。 忽略 检索结果是否可执行（能否生成可运行 SQL/Python）。 程序、数据与「技能」边界变糊 # Claude Code skill 中，程序描述、个人习惯与上下文记忆难以用经典 CS 的 compute/data 二分（演讲者观点，未形式化验证）。Spawn sub-agents 类似动态创造 compute，资源边界变模糊——这与传统 DB 可预期资源模型冲突，也解释了为何嘉宾同时押注 声明式查询优化 与 公开 benchmark 两条线。\n若你要落地 # 用 DAB 或自建多库小题集做回归，报告 pass@1、pass@k、trials、hints；别只用单库 Spider 类分数说服自己。 把「采样 → 清洗 → join」做成可观测步骤，限制 LIMIT 5 即停与无脑 regex；对语义过滤题单独设 rubric 或引入 LOTUS 等声明式算子作对照（非 DAB 官方轨）。 工具策略：允许 Python，但对 SELECT * 全表导出设成本/行数护栏；结构化题默认 SQL-first。 记忆：按 Tk-Boost 思路存 misconception + applicability，并固定评测模型版本；避免一次性「洗知识库」。 基础设施：若 agent 高频试错，评估 Neon branching 或同类 COW 分支与 agent-first 架构是否匹配你的写入语义与合规要求。 参考与延伸阅读 # Can AI Agents Answer Your Data Questions? (DAB 论文) DataAgentBench 代码与 Leaderboard Retrieval-Augmented Generation (Lewis et al., 2020) Spider 2.0：企业级 text-to-SQL workflow CUAD 合同理解数据集（510 合同 / 41 条款） Atticus Project CUAD 官方页 DocETL：声明式文档处理与 agentic rewrite DocETL 项目站与文档 LOTUS：Semantic Operators 与 model cascade Palimpzest：声明式 AI 负载优化 SemBench：语义引擎横向评测（含 LOTUS、Palimpzest） Supporting Our AI Overlords（agent-first 数据系统） Arming Data Agents with Tribal Knowledge (Tk-Boost) Weaviate Query Agent 文档 Weaviate Transformation Agent 文档 Neon 数据库 branching 文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-data-agents-with-shreya-shankar-weaviate-podcast-135/","section":"文章","summary":"Data Agent：当「会写代码的模型」撞上真实数据栈","title":"Data Agent：当「会写代码的模型」撞上真实数据栈","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/dataframe/","section":"Tags","summary":"","title":"Dataframe","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/debugger/","section":"Tags","summary":"","title":"Debugger","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/dspy/","section":"Tags","summary":"","title":"Dspy","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/ebpf/","section":"Tags","summary":"","title":"Ebpf","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/ebpf/","section":"Categories","summary":"","title":"EBPF","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/echo/","section":"Tags","summary":"","title":"Echo","type":"tags"},{"content":" eCHO 201：2026 网络、eBPF 与安全预测——技术笔记 # 开篇：这集值得留下什么 # eCHO 第 201 期不是功能发布会，而是一次可对照原文的年度预测对谈：嘉宾 Nico Vibert（Isovalent / Cisco）先给 2025 预测博文 逐条「验票」，再展开 2026 版八条预测（2026-01-22）。若你关心云原生网络、运行时安全与 AI agent 治理的交叉点，值得保留的是：哪些断言已有官方文档背书、哪些仍是 Isovalent 内部民调或演讲者判断——后者在文中会单独标出。\n社区侧还预告了 Cilium 1.19 专场 eCHO（节目时点指向 2 月初）、Cilium Up and Running 纸质/PDF 与 KubeCon Amsterdam 议题；Isovalent Labs（Labs）强调可在 CPU 上跑实验、无需 GPU 预算。这些属于参与渠道，不构成版本承诺；技术判断仍以博文与发行说明为准。\n屏幕共享：Isovalent 博客《Networking and eBPF Predictions for 2026 and Beyond》，目录含 Open source scrutiny、Kubernetworker、VM on K8s、Multicloud、Nano segmentation、Cisco 整合、MCP、Identity 等八节。\n2025 预测复盘：验票而非算命 # 复盘打分来自 Isovalent 内部民调（约 1–10 分），方法论未公开，仅能作节目语境参考。\n主题 节目结论 可核对依据 eBPF 进入 Windows 未达成（约 1/10） Microsoft 2021 博文 与 ebpf-for-windows 仍强调开发模式；2025 博文预测 GA，「未达成」为节目评判 AI + eBPF 安全「革命」 部分（约 3/10） 博文标题含 remain cautious；LLM 生成 NetworkPolicy 仍易 hallucinate（演讲者观点） eBPF 初创融资 soar 部分（约 6.5/10） 标题与 Odigos 等融资链接 一致；「soaring」与 IT 支出收缩为经济判断 Netkit 准即时普及 未广泛达成 博文引 ByteDance 约 10% 吞吐提升、Meta rollout 计划；「10–15%」「内核 ≥6.8」为演讲者口述，2025 博文未写 6.8 VM on K8s 成运维模型 方向对、体验难（约 7.5/10） 与 KubeVirt CNCF incubating 方向一致 K8s 网络适配 AI（DRA） 推进中 DRA 概念文档（v1.35 stable）、google/dranet；「Google KubeCon 捐 CNCF」为演讲者转述，本次未抓到逐字来源 2025 幻灯片 Prediction #1：「eBPF will come to Windows」。\n2025 幻灯片 Prediction #6：「Netkit will see a quasi-immediate widespread adoption」；聊天区追问 What\u0026rsquo;s next after netkit?（演讲者观点）。\n聊天区 @TonyNorlin 希望 2027 年 eBPF 进入 FreeBSD——无官方路线图在本次来源中；仅作社区愿望记录。\n聊天叠加：@TonyNorlin 提议 2027 eBPF on FreeBSD；幻灯片同期显示 2025 Prediction #3「Funding of eBPF startups will soar」。\n2026 预测：八条主题与证据边界 # 以下 h2 顺序以 2026 博文 为准；节目口述曾将 Agent 身份 与 MCP 对调讲解。\n开源使用受审视（Ingress NGINX） # Kubernetes 官方已宣布 Ingress NGINX 退役：best-effort 维护至 2026 年 3 月，之后无安全更新；2026-01 声明 引用第三方研究约 50% 集群仍依赖。迁移方向为 Gateway API（退役文 meta）。节目中 F5 维护分支、厂商 demo 却不回馈社区——F5 公告未在本次调研中核实；2026 博文正文亦未出现 F5。\n2026 幻灯片 Prediction #1：「Open Source Usage goes under scrutiny (Ingress-Nginx)」。\n「Kubernetworker」与认证路线 # 博文预测平台工程师与传统网工鸿沟缩小，BGP、IPv6、overlay、mesh、加密等推高专职角色。CKNE 项目页 写明考试仍在开发中（schema.org：certification exam is being developed），与博文 upcoming certification 一致。NetDevOps → K8s 网络 / BPF / Cilium 为论述性路径，非规范要求。\nVM on Kubernetes：失去「天真」 # 兴趣上升不等于一夜迁完。博文 h2 为 VMs on Kubernetes lose their innocence，并指向虚拟化网络简报；「VMware 作新 mainframe」为演讲者隐喻（博文未逐字）。KubeCon 共址 VM on Kubernetes Day（Portworx 主办）来自字幕，官方议程页本次未抓取确认。\n2026 幻灯片 Prediction #3：「VMs on Kubernetes lose their innocence」。\n多云互联开始像真事 # 过去许多「多云」停留在策略口号，或仅是 SaaS（如 Office 365）加单一 IaaS 的组合；跨云 VPC 互联仍常依赖 peering、VPN、transit 或第三方编排（演讲者观点）。2026 博文将拐点放在 AWS 与 Google 的托管 L3 连通与可编程 API：AWS Interconnect、开源 aws/Interconnect（OpenAPI 3.0）。可参考 AWS re:Invent 2025 多云指南。Azure 是否推出对等能力仍为预测性表述；Neocloud（GPU/API 交付、弱控制台）无独立标准定义。\nNano segmentation # 介于 macro（区域/边界）与 micro 之间，向进程级 enforcement 推进；博文关键词含 Tetragon、Runtime Security。落地取决于策略生成、下发与运维成本——产品细节宜查 Tetragon 文档。\nIsovalent 在 Cisco 体系内可见 # 博文链接 Cisco Live Protect 等，称 Cisco 采用 Tetragon 做漏洞缓解。CPO「最战略收购之一」为演讲者转述；收购新闻稿与 cisco-to-acquire-isovalent 正文本次未能抓取。\nMCP：用例与风险并存 # Model Context Protocol 将 AI 应用接到外部系统。博文记载 Azure 工程团队搭建带 Cilium + Hubble 的 MCP server，可观察流量并生成 NetworkPolicy；Nico 2025 年 10 月个人实验日期、大客户 PoC 细节为演讲者口述，无公开仓库链接。Agent 非确定性 → 需 identity + policy 绑定；用 Tetragon 限制 agent 系统调用为演讲者观点，非 MCP 规范要求。\n节目口述环境：VS Code Copilot → MCP server（binary/container）→ 经 kubeconfig 读集群；只读权限下 agent 可结合 hubble observe 做流量归因，写权限下可在本地 kind 生成 NetworkPolicy——Nico 自述这接近「给实习生 root」的风险模型。企业侧更稳妥的路径是：PoC 集群 + GitHub CI/CD 推送策略 + 人工审批命名空间（演讲者转述，无公开案例链接）。MCP 被比作 agent 的「USB-C 式」接口，仅为比喻，不等同于 USB 安全模型。\nCilium v1.19.4 文档树（在线站若异常，以 GitHub 为准）：\n# 确认 Hubble 已启用（v1.19.4 setup.rst） cilium status # 观察流（示例带 -P；完整 flag 见 CLI 参考） hubble observe Hubble setup — Cilium v1.19.4 Cilium Releases Identity becomes humanoid # 博文 h2：Identity becomes humanoid, and policy has to follow——编码 agent、MCP 工具调用难以像传统 ServiceAccount 那样用固定动词/资源白名单；若策略不跟进，易出现「YOLO」式越权（定性论述）。\n延伸阅读（一手链接） # 2026 预测全文 — Isovalent 2025 预测全文 — Isovalent Ingress NGINX 退役 — Kubernetes AWS re:Invent 2025 多云指南 — AWS 未核实边界（撰写时保留）：内部打分样本量、具体 CVE 时间线、F5 分支维护能力、Azure 互联 API 对等进展、MCP 实验仓库与 PoC 名称——需随官方公告与项目 Release 更新。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-echo-ebpf-cilium-office-hours-echo-episode-201-2026-networking-security-and-ebpf-predictions/","section":"文章","summary":"eCHO 201：2026 网络、eBPF 与安全预测——技术笔记","title":"eCHO 201：2026 网络、eBPF 与安全预测——技术笔记","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/echo-ebpf-cilium-office-hours/","section":"Tags","summary":"","title":"Echo-Ebpf-Cilium-Office-Hours","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/embed/","section":"Tags","summary":"","title":"Embed","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/eval/","section":"Tags","summary":"","title":"Eval","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/exabyte/","section":"Tags","summary":"","title":"Exabyte","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/for/","section":"Tags","summary":"","title":"For","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/gepa/","section":"Tags","summary":"","title":"Gepa","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/gpu/","section":"Tags","summary":"","title":"Gpu","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/graphql/","section":"Tags","summary":"","title":"Graphql","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/group/","section":"Tags","summary":"","title":"Group","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/hat/","section":"Tags","summary":"","title":"Hat","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/infosec/","section":"Tags","summary":"","title":"Infosec","type":"tags"},{"content":" Infosec 简报：CA 支持台沦陷、内核提权与 ATT\u0026amp;CK 战术重组 # 本期覆盖 DigiCert 支持门户入侵与 EV 代码签名吊销、MOVEit 在野利用信号、cPanel 认证绕过、Linux「Copy Fail」本地提权、AI 代理删库警示、Musk 蒸馏作证、犹他州年龄验证与 VPN 立法、诊室 AI 病历争议、MITRE ATT\u0026amp;CK v19 拆分，以及 Mitchell Hashimoto 迁出 GitHub。各节标注证据边界。\n信任链被社工击穿：DigiCert 支持门户 # 发生了什么。 SecurityWeek 转述 DigiCert 事件报告：4 月 2 日攻击者经客户聊天渠道投递伪装为截图的恶意载荷，感染支持人员终端后进入内部支持门户，利用分析师代客操作获取 initialization codes，为已批准订单签发约 60 张 EV Code Signing 证书（其中 27 张明确关联威胁行为者）；社区报告称 11 张用于签署 Zhong Stealer。第二台终端 4 月 14 日才被发现，厂商归因于终端上安全产品故障（malfunctioning security solutions）。4 月 17 日前相关证书已吊销。ZIP 内 .scr 投递细节见于节目讨论，SecurityWeek 正文未写扩展名（待核实是否见于 DigiCert 原文；厂商新闻页本次未能抓取正文）。\n技术含义。 EV 代码签名主要改善静态信誉与落地阶段；不等于可跳过 EDR 行为检测——「进门证件」之后仍应监控进程与网络。\n工程师应对。 审计本组织 EV/代码签名证书链；对支持渠道附件做隔离沙箱；签名样本仍走行为检测。\nSecurityWeek：DigiCert 吊销被盗签发证书（转述厂商报告） DigiCert 事件说明（HTTP 200，正文待核实） SecurityWeek：DigiCert Revokes Certificates After Support Portal Hack；摘要载 4 月 2 日攻击与聊天渠道恶意载荷。\n热门文件传输与托管面板：补丁优先级 # 发生了什么。 节目称 MOVEit 再次出现在野利用，厂商已发布补丁且尚无公开 PoC；浏览器标签 OCR 为「Progress warns of critical MO…」。本轮具体 CVE 编号与 Progress 公告正文本次未能从 NVD/CISA/厂商站核实（unable to verify）。cPanel/WHM 侧，CVE-2026-41940 为登录流认证绕过（CWE-306），CVSS 9.8，未授权远程访问控制面板；CISA KEV 于 2026-04-30 列入。\n技术含义。 热门 SaaS 与托管面板常成「影子 IT」——市场部门多年前买的站仍暴露 cPanel，与企业 LDAP/密码复用叠在一起。MOVEit 细节待厂商 primary 补齐前，有部署即应跟踪 Progress 安全频道。\n工程师应对。 有 MOVEit 实例：立即查 Progress 安全公告并打补丁。资产清点 cPanel/WHM（含 GoDaddy 等代建站）；升级至厂商 2026-04-28 安全更新所列修复版本。\nNVD：CVE-2026-41940 CISA KEV：CVE-2026-41940 cPanel 安全更新（2026-04-28） Progress 安全入口（MOVEit 公告待从此跟踪） 内核逻辑缺陷：Copy Fail（CVE-2026-31431） # 发生了什么。 社区称 Copy Fail；CVE-2026-31431 源于 Linux 内核 crypto: algif_aead 就地优化引入的逻辑缺陷，经 AF_ALG / splice 路径可对 page cache 产生可控写，实现本地提权（oss-security、copy.fail）。copy.fail 称 PoC 约 732 字节、no race；CISA KEV 2026-05-01 列入。NVD CVSS 7.8（AV:L）。\n技术含义。 修复需内核包更新并重启；仅装 unattended-upgrades 而不重启，暴露窗口会被拉长（运维推断，非统计结论）。\n工程师应对。 对照发行版 stable 公告打内核补丁；维护窗口内重启；云单机亦纳入提权面评估。\noss-security：CVE-2026-31431 copy.fail 项目页 CISA KEV：CVE-2026-31431 AI 代理权限：PocketOS 删库与 Musk 蒸馏作证 # 发生了什么。（删库） The New Stack 报道（2026-04-25，第三方叙述）：汽车租赁 SaaS PocketOS 的 Cursor 编码代理在不足十秒内删除生产库，且卷级备份与生产处于同一 blast radius；根因包括 Railway CLI token 权限过宽、代理未先征得人工确认。节目对「创始人甩锅 AI」持怀疑（人为 rm、营销等，演讲者观点）；即便属实，风险在 God-mode OAuth/MCP 与无隔离备份。\n发生了什么。（蒸馏） TechCrunch 报道：Musk 在诉 OpenAI 非盈利使命违约案中作证，被问及是否用 distillation 在 OpenAI models 上训练 Grok 时，先称行业普遍，追问下回答 「Partly.」（报道原文引号）。\n工程师应对。 AI 代理：只读/分环境凭证、plan mode 审阅、备份与生产隔离。模型供应链：把 API 输出蒸馏视为与权重窃取同级的威胁建模输入（厂商公开反对第三方蒸馏与诉讼证词并存，动机解读为演讲者观点）。\nThe New Stack：Cursor 代理删除 PocketOS 生产库 TechCrunch：Musk 作证 xAI 在 OpenAI 模型上训练 Grok TechCrunch：Musk is in the process of suing OpenAI… distillation 段落；OCR 可见「Chinese firms using distillation」。\n年龄验证立法与 VPN：犹他州 SB 73 # 发生了什么。 据节目屏摄 Tom\u0026rsquo;s Hardware 等 secondary 报道（官方法条全文本次未能抓取核实）：Utah Online Age Verification Amendments（Senate Bill 73） 拟于 5 月 6 日生效；用户在犹他州物理位置即视为本州访问，不得向 covered websites 提供如何用 VPN 绕过年龄检查的教程。NordVPN 称 unresolvable compliance paradox、liability trap；EFF 警告可能封禁已知 VPN IP 或对全球访客强制年龄验证。网站侧难以可靠区分 VPN 与真实地理位置（报道论点）。\n技术含义。 合规压力可能外溢为 IP/ASN 封禁或全球年龄门，与隐私工具生态冲突；检察官若要证明用户「用 VPN 违法」举证路径仍极难（演讲者观点）。\n工程师应对。 在美运营且触达犹他用户的产品：做法务与地理合规评估；勿假设仅 IP 库即可区分 VPN。\n犹他州 SB 73 法案文本（待浏览器核对） Tom\u0026rsquo;s Hardware：Utah\u0026rsquo;s Online Age Verification Amendments, formally Senate Bill 73, take effect on May 6…；同屏可见 NordVPN \u0026ldquo;unresolvable compliance paradox\u0026rdquo; 引述。\n诊室 AI 病历与 ATT\u0026amp;CK v19 # 发生了什么。（病历） Emily M. Bender 与 Decca Muldowney 在 buttondown.com 发文（节目屏摄，精确 URL 待核实），标题大意 Why you should refuse to let your doctor 使用 AI scribing，列举九条理由建议患者拒绝同意第三方录音转病历（隐私、同意、自动化偏见、disparate impact 等）。节目辩论：隐私/HIPAA 是独立合规议题，临床医生亦依赖转录检索（多方观点，非单一结论）。\n发生了什么。（ATT\u0026amp;CK） ATT\u0026amp;CK v19（2026 年 4 月）将原 Defense Evasion 拆为 Stealth — TA0005（继承编号，hide and conceal）与 Defense Impairment — TA0112（削弱防御）；T1562 退役，由 T1685 Disable or Modify Tools 承接；新增 T1687 Exploitation for Defense Impairment 等。\n工程师应对。 医疗场景：知情同意与数据用途写清，勿默认 opt-in。蓝队：按 MITRE transition 材料重映射检测规则与标注（节目玩笑称用 Claude 填表，非运维建议）。\nMITRE ATT\u0026amp;CK v19 更新说明 Emily M. Bender 主页（通讯原文链接待补） buttondown：nine reasons why we recommend refusing to consent… 1. Privacy: These systems always involve third-party software…\nATT\u0026amp;CK v19：Defense Evasion (TA0005) is retired and replaced by two tactics: Stealth… and Defense Impairment (TA0112).\n开发托管可用性：Ghostty 迁出 GitHub # 发生了什么。 HashiCorp 联合创始人 Mitchell Hashimoto（2026-04-28）发文：近月几乎每日因 GitHub outage（含 GitHub Actions）影响工作，称 GitHub「no longer a place for serious work」，将把终端 Ghostty 主开发迁出；GitHub 只读镜像与个人项目仍保留。文内将 4 月 27 日 Elasticsearch 大规模中断与自己所抱怨的中断明确区分（非同一事件）。The Register 同期报道细节本次未二次抓取。\n技术含义。 争议在可用性/CI 中断而非隐私；Git 分布式仍保留本地克隆恢复路径（节目讨论）。\n工程师应对。 关键私有仓评估第二托管或自托管（如 GitLab）；对 Actions 依赖做降级与镜像预案。\nHashimoto：Ghostty Is Leaving GitHub The Register：Hashicorp co-founder Mitchell Hashimoto says GitHub \u0026rsquo;no longer a place for serious work\u0026rsquo;。\n整理自公开来源与节目讨论；Claude 侧栏报道、Trellix 源码库闲聊等无技术锚点话题从略。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-bhis-2026-bhis-talkin-bout-infosec-news-2026-05-04/","section":"文章","summary":"Infosec 简报：CA 支持台沦陷、内核提权与 ATT\u0026amp;CK 战术重组","title":"Infosec 简报：CA 支持台沦陷、内核提权与 ATT\u0026CK 战术重组","type":"posts"},{"content":" Infosec 简报：社工归案、Teams 钓鱼链与前沿 AI 防御信号 # 本期覆盖 Tylerb 认罪、Seguritech 边境监控调查、霍尔木兹 crypto 短信诈骗、Apple 通知库 CVE、UNC6692 Teams 链、CERT-In AI 通告，以及 Glasswing/Mythos、OpenAI Cyber 模型、Project Deal 与 token 支出讨论。证据分级见各节。\n社工团伙与 OPSEC：Tylerb 认罪 # 发生了什么。 英国籍 Scattered Spider 成员 Tyler Robert Buchanan（代号 Tylerb）对 wire fraud conspiracy、aggravated identity theft 认罪；2024 年 6 月在西班牙被捕，2025 年 4 月起引渡美国羁押。KrebsOnSecurity 披露调查靠钓鱼域名注册信息复用同一 username/email，且 Namecheap 记录显示注册账户在钓鱼前不足一个月曾从其家庭 IP 登录。\n技术含义。 身份与基础设施复用可把多年活动串成可起诉链；认罪书受害方含 Twilio、LastPass 等，MGM/M\u0026amp;S 为团伙历史声誉，不在该认罪稿中。\n工程师应对。 对钓鱼域名、注册邮箱与被动 DNS 做关联；强化 helpdesk/协作类社工演练。\nKrebs：Tylerb 认罪稿（2026-04-21） 节目画面：Notion 摘要页标题「\u0026lsquo;Scattered Spider\u0026rsquo; Member \u0026lsquo;Tylerb\u0026rsquo; Plea…」，旁开 Rest of World 边境监控调查文。\n边境监控能力与跨境数据 # 发生了什么。 Rest of World 调查指墨西哥 Grupo Seguritech 及其二十余家子公司在墨西哥建设 C5 指挥中心、摄像头、无人机、车牌识别等一体化监控包；公开记录显示自 2012 年起至少 63 份政府合同、合计约 218 亿比索（约 12.7 亿美元，未按通胀调整）。边境侧有 Plataforma Centinela（华雷斯）及 2022 年德克萨斯—奇瓦瓦 MOU 共享奇瓦瓦侧数据等安排。\n技术含义。 风险在监控能力输出与跨境数据治理，而非单点漏洞。不宜概括为「美国联邦直签总包」——向 CBP 索取联邦合同记录未获匹配披露。\n工程师应对。 采购合同明确数据驻留、子处理方与审计权；对第三方监控平台做独立评估。\nRest of World：Seguritech 公司画像（2026-04-08） Rest of World：127 亿美元监控帝国（2026-04-16） 地缘政治叠加的 SMS 钓鱼 # 发生了什么。 据 Reuters 标题链（全文本次未能直抓，细节见次级报道），希腊海事风险管理公司 MARISKS 警告：诈骗者向航运方发送短信，冒充伊朗当局，要求以 bitcoin 或 tether 支付「transit fee」以换取霍尔木兹「安全通行」。\n技术含义。 典型时事钓鱼：真实海峡通行与收费新闻降低受害者戒心。报道按 scam 定性；诈骗规模与攻击者归属未验证。\n工程师应对。 财务与运营岗位做时事钓鱼演练；支付变更须二次确认。\nReuters：霍尔木兹「安全通行」诈骗短信（2026-04-21） Ars Technica：转述 MARISKS/Reuters（2026-04） 通知库残留与移动取证 # 发生了什么。 Apple 在 iOS 26.4.2 / iPadOS 26.4.2 等版本修复 CVE-2026-28950：Notifications marked for deletion could be unexpectedly retained on the device（日志类问题，改进数据脱敏）。次级报道指执法机构曾从通知残留恢复已删 Signal 等应用的消息摘要；Apple/CVE 文案未点名 Signal。\n技术含义。 E2EE 不消除通知与 UI 层泄露；卸载后残留可扩大取证面。\n工程师应对。 关闭锁屏正文预览；推送 26.4.2+；DFIR 纳入通知库检查。\nApple：iOS 26.4.2 安全内容 NVD：CVE-2026-28950 协作软件上的假 IT 台：UNC6692 # 发生了什么。 Mandiant 披露 UNC6692（Snow Flurries）：邮件洪泛制造紧迫感 → 组织外 Microsoft Teams 账号冒充 helpdesk → 诱导安装「反垃圾邮件补丁」→ 从攻击者 AWS S3 下载同名 AutoHotkey 二进制与脚本 → 落地恶意 Chromium 扩展 SNOWBELT（非 Chrome Web Store）。2025 年 12 月下旬曾大规模邮件活动。\n技术含义。 技术组件并不新颖，但社工深度 + 协作工具信任缩短了从接触到执行的时间。Mandiant 文未描述 AI 辅助；节目中「威胁方普遍用 AI」与「报告未写 AI」存在分歧（演讲者观点）。\n工程师应对。 限制组织外 Teams 聊天；管控 S3 外链与 AutoHotkey；监控 Startup/计划任务异常。\nMandiant：UNC6692 社工与定制恶意软件（2026-04-24） Mandiant 博文可见：UNC6692 冒充 IT helpdesk，诱使受害者接受组织外 Microsoft Teams 聊天邀请。\n国家 CERT 对前沿 AI 风险的回应 # 发生了什么。 印度 CERT-In 发布 CIAD-2026-0020《Defending Against Frontier AI Driven Cyber Risks》（2026-04-26，严重级别 High）。OCR 与节目一致列出七类能力关切（大规模代码漏洞分析、加速 exploit/PoC、自动化侦察、凭证收集、AI 生成钓鱼、多阶段编排等），并建议组织侧高度戒备、ZTNA、面向互联网系统关键补丁 24 小时内应用、OpenSSF Scorecard 与 SBOM/QBOM/CBOM/AIBOM 等。cert-in.org.in 本次未能二次抓取，字段以 OCR 为准。\n技术含义。 官方将关键漏洞响应窗口压到「小时级」，与 UNC6692、受限 Cyber 模型争议同周共振。\n工程师应对。 变更流程对齐 24h 打补丁；外联 MFA；订阅 CERT-In 与厂商 advisory。\nCERT-In 官网 CERT-In 通告 CIAD-2026-0020：Defending Against Frontier AI Driven Cyber Risks，Severity Rating: High。\n同站「For Organisations」：Heightened Vigilance；ZTNA 与面向互联网资产的 MFA。\n受限网络安全模型的访问争议 # 发生了什么。 媒体报道某 Discord 群体宣称取得 Anthropic 受限能力访问；发言人称正在调查「通过 third-party vendor 环境对 Claude Mythos Preview 的未授权访问」报告，尚未发现其所声称影响的证据（引文以 TechCrunch 全文为准）。官方 Project Glasswing 页面将试点模型称为 Claude Mythos 2 Preview，约 40+ 组织经 Claude API、Bedrock、Vertex AI、Microsoft Foundry 等获扩展访问。\n技术含义。 小范围试点仍受供应链与 API 密钥约束；短期查询≠权重泄露（「噱头入侵」为演讲者观点）。\n工程师应对。 密钥轮换、最小权限与异常调用监测；先取证再对外通报。\nAnthropic：Project Glasswing TechCrunch：Mythos 未授权访问报道（2026-04-21） 商用模型的「网络特化」路线 # 发生了什么。 在 Mythos 舆论背景下，OpenAI 推进面向网络防御方的受限访问（官方博文本次返回 403）；TechCrunch 称 GPT-5.5 Cyber 将仅向 critical cyber defenders 有限开放。节目中 GPT-5.4 Cyber、Cyber Gym ~84 分、Mythos「约 270 漏洞 / 180 exploit」均为演讲者转述，未在可访问 primary 中核对。Anthropic 与 Mozilla 合作文记载 Claude 在开源软件中发现 500+ zero-day vulnerabilities（统计口径不同于节目数字）。\n技术含义。 模型访问已成安全政策议题；自主运行时间拉长会放大自动化攻击链担忧。\n工程师应对。 高能力模型纳入供应商评估；检测侧重自动化编排行为，而非品牌叙事。\nOpenAI：Scaling trusted access for cyber defense TechCrunch：OpenAI 亦限制 Cyber 访问（2026-04-30） Anthropic：Mozilla Firefox 安全合作 AI 代理交易实验 # 发生了什么。 Anthropic 在旧金山办公室运行 Project Deal（2025 年 12 月一周）：员工 Claude agents 代售/代购个人物品并议价，每人 $100 额度；官方记载 186 deals、总交易额 just over $4,000，并并行测试更强模型是否议价占优。\n技术含义。 更强代理议价占优、用户可能无感——可映射到 agent 商务与授权滥用面（演讲者延伸）。\n工程师应对。 明确模型档位与人工复核；监控代理支付与 OAuth。\nAnthropic：Project Deal 实验说明 Anthropic 站点可见 Project Deal：员工 Claude agents 在办公内代为买卖个人物品。\n高 Token 账单与单位经济 # 发生了什么。 节目讨论创业团队在社交媒体炫耀上月约 11.3 万美元 AI 账单（4 人公司）及 token maxing 文化——高消耗被当作投入强度信号。具体推文 primary 本次未定位，数字无法独立核实；OpenAI 是否盈利、Amazon 做 AI 基础设施等属演讲者观点，非财报引用。\n技术含义。 若属实，属单位经济问题，不等同安全成熟度（$113k 账单待核实）。\n工程师应对。 API 预算告警与模型路由；安全 spend 与产品 burn rate 分开核算。\n整理自公开来源与节目讨论；Cult of the Dead Cow 重启等无技术锚点话题从略。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-bhis-2026-bhis-talkin-bout-infosec-news-2026-04-27/","section":"文章","summary":"Infosec 简报：社工归案、Teams 钓鱼链与前沿 AI 防御信号","title":"Infosec 简报：社工归案、Teams 钓鱼链与前沿 AI 防御信号","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/interaction/","section":"Tags","summary":"","title":"Interaction","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/java/","section":"Categories","summary":"","title":"Java","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":" Java 平台横切面问答：模块、构建工具、Lombok 与兼容纪律 # 这是一场面向资深工程师的多主题架构师问答摘要，按问题跳转阅读即可；下文将 JEP 261 等规范表述与台上发言人的工程判断分开标注。\n开场问答：左侧发言者抬手强调观点，右侧 Alex Buckley 倾听；纯蓝幕布舞台，无幻灯片文字。\n模块系统为何普及缓慢？ # 背景\nJava 平台模块系统（JPMS） 自 JDK 9 起提供强封装、可靠配置与可链接镜像，但库与应用侧采用率长期偏低。现场提问直指：是设计失败，还是生态还没跟上？\n要点\n为什么\n若构建链不把 --module-path、module-info.java、对 未命名模块 的 --add-exports 等选项做成「默认正确路径」，JDK 再强的机制也难落地。Alex Buckley（演讲者观点）把 Java Agent 当作缩影：命令行可挂 classpath，在 Maven/Gradle 里却常被包装成「又一个依赖 jar」。当前收益主要在演进边界与安全封装；性能红利更多被放在未来，而工具不顺时团队缺少迁移激励。\n机制/约束\nJEP 261 明确：模块路径与类路径语义不同；落在 classpath 上的代码进入未命名模块，封装弱于具名模块。链接期组装见 JEP 282 与 jlink。\n怎么做\njavac --module-path lib --module-source-path src -d out \\ --add-modules com.example.app java --module-path out:lib -m com.example.app/com.example.Main 常见误区\n把「模块难用」等同于「JPMS 设计错误」；更常见的是构建/测试/Agent 插件仍按 classpath 思维编排。\n延伸阅读\nJEP 261 — 模块系统 jlink 手册 — 自定义运行时镜像 模块话题中段：三位架构师并排，居中发言者手势展开；画面无可见幻灯片 API 名称。\njlink 愿景、库模块化与 8→11 阵痛 # 背景\n理想路径是用 jlink 裁出只含 java.base 与业务依赖模块的运行时镜像再进容器；现实是第三方库大量仍以未模块化 jar 分发。\n要点\n为什么\n演讲者观点：库不模块化，应用侧就很难有动力做端到端模块化与 jlink 镜像。许多 8→11 升级破损来自 JEP 260 起的内部 API 封装（及后续 JEP 403 强封装），不宜笼统归咎 JPMS。关于 sun.misc.Unsafe：JEP 260 将其列为 JDK 9 仍不封装的 critical API；JEP 403 写明强封装下 sun.misc.Unsafe will remain——与「Unsafe 已不可用」的传言不一致；出问题的是其他内部 API 消费者。\n机制/约束\njlink --module-path $MODPATH --add-modules com.app,jdk.unsupported \\ --launcher app=com.app/com.app.Main --output runtime 怎么做\n先核对依赖是否仍反射 sun.* / jdk.internal.*；再决定是否编写 module-info 或依赖 automatic module 名。\n常见误区\n把 Spring 等框架「已模块化」当作全行业现状（本场提及，未在 OpenJDK 文档核实）；忽略 automatic module 与真实 module-info 的差异。\n延伸阅读\nJEP 260 — 封装大部分内部 API JEP 403 — 默认强封装 JDK 内部 API 讨论内部 API 时段帧对齐；OCR 可见片段「A 3 « —_ @」，不可当作幻灯片术语。\n七人全景：一人双手比划作答，右侧 Java 杯形标识；无屏上类名可读。\nclasspath 上的库为何「默认透视封装」？ # 背景\nJDK 模块可 exports 控制可见性；作者发布的普通 jar 若被用户放在 classpath，则整体进入未命名模块，包级「私有」在反射面前仍脆弱。提问者质疑这对库作者是否公平。\n要点\n为什么\nJEP 261：classpath 代码位于未命名模块，可通过 --add-exports pkg=ALL-UNNAMED 等方式放宽边界。演讲者观点分歧：Alex Buckley 称跑进程的人选择权最大，应明确引导 module path；Ron Pressler 称 classpath 与 module path 并列、无单一「默认」；Brian Goetz 承认默认体验有问题，长期方向是逐步弱化 classpath 习惯，但会触怒部分用户，故在收紧与兼容间权衡（「damned if you do, damned if you don\u0026rsquo;t」）。\n机制/约束\n规范描述机制差异，不裁决「公平性」。\n怎么做\n库作者若希望用户走模块路径：在 README 与构建产物中写明 --module-path 启动示例；勿假设「包私有 = JDK 级强封装」。\n常见误区\n要求 JDK 给第三方 jar 与平台模块同等级别的强封装开关——当前平台策略更倾向生态迁移而非对称开关。\n延伸阅读\nJEP 261 — 未命名模块与类路径 classpath 公平性讨论附近帧；OCR 片段「cm ci ms 8 =| ah」。\nLombok：减少样板，还是编译器公民性问题？ # 背景\n企业仍在 Java 25 上强推 Lombok；台上被问：你们是敌人、盟友，还是会让它自然过时？\n要点\n为什么\nBrian Goetz（演讲者观点）：Lombok 要解决的样板痛点合理，但实现上通过未在 JLS/JVMS 中规定的接口改写字节码/AST，属于对编译器生态的「坏公民」；存在完全守规矩的平行宇宙版本，属工具作者选择。其绑定可变 JavaBean + getter/setter 语义，与平台多年倡导的不可变、显式构造方向相悖；生成 Javadoc 需 delombok 是异味。更可能拖慢而非加速风格现代化。官方正道是 javac 注解处理（-processor、-processorpath）；Immutables、AutoValue 等走注解处理器路线（列举为演讲者观点）。\n机制/约束\nOpenJDK 文档未评价 Lombok；只规定注解处理扩展点。\n怎么做\njavac -processor com.google.auto.value.processor.AutoValueProcessor \\ -classpath auto-value.jar:. 常见误区\n把「少写代码」等同于「编译器集成无成本」；在库对外 API 上依赖预览或私有编译器钩子。\n延伸阅读\njavac 手册 — Annotation Processing Project Lombok 官方站点（实现细节以项目文档为准） Lombok 问答前后帧；OCR 片段「| pote pet i lag ld」。\n同场七人席：粉衬衫架构师托腮，右侧 Java 标识；无技术幻灯片。\n构建工具跟不上 JDK 时，语言组该接管吗？ # 背景\nGo 将 go build 与语言一体交付；Java 侧 Maven/Gradle 对预览特性、模块路径的支持时常滞后，是否应由 JDK 团队做「官方构建」？\n要点\n为什么\nBrian Goetz（演讲者观点）：历来优先 JDK 独占能力（如模块链接、预览开关），但承认社区在构建工具上交付不理想，可能不得不从语言/库挪资源——已是高优先级。Alex Buckley（演讲者观点）：「社区 fallen down」不是 Maven/Gradle 维护者个人的错；大量商业用户几乎不资助这些项目，应通过基金会等渠道资助工具链。机制旁证：预览需 javac --enable-preview 与运行时 --enable-preview（JEP 12），构建插件必须透传。\n怎么做\n在 CI 模板中锁定 --release + --enable-preview 的 Maven maven-compiler-plugin / Gradle JavaCompile 选项；升级 JDK 时先跑依赖库的模块/预览兼容性矩阵。\n常见误区\n等待 Oracle 发布替代 Maven 的单一官方构建器——本场未承诺；更现实的杠杆是资助与插件质量。\n延伸阅读\nJEP 12 — 预览特性与工具链 Apache Maven Compiler Plugin 文档 构建工具讨论：居中 Pink 衬衫发言，两侧倾听；蓝幕布无幻灯片。\n会不会有一天抛弃兼容，推出「Java Next」？ # 背景\n序列化、遗留并发原语、类型系统历史包袱让开发者频繁撞墙；社区偶发「重写一门 Java」情绪。\n要点\n为什么\nBrian Goetz（演讲者观点）：不会跳过 Java 28/29 进入模糊的「Java Next」并扔掉兼容；若变革值得做，会带可执行的迁移路径与刻意重设计，而非 bait-and-switch。「Compatibility—we\u0026rsquo;re all about compatibility」；断裂成本极高，值得做的事会用多阶段迁移完成。旁证：JEP 223 体现版本号与发布节奏纪律，非兼容哲学的完整法条。\n机制/约束\n平台能力通过 JEP 渐进引入（预览/孵化器/正式），而非大爆炸替换。\n常见误区\n把语言演进缓慢等同于团队「不敢改」；许多改动卡在生态迁移成本而非缺乏想法。\n延伸阅读\nJEP 223 — 新版本号方案 JEP 12 — 预览特性作为低风险试验通道 闭幕前三人席：花衬衫与 Navy 衬衫对谈，橙黄曲线背景元素。\n全场七人收尾画面：听众头顶剪影，舞台侧 Java 杯形标识。\n什么该进 JDK，什么该留在生态？ # 背景\n为何当年没有内置 HTTP Client？为何不把 Jackson「搬进」JDK？\n要点\nBrian Goetz（演讲者观点）：在有限人力下，只有 JDK 能做的优先（类文件 API、VM 语义），再填竞争力缺口，最后主观评估社区 ROI。Paul Sandoz（演讲者观点）：也看 JDK 自身是否越来越需要该能力——例：虚拟线程转储已支持 JSON（jcmd Thread.dump_to_file -format=json）；「用 JSON 配置 JDK」仍为设想。Brian：不会把生态库原样 plop 进 JDK——历史上多次迁入后以昂贵方式移除；生态库的「当下最好」与 JDK 需要的「面向几十年的 X」不是同一种兽（ASM vs 类文件 API 类比，演讲者观点）。已核实范例：JEP 321 将 HTTP Client 自 JEP 110 孵化器标准化为 java.net.http。\n延伸阅读\nJEP 321 — HTTP Client API jcmd 手册 — Thread.dump_to_file 结构化并发还要预览多久？ # 背景\nStructuredTaskScope 多轮预览，开发者关心何时可写进库 API。\n要点\nRon Pressler（演讲者观点）：已非常接近摘掉预览，因仍有少量预览点，可能再 preview 一轮；应用可在预览下使用，不建议库在预览 API 上定型。Alex Buckley（演讲者观点）：「反馈」指真实试用体验，非空想 redesign；以 HTTP Client 类比——Java 9 亮相后长期安静，临近收尾才涌入反馈，Java 11 定稿延续至今；JEP 517 显示 HTTP/3 仍在演进。文档侧：JEP 533（JDK 27 第七次预览）写明 preview the API once more，比口语更具体。\n延伸阅读\nJEP 12 — 预览特性 JEP 533 — 结构化并发（第七次预览） JDK 边界讨论时段帧；OCR 片段「erson = en Fo i」。\nProject Babylon 何时进入正式流程？ # 背景\nCode reflection、GPU/SQL 等实验方向需要社区试什么？\n要点\n演讲者观点：已有 Submitted 状态 JEP，沿 incubator 推进；时间表「就绪再就绪」。文档可核对：Project Babylon、JEP 8361105 — Code reflection（孵化器，Submitted），模块 jdk.incubator.code，启用示例 javac --add-modules jdk.incubator.code。Alex Buckley：演讲者观点称 Babylon 比多数特性更需要社区反馈。\n延伸阅读\nOpenJDK Project Babylon JEP 11 — 孵化器模块 Stream API 会做系统性性能重写吗？ # 背景\nStream 的多态与装箱带来开销，实践里常被建议改回 for 循环。\n要点\nBrian Goetz（演讲者观点）：设计优先表达清晰；多数场景数据量小，深挖 Stream 框架 ROI 不高；曾在「人类可读」与「机器友好」间优先人类；Valhalla 等或开辟后续路径。John Rose（演讲者观点）：视为 loop customization；megamorphic cliff 限制组件复用；Babylon/stream fusion（Scala 先例）或给 JIT 更好静态结构——数年尺度，非下个小版本。注意：Stream 属 Java 8 java.util.stream 历史交付；勿将今日 JEP 189（Shenandoah GC）误作 Stream 依据。\n延伸阅读\njava.util.stream 包文档 OpenJDK Project Babylon Stream 性能问答附近；OCR 片段「= C7 |e Fe |」。\nAI 能否加速 OpenJDK，架构师怎么用？ # 背景\nBacklog 巨大，Conformance 测试维护繁重，AI agent 能否分担？\n要点\nBrian Goetz（演讲者观点）：能帮忙，关键在如何帮，尚无定论。Alex Buckley（演讲者观点）：OpenJDK 不能像普通 GitHub 仓库随意 PR；请勿用 AI agent 骚扰维护者——流程见 OpenJDK Developers\u0026rsquo; Guide（需 OCA、赞助人、JBS）。Paul Sandoz 等（演讲者观点）：试验用 AI 随 spec 更新测试。John Rose（演讲者观点）：援引 Amdahl 定律——快 70% 不等于剩 30% 免费；对 agent 不信任、近期也不打算信任；需防「永远通过的测试」式取巧。\n延伸阅读\nOpenJDK Developers\u0026rsquo; Guide OpenJDK 贡献流程概述 AI 话题时段三人席：粉衬衫居中，两侧倾听；无幻灯片文字。\n紧凑对象头能否显著缩小堆？ # 背景\nJEP 519（JDK 25 产品化）延续 JEP 450：对象头由 96–128 bit 降至 64 bit（8 字节），相对常见 12 字节头约省 4 字节/对象。\n要点\nJohn Rose 等（演讲者观点）：理论可回收内存，实际取决于碎片与分配模式——try it, measure it。JEP 450 引用 Lilliput 实验：live data 常降约 10%–20%，不宜写成确定比例。启用（JDK 24 实验期示例）：-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders；JDK 25 见 JEP 519 正文。默认开启时间表见 JEP 534（提议 JDK 27），抓取日未核实是否已交付。\n延伸阅读\nJEP 450 — Compact Object Headers（实验） JEP 519 — Compact Object Headers（产品，JDK 25） 闭幕附近帧；OCR 片段「D8 ion 5 = ——— SE as aS aT 2 SS」。\n参考与延伸阅读 # JEP 261 — Java 平台模块系统 JEP 282 — jlink：Java 链接器 jlink 命令参考 JEP 260 — 封装大部分 JDK 内部 API JEP 403 — 强封装 JDK 内部元素（保留 Unsafe） JEP 12 — 预览特性：编译与运行双开关 JEP 533 — 结构化并发（第七次预览，JDK 27） JEP 8361105 — Code reflection 孵化器草案 JEP 321 — 标准 HTTP Client API JEP 517 — HTTP/3 for the HTTP Client API javac 手册 — 注解处理与 --enable-preview OpenJDK Developers\u0026rsquo; Guide — 贡献与赞助人流程 JEP 450 / JEP 519 — 紧凑对象头实验与产品化 JEP 445 / JEP 495 — 简易入口与实例 main 预览链 JEP 454 — Foreign Function \u0026amp; Memory API 深度边界：本文为约 58 分钟广度问答的摘要，非 JPMS/Maven 实现教程，亦未剖析 Lombok 内部 hook；标为「演讲者观点」处请结合你方 JDK 版本与依赖树自行验证。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-module-adoption-jdk-build-tool-lombok-backwards-compatibility-ask-the-ar/","section":"文章","summary":"Java 平台横切面问答：模块、构建工具、Lombok 与兼容纪律","title":"Java 平台横切面问答：模块、构建工具、Lombok 与兼容纪律","type":"posts"},{"content":" Java 平台与后量子密码：从威胁模型到 JDK 交付路径 # 大规模量子计算机若达到密码学相关规模（Cryptographically Relevant Quantum Computer，CRQC），Shor 算法可在多项式时间内分解整数、求解离散对数，从而瓦解今日 TLS 与 PKI 所依赖的 RSA、有限域 Diffie-Hellman 与椭圆曲线体系。对称算法不受 Shor 直接影响；Grover 算法对对称密钥搜索仅有平方级加速，业界常据此建议在高价值场景倾向 AES-256——此为常见风险判断，并非 JDK 默认强制行为。\n对 Java 服务端而言，真正紧迫的往往不是「等量子机上市再改一行配置」，而是先采集、后解密（harvest now, decrypt later，亦称 HNDL）：攻击者今日即可存档握手与密文，待日后 CRQC 可用再还原会话密钥。JEP 496 与 JEP 527 的动机段落均采用类似表述。相对地，证书与 JAR 签名更多关系身份伪造与供应链信任，轮换节奏与「历史流量机密性」不同；将密钥交换优先于签名升级属于风险建模（演讲者观点），NIST/JEP 并未用同一句式规定全局排期——企业仍须并行规划 ML-DSA 等签名迁移。\n下文按「威胁 → 标准命名 → 平台 API → 协议落地 → 运维」组织，面向已有 JCA/JSSE 经验的工程师。每个技术块尽量交代：为何需要、机制与约束、最小可运行示例、常见误区。JDK 版本以 OpenJDK JEP 与发行说明为准；幻灯片提到的 ML-KEM/ML-DSA backport 至 JDK 21/17（2026 下半年）未在对应 JEP 正文核实，生产排期应跟踪 Oracle 更新公告。\n架构团队宜在合规台账中同时记录三列：FIPS 算法名、IANA TLS group 名、最低 JDK 版本——三者任一错位，都会出现「算法库已升级、线上握手仍 ECDHE」的假象。\nTLS 握手里的脆弱环节 # 为何：企业机密流量、医疗与金融报文、云 API 调用大多走 TLS。威胁模型若只问「CRQC 何时商用」，会低估对手今日抓包、明日解密的能力；这与签名证书「到期轮换」的时间尺度不同。\n机制：RFC 8446 将 TLS 1.3 分为密钥交换、认证（数字签名）与记录层对称加密三层。前两者依赖传统非对称原语；记录层在握手完成后由协商出的对称密钥保护，不受 Shor 直接威胁，但在长期日志场景下，一旦握手阶段的密钥交换被攻破，历史 Application Data 的机密性随之丧失。认证失败导致的是伪造身份与中间人风险，与 HNDL 针对过去机密性的攻击面应分开评估。\n图：A CRQC Will Break Internet Communications Using Traditional Asymmetric Cryptography — TLS 1.3 握手的 Key Exch 与 Authn 依赖传统非对称密码。\n机制与约束：Shor 作用于公钥体系，不替代对 TLS 实现缺陷、弱套件或密钥管理的防护。探测对端是否协商后量子混合组可用 OpenSSL s_client -groups（取决于对端与本机 OpenSSL 版本，非 JDK 行为）。\n最小观测（JDK 应用通常无需改代码即可在 JDK 27 受益混合组，见后文）：\nopenssl s_client -connect api.example.com:443 -tls1_3 -groups X25519MLKEM768 \u0026lt;/dev/null 2\u0026gt;/dev/null \\ | grep -E \u0026#39;Protocol|Cipher|Named Group\u0026#39; 常见误区：把「对称套件仍安全」等同于「整条 TLS 链路可推迟迁移」；忽略已落盘的握手与密文档案。另一误区是把 HNDL 当作唯一优先级而无限期推迟 ML-DSA 证书与 JAR 签名升级——联邦与行业指引（如 CNSA 2.0 方向）仍要求签名体系迁移，只是紧迫性场景不同。\nNIST 标准与工程命名对齐 # 为何：采购合同、渗透测试报告与 JDK 发行说明使用不同命名空间；不对齐会导致验收失败或错误选型（例如把竞赛版 Kyber 与 FIPS ML-KEM 混为一谈）。\n机制：NIST 后量子密码项目 自 2016 年征集，2022 年公布首批入选算法，2024 年 8 月发布 FIPS 203 ML-KEM、FIPS 204 ML-DSA、FIPS 205 SLH-DSA。竞赛昵称 Kyber、Dilithium 与 FIPS 定稿名不保证与早期实现互通——JEP 496 明确 ML-KEM 与 Kyber 不互通，JEP 497 对 ML-DSA 与 Dilithium 同理。Falcon 对应进行中的 FN-DSA（FIPS 206 尚无 final 页面）；HQC 于 2025 年 3 月入选标准化（NIST 项目页）。\n图：NIST PQC Standardization — December 2016 Call for Proposals 至 August 2024 Final FIPS Published。\n竞赛/旧称 FIPS JDK 族名（示例） Kyber 203 ML-KEM、ML-KEM-768 Dilithium 204 ML-DSA、ML-DSA-65 SPHINCS+ 205 SLH-DSA（JDK 内置范围本篇不展开） 怎么做：在架构决策记录（ADR）或基础设施即代码中固定 FIPS 编号，避免口头昵称漂移：\ncrypto_standards: kem: { name: ML-KEM, fips: \u0026#34;203\u0026#34;, jca: ML-KEM-768 } signature: { name: ML-DSA, fips: \u0026#34;204\u0026#34;, jca: ML-DSA-65 } 常见误区：采购清单写「Kyber」，验收脚本却调 ML-KEM 参数集，互操作失败却被归咎于 JDK bug。另一误区是假设 FIPS 206（FN-DSA）已发布——截至常见核实路径，final 页面可能仍不可用，应写「标准化进行中」。\n混合方案：过渡期的安全叙事 # 为何：纯后量子算法仍在持续密码分析中；纯经典算法则无法抵御 CRQC 下的 HNDL。混合是在已知风险与新兴算法审慎之间的工程折中。\n机制：过渡期普遍采用经典 + 后量子组合：只要组合中有一侧在可预见未来内未被攻破，混合密钥交换即可维持「可辩护」的安全主张。JEP 527 原文：secure as long as one of the algorithms remains unbroken。NIST FIPS 覆盖单算法语义；TLS named group 的组合由 IETF 定义（如 draft-ietf-tls-hybrid-design），IANA TLS Supported Groups 已登记 X25519MLKEM768 等。\n怎么做：产品层 ADR 可写明默认偏好 X25519MLKEM768，并与 JDK 27 默认策略对齐；合规侧说明「NIST FIPS 覆盖单算法，TLS 组合由 IETF 定义」，避免审计方要求 NIST 出具「混合套件证书」——该组合通常不在 FIPS 单算法认证范围内。\n常见误区：认为「实验室已测 ML-KEM-768」即生产 TLS 已协商混合组；JDK 24 交付 ML-KEM API 时 JEP 496 Non-Goals 明确尚未在 javax.net.ssl 支持 ML-KEM TLS——PQ 混合 TLS 在 JDK 27（JEP 527）。将「默认混合」当作无需变更管理：IETF 草案仍可能调整 wire format，须预留回归窗口。\nJCA 分层与路线图门禁 # 为何：Java 长期通过 Provider 抽象屏蔽硬件与 FIPS 模块差异；PQC 仍走同一模型，否则 HSM、云 KMS 与 SunJCE 将各自为政。\n机制：应用通过 JCA 的 Signature、KeyPairGenerator、Cipher 等引擎类编程，由 Provider（SunJCE、第三方 HSM）落地。JDK 21 起新增 KEM；JDK 25 定稿 KDF（JDK 24 预览 JEP 478）。弱算法淘汰节奏见 Oracle Java Cryptographic Roadmap（页面可随时更新，CI 应绑定具体 JDK 版本的 Release Notes）。\n图：Building the PQC Foundation — JCA/JCE 中 KEM、KDF 标注 New，对应 ML-KEM、HKDF 等实现。\n怎么做：升级流水线应同时跑三类检查——(1) java.security 禁用算法是否与路线图一致；(2) 标准算法名是否包含 ML-KEM/ML-DSA；(3) 若使用 Bouncy Castle 等第三方 Provider，其 PQ 实现是否与 SunJCE 参数集一致。\n常见误区：只盯 JDK 大版本号，忽略 java.security 属性与 Release Notes 的增量变更；在 JDK 24 启用 ML-DSA API 却用 JDK 21 的 jarsigner 做发布门禁。\nKEM API：为何 KeyAgreement 不够 # 为何：TLS 1.3、HPKE 等现代协议以 KEM 为中心交换对称密钥；用 RSA 加密随机 AES 密钥或滥用 KeyAgreement 表达 encapsulation，既难审计也易踩侧信道与语义错误。\n机制：密钥封装（KEM）在不安全信道上封装对称密钥，语义上优于「随机对称密钥 + RSA 公钥加密」的旧模式。JEP 452 指出既有 KeyAgreement、KeyGenerator、Cipher 不足以表达 KEM；故引入 javax.crypto.KEM 及 KEM.Encapsulator / KEM.Decapsulator。\n图：KEM API JDK 21 — None of existing APIs (KeyAgreement, KeyGenerator, Cipher) were sufficient；Two main classes: KEM.Encapsulator and KEM.Decapsulator。\n图：JEP 452: Key Encapsulation Mechanism API KEM API Owner Weijun Wang Type Feature JDK 21 Scope SE Status Closed/Delivered Release 21。\nimport javax.crypto.KEM; import javax.crypto.SecretKey; KEM kem = KEM.getInstance(\u0026#34;ML-KEM\u0026#34;); KEM.Encapsulator enc = kem.newEncapsulator(peerPublicKey); KEM.Encapsulated out = enc.encapsulate(); byte[] kemMessage = out.encapsulation(); SecretKey shared = out.key(); 常见误区：在 JDK 21 上调用 KEM.getInstance(\u0026quot;ML-KEM\u0026quot;)——算法实现随 JEP 496 在 JDK 24 交付。幻灯片称 API backport 至 JDK 17：JEP 452 正文未记载，若需写入生产基线应查 Oracle 商业更新说明或标为演讲者表述。\nML-KEM：参数集、PKI 与封装方向 # 为何：HNDL 场景下需要可互操作的 lattice KEM；FIPS 203 定稿后 JDK 以 JCA 标准名交付，便于与 NIST 测试向量及 TLS 草案对齐。\n机制：JEP 496（JDK 24，security-libs/javax.crypto）实现 FIPS 203，参数集 ML-KEM-512、ML-KEM-768、ML-KEM-1024，默认 ML-KEM-768。keytool 可生成 ML-KEM 密钥对与证书，但 ML-KEM 不是签名算法，证书须由 RSA 或 ML-DSA 等签发——JEP 原文：cannot be used to sign the certificate containing the ML-KEM public key。\n图：JEP 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism ML-KEM … mechanism specified in FIPS 203。\n在 draft-ietf-tls-ecdhe-mlkem 语义下，客户端 key_share 携带封装公钥，服务端返回密文；与 API 层 sender encapsulate、receiver decapsulate 角色一致。\nkeytool -genkeypair -alias pq-ca -keyalg ML-DSA -groupname ML-DSA-65 \\ -dname \u0026#34;CN=PQ CA\u0026#34; -keystore ks -storepass changeit -ext bc:critical=ca:true keytool -genkeypair -alias mlkem-ee -keyalg ML-KEM -groupname ML-KEM-768 \\ -signer pq-ca -keystore ks -storepass changeit KeyPairGenerator kpg = KeyPairGenerator.getInstance(\u0026#34;ML-KEM\u0026#34;); KeyPair clientKp = kpg.generateKeyPair(); KEM kem = KEM.getInstance(\u0026#34;ML-KEM\u0026#34;); KEM.Decapsulator dec = kem.newDecapsulator(clientKp.getPrivate()); // SecretKey shared = dec.decapsulate(kemMsgFromServer); 参数集选择：ML-KEM-512 体积更小、ML-KEM-1024 安全余量更高；通用默认 ML-KEM-768 与 NIST Level 3 叙事及 TLS 混合组中的 768 实例一致。NamedParameterSpec 常量 ML_KEM_768 等可在显式 initialize 时使用。\n常见误区：用 ML-KEM 自签证书；或假设 JDK 24 默认 HTTPS 已启用 PQ——须 JDK 27 + 对端支持混合组。把 API 演示中的「客户端 decapsulate」硬编码到所有协议角色——在纯 KEM 消息协议中角色可能相反，须以协议规范为准。\nML-DSA：签名、JAR 与版本分叉 # 为何：供应链签名、代码分发与 TLS 证书链长期依赖 RSA/ECDSA；CRQC 下需可验证的后量子签名，且与 FIPS 204 测试向量一致。\n机制：JEP 497（JDK 24，java.security）交付 FIPS 204，参数集 ML-DSA-44、ML-DSA-65、ML-DSA-87，默认 ML-DSA-65。JEP 497 的 Non-Goals 包含 JDK 24 内对 JAR/TLS 的 ML-DSA 支持；jarsigner 对 ML-DSA 的签名与验证在 JDK 26 发行说明中交付（RFC 9882）。\n图：JEP 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm ML-DSA … lattice-based signature algorithm specified in FIPS 204。\njarsigner -sigalg ML-DSA-65 -keystore ks -storepass changeit test.jar ML-DSA \\ -signedjar signed.jar jarsigner -verify -verbose signed.jar import java.security.*; import java.security.spec.NamedParameterSpec; KeyPairGenerator kpg = KeyPairGenerator.getInstance(\u0026#34;ML-DSA\u0026#34;); kpg.initialize(new NamedParameterSpec(\u0026#34;ML-DSA-65\u0026#34;)); KeyPair kp = kpg.generateKeyPair(); Signature sig = Signature.getInstance(\u0026#34;ML-DSA\u0026#34;); sig.initSign(kp.getPrivate()); sig.update(messageBytes); byte[] signatureBytes = sig.sign(); 有状态哈希签名 HSS/LMS（RFC 8554）自 JDK 21 起内置验证路径；生产签名与 keytool 生成有状态密钥对通常仍需第三方 Provider——误把「能 jarsigner -verify LMS JAR」当作产线签名能力会导致密钥重用类事故。\n常见误区：在 JDK 24 CI 上要求 ML-DSA 签名 JAR 通过，却未升级到 JDK 26。\nKDF 与 HKDF：KEM 输出到会话密钥 # 为何：KEM 输出的是短共享秘密，协议通常还需 HKDF 派生多把密钥（客户端/服务端写密钥、IV、完成标签等）。SecretKeyFactory 面向 PBKDF2 等密码存储场景，无法表达 extract-then-expand 语义。\n机制：javax.crypto.KDF 补齐现代 KDF 抽象；内置 HKDF（HKDF-SHA256/384/512），用于 HPKE、TLS 1.3 混合交换等。JEP 510 称计划实现 Argon2，截至 JEP 正文尚未作为内置算法交付。\n图：JEP 510: Key Derivation Function API KDF API Owner Kevin Driver … Previewed in JEP 478 (JDK 24) and finalized in。\nimport javax.crypto.KDF; import javax.crypto.spec.HKDFParameterSpec; KDF hkdf = KDF.getInstance(\u0026#34;HKDF-SHA384\u0026#34;); // HKDFParameterSpec：extract 的 IKM 可为 KEM 输出；thenExpand 派生 AES 等 // SecretKey aesKey = hkdf.deriveKey(\u0026#34;AES\u0026#34;, spec); JEP 510 明确 TLS 1.3（含混合密钥交换）是 KDF API 的目标用例之一；与 JEP 527 在路线图上的耦合意味着：底层 KDF 定稿（25）早于或并行于 JSSE 混合 TLS（27），应用开发者多数情况下仍不直接调用 HKDF，但自定义协议栈（如 gRPC 实验性 PQ 通道）会用到。\n常见误区：在 JDK 24 生产代码依赖定稿 KDF API——定稿在 JDK 25。在预览 API 上 @PreviewFeature 未移除就发布库构件。误以为 Argon2 已内置——JEP 仅表达意图，须跟踪后续 JEP。\nHPKE：KEM + KDF + AEAD 三元组 # 为何：应用常需「用接收方公钥加密任意长度明文」；在 KEM 层之上用 AEAD 封装是 RFC 9180 的标准做法，避免自行拼接 KEM 输出与对称加密。\n机制：RFC 9180 将 HPKE 定义为 KEM、KDF、AEAD 的组合；JDK 26 通过 Cipher.getInstance(\u0026quot;HPKE\u0026quot;) 与 HPKEParameterSpec 暴露。RFC 9180 标准化 profile 以 DHKEM（如 DHKEM(X25519, HKDF-SHA256)）为主，未定义 ML-KEM 的 HPKE code point；JDK 26 初版因而以经典套件为主，PQC profile 待 IETF 定稿 RFC 后再扩展——演讲者/路线图表述 + RFC 范围推断，非单独 JEP 页面在本次可引用条文。\n图：Hybrid Public Key Encryption (HPKE) JDK 26 — Modern encryption scheme … defined in RFC 9180。\nimport javax.crypto.Cipher; // Cipher hpke = Cipher.getInstance(\u0026#34;HPKE\u0026#34;); // hpke.init(Cipher.ENCRYPT_MODE, recipientPublicKey, hpkeParameterSpec); 常见误区：无 HPKEParameterSpec 即调用 getInstance(\u0026quot;HPKE\u0026quot;)；在 JDK 26 期望 ML-KEM HPKE profile——RFC 9180 当前标准化的是 DHKEM 族，PQC profile 须等 IETF 增补。\nTLS 1.3 后量子混合密钥交换（JDK 27） # 为何：这是多数 Java 微服务零业务代码改动即可缓解 HNDL 的主路径：握手在 JSSE 内完成，无需应用直接调用 KEM.encapsulate。\n机制：JEP 527 将混合密钥交换整合进 javax.net.ssl TLS 1.3：默认在客户端 offer 列表首位启用 X25519MLKEM768；另支持 SecP256r1MLKEM768、SecP384r1MLKEM1024（默认未启用后两者）。未手动限制 named groups 时，多数应用 without change to existing code 即可获得混合保护；自定义顺序可用 SSLParameters.setNamedGroups 或系统属性 jdk.tls.namedGroups。\n图：Post-Quantum Hybrid Key Exchange for TLS 1.3 JDK 27 Combines classical and post-quantum key exchange algorithms Protects against HNDL。\njdk.tls.namedGroups=X25519MLKEM768,SecP256r1MLKEM768,SecP384r1MLKEM1024 import javax.net.ssl.*; SSLParameters p = sslSocket.getSSLParameters(); p.setNamedGroups(new String[] { \u0026#34;SecP256r1MLKEM768\u0026#34;, \u0026#34;X25519MLKEM768\u0026#34; }); sslSocket.setSSLParameters(p); 风险（JEP 527）：IETF 规范仍为 draft，RFC 定稿后 JDK 行为可能调整。JEP Non-Goal 包含纯 ML-KEM TLS（非混合）。\n自定义 SSLEngine、Netty SslContext 或显式 jdk.tls.namedGroups 时，应做互操作矩阵：JDK 27 客户端对 OpenSSL 3.5+、BoringSSL 等 peer 的混合组支持不一。FIPS 140 模块认证边界：混合 TLS 是否落入模块安全策略须问 HSM/JDK FIPS 供应商，超出 OpenJDK 通用文档范围。\n常见误区：仅升级 JDK 27 而未确认负载均衡、API 网关、旧版 Android/浏览器是否支持混合组，导致静默回退经典 ECDHE。手动把 jdk.tls.namedGroups 设为仅 X25519 以「提速」，实则关闭 PQ 保护。\n运维：可观测性与性能边界 # 为何：车队成百上千 JVM 时，需要证明「未偷偷放宽 TLS」而非抽样一台手工 openssl s_client。\n机制：OpenJDK 提供 JFR 事件 jdk.TLSHandshake（字段含 protocolVersion、cipherSuite、certificateId 等）。该事件不包含 negotiated named group——仅凭 JFR 难以证明混合组已生效；可结合 JDK 26+ 的 java -XshowSettings:security:tls、抓包或供应商扩展事件。JEP 527 / 本场材料未提供 PQ TLS 握手量化性能数据，容量规划应引用独立基准，不得编造延迟或吞吐数字。\njava -XX:StartFlightRecording=duration=60s,filename=pq-tls.jfr -jar app.jar java -XshowSettings:security:tls -version 对需要 named group 证据的环境，可在预发用 Wireshark 解析 supported_groups / key_share，或编写一次性探针连接已知支持混合组的测试端点。Q\u0026amp;A 中提到的 JFR 观察 TLS 宜理解为协议版本、套件与证书等字段；若演讲暗示可一眼看出 X25519MLKEM768，与当前 TLSHandshakeEvent 字段集可能不一致——以源码为准。\n常见误区：用本场未给出的 handshake 延迟数字做容量规划；把 Grover 威胁写成「必须立刻禁用 AES-128-GCM」——须结合协议互操作与性能预算逐项决策。\n迁移检查清单（工程视角） # 运行时：计划 JDK 27（或带 JEP 527 的 EA）用于对外 TLS；JDK 26+ 用于 ML-DSA JAR 签名流水线；JDK 24+ 用于生成 ML-KEM/ML-DSA 密钥材料。 边缘与客户端：梳理 L7/L4 终止代理、Service Mesh、mTLS 双向认证对 named group 的影响。 PKI：区分 KEM 证书载体与签发算法；CA 先具备 ML-DSA（或过渡期 RSA）签发能力。 供应链：jarsigner -sigalg ML-DSA-65 与依赖方 JDK 验证版本对齐。 合规台账：FIPS 203/204 编号 + IANA group 名 + JDK 版本三列绑定。 观测：JFR + security:tls 设置 + 抽样抓包，避免单一信号误判。 能力叠代速查 # 能力 JDK 版本 依据 KEM API 21 JEP 452 ML-KEM / ML-DSA 算法 24 JEP 496 / 497 KDF 定稿 / HKDF 25 JEP 510 ML-DSA jarsigner 26 JDK 26 RN HPKE Cipher 26 JDK 26 RN PQ 混合 TLS 27 JEP 527 早期访问构建见 jdk.java.net/27。PEM API、Argon2、TLS 证书压缩、LMS RFC 9858、jlink 安全插件等路线图条目须逐条查 JEP 当前状态，未在本篇核实。\n参考与延伸阅读 # NIST 后量子密码项目 FIPS 203 ML-KEM FIPS 204 ML-DSA RFC 8446 TLS 1.3 RFC 5869 HKDF RFC 9180 HPKE RFC 8554 HSS/LMS RFC 9882 ML-DSA 证书与 CMS JEP 452 KEM API JEP 496 ML-KEM JEP 497 ML-DSA JEP 510 KDF API JEP 527 TLS 混合密钥交换 Oracle Java Cryptographic Roadmap IANA TLS Supported Groups ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-java-and-post-quantum-cryptography/","section":"文章","summary":"Java 平台与后量子密码：从威胁模型到 JDK 交付路径","title":"Java 平台与后量子密码：从威胁模型到 JDK 交付路径","type":"posts"},{"content":" Java 生态里的 Agentic AI：三套框架如何用同一业务讲清编排差异 # 企业把 LLM 接进 Java 服务时，真正的分歧往往不在「能不能调 OpenAI」，而在 谁决定下一步：应用代码、框架 DSL，还是模型在工具集合里自路由。Spring I/O 2026 上，Timo Salm 与 Sandra Ahlgrimm 用同一个 AI 营养周计划 场景，在 Spring AI、LangChain4j 的 langchain4j-agentic 与基于 Spring AI 的 Embabel 之间做横向对照。下文把该对照提炼为可落地的选型与实现要点；现场对比表与主观评价属演讲者观点，公开文档未逐格背书的部分会单独标明。\n从 RAG 到 Agentic：工具面与协议层 # LLM 应用通常沿能力阶梯演进：独立模型与提示工程 → RAG 接地业务数据 → 工具调用与代码解释器 → 多模态输入 → 编排、记忆、规划 与标准化工具协议。Agentic 阶段的关键变化是：在工具/RAG 路径变多时，路由决策更多交给模型，应用侧则需提供可预期的工具契约与观测。\nModel Context Protocol (MCP) 由社区以开放标准形式维护（现行治理见 LF Projects 托管说明），用于把数据源、工具与工作流以统一方式接到 AI 应用；Agent2Agent (A2A) 则面向跨厂商 agent 互操作。Java 团队若已用 Spring Boot，Spring AI 的 MCP 注解与 Starter 能把 @McpTool 等方法暴露为 MCP 工具，而不必为每个集成重写一套 RPC 形状。\nThe Evolution of LLM Based Solutions：从 Standalone LLM 到 Agentic AI Systems（含 Orchestration、Memory、Planning、MCP）\n为什么：Java 后端往往已有安全、事务与可观测栈；若每个 Copilot 集成各自定义 tool schema 与传输层，联调成本会指数上升。MCP 把「工具长什么样」从业务代码里抽出一层；A2A 则回答「多个自治 agent 如何互发任务与结果」——二者互补，而非二选一。\n怎么做（最小 server 侧）：在 Spring Boot 中启用 MCP Server Starter 后，用 @McpTool 标注现有 service 方法，由框架生成 JSON schema 并挂到 MCP 端点；客户端侧用 McpClient 拉取工具列表再交给 ChatClient 的 tool calling 循环（详见 MCP Overview）。\n常见误区：把「接了 MCP」等同于「已是可控的 agent 系统」。协议解决的是连接标准化；任务分解、校验环、成本上限仍要在应用或框架层显式建模。另：对外表述 MCP 时，宜区分「早期由 Anthropic 推动」与「现行 LF Projects 治理」（以 governance 页 为准），避免与托管主体混淆。\nWorkflow 与 Pure Agent：先定编排哲学 # Anthropic 的工程文 区分两类系统：Workflows 通过预定义代码路径编排 LLM 与工具；Agents 则由 LLM 动态决定过程与工具使用。企业场景里，可预测、可测试、延迟与 token 成本更可控的通常是 workflow；pure agent 适合步骤无法事先穷举的开放任务，但需要更强护栏与测试投入（演讲者观点，与 Anthropic 文内经验判断一致）。\nUse workflows for predictability. Use agents only when flexibility and model-driven decision-making are genuinely needed.\n维度 Workflow（预定义路径） Pure Agent（模型自路由） 可测性 高：步骤边界清晰 低：路径非确定 成本/延迟 相对易封顶 易随轮次膨胀 适用 营养计划、审批流、ETL 式 AI 探索性研究、开放域助手 常见误区：为「显得先进」默认上 fully autonomous agent。若业务步骤可画成流程图，应优先 workflow，仅在子步骤内局部使用模型决策。\n营养周计划：Parallelization + Evaluator–Optimizer # 对照 Demo 刻意组合两种 Anthropic 归纳的 workflow 模式（名称以官方为准）：\nParallelization：并行获取用户档案与时令食材； Evaluator–Optimizer：生成周计划 → 按过敏/热量等规则校验 → 不通过则带反馈重试。 Sample Application：并行获取 profile 与 seasonal ingredients，再进入 Create / Validate 闭环\nWorkflow Patterns for Agentic Systems：含 Prompt Chaining、Parallelization、Evaluator-Optimizer 等\n为什么：单次 prompt 很难同时满足结构化输出与硬约束；分阶段调用便于插入程序化校验与可观测 span。\n机制/约束：每一轮 LLM 调用都消耗 context；校验失败时的反馈会叠加进后续 prompt，需设 maxAttempts / maxIterations 防止成本失控（三框架实现均体现此约束，属演讲者观点）。\n怎么做（逻辑骨架，三实现共用）：\nPhase1: parallel(fetchProfile, fetchSeasonalIngredients) Phase2: createWeeklyPlan(profile, ingredients, request) Phase3: loop until nutritionRulesPass(plan) or maxAttempts 营养 Demo 的 UI 侧要求用户勾选一周餐次、填写国家 ISO 码与「30 分钟内完成」等约束；生成结果需带食材克数、营养表与烹饪步骤——这类强结构 + 硬规则场景，正是 evaluator–optimizer 比单次 call().content() 更划算的地方。\n常见误区：把「校验」也全部交给同一个 LLM 自评，而不保留可执行的规则函数（如钠含量、过敏原枚举）；或在 parallel 阶段共享可变对象而不做线程安全隔离。\nSpring AI：手写并行 + Advisor 校验环 # 并行阶段：演示级 Workflow.parallel，非框架 API # Spring AI 参考文档 中未出现 Workflow.parallel；现场 Workflow.java 是用 CompletableFuture 与固定线程池封装的演示工具类，语义上就是「标准 Java 并发跑两个 Supplier」（演讲者口播与文档检索结论一致）。\nWorkflow.java：parallel 使用 CompletableFuture 与 Executors.newFixedThreadPool\nNutritionPlannerAgent：var result = Workflow.parallel(() -\u0026gt; fetchUserProfileForUser(name), () -\u0026gt; fetchSeasonalIngredients(request))\n// 演示仓库工具类 + Spring AI ChatClient（非 Spring AI 内置 API） var result = Workflow.parallel( () -\u0026gt; fetchUserProfileForUser(name), () -\u0026gt; fetchSeasonalIngredients(request)); var userProfile = (UserProfile) result.getFirst(); var seasonalIngredients = (SeasonalIngredients) result.getLast(); MCP 入口可用 @McpTool 暴露同一业务能力，并与 Spring Security 集成（从 SecurityContextHolder 取当前用户再委托内部方法——演讲演示逻辑）。\n@McpTool(description = \u0026ldquo;Provides a nutrition plan for the week\u0026rdquo;) 与 Workflow.parallel 同屏\nEvaluator–Optimizer：Recursive Advisors # Spring AI 的 Advisor 链可拦截 ChatClient 调用。官方 StructuredOutputValidationAdvisor 在 JSON schema 校验失败时通过 callAdvisorChain.copy(this) 重试。现场 ValidationRetryAdvisor 是演示类名，模式相同：对 WeeklyPlan 做业务规则校验，失败则把反馈送回模型。\n// 演示思路；生产可优先 StructuredOutputValidationAdvisor.builder() var advisor = new ValidationRetryAdvisor\u0026lt;\u0026gt;( WeeklyPlan.class, plan -\u0026gt; validateWeeklyPlan(plan, userProfile)); WeeklyPlan plan = chatClient.prompt() .advisors(advisor) .user(/* ... */) .call() .entity(WeeklyPlan.class); ValidationRetryAdvisor 与 chatClient.prompt().system(Personas.RECIPE_CURATOR)\n为什么选 Advisor 链：营养计划校验失败时，需要把业务反馈（例如「周三钠超标」）送回模型重生成，而不是简单重试同一 prompt。ChatClient 的 advisor 与 ToolCallAdvisor 共用同一调用链，便于在 Micrometer / OpenTelemetry 里对齐 span，避免在校验逻辑里再手写一套 HTTP 客户端。\n机制/约束：Recursive Advisor 通过 CallAdvisorChain.copy(after) 复制 downstream；StructuredOutputValidationAdvisor 针对 JSON schema，演示里的 ValidationRetryAdvisor 则针对 WeeklyPlan 领域规则——二者可叠加，但要注意 maxRepeatAttempts 与总 token 预算。演讲提到 Spring AI 内部 structured output 已采用类似重试环（演讲者观点），与公开文档描述方向一致。\n怎么做：并行用 Java 并发或 Spring @Async；校验环优先 StructuredOutputValidationAdvisor.builder()，业务规则再包一层自定义 CallAdvisor。若暴露 MCP，保持「HTTP 控制器」与「@McpTool 入口」调用同一编排方法，避免双份逻辑漂移。\n常见误区：在文档中写成「Spring AI 内置 Workflow.parallel」；或把 Demo 的 ValidationRetryAdvisor 当成 GA 类型。当前 2.0.0-M6 仍为里程碑，路线图中的 tool search 等能力未在本次文档抓取中核实，不宜写成已 GA。另：把 advisor 链写得过深导致单次用户请求触发数十次嵌套 LLM 调用——应在设计评审时显式列出「最坏情况调用次数」。\nLangChain4j-agentic：声明式 sequence / loop # langchain4j-agentic 模块（教程标明 experimental）通过 @Agent、AgenticServices.agentBuilder、sequenceBuilder、loopBuilder 把子 agent 与校验环装配为流水线，减少手写编排。\nvar validationLoop = AgenticServices.loopBuilder() .subAgents(nutritionGuard, reviserAgent) .maxIterations(3) .exitCondition(scope -\u0026gt; { var r = scope.readState(\u0026#34;validationResult\u0026#34;, null); return r != null \u0026amp;\u0026amp; r.allPassed(); }) .build(); var planner = AgenticServices.sequenceBuilder(Agents.NutritionPlanner.class) .subAgents(seasonalAgent, weeklyPlanCreator, validationLoop) .outputKey(\u0026#34;weeklyPlan\u0026#34;) .listener(/* AgentListener / AgentMonitor */) .build(); AgenticServices.sequenceBuilder 与 subAgents(seasonalIngredientAgent, weeklyPlanCreator, validationLoop)\nloopBuilder、maxIterations(3) 与 scope.readState(\u0026ldquo;validationResult\u0026rdquo;)\n为什么：当步骤图稳定、子 agent 边界清晰时，DSL 比散落的手写循环更易读，也便于在 AgenticScope 里共享状态。\n机制/约束：maxIterations(3) 直接封顶 token；exitCondition 必须可判定，避免死循环。现场 OCR 中的 MicrometerAgentListener 未在上游仓库找到同名类，官方提供的是 AgentListener / AgentMonitor（未能核实是否为 Demo 包装）。\n为什么：Timo 在 Spring AI 分支里手写 parallel 与 loop，而 LangChain4j 侧用 builder 表达同一图——若团队已熟悉 LangChain4j 的 AiServices，agentic 模块能把「子 agent = 带 @Agent 的方法」这一心智延续下去，并把 AgenticScope 当作跨步骤的键值存储。\n机制/约束：parallelBuilder 与 sequenceBuilder 在 AgenticServices 中并列；loop 的 exitCondition 读取的 state 需由子 agent 写入（如 validationResult）。模块开篇即声明 experimental，API 可能在 minor 版本间变动。\n怎么做：把「时令食材 agent」「周计划 agent」「校验 + 修订 loop」拆成独立 @Agent 方法，再用 sequenceBuilder 串联；观测优先接官方 AgentListener / AgentMonitor，若需 Micrometer 指标再自行包装 listener（现场 MicrometerAgentListener 未能核实为上游类名）。\n常见误区：忽略 experimental 声明直接上生产；exitCondition 未处理 readState 默认值导致过早退出；在 loop 内调用未设上限的 reviser agent 导致 token 账单失控（演讲强调 maxIterations(3) 与成本关系，属演讲者观点）。\nEmbabel：规划器 + 领域类型 + 渐进工具暴露 # Embabel 构建于 Spring 生态之上（README 写明 JVM/Spring），用 @Action、@Agent 等把方法注册为 agent 步骤；默认规划为 GOAP（Goal Oriented Action Planning），每步后可重规划（README）。演讲口播 A* 与公开 README 默认算法不一致，实现细节应以仓库为准。\n@Action SeasonalIngredients fetchSeasonalIngredients( WeeklyPlanRequest request, Ai ai) { return ai.withAutoLlm() .createObject(prompt.formatted(month, country), SeasonalIngredients.class); } @Action 与 ai.withLLM / createObject 生成 SeasonalIngredients\nEmbabel NutritionPlannerAgent：@Action 与 SecurityContextHolder / MCP 注释\n@UnfoldingTools：按 category 缩小工具 metadata # 当 MCP/业务工具数量大时，一次性注册会撑爆 context。Embabel 的 @UnfoldingTools 与带 category 的 @LlmTool 实现渐进披露：先暴露入口工具，再按 category 展开（与 Spring AI 路线图中提及的 tool search 不同机制，对比属演讲者观点）。\n@UnfoldingTools(name = \u0026ldquo;weekly_meal_plan_tools\u0026rdquo;) 与 category = \u0026ldquo;nutrition\u0026rdquo; 的 @LlmTool\n统一入口：AgentInvocation # HTTP、MCP 或 Shell 可通过 AgentInvocation 触发同一 agent run：\n@PostMapping(\u0026#34;/plan\u0026#34;) WeeklyPlan create(@AuthenticationPrincipal Principal p, @RequestBody WeeklyPlanRequest request) { var invocation = AgentInvocation.builder(agentPlatform) .build(WeeklyPlan.class); return invocation.invoke(Map.of( \u0026#34;user\u0026#34;, p.getName(), \u0026#34;request\u0026#34;, request)); } NutritionPlannerController：AgentInvocation.builder(agentPlatform).build(WeeklyPlan.class)\n为什么：手写 if/else 决定「先拉 profile 再生成计划」在步骤增多时难维护。Embabel 用 GOAP 根据当前 state 与 @Action 方法签名推断下一步；方法参数缺失时规划失败，相当于把前置条件检查从 prompt 挪到类型系统（演讲者演示口径）。\n机制/约束：README 写明规划步骤 可插拔，且每步后可 replan；这与纯 prompt 编排不同。Ai 封装 Spring AI 的 ChatClient 能力，createObject 负责结构化抽取。工具暴露方面，@UnfoldingTools 先给模型一个「门控」工具，再按 category 展开 @LlmTool，减轻一次性注册上百 MCP 工具时的 metadata 压力。\n怎么做：每个外部能力一个 @Action；需要 LLM 的子步骤注入 Ai ai；领域返回值（UserProfile、SeasonalIngredients、WeeklyPlan）驱动后续步骤是否可执行。HTTP 层用 AgentInvocation 统一触发，MCP/Shell 走同一 AgentPlatform（MCP 细节未能用官方页逐条核对营养 Demo）。\n常见误区：把对比表「全绿」当生产成熟度；演讲明确 Embabel 尚未 GA（演讲者观点）。把口播 A* 直接写进架构文档——公开 README 默认 GOAP，应以仓库为准。调用 LLM 用 ai.withAutoLlm()（Ai.kt），勿误写 LlmOptions.withAutoLlm()。\n三框架如何分工（概念矩阵） # 框架 典型定位 编排表达 成熟度提示（2026-05 文档快照） Spring AI LLM 集成、Advisor、MCP/A2A 代码 + Advisor 链；并行需自建 1.1.x 参考 + 2.0.0-M6 LangChain4j-agentic 社区 agentic DSL sequence / loop / parallel builders 模块 experimental Embabel Spring 之上的高阶 orchestration 类型驱动步骤 + 规划器 SNAPSHOT / 非 GA（演讲者观点） Comparing Agentic AI Frameworks\u0026rsquo; Capabilities：Spring AI / LangChain4j / Embabel 能力矩阵（现场表，未独立核实每一格）\n现场幻灯片对 LangChain4j 的 MCP 格标注 No HTTP Server（未能核实是否涵盖 Quarkus 社区扩展或仅指核心库 HTTP server）。选型时应按「需要的 MCP 传输形态（stdio / HTTP）」自行验证，而非照搬单色格。\n怎么做（选型 checklist）：\n已深度使用 Spring Boot、要以最小依赖接 LLM + MCP → 优先评估 Spring AI + 手写 workflow； 希望用 Java DSL 表达 subagent / loop，且接受 experimental → LangChain4j-agentic； 需要 GOAP 式步骤前置条件与渐进工具暴露，并愿跟踪 SNAPSHOT → Embabel（仍建议与 Spring AI 能力对照）。 观测与测试（横切）：三实现都能挂 Micrometer 一类指标，但抽象层不同——Spring AI 在 advisor / ChatClient 链；LangChain4j 在 AgentListener；Embabel 在 AgentPlatform 与 invocation 生命周期。Deterministic workflow 的单元测试应断言步骤顺序与校验分支，而非 LLM 原文；对模型输出宜做 schema + 业务规则双层断言（与 P04 演示思路一致）。现场矩阵对 Testing、Observability 多标黄/绿混色，反映的是当时快照，不是长期路线图（演讲者观点）。\n常见误区：用现场矩阵替代自家 PoC；忽略 LangChain4j MCP 教程 与 Spring AI MCP 文档在「客户端/服务端」角色上的差异；在 Embabel SNAPSHOT 与 Spring AI 2.0-M6 混用时未锁定 BOM 版本。Koog（JetBrains）等框架未在本次材料中展开，不宜据单场会议推断「Java 只有三家可选」。\n同一营养周计划在代码形态上呈现三种答案：Spring AI 把编排留在 Java + Advisor，框架提供 LLM/MCP 基座；LangChain4j-agentic 用 builder 表达 sequence/loop；Embabel 用规划器与类型驱动步骤，并内置渐进工具暴露。没有绝对更优，只有与团队技能栈、可接受实验性、以及「workflow 占比」是否匹配的选择。若你已在生产使用其中一套，更务实的路径往往是在现有栈上补齐 parallel 与 evaluator–optimizer 两环，再按需评估是否引入更高层 DSL——而不是一次性迁移到对比表全绿的列。\n参考与延伸阅读 # Anthropic — Building effective agents Model Context Protocol — 入门 Model Context Protocol — 治理与托管 Agent2Agent (A2A) — 什么是 A2A Spring AI 参考文档（1.1.x） Spring AI 2.0.0-M6 文档索引 Spring AI — Advisors API Spring AI — Recursive Advisors Spring AI — MCP 概览 Spring AI — @McpTool 服务端注解 LangChain4j — Agents and Agentic AI LangChain4j — agents.md 源码 LangChain4j — MCP 教程 Embabel Agent — README Embabel — @UnfoldingTools 源码 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-spring-io-2026-comparing-agentic-ai-frameworks-for-java-by-timo-salm-sandra-ahlgrimm-sp/","section":"文章","summary":"Java 生态里的 Agentic AI：三套框架如何用同一业务讲清编排差异","title":"Java 生态里的 Agentic AI：三套框架如何用同一业务讲清编排差异","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/javafx/","section":"Tags","summary":"","title":"Javafx","type":"tags"},{"content":" JavaFX 26：桌面 UI 的工程基线与选型边界 # JavaFX 26 是 OpenJFX 在 2026 年的特性版（Tip）GA，与 JDK 26 同代发布。对已有 Swing/JavaFX 混合栈或计划升级 JDK 的团队，这一版的价值不在「又一个控件」，而在于三件事可核对：版本族如何配对、哪些 API 仍非最终、图形与无头路径如何在 macOS/CI 上落地。若你维护的是长期运行的工控/金融客户端，通常会在 Tail（25.0.x） 与 Tip（26） 之间做分叉：前者换补丁、控风险，后者吃 CSS/文本/Prism 新能力并接受 Preview 迁移成本。下文按工程决策组织；凡未在 发行说明 或 Javadoc 中出现的节奏判断，会标明为演讲者观点或待本地实测。\n场景图工具包在 2026 年的位置 # JavaFX 仍是基于 Scene Graph 的客户端 UI 工具包：节点级变换、CSS 样式、内置控件与 2D/3D 图形同属 OpenJFX 项目，桌面端覆盖 Windows、macOS、Linux；移动端与嵌入式通常经 Gluon 等生态扩展（属产品路线，非 26 GA 核心交付）。\nJavaFX Overview：Programming paradigm is scene-graph based with transformations specified at each node\n26 在「概览层」上的增量，集中在四类：文本与 CSS 表达能力、macOS Prism 后端、标题栏嵌入 UI（Preview）、孵化器富文本（Incubator）。稳定面仍建议锁定 javafx.controls、javafx.fxml 等已 GA 模块；实验能力需单独做架构评审与 CI 隔离。\nJavaFX Overview (continued)：RichTextArea (Incubator) 与 JavaFX Controls in the Title Bar: HeaderBar (Preview)\n常见误区：把 Preview/Incubator 当作「只是文档里多一行警告」。StageStyle.EXTENDED 与 HeaderBar 在 Javadoc 中均标注可能在未来版本变更或移除；25→26 已发生 HeaderBar API 更新。升级应预留编译期与 UI 回归窗口，而不是仅做 JDK 小版本替换。另一个实务点：Preview 遵循 JDK 预览 API 惯例，启用方式以你所用 JDK 的预览文档为准——JavaFX 26 发行说明未逐条列出全部 JVM 开关，CI 里应对「开启预览的构建」与「生产构建」分 job，避免把实验模块打进同一制品。\nTip 与 Tail：版本族对齐 # OpenJDK 自 JEP 14 起采用 Tip and Tail 信息模型：Tip 承载新特性，Tail 以补丁形式提供稳定修复。JEP 14 的 Scope 是 JDK，并未逐字点名 JavaFX；但从 javafx26 与独立的 javafx25.0.2 补丁线可推断，JavaFX 在运营上与之对齐。\nJavaFX Releases Tip and Tail：Use JavaFX 26 with JDK 26 (recommended)\n路线 推荐配对 官方边界 Tip（特性） JavaFX 26 + JDK 26 JavaFX 26 发行说明：运行需 JDK 24+，推荐 JDK 26 Tail（稳定补丁） 如 JavaFX 25.0.x + JDK 25.0.x 下载页提供 25.0.2 等三位补丁号；幻灯片上的 25.0.M 为示意，未在下载页出现字面 .0.M 为什么：JavaFX 含原生渲染与 Glass 窗口层，主版本错配容易在模块路径、字节码或 Prism 初始化阶段暴露问题，而不是等到业务逻辑才失败。\n机制/约束：javafx26 下载页 写明设计目标为与 JDK 26 协同，并已知可在 JDK 24 上运行。演讲者观点：开发阶段可临时使用 JavaFX N + JDK N-1，不推荐用于生产——与「24+ 可运行、26 推荐」方向一致，但无「仅开发」的正式措辞。\n怎么做（模块路径运行，可复现）：\nexport JAVAFX_SDK=/path/to/javafx-sdk-26 java --module-path \u0026#34;$JAVAFX_SDK/lib\u0026#34; --add-modules javafx.controls \\ -cp app.jar com.example.Main Gradle 示例应将 languageVersion 与 org.openjfx:javafx-* 版本号锁在同一族（如 26.0.x）。\n常见误区：在 Tail 生产线上追逐 Tip 才有的 Preview API；或仅升级 JDK 而不升级 JavaFX 构件，导致支持矩阵与漏洞修复渠道不一致。\n商业支持路线图：支持回归 ≠ 重新捆绑 JDK # 2026 年起，Oracle 在 Java Verified Portfolio 支持路线图 中重新写明：JavaFX 支持按对应 JDK 的 Premier Support 时间线提供，且对符合条件的 Oracle 客户不额外收费（以合同与 MOS 为准）。这与「把 JavaFX 塞回 JDK 安装包」是两回事。\nOracle Java Verified Portfolio Roadmap：JavaFX Oracle has reintroduced support for JavaFX following the Premier Support timeline\nOpenJFX Download Wiki 明文：自 JDK 11 起，javafx.* 模块不再随 JDK 分发，需单独下载 SDK 或通过构建工具引入 Maven 坐标。\n路线图页脚：JavaFX support for corresponding new Oracle JDK versions；LTS 行含 JDK 8 有限期支持说明（日期以 Lifetime Support Policy 为准）\n为什么：采购与运维需要可查询的 EOL；开发需要澄清依赖获取方式，避免假设 JAVA_HOME 自带 JavaFX。\n机制/约束：JVP 表中 JavaFX 21/25 为 LTS 行，26 为非 LTS 较短窗口（页面日期标注为 examples）。「自 JavaFX 21 起与 JDK LTS 五年对齐」宜弱化为：LTS 版本按 JVP 表与 JDK LTS 对应；非 LTS 特性版窗口更短。\n怎么做：在内部台账同时记录 jdk_vendor、jdk_major、javafx 构件版本与支持链接（Java SE 支持路线图 / JVP）。\n常见误区：法务把「商业支持回归」理解成「可以卸载独立 JavaFX 依赖」——JDK 11+ 仍须显式依赖 OpenJFX 构件。\nCSS：从系统主题到视口与缓动曲线 # JavaFX 25 起 CSS 支持 媒体特性查询（如 prefers-color-scheme）；26 扩展场景尺寸/纵横比等视口特性，并加入 piecewise linear easing（JDK-8358450、JDK-8372203），与 CSS Easing Functions Level 2 方向一致。\nJavaFX 25 Highlights — CSS media feature queries：@media (prefers-color-scheme: dark) 示例；Scene size 标注为 new in 26\n为什么：减少 Java 代码里硬编码主题分支；动画不必局限于内置关键字，便于表达 bounce 等曲线（现场演示属演讲者观点，可复现但不构成规范条文）。\n机制/约束：媒体查询作用于 Scene 上下文；视口特性随窗口尺寸变化重新求值。动画基础设施在 24–26 持续演进，升级时需查阅各版 RN 中的 CSS/动画破坏性变更。\n怎么做：\n.button { -fx-background-color: lightgray; -fx-text-fill: black; } @media (prefers-color-scheme: dark) { .button { -fx-background-color: darkgray; -fx-text-fill: white; } } piecewise linear 语法以 JavaFX CSS 参考 为准。\n常见误区：把 JavaFX CSS 当浏览器 CSS 全集；或在不支持的选择器上静默失败——应对照 CSS 参考的 JavaFX 专有属性表。\n文本布局：几何制表位与 Caret 几何 # 比例字体下用「字符数 × 固定宽度」模拟 Tab 会对不齐。TabStopPolicy（25，JDK-8314482）让 TextFlow 按几何位置定义制表位。26 另增 getLayoutInfo()、caretShape、getRangeShape 等（见 JavaFX 26 新 API 列表），便于 IDE 式命中测试与自动化断言。\n现场演示 Rich Text Editor Demo — tabstops.rich：标尺与 Use Paragraph Tab Stops 示例\n为什么：报表、代码编辑器、富文本工具需要像素级列对齐与光标矩形，而不是仅 Text 的简易布局。\n机制/约束：制表位策略绑定在 TextFlow（及富文本路径上的对应能力）；演讲者观点：RichTextArea 上的制表位增强在 27 路线中仍在评审，26 GA 不应假设已等价落地。\n怎么做：\nvar flow = new javafx.scene.text.TextFlow( new javafx.scene.text.Text(\u0026#34;Col1\\tCol2\\n\u0026#34;)); // flow.setTabStopPolicy(policy); // JavaFX 25+ var info = flow.getLayoutInfo(); 常见误区：在 Text 与 TextFlow 之间复制粘贴布局算法；应优先使用布局快照 API 做测试，而不是截图对比整窗。\nmacOS Prism：Metal 可选，默认仍为 ES2 # Apple 已弃用 OpenGL。JavaFX 26 在 macOS 引入基于 Metal 的 Prism 管线，但 JDK-8271024 明确：ES2 仍是 macOS 默认，Metal 为可选增强。jfx26 标签的 PrismSettings 中，默认尝试顺序为 es2 → mtl → sw；可用系统属性覆盖。\nJavaFX 26 Highlights — Metal Rendering Pipeline (optional)：java -Dprism.order=mtl ...\n为什么：在 OpenGL 退出窗口前验证 Metal，避免 GA 切换时集中爆雷。\n机制/约束：prism.order 接受逗号分隔列表；OpenGL 初始化失败时会按顺序尝试下一管线（回退语义来自源码顺序，非单独 RN 段落）。开发线 master 已将 macOS 默认改为 mtl, es2, sw，面向 27，≠ 已发布的 27-ea 二进制已切换——待本地实测（java -Dprism.verbose=true）。\n怎么做：\njava -Dprism.order=mtl -jar myapp.jar 虚拟机环境注意 JDK-8375466（Metal 在虚拟化 macOS 上的崩溃修复）。\n常见误区：在 26 生产环境默认开启 Metal 却未做 GPU/虚拟机矩阵测试；或把 master 分支默认顺序当成 26 GA 行为。\n无头 Window Toolkit：CI 与离屏渲染 # 26 将无头 Glass 平台标为 POC（JDK-8324941），可通过 -Dglass.platform=headless 启用（JDK-8364687 统一了小写 headless）。用例包括无显示器的 UI 测试、对 Scene/Node 做 snapshot 或 Canvas 导出。\nHeadless window toolkit (prototype)：java -Dglass.platform=Headless -Dprism.order=sw ...\n为什么：在 CI/云主机上跑 JavaFX 集成测试，避免依赖物理显示与窗口管理器。\n机制/约束：JavaFX 26 Highlights 与 RN 仅记载 glass.platform=headless 为官方启用方式。幻灯片同时给出 -Dprism.order=sw（软件渲染），与「需要软件渲染管线」一致——可视为运行无头时的实践组合；是否缺一不可，RN 未逐条枚举，生产清单宜先以单标志为准，组合用法需在目标 JDK/JavaFX 构建上验证。\n怎么做：\njava -Dglass.platform=headless -Dprism.order=sw \\ --module-path \u0026#34;$JAVAFX_SDK/lib\u0026#34; --add-modules javafx.controls \\ -cp tests.jar com.example.HeadlessRenderTest 常见误区：在无头环境仍强制 prism.order=mtl；或把 POC 当成长期稳定 API，未锁定 OpenJFX 补丁版本。\n标题栏嵌入：StageStyle.EXTENDED 与 HeaderBar # 现代桌面应用常把工具栏伸入标题栏，同时保留系统关闭/最小化/最大化行为。26 提供 Preview 级的 StageStyle.EXTENDED 与 HeaderBar：客户端区域扩展到标题栏，HeaderBar 负责系统按钮布局（随 OS 左右差异调整），空白区域仍可拖动窗口。\nControls in the title bar (Preview)：EXTENDED Stage style 与 HeaderBar 概述\n26 另支持 EXTENDED 对话框（JDK-8370446），部分装饰属性迁移到 Stage 附加属性（细节见 JDK-8369836）。\n为什么：避免自绘窗口 chrome 带来的平台不一致与安全区问题。\n机制/约束：Preview API；与 UNDECORATED 自绘方案并存但语义不同——EXTENDED 保留系统按钮管理。\n怎么做：\nvar stage = new Stage(); stage.initStyle(StageStyle.EXTENDED); var root = new BorderPane(); root.setTop(new HeaderBar()); stage.setScene(new Scene(root, 800, 600)); stage.show(); 常见误区：在 EXTENDED 窗口上再叠一层全屏自定义标题栏，导致双重点击区域；或忽略 26 的 API 破坏性变更未跑迁移编译。\n孵化器富文本：RichTextArea # RichTextArea 位于 jfx.incubator.richtext，面向需要样式范围、IME、嵌入节点的场景；讲者明确其不是开箱即用的完整 Word 替代品，且 HTMLEditor 对许多场景过重。\nRichTextArea：Illustrates many capabilities of RichTextArea；Save / Open files in rich text format\nRichTextArea Control (Incubator) — Overview：syntax highlighting、inline emphasis、embedded nodes\n核心写模型（已核实 Javadoc）：appendText 与 applyStyle，后者默认 merge 覆盖冲突属性。26 另有 insertStyles 属性（JDK-8374035）等变更。\nvar rich = new jfx.incubator.scene.control.richtext.RichTextArea(); // StyleAttributeMap 由 builder 构造 rich.appendText(\u0026#34;Hello \u0026#34;); rich.applyStyle(start, end, boldAttributes); 演讲者观点：粘贴优先级为 JavaFX native chunk → RTF → 纯文本——未在 Javadoc/RN 中找到相同表述；RTADemo.rich 示例在公开 jfx26 树路径上未能定位，不排除位于未同步的 demo 目录。\n常见误区：未在 module-info 中 requires jfx.incubator.richtext 或未按发行说明启用孵化器模块；或把 merge 语义当成「仅追加不覆盖」，导致样式叠床架屋。\n27 EA 与图形后端前瞻 # jdk.java.net 除 26 GA 外提供 JavaFX 27 EA 与独立的 JavaFX Direct3D 12 EA（基于 direct3d12 沙箱分支，非完整 27 构建；页面声明 EA 能力可能永不进入 GA）。Windows 上 D3D12 默认顺序在 EA bundle 中可为 d3d12, d3d, sw（JDK-8356815），与 26 GA 无关。\nFuture Releases — Possible Ideas：JavaFX Native Wayland support on Linux\nLinux 上当前多依赖 XWayland；原生 Wayland（Wakefield 等，JDK-8281970）属长期工具链议题。演讲者观点：D3D12 合并目标版本可能在 27 或 28；Charts 交互增强、Notebook 工具等多在「Possible Ideas」——非 26 GA 承诺。\nLinks：Download JDK 26 and JavaFX 26 GA (and 27-ea) from jdk.java.net\n常见误区：把 EA 沙箱当作生产依赖；或未在 macOS 上对 26（es2 默认）与 27-ea（master 上 mtl 优先）分别做 Prism 日志对比。\n获取构建、文档与反馈渠道 # 用途 入口 JDK / JavaFX 构建 jdk.java.net Javadoc、入门、示例 openjfx.io 源码与 PR github.com/openjdk/jfx 开发邮件列表 openjfx-dev@openjdk.org 缺陷 bugreport.java.com → JBS 缺陷报告宜附带 java -version、OS、prism.order/glass.platform 与最小 Main 复现。\n升级检查清单（可复现） # 在合并 Tip 版本前，建议至少跑通下面五步；它们不依赖特定构建工具，只依赖你目标平台的 JDK/JavaFX 安装包：\n版本指纹：java -version 与 JavaFX SDK 路径一致；记录 org.openjfx 坐标或 --module-path 目录哈希。 Prism 日志：macOS 上分别用默认与 -Dprism.order=mtl 启动冒烟窗体，保留 -Dprism.verbose=true 输出。 无头冒烟：CI 镜像内用 -Dglass.platform=headless（必要时加 -Dprism.order=sw）渲染一张 WritableImage 快照，与基线 PNG 做像素差。 CSS 回归：覆盖 prefers-color-scheme 与窗口 resize 触发的视口查询（26 新特性）。 API 边界：若依赖 HeaderBar/RichTextArea，在构建中开启 --release 与编译器 lint，确保孵化器模块未泄漏到不应发布的 module-path。 参考与延伸阅读 # OpenJFX 社区站 JavaFX 26 下载（jdk.java.net） JavaFX 27 早期访问构建 JavaFX Direct3D 12 早期访问 JavaFX 26 发行说明 JavaFX 25 发行说明 JavaFX 26 新 API 一览 JavaFX 26 Highlights JEP 14：Tip and Tail 模型 OpenJFX 下载与 JDK 11 后独立分发说明 OpenJFX 贡献指南 Oracle Java Verified Portfolio 支持路线图 Oracle Java SE 支持路线图 JavaFX CSS 参考 macOS Metal 渲染管线（JDK-8271024） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-javafx-26-today/","section":"文章","summary":"JavaFX 26：桌面 UI 的工程基线与选型边界","title":"JavaFX 26：桌面 UI 的工程基线与选型边界","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/javaone/","section":"Categories","summary":"","title":"JavaOne","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/javaone-2026/","section":"Tags","summary":"","title":"Javaone-2026","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/jcmd/","section":"Tags","summary":"","title":"Jcmd","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/jdk/","section":"Tags","summary":"","title":"Jdk","type":"tags"},{"content":" JDK 26 如何改善 G1 吞吐：写屏障同步与默认收集器路线 # 摘要 # 自 JDK 9 起，多数 server 配置在未显式指定收集器时会默认选用 Garbage-First（G1），以便在典型负载下优先控制 GC pause，而非单纯最大化 throughput。代价落在应用线程：每次堆上引用字段写入都会经过较重的 write barrier，并与后台 optimizer threads 共享 card table，历史上需要细粒度同步。JEP 522 已在 JDK 26 交付——引入第二张 card table、atomically swaps 两表，并借助 JEP 312 的 thread-local handshakes 在切换时对齐可见性，从而在 不新增用户必选开关 的前提下削减同步开销。更长周期的 JEP 523 则提案把「未指定收集器时一律 G1」推广到单 CPU、小内存等环境；截至 2026-05 其状态为 Proposed to Target（Release 27），尚未 Delivered。本文按机制链组织，数字与开关以 JEP / Oracle 文档为准；口述中未在 JEP 逐字出现的细节标注为 演讲者观点。\n演播室广角：左侧大屏显示 JAVAONE'26，右侧层架可见红色 ORACLE 标识与 Duke 摆件（画面无 GC 机制幻灯片）。\n默认路径为何先选延迟，再谈吞吐 # 为什么 # 在线服务与交互式负载里，秒级停顿往往直接体现在尾延迟上。JEP 248 的动机写明：Limiting GC pause times is, in general, more important than maximizing throughput。因此 HotSpot 在 32/64 位 server 配置 把默认从 Parallel GC 换成 G1，用并发标记、按 region 增量回收等手段避免 Parallel 式「整堆 stop-the-world」最坏情况。副作用是：大量「零调参」进程长期运行在 G1 上，而 G1 的 mutator 路径比 Parallel/Serial 更重——这是 JDK 26 之前吞吐常被认为弱于 Parallel 的工程背景之一。\n机制与约束 # 吞吐（throughput）：Oracle GC 实现与性能考量 定义为 percentage of total time not spent in garbage collection。 延迟（latency）：同一文档强调 pause 对响应性的影响。 G1 的暂停目标是 软目标：java 手册 对 -XX:MaxGCPauseMillis= 写明 JVM 会 make its best effort；Oracle G1 调优指南 选项表列出默认 200 ms 量级（以该表为准，非播客口述）。 JEP 522 Non-Goals：不以追上 Parallel 的吞吐为交付目标。 Parallel / Serial 仍保留：JEP 523 Non-Goals 明确 Deprecate or remove any existing collector 不是目标。 怎么做 # java -version java -Xlog:gc=info -version # 启动时打印实际选用的 GC jcmd \u0026lt;pid\u0026gt; VM.flags | grep -E \u0026#39;UseG1GC|UseSerialGC|UseParallelGC\u0026#39; 吞吐敏感批处理可显式对比：\njava -XX:+UseParallelGC -Xlog:gc*:file=parallel.log -jar app.jar java -XX:+UseG1GC -Xlog:gc*:file=g1.log -jar app.jar 常见误区 # 把「JDK 9 起默认 G1」理解成 所有 环境（含嵌入式、单核容器）——JEP 523 Motivation 仍记载：single CPU 或物理内存 \u0026lt; 1792 MB 时默认 Serial。 在未做 A/B 压测的情况下，断定「G1 一定比 Parallel 慢」或「一定更快」——应分 workload 与 JDK 小版本测量。 把 MaxGCPauseMillis=200 当成硬 SLA——手册与调优指南均强调这是 goal，GC 仍可能在极端分配尖峰下短暂超标；容量规划应结合 P99 pause 与分配速率，而非单点默认值。 约 2:11 处双人访谈中景：背景屏 JAVAONE'26，右侧层架 ORACLE 标识（无 card table 示意图）。\n写屏障、card table 与 region：吞吐税落在哪 # 为什么 # G1 将堆切成多个 region，回收时复制存活对象并维护跨区引用。若每次 pause 扫描整堆，停顿目标无法保证。于是 HotSpot 为 G1 注入 write barrier：every time an object reference is stored in a field 更新 card table，使 pause 只需处理 脏卡（JEP 522 Background）。Oracle G1 — Remembered Set 写明默认 512 B 堆块对应 1 B 卡表项。口述常举「老年代字段指向年轻代」；官方行文以 cross-region 为主，与 region 模型一致，不必把口语例子当成唯一规范表述。\n相对 Parallel/Serial，G1 还需与 GC 线程 coordinate，同步既 lowers throughput and increases latency（JEP 522 Motivation）。x64 上屏障曾约 50 条指令，优化后约 12（JEP 522 Performance——OpenJDK 官方 benchmark 结论，非本机承诺）。\n机制与约束 # Post-write barrier：字段 store 引用后的屏障主体；Parallel/Serial 亦有 card marking 类逻辑。 Pre-write barrier（用于 concurrent marking、Parallel/Serial 无）：演讲者观点——本次核对的 JEP 522 / Oracle 节选 未逐字写分工。 同 region 内写入可省略部分 remembered-set 工作：演讲者观点——公开调优文档 未给出该分支的逐句算法；若写内部架构说明，应对照 HotSpot 源码并标注版本。 漏标脏卡 可能导致收集集漏扫引用——属 GC 正确性风险；口述称可能 进程崩溃，JEP 522 未写 crash，宜标 演讲者观点。 怎么做 # java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xlog:gc*,safepoint -jar app.jar java -Xlog:gc+ergo=info,gc+region=debug -jar app.jar jcmd \u0026lt;pid\u0026gt; GC.heap_info 常见误区 # 将 G1 变慢笼统归因于「并发 GC 线程太多」——每写一次引用 的屏障税无法靠调线程数消除。 把 MaxGCPauseMillis 压得过低，指望吞吐不变——更激进的回收策略通常会牺牲 mutator 时间占比。 约 3:32 处：左侧屏 JAVAONE'26，主持方阐述写屏障与 card table（画面无源码级屏障反汇编）。\n约 6:00 处单人近景：发言者着 30 YEARS Java 纪念 T 恤，背景左侧为 Java 杯形标识、右侧绿植墙（无 region 示意图）。\n后台优化与 mutator 争用：为停顿做的工程如何反噬吞吐 # 为什么 # 分配与引用写入极快时，脏卡会在 pause 前堆积；若 pause 内冷启动扫描整张活动表，可能 exceed its pause-time goal（JEP 522 Background）。G1 因此用 optimizer threads optimizes the card table in the background，把部分工作前移到并发阶段，并配合 remembered set 让 pause 增量扫描。代价是：optimizer 与 mutator 必须 synchronize 以避免对同一张表的冲突更新——屏障因同步而 complex and therefore slow（同 JEP Background）。JEP 正文多用 optimizer threads；VM 旗标则含 G1ConcRefinement（见 Risks）——口语「concurrent refinement」宜与正式术语对照，避免混为同一官方名称。\n机制与约束 # -XX:-G1UseConcRefinement：完全关闭优化线程（JEP 522 Risks）。 -XX:G1ConcRefinementThreads=\u0026lt;n\u0026gt;：限制线程数。 关闭 refinement 不是 JDK 26 的「提速秘籍」——通常会让脏卡堆积、pause 变长，与 JEP 动机相反。 怎么做 # java -Xlog:gc+refine=debug,gc+task=debug -jar app.jar jcmd \u0026lt;pid\u0026gt; VM.flags | grep G1ConcRefinement 常见误区 # 在生产环境长期 -XX:-G1UseConcRefinement「减少后台线程」——可能以吞吐假象换取更长停顿。 仅看 GC 线程 CPU，而忽略 mutator 在屏障上的 icache / 指令数 成本。 约 8:00 处双人广角：JAVAONE'26 背屏，右侧 ORACLE 与 Duke 陈设（讨论 concurrent refinement 时段，无幻灯片公式）。\nJDK 26：双 card table、表交换与 thread-local handshake # 为什么 # JEP 522 目标第一条即 Reduce synchronization overhead。核心思路：introduce a second card table；mutator without any synchronization 更新活动表；optimizer 在另一张表上工作；当 pause 前扫描活动表可能超时时，atomically swaps 两表，mutator 转到空表继续打点，optimizer 消化已满表（JEP 522 Proposal）。交换瞬间需让所有 Java 线程看到新活动表指针；方案在 Alternatives 写明采用 generic thread-local handshakes（JEP 312）——without performing a global VM safepoint。口述「不必为此 Full GC pause」与 JEP 312 方向一致；JEP 522 Proposal 未逐步描述每次 swap 的 handshake 协议，不宜写成与 JEP 正文同级的实现细节。\n机制与约束 # 项 来源 无新增用户必选开关 JEP 522 Goals 每张表约占堆 0.2% native（约 2 MiB / 1 GiB） JEP 522 Native memory 第二张表 replaces auxiliary data structure；总 native 可能更低 同节 重度写引用场景吞吐 5–15%；屏障 50→12 指令（x64） JEP 522 Performance（官方 benchmark） 侵入性改动带来正确性/性能回退风险 JEP 522 Risks -XX:ThreadLocalHandshakes 默认 true JEP 312 怎么做 # java -version # 确认 26+ java -Xlog:gc+refine*=debug,gc+start=info -jar app.jar java -XX:StartFlightRecording=duration=120s,filename=jdk26-g1.jfr -jar app.jar 升级对照：在 相同负载、相同堆与相同 -Xmx 下对比 JDK 25 与 26 的应用吞吐（JMH / 业务压测）、gc+refine 日志与 JFR 中 jdk.GCPhasePause 分布。若业务指标几乎不变，先核对是否属于 写引用稀疏 或 已绑定 Parallel 的进程；JEP 522 Performance 的收益区间与这两类场景天然错位。\n从实现视角看，双表把「mutator 写」与「optimizer 读/整理」在时间上拆到不同物理表，日常路径去掉对同一张表的锁竞争；交换则是把「pause 前必须扫完的活动表」与「仍在增长的脏卡流」切开。JEP 522 Alternatives 还提到曾考虑 OS 级原子交换等方案，最终选用 thread-local handshake 以降低平台差异——具体 handshake 回调在 Proposal 未展开，生产排障时仍以统一日志与 JFR 为主，不宜在事故报告中臆造逐步协议。\n常见误区 # 把 JEP 522 的 5–15% 当作「所有 Java 应用 guaranteed 提升」——官方表述限定在 heavily modify reference fields 类 benchmark。 为排障关闭 ThreadLocalHandshakes（除非明确理解 JEP 312 依赖）——可能影响交换可见性路径（需结合版本 release note）。 假设 JDK 26 已等于 JEP 523 的「全环境默认 G1」——后者 尚未 Delivered。 约 9:37 处单人近景：30 YEARS T 恤与 Java 杯形背景（口述细粒度同步与双表方案时段）。\n约 10:00 处：彩色 Java 霓虹标识与绿植墙背景（邻近 JEP 522 讨论；不保证 屏上可见 JEP 编号）。\n画面 OCR 连续片段 JAVAONE'26_（品牌字；不展示 card table 结构）。\n原生内存、小配置默认与收集器存续 # 为什么 # 双表带来额外 footprint，但 JEP 同时 取代 旧辅助队列/结构，部分部署总 native 未必上升。更长周期上，JEP 523 认为 G1 经多年瘦身（口述提及 marking bitmap、压缩 remembered set 等），吞吐成为相对 Serial 的最后主要差距；JEP 522 视为补齐后，可把 always select G1, regardless of the number of processors and the available physical memory 作为默认策略。口述「marking bitmap 约 2.5%」——演讲者观点，JEP 522 未出现该数字；容量规划应以 NMT / 实测 为准。\n机制与约束 # JEP 523：Release 27，2026-05 抓取为 Proposed to Target。 Non-Goals：不废弃 Parallel / Serial；与 CMS 先例（JEP 291 废弃、JEP 363 JDK 14 移除）不能推导 Parallel「即将移除」——演讲者观点：维护多收集器有成本，未来是否下线 无定论。 怎么做 # java -XX:NativeMemoryTracking=summary -jar app.jar jcmd \u0026lt;pid\u0026gt; VM.native_memory summary scale=MB # 单核 / 小内存容器核对默认收集器 java -Xlog:gc=info -version 2\u0026gt;\u0026amp;1 | grep -i Using 常见误区 # 只看 Java heap 占用，忽略 card table、marking 结构等 native 段。 极小堆 + 严格 cgroup 上限时忽视 +0.2% 表开销——JEP 认为通常不显著，但应实测。 JEP 523 落地后仍假设「嵌入式默认 Serial」而不更新监控与 runbook。 约 12:00 处双人访谈：JAVAONE'26 背屏（讨论 native 与第二张表时段）。\n约 14:00 处：发言者 30 YEARS T 恤，背景 STREAM / JavaOne 标识（JEP 523 与小配置默认话题时段）。\n约 16:00 处收尾广角：JAVAONE'26 与 ORACLE 陈设（Parallel/Serial 存续讨论）。\n画面 OCR 含连续片段 ORACLE（上下文 ORACLE / Lu S S =a 含识别噪声）。\n诊断路径：从「用的谁」到「换表是否生效」 # 以下路径面向 已运行 JDK 26+ 的生产或预发实例，按成本从低到高排列。\n步骤 1 — 确认收集器与版本\njava -version java -Xlog:gc=info -version jcmd \u0026lt;pid\u0026gt; VM.flags | grep -E \u0026#39;UseG1GC|UseSerialGC|UseParallelGC|MaxGCPauseMillis\u0026#39; 如何读输出：-version 首行应含 26（或更高）；-Xlog:gc=info 在启动阶段通常打印 [gc] Using G1 或 Using Serial 等。若未显式 -XX:+UseG1GC 却看到 Serial，优先对照 JEP 523 所述 \u0026lt; 1792 MB / 单 CPU 启发式，而非假定 JEP 522 未生效。\n步骤 2 — 观察 refinement 与 pause 是否「脏卡积压」\njava -Xlog:gc+refine=debug,gc+ergo=info,gc+pause=info -jar app.jar # 或对已运行进程（需启动时已开统一日志或动态附加，视部署而定） 如何读输出：gc+refine 中若长期出现 refinement 跟不上、或 pause 日志显示 card 扫描占比异常升高，应怀疑 关闭 refinement 或 分配+写屏障产脏速度 超过 optimizer 能力——这与 JEP 522 要解决的「pause 前表过大」属于同一问题族。JDK 26 后若吞吐仍差，先排除 非 G1、再排除 负载本身写引用极稀疏（此类 workload 可能看不到 JEP 522 宣称的 5–15% 区间）。\n步骤 3 — JFR 对比升级前后\njava -XX:StartFlightRecording=duration=300s,filename=before.jfr -jar app.jar # 升级 JDK 26 后同负载 java -XX:StartFlightRecording=duration=300s,filename=after.jfr -jar app.jar 如何读输出：在 JDK Mission Control 或 jfr print 中对比 jdk.GCPhasePause 分布、应用线程 CPU 与 GC 子系统 CPU；mutator 吞吐提升往往体现在 GC 时间占比下降 或 同等 GC 开销下更高业务 QPS，而非单一 pause 数字。\n步骤 4 — Native footprint（可选）\njava -XX:NativeMemoryTracking=summary -jar app.jar jcmd \u0026lt;pid\u0026gt; VM.native_memory summary scale=MB 如何读输出：关注 GC 或 Internal 段在 JDK 26 前后差异；双表理论增加约 0.2% × 2 的卡表存储，但 auxiliary 结构退役可能抵消——以 diff 为准。\n步骤 5 — 容器与小内存启发式（JEP 523 预演）\n在 1 vCPU、≤1.5 GiB 的 Pod 中分别启动：\njava -Xlog:gc=info -version java -XX:+UseG1GC -Xlog:gc=info -version 对比两次 Using 行。JEP 523 落地前，第一次可能仍为 Serial；若业务已验证 G1 可接受，应在镜像或 JAVA_TOOL_OPTIONS 中 显式 -XX:+UseG1GC，避免误以为「升级 JDK 26 即自动全环境 G1」。JEP 523 落地后，runbook 应改为「默认 G1，极小 footprint 可显式 Serial 回退」。\n生产部署注意点 # 维度 建议 版本 JEP 522 仅 JDK 26+ G1 默认路径生效；JDK 25 及以下无此双表实现。 开关 无需为新行为添加必选参数；勿在生产长期 -XX:-G1UseConcRefinement 除非有明确排障记录。 期望管理 5–15% 来自官方 benchmark 的 heavily modify reference fields；CPU 绑定、少写引用的服务可能接近 0% 体感。 内存 极小堆、cgroup 硬顶环境请用 NMT 实测；JEP 认为额外 footprint 通常可接受，非合同保证。 默认收集器 JEP 523 未交付前，小配置仍可能 Serial；升级监控与启动脚本中的 Using 检测。 回退 吞吐仍不足可试 -XX:+UseParallelGC（批处理）；极小嵌入式可 -XX:+UseSerialGC——与 JEP 523 Non-Goals 一致。 风险 JEP 522 Risks 承认侵入性改动可能引入正确性或性能回退；灰度 JDK 26 时保留收集器切换与堆 dump 预案。 观测成本 -Xlog:gc+refine=debug 与 JFR profile 设置会抬高开销，仅用于短期诊断窗口，不宜常开。 文档版本 本文 Oracle 链接以 Java 21 文档域为例（与核验时一致）；JDK 26 行为以 JEP 522 与对应发行版 release note 为准，手册印刷值可能随 GA 微调。 灰度时建议固定三板斧：(1) 启动日志确认收集器；(2) 同负载 JDK 25/26 或「升级前后一周」JFR 抽样；(3) 对写引用密集服务单独看 mutator CPU 与 gc+refine，避免仅凭全局 P99 pause 否定吞吐收益。\n参考与延伸阅读 # JEP 522：G1 以减少同步提升吞吐（JDK 26，Delivered） JEP 523：未指定收集器时一律默认 G1（Proposed to Target，Release 27） JEP 248：JDK 9 起 server 配置默认 G1 JEP 312：Thread-Local Handshakes（无全局 VM safepoint 的按线程回调） Oracle G1 调优指南：region、Remembered Set、512B card Oracle：收集器实现与吞吐/延迟定义 Oracle：可用收集器选型总览 java 命令手册：-Xlog、收集器开关、MaxGCPauseMillis、NMT、JFR JEP 291：JDK 9 废弃 CMS JEP 363：JDK 14 移除 CMS OpenJDK HotSpot G1 源码树（src/hotspot/share/gc/g1） Unified JVM Logging 标签说明（-Xlog:help） Native Memory Tracking 使用说明 OpenJDK JDK 26 Release Notes（收集器变更条目） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-how-jdk-26-improves-g1-s-throughput-inside-java-podcast-54/","section":"文章","summary":"JDK 26 如何改善 G1 吞吐：写屏障同步与默认收集器路线","title":"JDK 26 如何改善 G1 吞吐：写屏障同步与默认收集器路线","type":"posts"},{"content":" JDK 桌面客户端在 2026：三十年栈上的维护、现代化与交付转型 # 1996 年 JDK 1.0 随 java.awt 与 java.applet 一同登场；三十年后，浏览器里的 Applet 已成历史，Swing 与 AWT 仍在企业工具、IDE 插件、工业控制与内部系统中承担 UI。OpenJDK Client Libraries Group 在 2026 年的工作重心不是「再发明一套 UI 框架」，而是在 契约兼容、平台变迁（Wayland、Metal/Vulkan/D3D 图形栈） 与 六个月固定发布节奏 下，让既有应用继续可构建、可部署、可测试。\n若你维护的是十年前写的 Swing 面板，真正要盯的是三类变更：符号被删（Applet）、打包与 native 策略（jlink/jpackage、JEP 472）、OS 显示栈（Wakefield）。下文按工程决策拆解；未在 JEP/CSR 中出现的路线图细节会标明为演讲者观点。\n图：2026 and still Swinging — The JDK desktop client libraries (UI not AI)\njava.desktop：一个模块里的分层与「维护优先」 # 为什么仍要理解模块边界 # JDK 9 模块化之后，AWT、Java 2D、Swing、javax.sound、Image I/O 等被收进 java.desktop。官方模块说明将其描述为「定义 AWT 与 Swing 的用户界面工具包，以及图像、打印、声音与媒体 API」。对运维与打包而言，这意味着：纯服务端镜像可以在 jlink 时 省略 java.desktop 以缩小体积；对排障而言，Swing 的「轻量组件」仍依赖底层 peer 与 Java 2D 绘制，UI 缺陷往往要同时看 javax.swing 与 java.awt。\n机制与约束 # 典型分层为：java.awt 负责窗口、事件与桌面集成；Java 2D 提供图形、文本、图像与打印（亦用于 headless 服务端渲染）；javax.swing 构建于二者之上。团队对外表述为：API 大体功能完备，新 API 主要用于补缺或应对生态变化；当前投入在 维护与现代化实现，使旧 Swing 应用能在现代桌面上运行，并与 JavaFX 等栈更好共存——演讲者观点，非 JEP 约束。\n图：High level view of the APIs / packages in the java.desktop module — java.applet 已划掉并标注 JEP 504\n图：Desktop in JDK 1.0 (1996) — java.awt 与 java.applet 的起点\n怎么做 # # 仅桌面应用需要的定制运行时 jlink --add-modules java.desktop --output ./jre-with-desktop 常见误区 # 误区：Swing 已死，可以忽略 AWT。\n实际：L\u0026amp;F、拖拽、剪贴板与多显示器仍走 AWT 通道。 误区：删掉 java.desktop 只影响 GUI。\n实际：部分图像/字体/打印 API 也在该模块内，headless 场景需逐项核对依赖。 JEP 504：Applet 退场与桌面交付范式 # 为什么现在彻底移除 # 现代浏览器不再运行 Applet；桌面与移动应用通过 安装包 交付。JEP 504（Release: 26，JDK 26 GA 2026-03-17）删除 java.applet 包（含 Applet、AudioClip 等）、javax.swing.JApplet 及 java.beans.AppletInitializer 等关联引用。动机链上与 JEP 486（JDK 24 永久禁用 Security Manager）相连：Applet 沙箱依赖 SM，SM 禁用后相关安全模型已无意义。\n渐进时间线（JEP 504 与发行说明）：JDK 9 弃用 Applet API；JDK 11 移除 appletviewer；JDK 17 标记 for removal；JDK 26 正式删除。\n机制与约束 # 音频是少数有官方替代的角落：JDK 25 引入 javax.sound.SoundClip（createSoundClip、play、loop 等），用于替代 java.applet.AudioClip。其余能力 无一对一替换 API——需改为独立桌面进程 + 安装包分发。\n图：JEP 504 - Remove the Applet API (JDK 26) — No modern web browser provides support for Applets\n图：JEP 504 时间线 — JDK provides jpackage to build native platform installers\n怎么做 # // 旧：java.applet.AudioClip — 随 Applet API 移除 // 新：javax.sound.SoundClip（JDK 25+） // SoundClip clip = SoundClip.createSoundClip(Path.of(\u0026#34;ding.wav\u0026#34;)); // clip.play(); jpackage --type app-image --name MyApp --input dist --main-jar app.jar 常见误区 # 误区：只要不用 JApplet 就不受影响。\n实际：间接引用 java.applet 类型、Applet 时代权限检查的遗留代码都需扫描。 误区：用 WebView 嵌 Applet 逻辑即可。\n实际：JEP 504 删除的是 类库符号；应规划为本机应用或 Web 前端，而非继续依赖 Applet 生命周期。 jlink + jpackage：自定义运行时与 JavaFX 混合打包 # 为什么成为 Applet 之后的默认路径 # jpackage（JDK 16 起随 JDK 提供）将应用 JAR 与（可选）jlink 生成的 runtime image 打成平台安装包：Windows .msi/.exe、macOS .dmg/.pkg、Linux .deb/.rpm。对 Swing + JavaFX 混合应用，需要把 JavaFX jmods 与 JDK jmods 一并链入，并在 JDK 24+ 处理 native access 策略。\n机制与约束 # JEP 472（JDK 24）起，对 JNI/FFM 的 native 访问默认警告，需显式启用。官方写法是运行时 java --enable-native-access=...，或在 jlink --add-options=... 中嵌入该参数——不是 jlink --enable-native-access=... 作为独立 jlink 开关（要点草案曾混用，以 JEP 472 为准）。\n图：Deploy desktop apps using native installers — jlink 合并 jmods 后 jpackage 打安装包\n图：Terminal — sh run_jpackage.sh 与 Invalid Option 排错（演示现场）\n怎么做 # export JDK_MODS=\u0026#34;$JAVA_HOME/jmods\u0026#34; export JFX_MODS=\u0026#34;/path/to/javafx-jmods\u0026#34; jlink --output ./jdk_jfx_runtime \\ --module-path \u0026#34;$JDK_MODS:$JFX_MODS\u0026#34; \\ --add-modules java.desktop,javafx.controls \\ --add-options=--enable-native-access=javafx.graphics,javafx.media,javafx.web jpackage --runtime-image ./jdk_jfx_runtime \\ --input ./dist --main-jar app.jar --name MyDesktopApp 演示中 run_jpackage.sh 曾因 Invalid Option: [ 失败，现场用 vi !$ 修正脚本后重跑——说明安装包流水线对 引号、方括号与 shell 转义 敏感，宜把 jlink/jpackage 参数写进可版本化的构建脚本（Maven/Gradle 插件或 Makefile），而非一次性手工命令。\n常见误区 # 误区：jpackage 会自动拉全 JDK。\n实际：应通过 jlink 控制模块集，否则安装包体积膨胀。 误区：native 警告可忽略。\n实际：JDK 24+ 可能限制未声明的 JNI；JavaFX 图形/媒体模块通常需列入 --enable-native-access。 误区：在 macOS 打好包即可直接用于 Linux CI。\n实际：安装包格式与运行时镜像平台相关；跨平台需在对应 OS 上分别执行 jpackage（或仅分发不含安装器的 app-image 再自行封装）。 Project Wakefield：Wayland、X11 兼容与 Robot # 为什么 Linux 桌面是特殊战场 # OpenJDK Project Wakefield 的目标是在 JDK 中支持 Wayland。当前 Linux 上 JDK 仍是 X11 客户端，在默认 Wayland 的发行版上依赖合成器提供的 X11 兼容模式（合成器内嵌 X server，应用仍发起 X 连接）。幻灯片原文：not completely seamless — not everything works and not everything in the same way。\njava.awt.Robot（截图、合成输入）在纯 XWayland 路径上曾出现黑屏等问题（JDK-8246305）。后续修复包括 Wayland 下基于 dbus ScreenCast（org.freedesktop.portal.ScreenCast）的截图路径（JDK-8335485，Fix Version 含 8/11/17 updates 池），以及 JDK 21 对 Robot 规范的放宽（JDK-8308012）。不宜简单概括为「仅 JDK 21 GA 一次性交付全部能力」——需按你使用的 具体更新版本 查发行说明。\n图：OpenJDK Project Wakefield — X11 compatibility mode — Xserver integrated into the Wayland compositor\n图：Wakefield — java.awt.Robot did not work at all — 需非 X11 实现与规范变更\n原生 Wayland 工具包（路线图） # 下一阶段为 Wayland 原生 AWT toolkit：选型 libwayland 而非 GTK（演讲称 GTK 约束过多）；渲染可走软件管线、OpenGL ES 或 Vulkan（JetBrains 参与 Vulkan 加速 Java 2D，演讲者观点）。Wayland 不报告窗口绝对坐标，影响「相对其他窗口布局」的测试与多窗口逻辑——幻灯片称 no solution yet。\n图：Wayland native toolkit — libwayland、Vulkan pipeline、JetBrains 主导移植\n怎么做 # Robot robot = new Robot(); BufferedImage shot = robot.createScreenCapture( new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); # 排障时确认会话类型与属性（属性名见 JDK-8335485） echo \u0026#34;$XDG_SESSION_TYPE\u0026#34; java -Dawt.robot.screenshotMethod=dbusScreencast -jar your-ui-tests.jar 常见误区 # 误区：装了 Wayland 就等于 JDK 原生 Wayland。\n实际：生产路径仍以 XWayland 为主；原生分支在独立仓库 pure Wayland 试验（演讲者观点，具体分支名以 Wakefield 项目页 为准）。 误区：Robot 行为与十年前规范完全一致。\n实际：CSR 已放宽以反映权限、合成器与门户 API 限制；CI 需缓存屏幕录制权限，否则自动化会被弹窗打断（演讲者观点）。 Swing JDatePicker：补缺型 API 与试用边界 # 为什么现在提案日期控件 # 多年未增核心 Swing 控件后，官方提案 JDatePicker（显示所选日期 + 打开日历）与 JCalendarPanel（月视图），返回 java.time.LocalDate，并遵循 L\u0026amp;F、MVC、I18N、A11Y。目标还包括日期范围、可选范围约束、周数显示等。截至 2026 年 5 月，公开 JEP 列表中尚无对应 JEP 编号——属 未验证 GA 时间表 的预览 API。\n图：Swing JDatePicker component — It will return a java.time.LocalDate\n怎么做（sandbox 试用） # 演讲者观点：在 JDK sandbox 的 datepicker-preview 分支构建运行；包名与 API 在 GA 前可能变动。\n// GA 前示例 — 包名以 sandbox 为准 // JDatePicker picker = new JDatePicker(); // panel.add(picker); // picker.addPropertyChangeListener(\u0026#34;value\u0026#34;, e -\u0026gt; // label.setText(String.valueOf(picker.getValue()))); 常见误区 # 误区：可立即替换生产环境中的 SwingX 日期控件。\n实际：预览分支 API 不稳定；迁移应等 JEP/CSR 冻结。 误区：仍应使用 java.util.Date。\n实际：新控件以 LocalDate 为值类型，与 java.time 生态一致。 图：Custom UI example — year view（复杂日历仍可用 Java 2D / Swing 自绘）\nJava 2D 管线：Metal、Vulkan 与跨栈复用 # 为什么图形栈牵动整个桌面 # 操作系统厂商正在弃用旧图形 API：macOS 上 OpenGL 已不推荐，JDK 侧已切到 Metal；Linux 上 OpenGL 同样式微，Wakefield 路线把 Vulkan 作为硬件加速选项（可能先于完整 Wayland 工具包合入主线，演讲者观点）；Windows 上 JavaFX 正从 D3D9 迁向 D3D12，演讲称希望将成果复用到 JDK Java 2D，但 JDK 侧 D3D12 尚未开始（演讲者观点，无对应公开 JEP 在本次核实中）。Linux 生产路径仍以 X11 + XRender 为主，与 Wayland 窗口集成可并行推进。\n对应用开发者，这意味着性能与像素级一致性更多取决于 JRE 更新 与 GPU 驱动，而非改几行 Swing 业务代码。升级 JDK 大版本时，应把 字体渲染、半透明、大画布刷新 列入回归清单。\n常见误区 # 误区：换管线对 Java 代码透明且无回归。\n实际：应在目标 OS/GPU 上做 Java2D 微基准与视觉回归；驱动差异仍会导致缺陷。 误区：Vulkan 合入等于 Wayland 完成。\n实际：二者可解耦推进——管线现代化 ≠ 窗口系统集成完成。 Panama FFM 与 ScopedValue：JNI 之外的试点 # 为什么桌面模块谨慎拥抱 FFM # java.desktop 含大量 JNI（演讲称 1500+ 方法，未在公开文档中逐条核实计数）。全量重写不现实；试点用 JEP 454 Foreign Function \u0026amp; Memory API 读取 OpenType 字表。经验结论（演讲者观点，基于内部原型）：upcall 绑定 Java 对象句柄开销极大；用 JEP 446 ScopedValue 在 downcall/upcall 间传递上下文，并部分替代 sun.misc.Unsafe；jextract 因需回调 Java IO 读字表而犹豫采用。\n// 概念示意 — 非 OpenJDK 源码摘录 private static final ScopedValue\u0026lt;ByteBuffer\u0026gt; FONT_SLICE = ScopedValue.newInstance(); ScopedValue.where(FONT_SLICE, buffer).run(() -\u0026gt; OpenTypeLib.readTablesNative()); 常见误区 # 误区：FFM 可零成本替换所有 JNI。\n实际：热路径 upcall + 对象绑定需专门设计，减少跨边界次数。 误区：ScopedValue 等同 ThreadLocal。\n实际：语义面向结构化并发与作用域生命周期；误用会导致可见性/生命周期 bug。 技术债：Security Manager、AppContext 与去 finalization # JEP 486 之后，桌面代码中大量 Applet 沙箱与权限检查成为死代码并已删除。AppContext 仍计划移除但进度更慢——演讲称需先彻底理解其语义，影响线程分组与旧库假设。去 finalization 为全 JDK 项目：桌面在 macOS CF retained、Java Sound 等仍有残留（演讲者观点）。新代码应使用 try-with-resources / Cleaner，勿再依赖 Object.finalize()。\n桌面 UI 测试与六个月发布节奏 # 桌面 API 拥有 成千上万 自动化回归测试（幻灯片 OCR 原文：JDK has many thousands of functional / regression tests for the desktop APIs）。历史上 Known Failure List 与 SQE 全量滞后，导致回归可能 6–12 个月 才暴露——对「硬截止日期、每六个月一发」的 JDK 节奏 Not so good（幻灯片原话）。新策略是减少「与本次变更无关」的红灯：不稳定测试、陈旧断言、未维护用例，以及因桌面环境（弹窗、小组件、分辨率）导致的偶发失败。\n六个月固定发布则要求 Mach5 级 CI（Oracle 内部系统名，演讲者观点）近乎全绿，并倾向 物理机 以避免 VM 与厂商桌面噪声。开发者本地应只跑 相关子集（jtreg/make 目标依 JDK 源码树而定）；自备流水线时常用 xvfb 或无头配置隔离显示，但无法完全替代真机上的焦点与门户权限行为。\n图：UI Testing Challenges — Many tests failed for reasons unrelated to their change\n常见误区 # 误区：本地全红可归咎于本次 diff。\n实际：不稳定、过时或未维护的测试长期存在；应先隔离环境再判因。 误区：headless VM 与真机桌面等价。\n实际：焦点、合成器与权限对话框在 CI 设计中是一等公民。 优先级与未来管线（边界分明） # 仍在推进的事项包括：契约兼容、关键缺陷、新平台适配、第三方库升级；管线含 OpenType font variations、Wayland 原生、更多 FFM、JDK D3D12（演讲者观点：尚未开始）。Vector API 因 java.desktop 不能依赖 incubator 模块 而暂无法用于桌面实现（演讲关联 Valhalla，无公开 JEP 证实具体合入时间）。Q\u0026amp;A 中提到未来 Leyden/AOT 产物或可由 jpackage 打包，但宜在 目标平台 生成/profile（演讲者观点）。\n图：Summary — Actively maintaining all of the client desktop libraries — Participate at client-libs-dev@openjdk.org\n参考与延伸阅读 # JEP 504：移除 Applet API JDK 26 项目与 GA 日期 JEP 486：永久禁用 Security Manager javax.sound.SoundClip API（Java SE 25） jpackage 命令参考（Java SE 24） jlink 命令参考 JEP 472：限制 JNI 使用（准备阶段） java.desktop 模块概要 OpenJDK Project Wakefield JDK-8335485：Wayland 下 Robot 截图 JDK-8308012：放宽 java.awt.Robot 规范 JEP 454：Foreign Function \u0026amp; Memory API JEP 446：Scoped Values OpenJDK client-libs-dev 邮件列表 Java SE 26 发行说明（随 GA 更新） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-the-jdk-client-desktop-2026-and-still-swinging/","section":"文章","summary":"JDK 桌面客户端在 2026：三十年栈上的维护、现代化与交付转型","title":"JDK 桌面客户端在 2026：三十年栈上的维护、现代化与交付转型","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/json/","section":"Tags","summary":"","title":"Json","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/judge-time/","section":"Tags","summary":"","title":"Judge-Time","type":"tags"},{"content":" Judge-Time Compute：当 LLM 评测从「单次打分」变成可组合管线 # 生产里的 RAG、Agent 与护栏，最终都要回答同一类问题：输出是否可信、可审计、可迭代。常见做法是跑静态 benchmark、挂一个 LLM-as-a-judge，或把 DSPy 的 metric 接到优化环里。Haize Labs 联合创始人 Leonard Tang 在公开访谈与开源项目 Verdict 中提出另一条轴：judge-time compute——在评判阶段堆叠结构化、可组合的弱模型调用，而不是默认「最贵模型评一次就够」。\n这条轴与「把 eval 当成发布前 checklist」不同：它把 judge 本身当作可工程化的子系统——有 schema、有执行树、有一致性指标，并能嵌进 RL、red-team 或 guardrail 蒸馏链路。本文把可核对文档与仍属演讲者观点或未公开细节分开写；不同 benchmark 上的结论并不收敛为单一配方，读者应带着自己的 SLO（延迟、美元/千次评判、可解释性）做条件判断。\n幻灯片标题：Verdict: A Library for Scaling Judge-Time Compute；署名 HAIZE LABS / Nimit Kalra / Leonard Tang。\nHaize Labs 落地页文案：Get Safe. Get Reliable. Get Haized. / Haize Labs brings your AI application out of POCs and into production.\n问题空间：评测、偏好与「Eval 公司」在争什么 # 为什么静态榜单填不满企业落地 # 行业习惯把「做 eval」等同于再做一个 SQuAD 式数据集或 LMSYS Chatbot Arena 式 Elo 榜。Leonard Tang 的立场（演讲者观点）是：多数企业没有接近 SQuAD 的标准答案集；真实需求是在极少偏好信号下，训练客户自有 judge，并尽量少问人。这与「for loop 跑分 → 表格」的 UX 并不矛盾——他认为表格类展示已相对成熟；缺口在领域专家如何标注当前 judge、以及如何把标注致密化成自动化 reward。\n从工程分工看，静态集适合回归「模型版本是否退化」；偏好集适合对齐「哪一个回复更像我们品牌/合规」；fuzz 适合在尚无生产 trace 时模拟恶意或边界用户。三者预算不同：静态集一次性标注贵但运行便宜；偏好标注便宜单次但难规模化；fuzz 算力开销大却能发现单点 judge 漏掉的系统性漏洞。访谈并未声称替代 MLOps 可观测性，而是强调 eval 与 red-team 应共享同一套 judge 语义，避免线上指标与离线评委各说各话。\n范式 常见做法 访谈中的张力 静态评测 有 GT 的数据集 + accuracy 企业数据形态往往只有输入、无标准输出 偏好评测 Pairwise / Arena Elo 需要定制 judge + 主动选样标注 对抗测试 Red-team、jailbreak 搜索 Haize 强调 fuzz 用户交互，而非只做 leaderboard 机制与约束 # Haize 自定位为定制 reward model（评委）与用户交互模拟（fuzz testing）（演讲者观点），DNA 来自对抗鲁棒性而非可观测性仪表盘。公开仓库侧：get-haized README 写明含 red-teaming, fuzzing, and optimization algorithms；访谈中提到的 VAE、reverse LM 等未在已查 README 中出现（证据边界：访谈列举 ⊃ 公开文档）。\n怎么做（最小心智模型） # 先定三类对象，再选工具，而不是先选「eval 平台」：\n被测系统（RAG / agent / 生成器） Judge 管线（单次 vs 多路 + 聚合） 人类信号入口（全量打分 vs 对比式 A/B + 质性理由） 常见误区 # 把「评测公司」默认成 benchmark 厂商——访谈明确区分（演讲者观点）。 在几乎没有偏好数据时仍照搬学术静态集——与落地数据形态可能脱节（演讲者观点）。 只买「eval 平台」却不建设领域专家工作流——表格再漂亮，judge 仍可能与业务标准错位。 证据边界：客户名单（OpenAI、Anthropic、AI21 等）、融资与定价均为访谈口述；对比 Patronus Lynx/Glider 等行业产品时，应视为外部参照而非 Haize 能力证明。\n约 10 分钟处双主持人分屏：左侧主持佩戴 Weaviate podcast 标识，右侧为嘉宾 Leonard Tang；画面无技术 API 文案。\nJudge-Time Compute 与 Test-Time Compute # 为什么命名值得较真 # Test-time compute 通常指生成侧在推理时加搜索、自洽或验证——例如让模型对同一 prompt 采样多条再选最优，或在代码任务上做 execution-based filtering。访谈中主持人曾口误为 test time；Leonard 坚持 judge-time compute：把额外算力花在评判路径上，且评判本身可以是并行探针 + 串行 verify + 聚合，而非单次 forward。开源 Verdict 论文 标题与 README 均使用 judge-time compute，并写明归纳偏置来自 scalable oversight（debate、ensembling 等）——已核实。\n若你的系统已经在生成侧用了 o1 式长推理，再在 judge 侧堆同等规模的推理，成本会近似相乘。judge-time compute 的经济性来自：多次小模型 + 结构化输出 的方差降低，而不是简单把 judge 也换成 reasoning model。是否采用，应看 judge 错误是「随机噪声型」还是「系统性盲点型」——前者适合 ensemble，后者可能需要改 rubric 或引入领域工具（检索、计算器、规则引擎）。\n机制：与 scalable oversight 的对应关系 # Weak-to-strong generalization 与 weak LLMs judging strong LLMs 讨论的是：弱监督者能否约束更强模型。Verdict 把 debate、ensemble、verify 等模式做成可组合原语（DebateUnit、ModelEnsembleJudge、MaxPoolUnit 等，见 仓库）——已核实类名与论文引用；访谈中的「wargaming」无名为 Wargame 的内置 primitive（术语为访谈用语）。\n怎么做 # 区分两条预算线：生成 token 与 评判 token。若 SLO 允许，优先在 judge 侧做 repeat、pairwise 与 pool，再考虑是否上 o1 类推理模型做单次评判——后文 P05 会说明这不总划算。\n常见误区 # 把 DSPy MIPRO 式「复合系统 + 元优化」与 judge-time compute 划等号——Verdict 可作 DSPy metric，但归纳偏置不同（访谈观点 + cookbook 已核实集成）。 假设「验证一定比生成便宜」——RAG 下若 judge 还要判定 retrieval 是否正确，难度可逼近完整 agent 任务（演讲者观点）。 约 9 分半：双方对谈分屏；左侧 Weaviate podcast 角标，右侧嘉宾侧无幻灯片文本。\nVerdict：声明式 Judge 管线 # 为什么需要库而不是每次手写 debate # 为客户重复搭 judge → verify → vote 流水线，被归纳为开源库 Verdict（演讲者观点与 README 动机一致）。核心是 Unit / Layer / Block / Pipeline 组合，以及 CategoricalJudgeUnit、PairwiseJudgeUnit、BestOfKJudgeUnit 等 judge 原语——已核实于 README Quickstart。Layer 的 repeat 语义是复制整段子图（例如 judge+verify 各一份），而不是只对最后一层采样；这与「同一 prompt 调三次 API 取众数」在实现上相关但抽象层级更高，便于在可视化执行树里对齐延迟与成功率。\n访谈还列举多种 debate 变体：并行辩手 + meta judge、round-robin、打乱顺序、pros/cons 双 judge 等。公开代码提供 DebateUnit、ConversationalUnit 等积木，但没有一键「全自动选最优 debate 拓扑」——拓扑选择仍依赖任务与人工实验（演讲者观点 + 源码 已核实有 primitive、未核实自动搜索）。\n机制：Judge → Verify → MaxPool # 演示帧与 README 一致（OCR 中 gpt-40、CategoricaljJudgeUnit 为识别误差）：\n声明式 pipeline：CategoricalJudgeUnit(name='Judge'...) → CategoricalJudgeUnit(name='Verify'...) → repeat=3 → MaxPoolUnit()；via gpt-4o，temperature 0.7 / 0.0。\nMaxPoolUnit 对 categorical 字段取众数；Layer(..., repeat=3) 复制整条子层——已核实。\n怎么做（最小示例） # pipeline = Pipeline() \\ \u0026gt;\u0026gt; Layer( CategoricalJudgeUnit(name=\u0026#39;Judge\u0026#39;, categories=DiscreteScale([\u0026#39;yes\u0026#39;, \u0026#39;no\u0026#39;]), explanation=True) .prompt(JUDGE_PROMPT).via(\u0026#39;gpt-4o\u0026#39;, retries=3, temperature=0.7) \\ \u0026gt;\u0026gt; CategoricalJudgeUnit(name=\u0026#39;Verify\u0026#39;, categories=DiscreteScale([\u0026#39;yes\u0026#39;, \u0026#39;no\u0026#39;])) .prompt(VERIFY_PROMPT).via(\u0026#39;gpt-4o\u0026#39;, retries=3, temperature=0.0) , repeat=3) \\ \u0026gt;\u0026gt; MaxPoolUnit() 「Declarative」在开源侧主要指 schema 校验与 pipeline 拼接（演讲者观点：商业侧 prompt/pipeline 优化器未完全开源）。\n常见误区 # 以为开源包内含与 DSPy MIPRO 同级的自动 prompt 搜索——访谈称优化器为商业「核心 alpha」（演讲者观点）。 忽略 Verify 与 Judge 可用不同 temperature——演示刻意用 0.7 生成解释、0.0 做核验（已核实于 README）。 Pairwise、一致性与执行树 # 为什么单次 categorical judge 不够 # 事实性、groundedness、内容审核等任务常需要 pairwise 比较与结构化输出：模型不仅输出 yes/no，还要给出可审计的 explanation，以便人类修正 judge 的推理链而非从头写 rubric（演讲者观点）。Verdict 提供 PairwiseJudgeUnit + StructuredOutputExtractor，实验面板展示 Agreement (N=10 samples) 及 Cohen κ、Kendall τ、Spearman ρ——已核实于 experiment.py 与 hierarchical notebook。\n演示样本涉及新闻式声明（如运动员退赛、企业销售数据）与文档对照：Streaming 面板展示 judge 如何在「大体一致」与「细节不符」之间折中。这类 UI 的价值在于把失败样本定位到执行树节点（某次 verify 0/10），而不是只给一个总分——对工程迭代比 leaderboard 名次更直接。\nAgreement (N=10 samples)：Acc. 80.00%，Cohen / Kendall / Spearman 均为 0.58；执行树含 Layer(Inner.NONE, Outer.DENSE) 与 10/10 PairwiseJudge via StructuredOutput(3x gpt-4o-mini)。\nStreaming 面板：PairwiseJudge 对 Dustin Johnson / Open Championship 声明做 explanation；部分 verify 步显示 0/10，体现流式调试而非最终精度结论。\n机制：Map + MaxPool 层级 # 执行树标签 Hierarchical_root.block...unit[Map MaxPool]_choice 来自运行时可视化（非独立 DSL 关键字 Hierarchical_root）。MapUnit 并行多路 judge/verify，再 MaxPool 聚合——与 OCR 及源码一致（部分核实：OCR 中 3x gpt-4o-mini 与 notebook 默认 gpt-4o 配置未必相同）。\n指标含义：Acc. 为 (ground_truth == prediction).mean()；κ/τ/ρ 衡量评价者一致性，不是 pass@k 或 MRR（已核实）。\n怎么做 # 在上线前对 judge 管线跑小规模 Agreement 面板：若 κ 长期偏低，先修 prompt/schema，再加模型档位；勿把单次 demo 的 80% Acc. 外推到全量生产。建议同时记录 每节点延迟（执行树中的 0:00:08 类计时）与 verify 通过率，避免只优化 Acc. 导致 verify 步形同虚设。\n对 pairwise 任务，明确「比较对象」是 两回复、回复与文档，还是 回复与检索片段；三者 rubric 不可混用。StructuredOutputExtractor 的价值在于把 explanation 与 choice 拆成可解析字段，便于下游规则（例如 choice=yes 但 explanation 含否定词则触发人工复核）。\n常见误区 # 把 Cohen κ=0.58 直接解释成「业务准确率 58%」——二者量纲不同。 认为 debate / round-robin / 打乱顺序等变体 Verdict 都已「一键开关」——多为可组合原语，需自建 pipeline（演讲者观点 + 源码 已核实有 DebateUnit）。 弱模型堆叠 vs 强单次模型：数字边界 # 为什么「评测必须用最贵模型」值得怀疑 # 访谈称多路 LLM judge 相对单次 judge 与前沿推理模型可有约 10–20% 绝对提升（演讲者观点）。论文 arXiv:2502.18018 报告的是分任务数值，例如幻觉检测情境下 Verdict(GPT-4o) 相对原 SOTA 约 +14.5 pp，换 GPT-4o-mini 仍 +3.05 pp；ExpertQA 上 Verdict(4o) 79.17% vs o1 69.91%（约 +9.28 pp）；XSTest 上 Verdict 96.44% vs o1 96.00%（约 +0.44 pp）——已核实，且不能合并为统一 10–20% 常数。\n读表时建议固定三个问题：（1）指标是 percentage points 还是相对提升？（2）基线 judge 是否与 Verdict 公平（同 prompt 预算、同温度）？（3）任务是否与你生产分布同域——内容审核接近饱和时，0.44 pp 与 14.5 pp 对业务含义完全不同。论文背景还引用 generative verifier / Best-of-N 文献中的「16–40% improvement」语境，那是另一套问题设定，不可直接贴到 Verdict 管线（证据边界）。\nBenchmark Verdict (4o-mini) o1 解读 ExpertQA 67.72% 69.91% mini 堆叠未超过 o1（已核实） ExpertQA 79.17% (4o) 69.91% 4o backbone 的 Verdict 高于 o1 JudgeBench mini 50.00% 75.43% 即使 Verdict(4o) 63.55% 仍低于 o1 成本方面 README/论文定性称「fraction of cost and latency」；访谈提到 prefix / KV cache 跨阶段复用（演讲者观点，skunkworks 未在公开表给出统一货币倍数）。\n机制 # 逻辑是：结构化冗余 + 聚合 用多次弱调用换单次强推理；是否划算取决于任务难度与 judge 方差。论文 Figure 3 的 Pareto 叙述支持「部分任务上更好且更便宜」，非全 benchmark 支配 o1/o3-mini。\n访谈中的工程 skunkworks 包括：跨 Verdict 阶段复用 prefix / KV cache；必要时对 prefix 加 nonce 以换取多样性（演讲者观点）。这在 API 计费模型下可能显著改变「多次 mini 调用」的总成本，但具体节省比例未在公开论文中以统一货币标注（证据边界）。自建部署若掌控权重与缓存，judge-time compute 的经济账可能与纯 API 用户不同。\n怎么做（成本粗算心智模型） # 记 (C) 为单次强 judge 成本，(c) 为单次 mini 调用成本，(k) 为 repeat 次数，(r) 为 verify 是否启用（2 倍链长）。粗略上 Verdict 单层成本 (\\approx k \\cdot r \\cdot c)（再加 pool 的 negligible 开销）。仅当任务误差主要来自随机性而非 rubric 错误时，增大 (k) 才单调有益；否则应优先改 prompt / schema / 工具。\n常见误区 # 泛化「gpt-4o-mini 堆叠 beat o1」——ExpertQA / JudgeBench 上不成立（已核实）。 用营销句 beat reasoning models like o1 代替具体表号——应注明 backbone 与数据集。 约 24 分钟：双人对谈分屏，讨论算力与缓存；画面无幻灯片 API 文本。\n对齐客户 Judge：标注 UX、RL 与 Meta Judge # 为什么「有标注就 SFT」在 judge 上可能被质疑 # Leonard 主张基本不做 judge 上的 SFT（易过拟合、损害隐式推理，演讲者观点），主推 RL + judge-time scaling；奖励来自 meta judge（用 Verdict 等堆叠 judge 评 judge，在少量客户数据上「方向正确」并由 reward 聚合抵消误差，演讲者观点）。公开文档无法核实三层框架 (实现 / 查询 / UI) 与 active labeling 算法细节。\n三层设计在访谈中的表述是：（1）judge 实现与学习算法；（2）查询哪些样本让人标注；（3）UI 如何呈现——且只展示对「当前训练中的 judge」有显著影响的样本（演讲者观点）。这与传统 active learning 的 uncertainty sampling 类似 spirit，但目标函数是「提升 judge 而非提升生成器」。若你团队只有几十条标注，优先把预算花在边界对（模型最不确定的 A/B）上，比均匀撒点更能拉动 κ。\n参数更新方面，访谈提到「超越 LoRA」的激进省参方案，细节未公开——落地时不应假设存在即插即用权重；更现实的起点是用 Verdict 作 离线金标准生成器，再蒸馏到小模型进在线路径。\n机制：对比式信号与三层设计 # 访谈强调 对比式回答（同输入两回复 A/B）收集「为何 A 优于 B」的质性梯度，优于不稳定的人类 1–10 分（演讲者观点）。这与 RLHF/DPO 的 pairwise 范式精神相近，但不能反向证明 Haize 产品实现（证据边界）。\nMeta judge 与 DeepSeek 通用 RM 推理时扩展 方向一致：论文用 SPCT、推理时自适应 principles + critiques、Voting@k / MetaRM@k——已核实标题与方法；访谈的「instance-specific rubric」宜读作概念类比，论文正文用 principles / critiques（非逐字术语）。\nLMUnit（Contextual AI，非 Stanford 主导项目）用自然语言 unit test + 打分模型做细粒度 eval——与「in-context LLM scoring」相近；访谈称「Stanford LM-Unit」易误导（已核实机构归属）。\n怎么做 # 若你要自建对齐环：优先设计 pairwise + 理由 的标注 UI，再决定 RL 还是 DPO；SFT judge 前先用 Verdict 管线测 Agreement 与任务 Acc. 的分离趋势。\n常见误区 # 把 meta judge 当成无误差 oracle——访谈明确需堆叠与聚合降噪（演讲者观点）。 忽略人类迭代的是 judge 的 reasoning 而非从头写 rubric——工作流主张，无公开 benchmark。 RAG、Fuzz 与生产护栏 # 为什么 RAG 打破「验证比生成易」 # 若 judge 只查 answer 是否 grounded in 已给 context，验证往往轻于生成；若还要判断 检索到的 context 是否正确，则等价于重做 retrieval（演讲者观点）。Parametric knowledge conflict（上下文与参数记忆冲突）可用有/无 context 的反事实 diff 定位，但无通用解（演讲者观点）。工程上可把 judge 拆成两级：一级只评 cite 是否支持句子；二级在抽样或高风险请求上才启动 retrieval 审计（额外索引查询、版本号、来源白名单），避免每条用户消息都付 agent 级代价。\n生产案例在访谈中以定性方式出现：轻量 judge 进入 agent 推理环、Constitutional Classifiers 与幻觉检测蒸馏等（演讲者观点，无公开复现包）。这与「评测只在离线跑」不同——guardrail 本质是 judge-time compute 的在线子集，延迟预算更紧，通常必须蒸馏。\nFuzz 与 Agentic Eval # 公开侧：BEAST-implementation 为 beam search 对抗攻击（对应 arXiv:2402.15570），dspy-redteam 多层 attack/refine。Agents for evals 被描述为 judge-time compute 结构更松的形态：多 token、多步；部分任务 judge 需与生产系统同等的 agentic 能力（演讲者观点）。\nFuzz 与静态集互补：前者发现 输入分布尾部（越狱、诱导泄露、畸形 JSON），后者监控 已知业务问答 回归。若只有 fuzz 没有金标 judge 校准，容易陷入「攻击成功率下降但用户体验变差」的假象——仍需人类或高保真 Verdict 管线定义什么是「坏」。\n通用 judge + 单 prompt 覆盖任意垂直场景，访谈认为「不合理且不可能」（演讲者观点）。落地默认路径应是：领域 rubric / unit test（可参考 LMUnit 思路）+ 少量偏好数据 + judge-time compute 降噪，而不是等待一个 70B 通用幻觉检测器解决所有行业。\n护栏：安全 vs 合规 # 云厂商（Azure、Bedrock 等）内置安全过滤对许多客户「够用」（演讲者观点）；企业更常缺 行业 / 品牌 级 guardrail。路径主张：高保真 judge（Verdict + 训练）→ 蒸馏到小模型、压延迟（演讲者观点）。Patronus Lynx/Glider 等仅为行业对照，非 Haize 产品声明。\n常见误区 # 再叠一层通用 safety API 即等于业务合规——访谈认为需求常在定制（演讲者观点）。 把 mechanistic interpretability（SAE 等）当作短期可审计工具——嘉宾认为仍脆弱，可行动性有限（演讲者观点）。 Verdict × DSPy：metric 钩子，不是替代框架 # 为什么两个框架常被打包讨论 # DSPy 被类比为「LLM 版 PyTorch」：用 program + teleprompter 优化 prompt 与模块图。Verdict 则提供 judge 侧的声明式原语 与 scalable oversight 偏置。二者交集在 评估函数：DSPy 文档中的 metric 接收 example、pred、可选 trace，返回标量或布尔信号，驱动 Evaluate 与 MIPRO 等优化器。\nVerdict 可作为 program 的 metric（已核实：dspy.md cookbook、dspy-redteam 中 metric(..., use_verdict=True) → verdict_judge() → ModelEnsembleJudge）。这意味着：你可以在 同一套 DSPy 程序 上，用 Verdict 堆叠 judge 作为「更贵的 metric」，而不必把 debate 逻辑写进每个 signature。\n机制与边界 # 已核实：dspy-redteam 将 ensemble Verdict pipeline 接到 dspy.evaluate.Evaluate。 演讲者观点：Verdict 也可作 guardrail / RL reward，DSPy 只是集成示例之一。 已核实：集成前建议 litellm.cache = None，避免 LiteLLM 与 Instructor 交互问题。 Omar Khattab (okhat) 在 contributors 中有提交（已核实关联账号；访谈「大量 feedback」无法用 commit 数完全印证）。\n怎么做 # 若你已在 DSPy 里用简单 answer_exact_match 或单次 LLM 打分，可渐进替换为 Verdict pipeline：先保持 优化目标不变，只替换 metric 实现，对比 pass rate 与优化迭代次数；再考虑是否把 judge-time compute 的开销纳入 teleprompter 预算。\n常见误区 # 以为接上 Verdict 就等于「DSPy 会自动帮你搜 debate 拓扑」——开源 Verdict 未包含与 MIPRO 同级的 pipeline 搜索器（演讲者观点：商业优化器未全开源）。 在 metric 内开启过多 repeat 导致 MIPRO 每一步评估成本爆炸——应对开发集做子采样或分层评估。 理论缝隙与未来工作（不给出统一答案） # 访谈 closing 提到：DPO / PPO / GRPO / KTO 等为何能用 pairwise preference 拟合 reward，在理论上仍欠完整解释（演讲者观点）。这与工程现实并存——团队已经在用 preference 训练，但 reward model 设计（含 judge-time compute、meta judge、generative RM）仍大量依赖经验与 ablation。\n另一条线索是 superintelligence 定义漂移：图灵测试、销售角色扮演、棋类 vs 日常任务（如 Slack 得体回复）标准不一（演讲者观点）。对从业者更实用的结论是：无论宏观叙事如何，verification、steering、evals 仍是可投资的工程主轴——只是要把「评什么」与「花多少 judge 算力」说清楚。\nHaize 2024 年商业化、曾服务多家模型厂商发布前测试等陈述属 访谈口述，未独立核实；读者在采购或对标时应要求可复现 benchmark 与合同范围内的数据处理方式，而非仅依赖品牌叙事。\n最后强调证据分级：标 已核实 的陈述可在 PR 评审中附 arXiv 表号或 GitHub 行链；标 演讲者观点 的陈述应进入「待 ablation」清单，而非写进对外 SLA。未标注处默认按工程常识理解，不视为 Haize 官方承诺。对架构师而言，可把本文读作三张叠图：（1）数据形态（静态 / 偏好 / 对抗）决定采集成本；（2）算力形态（test-time vs judge-time）决定错误从哪一侧被消化；（3）部署形态（离线评测 vs 在线 guardrail）决定能否蒸馏与缓存。任一层选错，都会在另一层以隐性债务出现——例如只用静态 Acc. 却在生产遭遇分布外越狱，或在线挡板过严导致可用性崩溃。没有一张图能单独给出「买哪家」的答案。\n若你要落地 # 先画清评判边界：只评 groundedness，还是要评 retrieval 正确性——后者预算应接近 agent 子任务，而非单次 LLM 打分。把 judge 输出 schema（label + explanation + cite spans）写进接口契约，便于与产品审计日志对齐。 用 Verdict 或等价原语搭 Judge→Verify→Pool，在 10–50 条金标上跑 Agreement（κ/τ/ρ）与 Acc.，再扩 benchmark；引用论文表号时写清 backbone（4o vs 4o-mini）与数据集。κ 低时先查标注指南是否含糊，再堆模型。 偏好数据稀缺时，优先对比式 A/B + 理由的标注 UX，再选 RL/DPO；勿默认 judge SFT（访谈反对点需你用自家 ablation 验证）。Meta judge 仅作方向信号时，保留人类抽检通道。 上线护栏：离线用 get-haized / dspy-redteam 类 fuzz + Verdict；在线蒸馏小 judge，并区分云厂商安全过滤与品牌合规规则。记录「拒绝/重写」触发节点，避免黑盒挡板。 与 DSPy 集成时把 Verdict 当 metric 而非整个优化栈——生成优化与 judge-time scaling 分开预算与观测；优化迭代中固定 judge 版本，否则 teleprompter 在追移动靶。 对外沟通：避免用单一百分比概括多 benchmark；对内用执行树定位失败层（judge vs verify vs pool），对外用任务域 SLA 说话。 参考与延伸阅读 # Verdict 论文（arXiv:2502.18018） — judge-time compute 与 benchmark 表 Verdict GitHub 仓库 — API、Quickstart、Debate/Ensemble 原语 Verdict 文档站 — 使用指南与结果页入口 Haize Labs GitHub 组织 — 相关工具链索引 Awesome LLM Judges 列表 — 论文与实现合集 DSPy 官方文档：Metrics — metric 契约与评估环 Verdict × DSPy Cookbook — 集成示例与缓存注意事项 dspy-redteam 示例 — Verdict 作 red-team metric Weak-to-strong generalization（OpenAI PDF） — scalable oversight 背景 Weak LLMs judging strong LLMs（arXiv:2407.04622） — 弱评委监督强模型 Generative Verifiers（arXiv:2408.15240） — 生成式验证与 test-time 叙述 DeepSeek：Inference-Time Scaling for Generalist Reward Modeling（arXiv:2504.02495） — 推理时扩展 generalist RM LMUnit（arXiv:2412.13091） — 自然语言 unit test 式细粒度评测 LMSYS Chatbot Arena 技术报告（arXiv:2403.04132） — 偏好/Elo 范式 Get Haized / fuzzing 套件 README — 自动化 red-team 与 fuzz 入口 BEAST 对抗攻击论文（arXiv:2402.15570） — 离散搜索式 jailbreak（与访谈技术栈部分重叠） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-haize-labs-with-leonard-tang-weaviate-podcast-121/","section":"文章","summary":"Judge-Time Compute：当 LLM 评测从「单次打分」变成可组合管线","title":"Judge-Time Compute：当 LLM 评测从「单次打分」变成可组合管线","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/jvm/","section":"Tags","summary":"","title":"Jvm","type":"tags"},{"content":" JVM 与 Spring Boot 可观测性：三信号如何真正串起来 # 生产里最常见的挫败，不是「没有监控」，而是 UI 上只有一个 HTTP 500，日志里却夹着下游 404，指标曲线又看不出是哪条调用链在抖。云原生与微服务把依赖图拉成「Death Star」式密度后，单靠进程内堆栈或单一 APM 面板往往不够：你需要 metrics 回答「有多糟、影响面多大」，logs 保留业务语义与异常栈，traces 还原跨跳时序与状态码。\nMicrometer 与 Spring Boot Observability 把三条信号收进同一套 Observation 管道；Grafana 栈（Loki / Prometheus / Tempo）则在排障 UI 里把同一次请求焊回去。文章不复述会议时间线，只保留可迁移到 Boot 2.7→4 的机制与操作要点。下文按「为什么 → 机制 → 怎么做 → 误区」组织，覆盖 Boot 2.7→4 的依赖演进与埋点模型；演示拓扑来自 Teahouse 样例（公开仓库 URL 未核实），Grafana 数据源关联为可复现思路而非唯一现场布线。\n为什么需要可串联的多服务样例 # 单进程 demo 很难暴露 跨服务 trace、下游 404 被上游包装成 500、JDBC 与 HTTP 子 span 谁更慢 这类问题。Teahouse 用 tea-service（8090）经 HTTP 调 water-service（8091）与 tealeaf-service（8092），两侧各有 JDBC；用户从 8099 的 UI 点「泡茶」触发整条链。\n机制：每个服务独立进程、独立 spring.application.name；观测标签里的 application=tea-service 等通常来自应用名，不是框架写死的默认值（见 management.metrics.tags.*）。\n怎么做：本地至少起三个 Boot 应用，并部署 OTLP/Zipkin 接收端、Prometheus scrape、Loki push 或 agent；对 GET /tea/{name} 传入 english breakfast 等组合，可稳定复现 ResourceNotFoundException 一类业务错误（日志包名 org.example.teahouse.core.error 为 现场 OCR 证据，非 Spring 产品包）。\n误区：把「能 curl 通」当成「能排障」；或在未统一 spring.application.name 时混用 application 与 service.name 标签，导致 Loki 与 Prometheus 过滤器对不上同一服务。\n排障顺序：日志怀疑 → 指标圈范围 → 追踪定根因 # 行业常见顺序（Spring 文档描述三信号，不强制该顺序）：先在 Loki 里锁定 application=tea-service 的 ERROR，再用聚合指标判断是局部毛刺还是全局故障，最后用分布式追踪还原调用链与 HTTP 状态码在各跳的真实值。\nMicrometer 默认 HTTP 服务端观测名为 http.server.requests；导出到 Prometheus 后常见 http_server_requests_seconds_count。outcome 标签来自 HttpStatus.Series：2xx 为 SUCCESS，5xx 系列为 SERVER_ERROR（DefaultServerRequestObservationConvention 源码）。\nsum(rate(http_server_requests_seconds_count{ application=\u0026#34;tea-service\u0026#34;, outcome=\u0026#34;SERVER_ERROR\u0026#34; }[$__rate_interval])) 典型 ERROR 行会同时出现 [tea-service]、[http-nio-8090-exec-*] 与方括号内的 trace 片段（如 69dfa97c9607cccdc@7fbe4eabce1cb1），以及 Feign/HTTP 客户端方法名 [TealeafClient#findByName]。把该 ID 复制到 Tempo Explore 的 queryType\u0026quot;:\u0026quot;traceql\u0026quot; 查询框，即可从「怀疑 tea-service」切换到「看见 tealeaf 先 404」。\n怎么做（最小 Loki 过滤思路）：{application=\u0026quot;tea-service\u0026quot;} |= \u0026quot;ERROR\u0026quot; 或 level_extracted=\u0026quot;ERROR\u0026quot;（标签名取决于 pipeline，演示栈用了 level_extracted）。确认时间窗与 Prometheus [$__rate_interval] 对齐，避免「日志有、曲线平」的错觉。\n误区：看到 tea-service 返回 500 就改 tea-service。演讲者观点：tealeaf-service 可正确返回 404，而 tea-service 将客户端错误映射为 500——属应用错误处理反模式，非框架缺陷；span 上可能出现非标准 status: 599 与 outcome: SERVER_ERROR 并存（演示/OCR 现场值，非 HTTP 标准）。修复方向应是 传播或保留下游 4xx，并在指标上区分 CLIENT_ERROR 与 SERVER_ERROR，而不是关掉追踪。\n日志关联：把 trace ID 写进每一行 # 为什么：多副本、多服务下，仅靠时间戳与 message 无法对齐同一次用户请求。\n机制：Spring Boot Tracing — Logging Correlation 写明：使用 Micrometer Tracing 时，默认在日志中包含 correlation ID；MDC 键为 traceId、spanId，格式可通过 logging.pattern.correlation 调整。\nlogging: pattern: correlation: \u0026#34;[${spring.application.name:},%X{traceId:-},%X{spanId:-}] \u0026#34; include-application-name: false # 与 correlation 联用时避免应用名重复 传播层默认对齐 W3C traceparent / B3（由 bridge 决定）；下游 water-service、tealeaf-service 若也在 classpath 上有 tracing，则 同 trace 下各服务日志共享 traceId，仅 spanId 随当前 span 变化。\n怎么做：升级自 Sleuth 时对照 Logging Correlation IDs 把 %X{traceId} 写入 logging.pattern.level 或 correlation 块；结构化 JSON 日志应把 traceId/spanId 提成字段，便于 Loki | json 解析。\n误区：以为「开了 Actuator」就等于「日志带 trace」——需 classpath 上有 tracing 与采样/导出配置；无 exporter 时仍可能在日志里看到 ID，但 span 未必进入 Tempo（见后文追踪依赖一节）。另一误区是 只打 traceId 不打 spanId，在并行 span 场景下仍难以对齐单个子操作。\n分布式追踪：Tempo 里看清 HTTP 与 JDBC # 在 Grafana Explore 选择 Tempo 数据源，用 TraceQL 或 trace ID 打开 tea-service: http get /tea/{name}，可展开 make.tea、下游 HTTP、water-dbquery / tealeaf-dbresult-set 等子 span，并对比各跳 http.url、exception、status。\nGrafana Tempo 的 Node graph 适合一眼看 fan-out；展开树可见 connection、water-dbquery、tealeaf-dbresult-set 等命名，对应 JDBC 与连接池观测（常由 datasource-micrometer 等集成产生，具体 span 名为演示应用）。Span attributes 中的 db.name=tealeaf-db 可把慢查询定位到库实例。\n怎么做：Boot 3+ 典型为 spring-boot-starter-actuator + micrometer-tracing-bridge-brave（或 OTel bridge）+ Zipkin/OTLP reporter；Boot 4 可改用 spring-boot-starter-opentelemetry 或 spring-boot-starter-zipkin，并配置 management.opentelemetry.tracing.export.otlp.* 或 management.tracing.export.zipkin.*。采样率示例：\nmanagement: tracing: sampling: probability: 1.0 # 演示环境；生产请下调 误区：只盯根 span 耗时——make.tea（约 30ms 量级，演示 OCR）下并行/串行的 HTTP 与 DB 子 span 才暴露「慢在水库还是茶叶库」。另一误区是 用 UI 上的 500 推断所有下游也是 5xx——应读各 client span 的 http.status_code。\n追踪 → 日志：按 trace ID 拉全链路 # Grafana 文档 说明：点击 「Logs for this span」 时，过滤依据是 trace ID，而非当前 span ID；Tempo 与 Loki 两侧都需配置（如 tracesToLogsV2、derived fields、${__trace.traceId}）。\n怎么做：在 Tempo 数据源 YAML 中配置 tracesToLogsV2，Loki 侧配置 derived field 指向 Tempo；日志行需包含 trace ID 字符串（或由 agent 解析 JSON 字段）。演示中 Explore 同时出现 debug Total:10、error Total: 2 等日志量统计，便于判断是 单服务噪声 还是 全链路边界服务都在报错。\n误区：UI 文案含 “span” 就以为只拉当前 span 的日志——实际是全 trace 参与服务的日志卷；未配置关联时按钮不可用或结果为空（现场 demo 布线，生产需自建）。也不要在 Loki 里只按 container 名过滤却省略 trace ID，否则多副本下仍会串线。\n指标 ↔ 追踪：低基数标签与 Exemplar # Spring Boot Observability：低基数 key 同时进入 metrics 与 traces；高基数（如用户输入）仅进入 traces。因此 span 与 PromQL 可在 application、outcome、exception 等维度对齐。\nExemplar 让聚合曲线上的某个点携带 trace ID，在 Grafana 中跳回 Tempo。Prometheus 需启用 exemplar 存储；Micrometer 在集成 tracing 时可提供 SpanContext（Metrics — Prometheus Exemplars）。\nsum(rate(http_server_requests_seconds_count{ application=\u0026#34;tea-service\u0026#34;, exception=\u0026#34;NotFound\u0026#34;, method=\u0026#34;GET\u0026#34; }[$__rate_interval])) Exemplar 回跳的前提链可拆成三层（缺一则演示中「从曲线点进 Tempo」不可用）：\n应用：Micrometer Prometheus registry 导出 exemplar，且 tracing 提供 SpanContext（Boot 文档 Prometheus Exemplars）。 Prometheus：启用 exemplar 存储，抓取 OpenMetrics 格式（feature flag 文档）。 Grafana：Prometheus 与 Tempo 数据源关联，面板开启 Exemplars（演示面板曾显示 Exemplars: false，需手动打开才有钻取入口）。 未验证边界：演示栈使用 Prometheus exemplar；路线图称 Boot 4.1 / Micrometer 1.17 将加强 OTLP registry 的 exemplar（1.17.0-RC1 发布说明 仅有 RC 级信息）。exception=NotFound 标签是否默认出现在 http.server.requests 上，取决于错误是否被 Observation 记录为低基数 key——未在本文逐条核对每个异常类型的默认标签。\n日志栈：Boot 2 / 3 / 4 的共同点与可选增强 # spring-boot-starter-logging（SLF4J + Logback）在各主版本一致；Logging 特性 写明默认 Logback。换 spring-boot-starter-log4j2、接入 Logbook、开启 Tomcat access log 均为可选：\nserver: tomcat: accesslog: enabled: true 误区：升级 Boot 主版本就重写日志 API——通常只需核对 correlation 与 appenders；GC 日志、Jetty access log 等按容器与运维需求单独加。Access log 记的是 边缘入口（Tomcat 见到的原始请求），与应用日志中的 trace 关联互补，但不能替代 MDC 里的 traceId——两者应通过时间戳与 X-Forwarded-* 头在需要时人工对齐，或统一由 service mesh 注入 trace 头。\nMicrometer 指标：维度化模型与可插拔 Registry # Micrometer 提供与后端无关的 Timer/Counter API，经 micrometer-registry-prometheus 等导出。Boot 4 另提供 spring-boot-starter-micrometer-metrics（是否避免引入完整 Actuator Web 端点需自行核对依赖树，要点中为部分核实）。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.micrometer\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micrometer-registry-prometheus\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 误区：在 Prometheus 里直接搜 http_server_requests 却忘了 Micrometer 侧原名是 http.server.requests——属导出命名转换，不是埋点丢失。\n追踪依赖演进：Sleuth、Micrometer Tracing、Boot 4 Starter # 时代 典型组合 Boot 2.7 + Spring Cloud Spring Cloud Sleuth（核心已迁至 Micrometer Tracing） Boot 3 Actuator + micrometer-tracing-bridge-brave | bridge-otel + Zipkin/OTLP reporter Boot 4 spring-boot-starter-zipkin 或 spring-boot-starter-opentelemetry 单 starter 迁移指南（Wiki） 仍值得对照包名与配置前缀变化。\n升级检查清单（避免只改 Boot 版本号）：\nStarter：micrometer-tracing-bridge-brave + zipkin-reporter-brave → spring-boot-starter-zipkin；或 OTel bridge + OTLP exporter → spring-boot-starter-opentelemetry。 配置前缀：management.tracing.export.zipkin.* 与 management.opentelemetry.tracing.export.otlp.* 不要混用却未删旧键。 与 Observation 集成：文档中的 TracingAwareMeterObservationHandler 仍负责把 trace 上下文绑进 metric/exemplar。 演讲者观点（未在 Boot 4 官方文档逐条核实）：OTel Logback/Log4j appender 在 JVM 侧仍不稳定，Boot 4 无完整 OTel logging starter。无 exporter 时日志里仍可能出现 trace/span ID，但 后端无 span——精确条件取决于是否引入 tracing starter 与采样（management.tracing.sampling.probability）。\nObservation API：一次埋点，多路输出 # 自 Micrometer 1.10 / Boot 3.0 起，micrometer-observation 在「为一次操作计时」层面统一 metrics 与 tracing，避免同一业务路径写三套埋点。\nObservation observation = Observation.start(\u0026#34;my.operation\u0026#34;, registry); try { // business work } catch (Exception ex) { observation.error(ex); throw ex; } finally { observation.stop(); } Boot 文档亦推荐 Observation.createNotStarted(...).observe(...) 风格——API 形态不同，语义一致。框架内置的 HTTP 服务端/客户端、DataSource 等已走 Observation；业务代码在 make.tea 一类路径上应用 Observation.start(\u0026quot;make.tea\u0026quot;, registry) 才能出现 与日志、指标同名的自定义 span。TracingAwareMeterObservationHandler 把 trace 与 timer 绑在一起，是 exemplar 与「指标—追踪同标签」的枢纽。\n全局低基数标签可用 management.observations.key-values.* 注入环境、区域等；高基数请用 .highCardinalityKeyValue(...) 显式标注，避免误入 Prometheus。\n注解路径（需 management.observations.annotations.enabled=true 与 AspectJ）：\n@Observed(name = \u0026#34;tea.make\u0026#34;) @ObservationKeyValue(key = \u0026#34;tea.name\u0026#34;, expression = \u0026#34;#name\u0026#34;) public TeaRecipe makeTea(String name) { /* ... */ } 误区：把用户 ID、商品全名等高基数维度塞进 @ObservationKeyValue 且期望进 Prometheus——会炸 cardinality。\n自定义 ObservationHandler # Spring Boot 自动配置 metrics 与 tracing 的 handler，并将容器中 ObservationHandler Bean 注册到同一 ObservationRegistry（Tracing 与 Observation 集成）。\n@Bean ObservationHandler\u0026lt;MyContext\u0026gt; myHandler() { return new MyObservationHandler(); } 适合挂审计、业务结构化日志等副作用，而无需 fork 框架内置的 HTTP 观测。实现 ObservationHandler 时注意 只处理你关心的 Context 类型，并在 supportsContext 中收窄范围，以免拖慢每次 HTTP 请求。\n误区：在 handler 里再开一条独立的 Brave/OTel tracer 手动建 span——会与内置 tracing handler 重复，导致双 span 或标签分裂。\n三信号对照：何时用哪一种 # 信号 擅长回答 在 Teahouse 场景中的典型入口 Logs 异常类型、参数、业务措辞 Loki {application=\u0026quot;tea-service\u0026quot;} |= \u0026quot;ERROR\u0026quot; Metrics 错误率、饱和度、回归对比 http_server_requests + outcome / exception Traces 跨服务顺序、每跳状态码、DB 子 span Tempo tea-service: http get /tea/{name} 三者不是替代关系：指标缩小时间窗与服务范围，追踪锁定 trace，日志补齐异常栈与业务上下文。Observation 模型的价值在于 同一次 Observation.start 可同步驱动其中两条（metrics + trace），日志则通过 MDC 挂接。若你只能先落地一种信号，优先 日志 correlation + 追踪导出（否则指标再全也无法落到具体请求）；若已有 Prometheus 却缺 tracing，则先补 exporter 与采样，再谈 exemplar 钻取。\n库作者与路线图：Convention、OTLP、语义约定 # 近期方向包括：@MeterTag、OtlpMetricsSender 抽象、MeterConvention 与 OpenTelemetry Semantic Conventions 对齐、虚拟线程与 Jakarta Mail instrumentation 等（幻灯片摘要，未逐条对照发布说明）。\n演讲者观点：OTel 语义约定不会「装依赖就全自动切换」，仍需显式 Convention 配置。Prometheus 侧可能出现 同名 metric、不同 tag 集合 的行为变化，升级 Micrometer 时需读 release note。对库维护者而言，@MeterTag 与 @ObservationKeyValue 的分工在于：前者偏向已有 @Timed/MeterRegistry 体系，后者服务 Observation/Tracing 统一管道——新库优先 Observation + Convention，可减少后端切换时的重命名成本。\n参考与延伸阅读 # Spring Boot — Observability（总述） Spring Boot — Tracing（含日志 correlation） Spring Boot — Metrics（含 Prometheus Exemplars） Spring Boot — Logging 特性 Spring Boot — Starters 列表 Spring Cloud Sleuth 参考与迁移说明 Micrometer — Observation components Micrometer — Prometheus registry Micrometer — OTLP 与 OtlpMetricsSender Micrometer Tracing — Sleuth 3.1 迁移 Wiki Grafana — Tempo 数据源 Grafana — Configure trace to logs Prometheus — Exemplars storage feature flag OpenTelemetry Semantic Conventions OpenMetrics — Exemplars 规范 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-spring-io-2026-i-can-see-clearly-now-observability-of-jvm-spring-boot-2-3-4-apps-spring/","section":"文章","summary":"JVM 与 Spring Boot 可观测性：三信号如何真正串起来","title":"JVM 与 Spring Boot 可观测性：三信号如何真正串起来","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/kafka/","section":"Tags","summary":"","title":"Kafka","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/langchain4j/","section":"Tags","summary":"","title":"Langchain4j","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/late/","section":"Tags","summary":"","title":"Late","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/letta/","section":"Tags","summary":"","title":"Letta","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/llm/","section":"Tags","summary":"","title":"Llm","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/logit/","section":"Tags","summary":"","title":"Logit","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/lombok/","section":"Tags","summary":"","title":"Lombok","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/memgpt/","section":"Tags","summary":"","title":"Memgpt","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/mipro/","section":"Tags","summary":"","title":"Mipro","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/muvera/","section":"Tags","summary":"","title":"Muvera","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/","section":"Neat Guy Coding","summary":"","title":"Neat Guy Coding","type":"page"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/notebook/","section":"Tags","summary":"","title":"Notebook","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/podcast/","section":"Categories","summary":"","title":"Podcast","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/project/","section":"Tags","summary":"","title":"Project","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/prompt/","section":"Tags","summary":"","title":"Prompt","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/query/","section":"Tags","summary":"","title":"Query","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/rag/","section":"Tags","summary":"","title":"Rag","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/rce/","section":"Tags","summary":"","title":"Rce","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/react/","section":"Tags","summary":"","title":"React","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/record/","section":"Tags","summary":"","title":"Record","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/refrag/","section":"Tags","summary":"","title":"Refrag","type":"tags"},{"content":" REFRAG：把 RAG 上下文从「token 串」变成可压缩表示 # 生产级 RAG 的常见路径是：检索若干 passage → 拼进 prompt → 让 decoder 做全长 self-attention prefill。瓶颈往往在 decoder 侧算力与 TTFT，而不是检索本身。向量库与 reranker 优化的是「找什么」；一旦 top-(k) 文本进入 LLM，prefill 仍按 token 长度计费。\nREFRAG（Representation For RAG，Meta Superintelligence Labs，一作 Xiaoqiang Lin）提出 compress → sense → expand：用 chunk 级表示缩短 decoder 序列，再用 RL 对高信息熵片段选择性展开。论文摘要给出相对先前工作的 3.75×、16× 有效上下文扩展，以及特定设定下最高约 30.85× TTFT——这些数字 强依赖 对照模型（如 CEPE）、compression rate (k)、是否使用 pre-computed cache 与序列长度，不能脱离 Figure 2 / Table 2 语境泛化。\n下文按机制、训练与评测边界组织；访谈中的产品判断与论文未写明的数字会单独标注。若你已在用 CEPE 等「压缩式长上下文」方案，可把 REFRAG 看作在 RAG 块稀疏注意力 假设上，把压缩与 按需展开 写进训练目标，而非仅改 inference 剪枝。\n问题：RAG prefill 在算什么 # 为什么：检索 top-(k) 后，passage 常以数百 token 计；多段拼接后 prefill 复杂度近似随序列长度二次增长（标准 Transformer attention）。行业侧更常盯 TTFT（time to first token），而不仅是吞吐。\n机制/约束：论文在 RAG 拼接上下文上观察到 block-diagonal attention patterns——passage 内注意力强于跨 passage（附录有 LLaMA-2-7B-Chat 可视化）。据此主张大量 跨 chunk 的细粒度 attention 可省略；这与「RAG 必须全文互看」的直觉相悖，但 未给出可部署的定量阈值（演讲者观点：跨 chunk 权重「很小」）。\n常见误区：把「压缩」理解成向量库的 ANN 量化。REFRAG 的 compression rate (k) 指每 (k) 个 token 在 decoder 占 1 个位置（(L=s/k)），与 embedding 的 bit-width 无关（§2）。\n双人对谈画面：左侧 Weaviate podcast 角标与波形装饰，右侧为嘉宾居家办公背景；本集无架构幻灯片，机制需对照论文 §2。\n多粒度表示：chunk embedding 进 decoder # 为什么：若仍把全部 raw token 喂给 decoder，prefill 长度几乎不变。REFRAG 用独立 encoder (\\mathcal{M}_{\\text{enc}}) 将每个 chunk (C_i) 编码为 (\\mathbf{c}_i)，经投影 (\\phi) 得到 (\\mathbf{e}^{\\text{cnk}}_i=\\phi(\\mathbf{c}i))，与问题 token embedding 一并输入 (\\mathcal{M}{\\text{dec}})（§2 Model Architecture）。\n机制/约束：访谈用 VLM 用少量 embedding 表示图像 作类比（演讲者观点；论文正文未写 modality / vision）。可核对的是：预计算 chunk embedding 与 在线并行编码 两条路径均成立；即使不做全库预计算（「REFRAG without cache」），因 decoder 序列变短仍可加速（Figure 2 报告 (k=16)、长度 16384 时最高约 16.53× TTFT，须对照基线与是否含 cache）。\n怎么做（算例）：(k\\in{16,32}) 为论文主设定。16 384 token、(k=16) → 1024 个 chunk 位置；(k=32) → 512。访谈「16k 压到约 1k」与 (k=16) 算术一致（已核实）。\n常见误区：认为「一个 embedding 代替整篇文档」。访谈称早期把 数百–上千 token 压成单个 embedding 完全失败（仅访谈）；论文仅有 (k) 增大后的性能回归曲线（Figure 10），无该探索叙事。\n论文在 (k=32) 时相对 LLaMA 报告 TTFT 32.99×、相对 CEPE 3.75×（§2），同时在 CPT perplexity 上仍 competitive——这与访谈「(k=32) 可与无压缩 LM 相当」方向一致，但论文表述更谨慎：并非 宣称在所有任务上等同 LLaMA-Full Context 无损。(k\u0026gt;32) 时性能回归趋势明确；生产上应把 (k) 当作 延迟–质量旋钮，而非越大越好。\n画面角标 OCR：Weaviate podcast —— o\u0026gt; NII iy, 5 e a o \u0026quot; %（多粒度压缩与 k 讨论时段）。\n画面角标 OCR：Weaviate [ee 1S - Hawt podcas — : ge NM Mat %, \\（压缩率边界 k=16/32 讨论时段）。\n注意力分工与块级 prefill # 为什么：在压缩模式下，decoder 只见 (L) 个 chunk 位置，块内 token–token attention 在 compressed 段消失；需要展开的 chunk 再以 (k) 个 raw token embedding 进入（§9.1 混合输入）。\n机制/约束：encoder 对 (C_i) 做标准编码（论文用 RoBERTa，未逐句写死「块内 full self-attention」，但符合常规 encoder 行为）。Decoder 在压缩段为 chunk 级 互注意力；展开段恢复 token 级。主持人提到的 block diagonal attention 与论文术语 一致。\n常见误区：以为去掉跨 chunk attention 后 永远不需要 raw token。高熵事实（数字、细粒度字段）往往需 selective expansion；单靠重建损失，perplexity 可能几乎不降，但「复述 chunk 内长数字」仍失败（§3.1 + 演讲者观点）。\n画面角标 OCR：@@\u0026amp; Weaviate f podcast（预计算 vs 在线编码、prefill 缩短讨论时段）。\n画面角标 OCR：e@\u0026amp;® Weaviate ; podcast Atlant By, s r., Me %（encoder/decoder 注意力分工讨论时段）。\n约 20 分钟处对谈：嘉宾阐述块级注意力与 prefill；画面仍为双人头，无公式屏。\nRL 选择性展开：二值决策及其边界 # 为什么：统一压缩会丢失 chunk 内高熵信息；需在 延迟 与 保真 间折中。\n机制/约束：策略在 chunk embedding 上 顺序 选 (T\u0026rsquo;=pL) 个 index 做 expand；用 Pointer Networks 的 masking 约束 action space（论文脚注）；优化用 GRPO，而非复现 Vinyals 原训练目标。Reward 与下一段 perplexity 相关（§9.1）。访谈描述「一次前向 + mask 重归一化 softmax 无放回采 (m) 个」与 sequential + 不重算 logits 精神一致，但不宜字面等同（部分核实）。\n与检索多样性的关系：主持人问 MMR/去重是否会削弱「可省略跨 chunk attention」；嘉宾答 RL 展开与多样性机制无关，多样性影响的是 不同 chunk 间 注意力，根因是 chunk 来自不同句子/文档（演讲者观点；论文提到 dedup 后的 block-diagonal，未讨论 MMR）。\n常见误区：把展开当成连续粒度控制。当前实现本质是 二值：整 chunk 要么 1 个 (\\mathbf{e}^{\\text{cnk}})，要么展开为 (k) 个 token（演讲者观点）；动态 chunker / 动态 tokenizer 被列为未来工作。\n策略网络 (g_\\theta) 在 chunk embedding 上堆 两层 Transformer（§9.1），复用 ({\\mathbf{c}_i}) 而不在每次选择后重算 logits——这是为 训练吞吐 做的工程取舍。推理延迟上，展开比例 (p)（展开 chunk 数 (T\u0026rsquo;=pL)）直接决定 prefill 长度在「纯压缩」与「接近全 token」之间的落点；若业务以 TTFT 为 SLA，应把 (p) 与 (k) 一并纳入 profiling，而不是只调 top-(k) 检索数。\n画面角标 OCR：‘4 B® Weaviate BOGCOST jie ~ Misatir Noten, = — %（RL 与 MMR 无关、高熵 chunk 展开讨论时段）。\n画面角标 OCR：ff Weaviate podcast py - ” ilaalr Mater —— %（Pointer 式 mask 采样讨论时段）。\n训练：四段流水线与对齐陷阱 # 为什么：若只做 RAG 式 SFT，decoder 可能 忽略 chunk embedding、退回参数记忆（访谈强调；论文用 reconstruction + curriculum 规避，§3.1）。\n机制/约束（论文可归纳为四段，无 “four-stage” 标题）：\nReconstruction：freeze decoder，训 encoder + (\\phi)；从单 chunk (\\mathbf{c}1) 重建 (x{1:k}) 起步。 CPT（continual pre-training）：next paragraph，unfreeze decoder。 RL selective compression。 SFT：RAG、多轮等。 Curriculum 在 CPT/reconstruction 内另有 Stage 1–9 数据混合（Table 8），≠ 访谈口中的「四阶段」专有名词。\n怎么做（资源）：主实验 LLaMA-2-7B decoder；论文默认 8 nodes × 8 H100、FSDP、Bfloat16（§10.2）。访谈「单机 8 卡 5–6 天 / 8 机×8 卡 1–2 天」（仅访谈；论文未给 wall-clock）。实现栈 PyTorch + Hugging Face（仅访谈）；facebookresearch/refrag 截至 2026-05 为 404，复现未能核实。\n常见误区：跳过 reconstruction/curriculum。附录 ablation 显示去掉任一项对齐 显著变差（已核实）。\nCPT 阶段使用 Slimpajama 子集约 20B tokens（§10.2），规模远大于 RAG SFT 的 1.1M 条样本。这解释了为何访谈建议读者 优先看 pre-training 与 RL 章节：主表 1–2 的 perplexity 反映的是 continual pre-training 是否学会「读压缩上下文」；RAG exact match 表则是 在已对齐模型上的应用层展示。若你的场景只有少量领域 QA 数据、无法承担 CPT，应预期 无法复现 论文级 TTFT–质量折中，最多借鉴架构思想做小规模试验。\n画面角标 OCR：ry Weaviate pod cas t _—\u0026lt;ai a Ailastir Meron,（四阶段训练与课程学习讨论时段）。\n画面角标 OCR：f Weaviate ofeyelefohyy “go\u0026gt; MII Hoy, G o ~~ — “ey（投影层与反传讨论时段）。\n约 16 分钟处：主持人与嘉宾讨论训练配方；背景仍为 Weaviate podcast 角标与 FAU 文凭框，无训练曲线屏。\n评测：该看哪张表、数字如何读 # 为什么：RAG 下游任务（QA、摘要）易掺入 SFT 与检索器差异；访谈主张 严格结论在 CPT perplexity 与 RL（演讲者观点）；论文 §5 仍花篇幅写 RAG 应用，不宜单方面贬低。\n维度 论文可核对 访谈/未核实 CPT 主指标 Perplexity（Table 1–2） — RAG Exact match，16 tasks 平均 % 提升；实现 stricter（脚注） 「200 万条」SFT → 论文 1.1M 同延迟 REFRAG 8 passages vs LLaMA 1 passage：strong retriever +1.22%，weak +1.93%（§5.1） 「约 +2%」量级接近但需标 retriever TTFT 摘要 30.85×；§2 有 16.53×（(k=16), 16384, cache）等 须写清基线与 cache 数据消融 未报告 needle-in-haystack 式消融 嘉宾：架构与长上下文数据 兼有，本工作 未做数据消融 常见误区：把 RAG 表的 exact match 平均提升 说成 pass@k 或 MRR；把 equal latency 理解成「相同 token 数」。\n画面角标 OCR：Weaviate ca pe a poacas! \u0026gt; ‘Milantis Maion, a\u0026quot; %（下游任务与 RAG SFT 演示讨论时段）。\n画面角标 OCR：@™ Weaviate podcast ye NUM ayy, = “%（7B 训练算力讨论时段）。\n约 40 分钟处：评测与 TTFT 议题；画面无实验表格，数值以 arXiv:2509.01092 为准。\n向量库与 Agent：产品张力（多为推断） # 向量库：常规做法对检索 embedding 重度量化 且推理时 丢弃，只保留 raw text。REFRAG 若产品化，访谈认为需要 全精度 chunk 向量进入 decoder，数据库重心从「存 passage 文本」转向「存可解码表示」（演讲者观点；论文未论证 ANN 量化对比）。量化 chunk 向量仅在与 预计算落盘 相关的工程场景被提及为「可另做」。\nAgent：论文提及 multi-turn / agentic；访谈将 search agent（大量搜索结果塞 prompt）与 GUI、多模态、长工具历史视为 同一类上下文管理（演讲者观点）。主持人展望「API 返回 REFRAG 向量、agent 间传递向量」—— 无实验支撑。\nTTFT 与 Serving：量化、线性注意力、专用芯片（Cerebras/Groq 等）与 REFRAG 可能正交（嘉宾判断），论文 未做组合实验。\n长上下文数据 vs 架构：主持人援引 lost in the middle / context rot——仅靠架构能否解决，还是必须配 needle-in-haystack 式长上下文训练？嘉宾答 二者兼有，但 REFRAG 未做数据消融（演讲者观点；论文亦无同类实验）。落地时若你的基座未行长上下文 CPT，block-diagonal 假设在域外数据上是否成立，需要 自行验证，不宜外推论文 LLaMA-2-7B 设定。\n结构化抽取：主持人问「按字段展开含 date published 的 chunk」是否会替代传统 LLM 结构化抽取。嘉宾称可能影响数据库/RAG 服务形态，但自称非数据库专家（演讲者观点）。这与 Weaviate 等 schema 化存储并不自动等价；更稳妥的路径是把 REFRAG 展开视为 推理期注意力预算分配，而非取代 ETL/抽取流水线。\n画面角标 OCR：Weaviate podcast ge MIMI Mayy, . ss ra m) — %（Agent 与非搜索场景扩展讨论时段）。\n画面角标 OCR：@\u0026amp; Weaviate 2 podcast R Ailaulir Naty, %（向量读写与函数调用历史展望时段）。\n约 28 分钟处对谈：长上下文数据与架构是否缺一不可；仍为双人头画面。\n若你要落地 # 先对齐评测口径：区分 CPT perplexity（Table 1–2）与 RAG exact match（§5.1）；复现 equal-latency 时固定 passage 数、retriever 强弱与是否 cache。 从 (k=16) 或 (32) 做延迟–质量曲线：勿假设任意 (k) 或「单 embedding 代替整段」；论文显示 (k=32) 仍 competitive，更大 (k) 回归。 训练侧预留 reconstruction + curriculum：不要指望纯 RAG SFT 让 decoder 信任 chunk 表示。 存储与 Serving 分开设计：检索 ANN 量化 ≠ REFRAG 的 (k)；若走预计算路径，评估全精度 (\\mathbf{e}^{\\text{cnk}}) 的存储与加载成本。 开源前以论文为准：代码仓库未公开时，用 HTML 版 §2–§3 与附录 §9.1 对照实现，访谈数字（2M 数据、训练天数、VLM 类比）仅作线索。 参考与延伸阅读 # REFRAG 论文摘要（arXiv:2509.01092） REFRAG HTML 全文 — 架构 §2 REFRAG — 训练配方 §3.1 REFRAG — RAG 实验 §5.1 REFRAG — RL 选择性展开 §9.1 REFRAG — 算力与超参 §10.2 REFRAG PDF 印刷版 Retrieval-Augmented Generation（Lewis et al., 2020） Pointer Networks（Vinyals et al., 2015） Lost in the Middle（Liu et al., 2023） — 长上下文位置偏差，与 block-diagonal 讨论相关 Needle In A Haystack — 长上下文评测范式 — 论文未做同类数据消融时的对照阅读 CEPE — 压缩式长上下文对照（论文 bib） GRPO — REFRAG RL 优化引用（Shao et al., 2024） — 见论文参考文献列表 Hugging Face Transformers — 访谈称实现基础；论文正文未写明 facebookresearch/refrag — 论文声明仓库 — 核实日 404，落地前需再查 Weaviate 向量数据库文档 — 与「存 passage vs 存 chunk 表示」的产品讨论对照（非 REFRAG 官方规范） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-refrag-with-xiaoqiang-lin-weaviate-podcast-130/","section":"文章","summary":"REFRAG：把 RAG 上下文从「token 串」变成可压缩表示","title":"REFRAG：把 RAG 上下文从「token 串」变成可压缩表示","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/rest/","section":"Tags","summary":"","title":"Rest","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/reward/","section":"Tags","summary":"","title":"Reward","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/saas/","section":"Tags","summary":"","title":"Saas","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/sdk/","section":"Tags","summary":"","title":"Sdk","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/search/","section":"Tags","summary":"","title":"Search","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/security/","section":"Categories","summary":"","title":"Security","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/seventeen/","section":"Tags","summary":"","title":"Seventeen","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/share/","section":"Tags","summary":"","title":"Share","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/spring/","section":"Categories","summary":"","title":"Spring","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/spring/","section":"Tags","summary":"","title":"Spring","type":"tags"},{"content":" Spring for Apache Kafka 4：迁移、Share Group 与新 Consumer 协议 # Spring for Apache Kafka（下文简称 Spring Kafka）4.0 随 Spring Boot 4.0 发布，捆绑 Kafka client 4.1、Jackson 3，并在客户端侧对接 KIP-932（Queues / Share Group）与 KIP-848（group.protocol=consumer）。本文按工程决策组织：先谈可复现的 3→4 迁移，再谈经典 Consumer Group 上仍适用的错误处理，最后分述 Share Group 的手工装配边界，以及新一代 rebalance 的 opt-in 升级。\n版本提示：Share consumer 在 Spring Kafka 4.0 文档 中标注为 preview / early access；ShareAckMode 枚举与 renew() 出现在 4.1 文档。下文在 4.0.x 与 4.1 有差异处会单独标明。\n若你维护的是「Boot 3.5 + 独立 spring-kafka 3.x」栈，升级路径可以概括为三层：依赖与 starter 模块化（Boot 4 BOM）、序列化与测试基础设施（Jackson 3、KRaft 嵌入式 broker）、消费模型扩展（经典组上的 KIP-848 opt-in，以及可选的 Share Group 手工装配）。前两层适合全量应用；第三层应按 workload 选型，而不是默认全开。\n用 OpenRewrite 做 Boot 3→4 与 Kafka 3→4 迁移 # 为什么 # 手工改 pom.xml、包名与测试注解，容易漏掉 Jackson starter 替换或 @EmbeddedKafka 的废弃属性。OpenRewrite 把「可重复的 AST 级变更」固化成 recipe，适合在 CI 或分支上批量执行。\n机制与约束 # 官方配方（可在 rewrite-spring 仓库 核对）包括：\nUpgradeSpringBoot_4_0 — 聚合 Boot 4.0 依赖与 MigrateToModularStarters MigrateAutoconfigurePackages — autoconfigure 包路径迁移 UpgradeSpringKafka_4_0 — 如 JsonSerializer → JacksonJsonSerializer 演示仓库里还有自写 recipe（如 MigrateSpringBootJsonToJacksonStarter、RemoveDeprecatedEmbeddedKafkaParameters），不在官方 recipes.csv 中，迁移时需自行维护。\n怎么做 # cd spring-kafka-3-to-4 \u0026amp;\u0026amp; ./mvnw rewrite:run rewrite.yml 中至少挂上官方 Boot/Kafka 升级配方，再按需追加自定义 visitor：\nrecipeList: - org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 - org.openrewrite.java.spring.kafka.UpgradeSpringKafka_4_0 # 演示仓库自定义（非官方） - com.example.spring.kafka.RemoveDeprecatedEmbeddedKafkaParameters 演示终端：cd spring-kafka-3-to-4 与 ./mvnw rewrite:run，IDE 中可见 Upgrade Spring Boot / Spring Kafka 配方说明。\n常见误区 # 把演示里的 CustomUpgradeSpringkafka_4_0 当成官方配方名 — 官方 Kafka 迁移名为 UpgradeSpringKafka_4_0。 只跑 OpenRewrite、不跑测试 — ZK 相关注解与序列化器变更仍需测试覆盖。 在 monorepo 多模块上只跑父 POM 配方 — 子模块若仍直接声明 spring-kafka 版本，可能需分模块执行或补充 ChangeDependency visitor。 模块化 Starter 与 Jackson 3 序列化 # 为什么 # Boot 4 将 Kafka、JSON 能力拆成独立 starter，并与 Jackson 3（tools.jackson）对齐。继续直接依赖 spring-kafka + spring-boot-starter-json（Jackson 2）会导致自动配置包与序列化行为漂移。\n机制与约束 # 迁移前（示意） 迁移后 spring-kafka spring-boot-starter-kafka spring-boot-starter-json spring-boot-starter-jackson spring-kafka-test spring-boot-starter-kafka-test JsonSerializer / JsonDeserializer 自 4.0 起弃用，应改用 JacksonJsonSerializer / JacksonJsonDeserializer（基于 Jackson 3 的 JsonMapper）。\n怎么做 # \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-kafka\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-jackson\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; spring: kafka: producer: value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer consumer: value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer Side-by-side：spring-kafka 3.5.0 与 spring-boot-starter-kafka 4.0.5 的 POM 差异。\n迁移前 application.yml 仍使用 JsonSerializer 与 ErrorHandlingDeserializer 委托配置。\n常见误区 # 只改依赖、不改 serializer 类名 — 运行时仍走已弃用的 Jackson 2 适配层。 忽略 spring.json.trusted.packages 等安全相关属性 — Jackson 3 下仍需按 What\u0026rsquo;s New 检查 JSON 信任包配置。 混用第三方库的 Jackson 2 绑定 — Boot 4 应用 classpath 以 Jackson 3 为主，第三方 starter 若强依赖 com.fasterxml.jackson 需单独评估兼容性。 ErrorHandlingDeserializer 仍可与 JacksonJsonDeserializer 委托搭配：外层捕获反序列化异常，内层 delegate 指向 Jackson 3 反序列化器，这样 poison pill 在进入 listener 前就能被分类处理。\n测试里的 @EmbeddedKafka 与 KRaft # 为什么 # Kafka 4 broker 仅支持 KRaft，不再使用 ZooKeeper。集成测试若保留 zookeeperPort、kraft = false 等属性，会在嵌入式 broker 启动阶段失败，且与生产拓扑不一致。\n机制与约束 # Spring Kafka 4.0 What\u0026rsquo;s New 明确移除 @EmbeddedKafka 的 zookeeperPort、zkConnectionTimeout、zkSessionTimeout、kraft 等属性；实现改为 EmbeddedKafkaKraftBroker。\n怎么做 # @SpringBootTest @ActiveProfiles(\u0026#34;test\u0026#34;) @EmbeddedKafka(partitions = 3) class SpringKafkaApplicationTests { } OpenRewrite 自定义 recipe 可批量删除废弃属性（演示仓库做法）。\n迁移对比：zookeeperPort = 2181、kraft = false 与精简后的 @EmbeddedKafka(partitions = 3)。\n常见误区 # 以为删掉 kraft = false 等于「关闭 KRaft」— 实际是 强制 KRaft-only。 在生产 broker 仍混用 ZK 的旧集群上直接升 Kafka 4 — 与测试注解无关，属 broker 升级范畴。 经典 Consumer Group 上的 Poison Pill 与 DLT # 为什么 # 不可反序列化或业务永久失败的消息若只在组内无限重试，会拖住整个 group.id 的分区进度（head-of-line blocking）。经典 consumer group 上，Spring Kafka 成熟的模式是把失败记录发到 Dead Letter Topic（DLT）。\n机制与约束 # DeadLetterPublishingRecoverer 实现 ConsumerRecordRecoverer，将失败 ConsumerRecord 发布到 DLT，常与 ErrorHandlingDeserializer 及 DLT Strategies 配合。\n边界（演讲者观点，与官方文档方向一致）：Share Group 目前没有 DLT 体系； poison pill 需依赖 reject()、投递次数上限等 KIP-932 语义，不能照搬 @RetryableTopic。\n怎么做 # @Configuration class KafkaExceptionHandlingConfiguration { @Bean DeadLetterPublishingRecoverer recoverer( KafkaTemplate\u0026lt;?, ?\u0026gt; bytesKafkaTemplate, Map\u0026lt;Class\u0026lt;?\u0026gt;, KafkaTemplate\u0026lt;?, ?\u0026gt;\u0026gt; templates) { return new DeadLetterPublishingRecoverer(bytesKafkaTemplate, templates); } } 演示类 KafkaExceptionHandlingConfiguration 与 DeadLetterPublishingRecoverer Bean 定义。\n常见误区 # 在 Share consumer 上配置 DLT recoverer — 官方 kafka-queues 未覆盖该组合。 未区分反序列化失败与业务失败 — 前者应优先用 ErrorHandlingDeserializer，后者再走 recoverer / retry topic。 KIP-932：Share Group 解决什么问题 # 为什么 # 经典 consumer group 下，活跃消费者数量通常 不超过分区数；单条慢消息会阻塞该消费者负责的分区（partition 级 head-of-line blocking）。突发流量时，团队常被迫过度增加分区，只为换横向扩展能力。\nKIP-932 引入 Share Group：同一 share group 内多个 ShareConsumer 可在记录粒度协作消费，带来接近消息队列的语义 — 按条确认、可 release 给他人重试。\n机制与约束 # 消息在 broker 侧维护 per-record 状态（KIP 与 Spring 文档共识）：\nAvailable → Acquired：消费者拉取时投递计数增加（配置名见 KIP：group.share.delivery.attempt.limit）。 Acquired → Acknowledged：成功处理。 release：暂时失败，可再次被组内其他实例获取。 reject：永久失败，进入 Archived（类似丢弃 / 归档，不是 Spring DLT）。 演讲者观点（未在本文逐条对照 Kafka 4.1 默认值）：默认约 5 次投递、约 30s 处理锁；Kafka 4.2 GA 与 Boot 4.1 在五月对齐的时间表 — 请以你使用的 broker / BOM 版本为准。Boot 4.0 Release Notes 写的是 Kafka 4.1.0。\n与经典 offset 提交不同，Share 消费不依赖「每分区一个逻辑位点」来界定进度，而是依赖 broker 对单条记录的状态机。运维上应关注 delivery count 与 Archived 比例，而不是仅看 consumer lag（经典指标对 share 的解释方式可能不同，需对照你使用的 Kafka 版本监控文档）。\n幻灯片「Queues for Kafka – How does it work?」：Consumer group 与 Share group 并行模型对比。\n消息状态：Available → Acquired（delivery count++）→ Acknowledged / Archived。\n常见误区 # 把 Share Group 当作「分区更多的 consumer group」— 它不保证分区内顺序。 在需要 Kafka Streams、批量 @KafkaListener、正则 topic、静态分区分配的场景硬上 Share — Spring 文档 明确不支持 多项能力。 Share Consumer 的手工装配 # 为什么 # 截至 Spring Kafka 4.0 文档，Share consumer 处于 early access，没有与 spring.kafka.consumer.* 对等的 Boot 自动配置。缺 ShareKafkaListenerContainerFactory 时，监听器无法在运行期正确创建 share 容器。\n机制与约束 # 核心类型：\nDefaultShareConsumerFactory ShareKafkaListenerContainerFactory ContainerProperties.setShareAcknowledgmentTimeout(Duration) — @since 4.0 4.0 vs 4.1：4.0.x 使用 setExplicitShareAcknowledgment(true) 开启 listener 侧 ShareAcknowledgment；4.1 起文档化为 ShareAckMode.MANUAL。演示代码中的 setShareAckMode 面向 4.1；在 4.0.5 上应改用 explicit 标志。\n部分经典 ConsumerConfig 对 share group 无效 — 最小集通常只需 bootstrap.servers（演讲演示如此；完整列表以 Kafka 文档为准）。\n演讲者观点：Share 场景的 Micrometer tracing / 与经典组对等的 metrics 仍不成熟。\n怎么做 # @Configuration @EnableKafka class ShareConsumerConfiguration { @Bean ShareConsumerFactory\u0026lt;String, TransactionEvent\u0026gt; shareConsumerFactory() { Map\u0026lt;String, Object\u0026gt; props = Map.of( ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, \u0026#34;localhost:9092\u0026#34;); return new DefaultShareConsumerFactory\u0026lt;\u0026gt;(props); } @Bean ShareKafkaListenerContainerFactory\u0026lt;String, TransactionEvent\u0026gt; manualShareKafkaListenerContainerFactory( ShareConsumerFactory\u0026lt;String, TransactionEvent\u0026gt; factory) { var f = new ShareKafkaListenerContainerFactory\u0026lt;\u0026gt;(factory); f.setConcurrency(6); f.getContainerProperties().setShareAckMode(ShareAckMode.MANUAL); // 4.1+ f.getContainerProperties() .setShareAcknowledgmentTimeout(Duration.ofSeconds(30)); return f; } } 4.0.x 等价写法（无 ShareAckMode 时）：\nf.getContainerProperties().setExplicitShareAcknowledgment(true); IDE：ShareConsumerConfiguration 与 DefaultShareConsumerFactory import。\nManualShareConsumerConfiguration：ShareKafkaListenerContainerFactory、setConcurrency(6)、ShareAckMode MANUAL。\n演示仓库模块：spring-kafka-4-producer 与 spring-kafka-4-share-consumer。\n常见误区 # 只加 @KafkaListener、不声明 containerFactory — 会落到经典容器工厂。 把 spring.kafka.listener.concurrency 当成 share 并发唯一旋钮 — share 容器需单独 setConcurrency，且与分区数无经典组那种硬上限关系（协作消费模型不同）。 ShareAcknowledgment：acknowledge、release、reject # 为什么 # 工作队列场景要区分「下游暂时不可用」与「业务上永远无效」。仅靠抛异常一种路径，无法精确映射 KIP-932 的 release（可重试）与 reject（归档）。\n机制与约束 # 方法 语义（Javadoc） acknowledge() 成功 release() 暂时失败，可再次被组内消费 reject() 永久失败，不再投递 在 explicit / MANUAL 模式下，未 ack 的同线程记录会阻塞后续 poll（Poll Constraints）。implicit 模式下，方法正常返回可触发自动 ack — 勿与 classic 的 AckMode 混淆。\n4.1+：ShareAcknowledgment.renew() 可延长 acquisition lock；默认锁时长与 group.share.record.lock.duration.ms 相关（文档示例 30 seconds）。长事务处理应规划 renew，而非单纯调大超时。\n怎么做 # @KafkaListener( id = \u0026#34;annotation-manual-transaction-event-processor\u0026#34;, topics = \u0026#34;transaction-events\u0026#34;, containerFactory = \u0026#34;manualShareKafkaListenerContainerFactory\u0026#34;, groupId = \u0026#34;annotation-manual-group\u0026#34;) void handle(ConsumerRecord\u0026lt;String, TransactionEvent\u0026gt; record, ShareAcknowledgment ack) { try { service.process(record.value()); ack.acknowledge(); } catch (TransientException e) { ack.release(); } catch (Exception e) { ack.reject(); } } 演示还通过 HTTP POST /api/transactions 注入测试事件（handler 侧为 REST Controller + 上述 listener）。\nAnnotationDrivenManualKafkaShareConsumer：@KafkaListener 与 containerFactory = \u0026quot;manualShareKafkaListenerContainerFactory\u0026quot;。\n控制台：Acknowledged / Rejected / Released for retry 三类日志。\n包结构：explicit、implicit、manual 等示例并列。\n常见误区 # 在 MANUAL 模式下既不调用 ack、又期望自动提交 — 会阻塞同线程消费。 把 release() 当成经典 consumer 的 seek 回退 — 语义是重新进入 Available，由组内竞争，而非固定分区 offset。 在 4.0.5 上使用 ShareAckMode.MANUAL 编译 — 应改用 setExplicitShareAcknowledgment(true)。 Share Group 与经典组的选型 # 维度 经典 Consumer Group Share Group 并行度 vs 分区 消费者数通常 ≤ 分区数 可大于分区数（记录级协作） 顺序 分区内有序 不保证 DLT / @RetryableTopic 成熟 尚无（演讲归纳 + 文档未覆盖） Boot 自动配置 spring.kafka.* 需手工 factory 批量 / 正则 topic / Streams 支持 文档 列明不支持 可观测性 较完整 演讲者观点：metrics/tracing 仍缺口 生产环境若依赖有序事件流、Streams、批量监听或成熟 DLT，应继续使用经典组。Share Group 更适合可接受乱序、需要多实例抢活的工作队列。演讲者观点：当前 Spring Boot 发行版中 share 仍为 preview，生产宜等待与 Kafka Queues GA 对齐的 Boot/Kafka 组合 — 具体月份以官方发布为准。\n落地前可用下列问题自检：（1）能否接受同 key 的消息乱序到达？（2）失败重试是否接受「其他实例接手」而非固定分区重放？（3）永久失败是否接受 Archived 而非 DLT 审计？（4）监控与告警是否已覆盖 share 专属指标？任一项为否，应留在经典组并优先评估 KIP-848 降低 rebalance 成本。\nKIP-848：group.protocol=consumer # 为什么 # 分区多、消费者多的经典组在 rebalance 时可能出现整组停顿，与 K8s HPA、滚动发布频繁扩缩容冲突。KIP-848 把大量分配逻辑下沉 broker，消费者 opt-in 新协议。\n机制与约束 # Kafka 客户端源码（ConsumerConfig）：\n配置项：group.protocol 默认值：classic 合法值：classic | consumer Spring 绑定：\nspring: kafka: consumer: properties: group.protocol: consumer 启用 consumer 后，partition.assignment.strategy 等经典配置会触发 ConfigException（与「自定义 PartitionAssignor 失效」一致）。演讲者观点：rebalance 体验改善的量化表述应以 KIP 正文为准，不宜脱离版本夸大「零停顿」。\n本协议仅适用于经典 consumer group，与 Share Group（KIP-932）无关。\n怎么做 # group.protocol=consumer 确保 broker 已启用 KIP-848 所需升级策略（KIP 中的 bidirectional / upgrade 等集群配置 — 细节见 KIP Case Studies）。\n常见误区 # 在 Share consumer 上设置 group.protocol — 路径错误。 同时保留自定义 assignor 与 consumer 协议 — 启动即失败。 升级后仍用旧 Micrometer 仪表盘 — 演讲者观点：broker 侧 metrics 有 protocol=consumer|classic 等变更，需对照 KIP 更新告警。 在蓝绿部署中同时启动两套不同 group.id 却共用同一业务逻辑 — 与协议升级无关，但会掩盖 rebalance 问题；协议迁移应在同一 group.id 下滚动。 对 Spring 应用，只需确保 ConsumerFactory / @KafkaListener 最终 Consumer 属性含 group.protocol=consumer；无需改 Spring API。回滚时将属性改回 classic 并滚动实例即可，前提是 KIP-848 允许的降级路径在你的 broker 版本上仍开启。\n滚动从 classic 切到 consumer 协议 # 为什么 # 大规模 consumer group 往往不敢改协议，担心一次性 rebalance 造成消费空洞。KIP-848 写明可通过 rolling consumers 升级或降级。\n机制与约束 # KIP-848 摘录（已核实方向）：\n\u0026ldquo;Upgrading to the new protocol or downgrading from it is possible by rolling the consumers\u0026rdquo;\n演讲者观点（未对照 Kafka 4.1 broker 源码逐行验证）：可先混跑 classic 与 consumer 实例；broker 在检测到新协议成员后加入组时，将组迁移到新协议；再逐步缩掉 classic 实例；若旧协议成员回归，可回退。演讲者建议：尽快结束混跑，避免长期双协议增加 broker 开销。\n怎么做 # 演示使用 Docker Compose 扩缩（命令来自现场 OCR，演示仓库 URL 未公开验证）：\ndocker compose -f docker-compose-applications.yml -p demo up -d docker compose -f docker-compose-applications.yml -p demo \\ scale consumer-new-consumer-protocol=1 # 验证组协议切换后，逐步 scale 到 3 并下线 classic 实例 docker compose -f docker-compose-applications.yml -p demo \\ scale consumer-new-consumer-protocol=3 终端：Scale from 0 to 1 instance 与 consumer-new-consumer-protocol。\nScale from 0 to 1 与 Group protocol 日志中的 classic / consumer 切换。\n常见误区 # 未先升级 broker / 未启用集群升级策略就改客户端 — 行为未定义或启动失败。 长期混跑两种协议当作稳态 — 与 KIP 及演讲建议相反。 参考与延伸阅读 # Spring for Apache Kafka 参考文档（4.0） Spring Kafka 4.0 — What\u0026rsquo;s New Spring Kafka 4.0 — Kafka Queues (Share Consumer) Spring Kafka 4.1 — ShareAckMode 与 renew() Spring Boot 4.0 — Release Notes Spring Boot 4.0 — Migration Guide（模块化 starter） Javadoc — JacksonJsonSerializer Javadoc — @EmbeddedKafka Javadoc — DeadLetterPublishingRecoverer Spring Kafka — DLT Strategies KIP-932: Queues for Kafka KIP-848: The Next Generation of the Consumer Rebalance Protocol Kafka ConsumerConfig 源码（group.protocol 默认值） OpenRewrite — UpgradeSpringKafka_4_0 配方 OpenRewrite — UpgradeSpringBoot_4_0 配方 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-spring-io-2026-what-s-new-in-spring-for-apache-kafka-4-by-tim-van-baarsen-spring-io-202/","section":"文章","summary":"Spring for Apache Kafka 4：迁移、Share Group 与新 Consumer 协议","title":"Spring for Apache Kafka 4：迁移、Share Group 与新 Consumer 协议","type":"posts"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/spring-io/","section":"Tags","summary":"","title":"Spring-Io","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/spring-io-2026/","section":"Tags","summary":"","title":"Spring-Io-2026","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/springio/","section":"Categories","summary":"","title":"SpringIO","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/ssm/","section":"Tags","summary":"","title":"Ssm","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/stark/","section":"Tags","summary":"","title":"Stark","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/swe-bench/","section":"Tags","summary":"","title":"Swe-Bench","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t1/","section":"Tags","summary":"","title":"T1","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t10/","section":"Tags","summary":"","title":"T10","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t2/","section":"Tags","summary":"","title":"T2","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t3/","section":"Tags","summary":"","title":"T3","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t4/","section":"Tags","summary":"","title":"T4","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t6/","section":"Tags","summary":"","title":"T6","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/t9/","section":"Tags","summary":"","title":"T9","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/teams/","section":"Tags","summary":"","title":"Teams","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/token/","section":"Tags","summary":"","title":"Token","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/tts/","section":"Tags","summary":"","title":"Tts","type":"tags"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/categories/weaviate/","section":"Categories","summary":"","title":"Weaviate","type":"categories"},{"content":"","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/tags/weaviate-podcast/","section":"Tags","summary":"","title":"Weaviate-Podcast","type":"tags"},{"content":" 把 Copilot 嵌进 Java 工具链：从终端 CLI 到 SDK 与插件 # GitHub Copilot 早已不只是 IDE 里的补全。对 Java 团队而言，更现实的问题是：Issue 能否在云端自动变成可 review 的 PR？CI 里能否跑「只打标签、不写代码」的推理步骤？JMeter、Office 或自研桌面产品能否内嵌同一套 agent，而不是让用户在外部 Chat 与工具之间来回粘贴？本文围绕 Copilot CLI、Copilot cloud agent、Agentic Workflows（gh aw） 与 Copilot SDK for Java 四条主线，说明机制、约束与可落地的最小配置；演示仓库细节（如 brunoborges/todo-app）仅作行为示例，不当作产品契约。\n多入口 agent：同一仓库，不同触发面 # 为什么 # Java 工作流分散在 GitHub Issue、IntelliJ/Eclipse、终端脚本和 Slack/Teams/Jira 等协作面。若每个入口各自维护一套 prompt，很快会出现「云端 agent 与本地 CLI 行为不一致」的漂移。官方文档将 Copilot cloud agent 描述为在 GitHub Actions 驱动的临时开发环境 中探索代码、修改并提 PR 的 agent；入口则覆盖 Issue 指派、IDE、gh、Mobile、MCP，以及 Jira、Slack、Teams、Azure Boards、Linear 等集成（见 启动 cloud agent 会话）。演讲者观点：「prompt once, agents everywhere」是对这一矩阵的概括，并非文档固定标语。\n机制与约束 # 将 Issue 指派给 Copilot 会触发后台 coding 会话，完成后 raise pull request 并请求人工 review。 无模型选择器的入口默认使用 Auto 模型（可用性与限流下的路由选型）。 Cloud agent 默认 MCP 含 GitHub MCP（仓库只读，可扩权限）与 Playwright MCP（读页、交互、截图；默认仅 localhost / 127.0.0.1）——见 MCP 与 cloud agent。 怎么做 # gh issue edit 1 --repo org/todo-app --add-assignee \u0026#34;@copilot\u0026#34; # 或创建时指派 gh issue create --repo org/todo-app \\ --title \u0026#34;Show timestamps on todo items\u0026#34; \\ --body \u0026#34;Add createdAt/completedAt to UI.\u0026#34; \\ --assignee \u0026#34;@copilot\u0026#34; 常见误区 # 把「指派 Issue」等同于「本地 IDE 里开 Copilot Chat」——前者在 云端 Actions 环境 跑完整仓库任务，后者不自动提 PR。 假设 Playwright 每次都会在 PR 里贴截图——官方只保证能力存在，是否贴图取决于任务与 agent 决策。 图：幻灯片「GitHub Copilot is available across IDEs, web, and mobile」——含 Terminal (Copilot CLI)、Slack \u0026amp; Teams 等入口。\n图：IDE 侧 Pair program with GitHub Copilot，展示 Agent 模式与模型选择（具体模型名以 支持模型列表 为准，可能随版本变化）。\nCopilot CLI：终端 agent、权限与 slash 命令 # 为什么 # 探索期任务（脚手架、一次性脚本、本地多文件重构）需要 低摩擦的 REPL：一句话启动、可读目录、可恢复会话。若每次都走 GUI，很难与 cron、gh 脚本或 SSH 会话结合。\n机制与约束 # CLI 命令参考 中与现场 demo 对齐的符号包括：\n符号 作用（官方摘要） /yolo 等价放开 tools/paths/URLs（同 --allow-all） /context 展示 token 使用分解 /plan 先出实现计划；亦可 Shift+Tab 切换 plan 模式 /resume 恢复交互会话 !cmd 直通 shell，不经模型解释 /mcp 管理 MCP 服务器 /fleet 并行 sub-agent（见后文） 官方明确建议：自动批准模式应在 VM、容器或专用系统 上运行（配置 CLI 权限）。演讲者观点：将 Docker Sandboxes 与 YOLO 并列是隔离宿主机风险的产品组合建议；「Docker Sandbox」不是 Copilot CLI 文档中的内置子命令——Docker 侧见 Sandboxes 中的 Copilot。\n怎么做 # # 非 YOLO：适合日常 copilot -p \u0026#34;Explain this Maven multi-module layout\u0026#34; # 高风险：仅隔离环境 copilot --yolo -p \u0026#34;Refactor package structure and run ./mvnw test\u0026#34; REPL 内：/plan 先规划；/context 查窗口；!git status 直接跑 git。\n常见误区 # 在生产笔记本上长期开 /yolo——官方风险提示与现场演示一致：agent 可能直接执行 rm 等操作。 把 docker run … ghcr.io/github/copilot-cli 当作唯一安装路径——copilot-cli README 推荐 copilot-install、brew、npm -g @github/copilot 等。 图：终端会话显示「No copilot instructions found. Run /init」与 MCP server playwright 连接状态。\n云端 Coding Agent：Issue → PR 与 Playwright # 为什么 # 「实现 + 跑测试 + UI 自检 + 给 reviewer 证据」若拆成人工多步，延迟和上下文切换成本很高。Cloud agent 把实现与验证尽量收进 一次后台任务，人主要做 review-only。\n机制与约束 # 演示仓库 brunoborges/todo-app 中，Issue 要求为 todo 项展示创建/完成时间；agent 在实体已有 createdAt/completedAt 的前提下补 Thymeleaf 展示（演示内容，非官方样例）。\n怎么做 # gh pr checkout 2 --repo brunoborges/todo-app ./mvnw spring-boot:run 模板片段（与 PR OCR 一致）：\n\u0026lt;span class=\u0026#34;todo-timestamp\u0026#34; th:if=\u0026#34;${todo.createdAt != null}\u0026#34; th:text=\u0026#34;${\u0026#39;Created: \u0026#39; + #temporals.format(todo.createdAt,\u0026#39;MMM d, yyyy HH:mm\u0026#39;)}\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;todo-timestamp\u0026#34; th:if=\u0026#34;${todo.completedAt != null}\u0026#34; th:text=\u0026#34;${\u0026#39;Completed: \u0026#39; + #temporals.format(todo.completedAt,\u0026#39;MMM d, yyyy HH:mm\u0026#39;)}\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; 常见误区 # 认为 cloud agent 只在 main 分支上工作——实际在 临时 Actions 环境 checkout 并改代码；本地需 gh pr checkout 验证。 忽略 Playwright 的 localhost 限制——测远程 staging 需在 自定义环境 中显式配置。 图：PR 说明 createdAt and completedAt were persisted on every Todo entity but never surfaced in the UI。\n图：Copilot AI commented on PR #2，说明将按计划更新描述并推进实现。\n给云端 agent 配 JDK：copilot-setup-steps.yml # 为什么 # GitHub-hosted Ubuntu 24.04 镜像默认 JDK 17（见 runner 软件表）。若 pom.xml 使用 \u0026lt;release\u0026gt;25\u0026lt;/release\u0026gt;，agent 内 mvn test 会报 release version 25 not supported，PR 可能在 CI 阶段集体失败——这与「agent 已写完代码」无关，而是 启动前环境未对齐。\n机制与约束 # 路径：.github/workflows/copilot-setup-steps.yml 必须包含名为 copilot-setup-steps 的 job；在 agent 开始工作之前 执行。 文件须在 默认分支 上才会被拾取（可用 workflow_dispatch 自测）。 文档：配置 cloud agent 开发环境 怎么做 # # .github/workflows/copilot-setup-steps.yml name: Copilot Coding Agent · Setup Steps on: workflow_dispatch jobs: copilot-setup-steps: runs-on: ubuntu-latest steps: - uses: actions/setup-java@v4 with: distribution: temurin java-version: \u0026#34;25\u0026#34; - run: java -version \u0026amp;\u0026amp; ./mvnw -q -v gh aw init 也会生成同名文件（Agentic Workflows 与 cloud agent 共用钩子）。\n常见误区 # 只在 feature 分支添加 setup 文件——未合并到 default branch 时 agent 看不到。 只改 GitHub Actions CI workflow，不改 copilot-setup-steps——两者执行时机不同。 图：终端输出 The environment has Java 17 but the project targets Java 25。\nCode Review Agent：写—评—改闭环 # 为什么 # 主 coding agent 的 diff 常有可预测的 nit：Thymeleaf 空值、CSS 布局、测试遗漏。若全部依赖人工 comment，reviewer 时间耗在格式层而非业务风险。\n机制与约束 # 显式 PR review：在 Reviewers 中选择 Copilot（使用 Copilot code review）。 Cloud agent 内置二次审查：文档记载 agent 生成代码时会做安全扫描，并 gets a second opinion on its code with Copilot code review（配置 agent 设置）——与 PR 上的独立 review UI 相关但不完全等同。 以下 Thymeleaf/CSS 级评论来自现场 OCR，非官方模板： 怎么做 # 在 PR 界面触发「Review with Copilot」，或在流程中依赖 cloud agent 内置 review。修复示例：\n.todo-timestamp { display: block; font-size: 0.75rem; opacity: 0.6; } 常见误区 # 把「内置 second opinion」当成「一定会在 PR Conversation 里出现与 OCR 相同的逐行评论」。 期望 Copilot 给出 Approve——文档明确其 review 类型为 Comment，不构成 approve/request changes。 图：Found 2 review comment(s)，含 #temporals.format() 前缺少 null check 与 .todo-timestamp 的 display: block 建议。\nAgentic Workflows：Markdown prompt 与 safe-outputs # 为什么 # CI 里嵌入 LLM 时，最大风险是 agent 获得 contents: write 后误改仓库、推分支或删资源。Agentic Workflows 把「推理」与「写操作」拆开：主 job 只读，写操作仅能走编译期校验的 safe-outputs 白名单。\n机制与约束 # 仓库以 github/gh-aw 为准（安装：curl -sL …/install-gh-aw.sh | bash，见 install.md）：\n源文件：Markdown + YAML front matter；变更后须 gh aw compile 生成 .lock.yml（勿手改）。 engine: copilot（亦支持 claude、codex、gemini 等）。 生产建议 strict: true；主 job 禁止 直接 issues: write / pull-requests: write / contents: write。 Job summary 中的 MCP Gateway、Agent Workflow Firewall (AWF)、Firewall Activity 与 安全架构 一致。 怎么做 # --- on: issues: types: [opened] safe-outputs: add-labels: allowed: [effort:small, effort:medium, effort:large] add-comment: max: 1 engine: copilot strict: true --- Read the issue; apply exactly one effort:* label and one brief comment. gh aw compile git add .github/workflows git commit -m \u0026#34;feat: agentic effort labeler\u0026#34; 常见误区 # 手改 .lock.yml——下次 compile 会覆盖，且可能绕过校验。 使用已弃用的 githubnext/gh-aw 安装路径——以 github/gh-aw 文档为准。 图：Workflow run summary 展示 safeoutputs-add_labels、MCP Gateway 与 Firewall Activity。\n图：jairosvg 仓库文件树含 copilot-instructions.md 与 workflows。\n自定义 LabelOps：/effort 与 Effort Labeler # 为什么 # 并非每个 Issue 都需要 coding agent 写代码。体量估算、实现提示、标签分类适合 只读推理 + 受限写入（标签/评论），供排期或后续再 assign coding agent。\n机制与约束 # /effort 是演示仓库自定义 slash，非 GitHub 全局内置命令；官方仅提供 on.slash_command 范式（triggers、safe-outputs）。 历史 issue #154 上可见 effort: medium 与实现要点（BufferedImage、bbox 等）——演示 OCR。 演讲者观点：现场 demo 曾遇「Effort Labeler is disabled」；workflow 列表可作旁证，无官方「必成功」保证。 怎么做 # 在 Issue 评论输入 /effort（仓库已配置对应 agentic workflow 时）。等效事件载荷概念：\n{ \u0026#34;comment\u0026#34;: { \u0026#34;body\u0026#34;: \u0026#34;/effort\u0026#34; }, \u0026#34;issue\u0026#34;: { \u0026#34;number\u0026#34;: 164 } } 常见误区 # 把 Effort Labeler 当成开箱即用的 GitHub 产品功能。 workflow 被 disable 后仍指望评论触发——需先检查 Actions 启用状态与 default branch 上的 lock 文件。 图：Issue #154 评论含 swap BufferedImage allocation 与 Generated by Effort Labeler。\n图：jairosvg 仓库 Actions 工作流含 Copilot coding agent 与 Copilot Setup Steps。\n/fleet：CLI 内并行 sub-agent # 为什么 # 单线程 LLM 处理多语言翻译、多模块脚手架时墙钟时间过长。/fleet 由主 agent 拆分子任务，sub-agent 并行执行（fleet 概念）。\n机制与约束 # 主 agent 作 orchestrator，负责依赖与合并。 并行放大 prompt 漂移（例如对 \u0026lt;code\u0026gt; 块误加 RTL CSS）——需在 AGENTS.md / copilot-instructions.md 写明禁止项。 UI 上并行任务列表 不一定对应当前 demo 仓库（演讲者已提示读图核对）。 怎么做 # copilot --yolo -p \u0026#34;/fleet translate the UI to Portuguese and Spanish\u0026#34; 主 agent 典型分解（演讲者演示归纳）：i18n 配置 → messages.properties → Thymeleaf → 语言切换 CSS → ./mvnw test。\n常见误区 # 忘记把 --yolo 传给需要自动执行子任务的会话——子 agent 可能停在确认步骤。 未在指令中约束并行子 agent 的目录边界，导致多子任务改同一文件冲突。 图：javaevolved.github.io 相关 PR 与多语言翻译类提交。\nCopilot SDK for Java：协议握手与 JMeter 插件 # 为什么 # 当 agent 需要嵌进 已有 Java 桌面/服务器产品（JMeter GUI、Office 插件、浏览器扩展）时，解析 CLI stdout 不可靠：需要稳定会话、权限回调与 协议版本握手。官方 copilot-sdk-java 通过子进程启动 Copilot CLI 作 server；社区仓库 copilot-community-sdk/copilot-sdk-java 已 archived 并指向官方库。\n机制与约束 # Maven 坐标（README）：com.github:copilot-sdk-java；要求 Copilot CLI 1.0.17+、Java 17+（推荐 JDK 25 以使用 virtual threads）。 源码 CopilotClient.verifyProtocolVersion：MIN_PROTOCOL_VERSION = 2，经 connect / ping RPC 交换 protocolVersion；不匹配则 fail fast（与现场堆栈一致）。 官方 README「Projects Using This SDK」列出 brunoborges/jmeter-copilot-plugin——集成模式存在；57.2/sec 等指标仅来自演示 OCR。 怎么做 # import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.json.*; try (var client = new CopilotClient()) { client.start().get(); var session = client.createSession( new SessionConfig() .setModel(\u0026#34;auto\u0026#34;) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(); var result = session.sendAndWait( new MessageOptions().setPrompt( \u0026#34;Emit JMeter .jmx: 50 users, 5 min, GET http://localhost:8080/\u0026#34;)) .get(); // SaveService.loadTree(...) → 挂到 JMeter 测试树 } 排错（可复现日志）：对齐 SDK 与本地 copilot CLI 版本。\nSDK protocol version mismatch: SDK expects version 2, but server reports version 3 Please update your SDK or server to ensure compatibility. 常见误区 # 继续使用已归档社区坐标或旧包名 com.github.copilot.sdk（以官方 README 为准）。 升级 CLI 却不升级 SDK（或反之）——协议号变化会直接导致启动失败，而非「模型回答变差」。 图：JMeter 日志 org.apache.jmeter.copilot.CopilotChatService 与 verifyProtocolVersion 堆栈。\n图：终端完整异常 SDK protocol version mismatch。\n图：演示加载生成的 Test Plan 后 Aggregate Graph 显示 50 samples（演示指标，非基准承诺）。\nCLI 还是 SDK：选型与仓库级契约 # 为什么 # 全 CLI 难以嵌入产品 UI；全 SDK 又浪费在一次性脚本上。更稳妥的是：探索与自动化脚本用 CLI；需要进程内会话、权限 UI、协议错误回流用 SDK。官方分别定位终端交互与 程序化控制 Copilot CLI，无单一「决策矩阵」页——下表为工程归纳。\n场景 倾向 理由 本地试错、REPL、cron CLI 零嵌入成本，/plan /fleet 开箱 桌面/IDE 插件内 Chat SDK 协议握手、Session、PermissionHandler Issue → PR Cloud agent + 仓库指令 平台托管环境 + MCP CI 只读推理 + 打标签 gh aw + safe-outputs 最小写权限 机制与约束 # .github/copilot-instructions.md：仓库级自定义指令，cloud agent 与 code review 可消费（可用 frontmatter excludeAgent 排除）。 AGENTS.md：CLI 从仓库根或 COPILOT_CUSTOM_INSTRUCTIONS_DIRS 读取；GitHub 仓库亦可多处放置，路径最近者优先（CLI 自定义指令）。不宜夸大为「IDE/CLI/云端 100% 同文件同语义」——IDE 另有独立配置路径。 MCP：cloud agent 文档警告第三方 MCP 可能影响 performance and quality，建议 tools allowlist。演讲者转述：keynote 中「200+ 工具反而增错率」未能在官方文档中找到同等表述。 # AGENTS.md（示例片段） - Java release: 25 (see pom.xml). - Build: ./mvnw test - Never apply RTL to \u0026lt;pre\u0026gt;/\u0026lt;code\u0026gt; (i18n). - UI changes: prefer Playwright MCP on localhost; attach screenshot in PR when applicable. 常见误区 # 堆叠过多 MCP 服务器却不配 allowlist——工具发现噪声上升，延迟增加。 只写 copilot-instructions.md 却忽略 CLI 侧的 AGENTS.md（或相反），导致本地与云端行为分裂。 图：JavaOne 现场幻灯片「GitHub Copilot for JetBrains Roadmap」——2026 Q2 提及 Agents.md 与 Background agents powered by the Copilot CLI（路线图，非当前 GA 承诺）。\n演示失败同样是边界样本 # 两场「失败」反而标出了工程边界：\nEffort Labeler 未跑通——自定义 workflow 的生命周期与 Actions 启用状态相关，不是模型能力问题。 JMeter 插件 SDK v2 / CLI v3 不匹配——说明集成点是 版本化的 RPC 协议，而非聊天 API 字符串。 把这两类错误纳入 runbook（对齐 copilot-setup-steps、对齐 SDK/CLI、检查 workflow disable），比只看 happy path 更能指导生产落地。\n参考与延伸阅读 # Copilot cloud agent 概念 启动 cloud agent 会话（含指派 Issue） Cloud agent 的 MCP 默认能力（含 Playwright） 自定义 cloud agent 环境（copilot-setup-steps） Ubuntu 24.04 runner 软件列表（Java 版本） 使用 Copilot code review 配置 cloud agent 校验与二次审查 Copilot CLI 命令参考（slash 表） Copilot CLI 权限与 YOLO /fleet 并行 sub-agent 概念 github/gh-aw 仓库 Agentic Workflows 安全架构（AWF / MCP Gateway） 官方 Copilot SDK for Java CopilotClient 源码（协议握手） 仓库 copilot-instructions.md Awesome Copilot 社区合集 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-copilot-in-your-java-tooling-from-cli-to-sdk-to-plugins/","section":"文章","summary":"把 Copilot 嵌进 Java 工具链：从终端 CLI 到 SDK 与插件","title":"把 Copilot 嵌进 Java 工具链：从终端 CLI 到 SDK 与插件","type":"posts"},{"content":" 摆脱多栈陷阱：用 Java 现代化桌面 UI，而不必拥抱全量 React 重写 # 大型 Swing、JavaFX 或 RCP 应用往往带着十年以上的业务逻辑、MVC 分层和机构知识。它们的问题很少是「业务算错了」，而是交付通道仍绑在桌面、Citrix 或 RDP 上。与此同时，「上浏览器」的默认答案常常是：把后端 REST 化，再用 React 或 Angular 重写整块 UI——于是你得到两套构建、两套流水线、两套招聘画像，以及一条在联调时很难在 Java 里对 UI 下断点的边界。\n另一条路径是：先用 Webswing 把现有 GUI 搬进浏览器，再用 webforJ 按屏逐步替换高频界面，全程以 Java 为主栈。讲者 Stephan Wald（BASIS International、webforJ 创建者）在 JavaOne \u0026lsquo;26 的论证并非「抵制一切前端技术」，而是把组织成本写进架构选型：当团队已经是 Java 中心、且遗留 UI 体量巨大时，用渲染桥接 + 渐进替换，往往比默认 REST+SPA 更可控。本文整理该组合的工程叙事，并对照官方文档标出可核验与未验证的边界；文中未展开 HTTP 版本化或移动端适配，因会议未涉及相关内容。\n图：开场标题「Escape the Multi-Stack Trap / Modernizing Java UIs Without JavaScript」。\n遗留系统的真实敌人：交付通道，而非业务逻辑 # 为什么：全量重写在 ERP、制造、金融类系统里常撞上「第三年仍在追第二年范围」的执行风险；更糟的是，懂领域规则的人会在漫长迁移中流失，团队多年只做特性对等、零新业务（演讲者观点；规模区间「50 万–200 万行」亦为口述，无独立公开基准）。\n机制/约束：已分层良好的 Java GUI 里，服务层与领域模型往往仍可复用；真正昂贵的是 UI 语义、桌面文件对话框、MDI 窗口模型与运维交付方式。Webswing 与 webforJ 的产品叙事都强调「低风险上 Web」，与「推倒重来」形成对照——见 Webswing 现代化框架。\n怎么做：先把「能否在浏览器里跑起来」与「是否用新框架重写每个屏」拆开决策；用业务价值（访问性、部署、协作）驱动优先级，而不是用技术时髦度驱动全替。\n常见误区：把「legacy」等同于「该扔掉」；在未度量使用频率前就承诺全 ERP 替换。\n图：幻灯片「Why \u0026lsquo;Just Rewrite It\u0026rsquo; Is the Wrong Default」——Scale (500K–2M)、Execution Risk、Knowledge Loss、Opportunity Cost。\n多栈税：REST 边界上的组织摩擦 # 为什么：经典路径把 Swing 应用拆成「Java API + JS SPA」。Java 团队维护业务与端点；JS 团队维护 React 状态与构建链。每个特性都要跨 REST 谈判契约；每个缺陷都可能变成「前端还是后端」的扯皮（演讲者观点；webforJ 官网亦强调单栈 Java 叙事，见 Client/Server Interaction）。\n机制/约束：双栈不仅是多一门语言，而是双倍 CI/CD、双倍依赖升级面、双倍安全补丁节奏。当资深前端离职，Java 团队可能被迫维护一份自己并不擅长的 React 代码库——这是组织层面的单点风险，而非单纯语法问题。\n怎么做：若战略目标是「Java 团队能独立交付 Web」，应把 UI 事件处理拉回 JVM（webforJ 模型），或先用 Webswing 零改源码 获得浏览器交付，再规划混合期。\n常见误区：以为「只多一个 React 项目」成本可控；低估 API 版本化、DTO 映射与 E2E 测试的长期税。\n图：「The Multi-Stack Tax」——Java Team 与 JavaScript Team 并列，REST 边界与维护风险。\nWebswing：服务端 GUI、浏览器 Canvas # 为什么：业务方常要求「明天能在浏览器里用」，而不是等十八个月重写。Webswing 定位是不改应用源码即可在浏览器运行 Swing、JavaFX、SWT 等桌面栈（官网 表述 without changing a single line of source code）。\n机制/约束（分层理解）：\n层级 官方可核对表述 演讲者补充（未在文档逐字出现） 渲染 服务端捕获 GUI，经 HTML5 canvas 流式传到浏览器 在 AWT 渲染层挂钩、截取绘制指令 传输 Webswing Server + 浏览器会话 类比「Java 版 RDP」 部署 Admin / 配置文件指定 classpath、mainClass、JVM 参数 捆绑 Jetty（演示站响应头可见 Jetty） webforJ 集成概述 原文：renders the desktop app on the server and streams the interface to the browser using HTML5 canvas。实现层「AWT 注入」宜视为演讲者机制类比，写作时以「服务端 GUI + Canvas 流」为准。\n怎么做：\n# 本地验证 demo 是否监听（URL 因安装而异） curl -I http://localhost:8080/webswing-demo/ curl -I http://localhost:8080/admin 在 Admin 或配置文件中设置 Application Name、Main Class、Classpath、Home Directory、JVM Arguments（字段见 Setup）。桌面 JFileChooser 在浏览器侧通常映射为上传/下载（File handling）。\n常见误区：期待 Swing 像素级「像原生 Web 组件」；忽视文件对话框、窗口装饰、模态等语义差异，需用 Webswing API 单独适配。Demo 中的 Configure window（Parent、Undecorated、Maximized、Modality）说明运维侧可调窗口行为，但无法替代产品层对「Web 原生交互」的预期管理——应在 PoC 阶段让业务方亲测打印、字体与多显示器场景（演示页含 Printing、Rendering mode）。\n图：Chrome 中 localhost:8080/webswing-demo — Webswing API、Toggle Rendering mode / native rendering。\n图：Webswing Demo — Configure window、Parent (-/Undecorated) (/ Maximized)。\nWebswingConnector：混合 UI 的薄集成层 # 为什么：全屏旧 UI 能跑起来后，产品仍需要现代导航、深链接和新表单。一次性重写所有屏不现实；需要在同一 Web 应用壳里嵌入已部署的 Webswing URL。\n机制/约束：WebswingConnector 自 webforJ 25.10 起提供（Release 25.10）。典型形态：@Route + Composite\u0026lt;Div\u0026gt;，构造时传入 Webswing 基址，setSize 后加入 getBoundComponent()。跨栈协作应优先使用文档中的 performAction / onAction（Communication），而非假设默认同 JVM 共享对象。\n演讲者观点（文档未保证）：Webswing 与 webforJ 若部署在同一 Web 服务器 / 同一 JVM，可共享 ClassLoader 并直接传递 session 上下文；官方默认示例多为分端口 + CORS，合并部署需自行验证类加载与 Swing EDT 规则。\n怎么做：\n@Route public class SwingAppView extends Composite\u0026lt;Div\u0026gt; { public SwingAppView() { var connector = new WebswingConnector(\u0026#34;http://localhost:8080/myapp/\u0026#34;); connector.setSize(\u0026#34;100%\u0026#34;, \u0026#34;600px\u0026#34;); getBoundComponent().add(connector); } } 生产环境请按 Setup 配置 allowedCorsOrigins，避免与 webforJ 默认 8080 端口冲突（演示中常先停 Webswing 再 mvn jetty:run）。\n常见误区：把「8 行幻灯片」当成无需 webforj-webswing 依赖；忽略双向消息协议，仍用 REST 绕一圈。\n图：幻灯片 — SwingAppView、new WebswingConnector(\u0026quot;http://localhost:8080/myapp/\u0026quot;)、docs.webforj.com/docs/integrations/webswing/overview。\n图：浏览器 — Webswing Modernisation DevX、webswing.webforj.com、表格与 Edit record。\nwebforJ：单栈 Java 写现代 Web UI # 为什么：混合期之后，高频屏需要响应式布局、路由、数据绑定与 Spring 集成，同时让 Swing 背景开发者沿用「组件 + 事件」心智，而不是强制团队分裂。\n机制/约束：\n应用类继承 App，在 run() 中创建 Frame，挂载 Paragraph、Button 等组件；Button.onClick 等事件由客户端发往服务端，handler 在 JVM 执行（与「薄客户端」产品表述一致）。 默认 WAR 打包；Archetype 25.11 模板为 jetty-ee11-maven-plugin、Jetty 12.1.2（以 Maven Central 工件为准；现场 OCR 曾出现 Jetty 12.2.2，以 Central 为准）。 路由：当前模板使用 @Routify 扫描包 + 视图类 @Route（演讲 OCR 中的 @Routes 已演进，见 Routing）。 怎么做：\n@AppTitle(\u0026#34;Simple Counter\u0026#34;) // 新模板可能为 @AppProfile — 以文档为准 public final class Application extends App { private int count = 0; private final Paragraph text = new Paragraph(\u0026#34;Count: 0\u0026#34;); private final Button button = new Button(\u0026#34;Counter\u0026#34;); @Override public void run() throws WebforjException { var mainFrame = new Frame(); mainFrame.add(text, button); button.onClick(e -\u0026gt; { count++; text.setText(\u0026#34;Count: \u0026#34; + count); }); } } mvn archetype:generate \\ -DarchetypeGroupId=com.webforj \\ -DarchetypeArtifactId=webforj-archetype-sidemenu \\ -DarchetypeVersion=25.11 \\ -DgroupId=io.example -DartifactId=demo cd demo \u0026amp;\u0026amp; mvn jetty:run curl -I http://localhost:8080/ 能力面（均有文档入口，但「Zero deployment」应理解为开发期热部署，见 Deploy / Live Reload）：Dynamic Web Client (DWC)、数据绑定、Spring、Kotlin DSL、AI / MCP 插件（演讲未演示 MCP 配置）。幻灯片口号「No HTML. No CSS. No JavaScript」与官方 Styling / Client Components 文档并存——应理解为默认路径在 Java，而非运行时零前端资源。\n常见误区：期待 BorderLayout 思维直接平移——webforJ 以 FlexLayout 等响应式模型为主（讲者坦承短学习曲线）；把 MCP 当成生产运行时依赖。\n图：幻灯片 — Do It All in Java.、Application extends App、Button button = new Button(\u0026quot;Counter\u0026quot;)。\n图：同一 Counter 示例运行中显示 Count: 8。\n图：New Project — webforj-archetype-sidemenu、webforj-archetype-hello-world、webforj-archetype-tabs。\n图：生成项目 — \u0026lt;packaging\u0026gt;war\u0026lt;/packaging\u0026gt;、io.skillsplot / Session、Maven 结构。\n图：Archetype 视图 — package io.skillsplot.views、public class InboxView。\n渐进现代化路线图 # 为什么：技术选型需要可停驻的阶段，避免「全有或全无」的范围蔓延。Webswing 官方框架与 webforJ 现代化教程 均描述分阶段替换；讲者补充：低频屏可长期留在 Webswing（官方教程终点倾向逐步淘汰遗留，二者为策略选择而非矛盾）。\n机制/约束：\n阶段 目标 典型周期（演讲者口径，非 SLA） 1 Web-enable Webswing + classpath/mainClass 天–周 2 Hybrid @Route + WebswingConnector + 双向消息 月级 3 Refresh 高频屏 webforJ 原生 + 复用服务层 数月 4 Retain 低频屏可不碰 按需 怎么做：内部指定 champion，先选一个最痛的应用做 Webswing PoC；并行用 Archetype 起一个 webforJ 侧栏壳；按菜单 telemetry（若有）排优先级。Webswing 营销页写「15 分钟」上手、讲者口语有「明天进浏览器」——应理解为环境就绪 + 小型应用的理想情况；含复杂登录、打印、多模块 classpath 的企业应用仍需按周规划集成与回归测试，不宜写进对外 SLA。\n常见误区：把「明天上线」当成合同条款（讲者自嘲法庭责任）；跳过 CORS/端口规划导致 PoC 通过后生产卡住。\n图：「Do It All in Java.」— Webswing runs your existing app in the browser today；webforJ builds your new screens in pure Java。\n图：「webforJ: The Honest Trade-off」— 增量采用、Spring/Kotlin/AI，以及 FlexLayout vs BorderLayout。\n案例信号：Prodin 与未验证边界 # 演讲者介绍（未独立核实）：荷兰 ERP 厂商 Prodin（Patrick Aries）在否决 low-code（全量重写 + 约束）与 Angular/React（多栈）后选用 webforJ；6 名 Java 开发、约 4 个月、0 行 JavaScript 交付首批模块；500+ 菜单的 Swing MDI 仍在部分制造楼层运行。否决理由与数字均来自会议口述，无在本次检索到的公开案例研究中交叉验证。\n图：「Prodin: 40 Years of ERP, Now on the Web」— 否决 Low Code 与 Angular/React，选用 webforJ。\n写作时应保留的未验证边界：\n安全 / SSO / 多租户：仅提及可集成，无演讲内配置 并发会话、延迟、横向扩展：无基准；Citrix 类比为演讲者观点 SEO、移动端：未讨论 与 Vaadin / Hilla / JSF 对比：讲者称未做直接对比测试（选型时需自行 PoC） Webswing 线协议与线程模型：无公开 wire spec AWT 层实现、同 JVM 共享 ClassLoader：文档未支持默认可用 周一可执行的三步实验 # 若你负责遗留 Java GUI 的现代化路线，可把采购决策推迟，先做可复现实验：\n下载 Webswing，按 Quickstart 挂载一个内部 Swing 应用（classpath + mainClass）。 用 Getting Started 生成 Archetype 项目，mvn jetty:run 熟悉 App / Frame / @Route。 跟随 Webswing 现代化教程，在本地接 WebswingConnector，并对照 webswing.webforj.com 混合 Demo。 这三步分别验证「零改源码上 Web」「单栈新屏开发」「壳 + 遗留 Canvas 共存」——比一次性选型会议更能暴露组织与架构约束。若 PoC 成功，再讨论是否引入 Spring Boot（Spring 集成）统一服务层，以及是否在混合期保留独立 Webswing 进程；若 PoC 失败，失败原因也会比「先签三年 React 外包」更早浮出水面——通常是 EDT 线程假设、本地 JNI、或极端自定义绘制，而非 Java 语言本身。\n参考与延伸阅读 # webforJ 文档首页 webforJ — Getting Started webforJ — Client/Server 交互模型 webforJ — Webswing 集成概述 webforJ — Webswing 配置与 CORS webforJ — Webswing 双向通信 webforJ — 渐进现代化教程 webforJ — 路由与深链接 webforJ — 数据绑定 webforJ — Spring 集成 webforJ — Archetype 一览 GitHub — webforj 25.10 Release（Webswing 集成） Maven Central — webforj 版本元数据 Webswing 官网与零改码叙事 Webswing — 现代化框架（Web-Enable / Extend / Facelift / Rebuild） Webswing — 文件处理与 JFileChooser 映射 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-modernizing-java-uis-without-javascript-escape-the-multi-stack-trap/","section":"文章","summary":"摆脱多栈陷阱：用 Java 现代化桌面 UI，而不必拥抱全量 React 重写","title":"摆脱多栈陷阱：用 Java 现代化桌面 UI，而不必拥抱全量 React 重写","type":"posts"},{"content":" 半结构化检索上的 Agent：STaRK 基准与 AvaTaR 优化 # 当知识库同时承载自由文本与显式关系（商品属性、论文引用图、药物–蛋白–疾病边）时，「接好向量库 + function calling」并不自动等于会检索。Stanford 博士生 Shirley Wu 在 STaRK 与 AvaTaR 两条工作线上，把问题拆成可测的 benchmark 与可优化的工具策略；下文综合其公开论文与一线工程讨论，不强行收束为单一结论——访谈主张与已发表表格存在张力处会标明。\n画面可见 Weaviate Ailantir 9, we * Mle, n \\o \u0026ldquo;, a) 4 等叠字条；双人对谈开场，无独立架构幻灯片。\n问题空间：为何需要「检索 Agent」专用基准 # 为什么：企业知识库越来越像半结构化知识库（SKB）——节点带多段文本、多属性，边表达品牌、适应症、合著关系。传统做法把 textual retrieval（BM25 / dense VSS）与 relational retrieval（text-to-SQL、图遍历）分开评测，却回避了真实查询的并集需求：「Nike + cute design」既要结构过滤又要语义匹配（演讲者观点）。\n机制/约束：STaRK 在三个 SKB 上统一检索任务：STaRK-Amazon（商品）、STaRK-MAG（学术图谱子图）、STaRK-Prime（基于 PrimeKG 的生物医学图）。指标沿用检索文献口径：Hit@k（正确答案是否出现在 top-k）、Recall@k（top-k 覆盖的相关项比例；合成集常用 k=20）、MRR（首个相关结果的平均倒数排名）——见 STaRK 论文 实验节。\n怎么做：用官方 snap-stanford/stark 的 eval.py 在同一套查询上对比 BM25、ada-002 单向量 VSS、图神经网络基线与 agent 管线，避免各域各写一套脚本。\n常见误区：把 STaRK 当成「又一个 RAG 问答集」。它是 entity-level retrieval（返回节点/商品/论文 ID），中间是否调用 SQL 或向量搜索由系统决定，最终仍按 Hit@1 / Recall@20 打分。\nflowchart LR Q[Query] --\u0026gt; A{Agent policy} A --\u0026gt; T[Textual tools\u0026lt;br/\u0026gt;VSS / BM25] A --\u0026gt; R[Relational tools\u0026lt;br/\u0026gt;filter / traverse] T --\u0026gt; SKB[(Semi-structured KB)] R --\u0026gt; SKB SKB --\u0026gt; M[Hit@k / Recall@k / MRR] 约 8 分钟处双人对谈画面：左侧 Weaviate podcast 标识与麦克风，右侧嘉宾办公室白板背景；无技术示意图。\nAgent 的操作性定义与检索分工 # 为什么：「Agent」一词被泛化到单次 completion。若优化对象模糊，prompt 搜索与架构搜索都无法归因。\n机制/约束（演讲者观点）：Agent ≈ LLM + 多步工具使用 + 对环境有可验证的改动；检索 agent 特指在 SKB 上选择 textual tool（语义搜索）与 relational tool（属性过滤、邻域扩展）的序列。与之对照的是手工 workflow（固定 DAG）与 compound multi-agent（多角色、多中间产物）；后者在 STaRK 式端到端指标下，中间步失败可能被最终答案掩盖（演讲者观点）。\n怎么做：为每个 tool 写清输入 schema 与失败语义——访谈中 zero-shot 常混淆「向量相似」与「metadata filter」，类似 Weaviate 上 nearText 与 where 的分工（主持人实践，非 STaRK 论文要求）。\n常见误区：用「是否调用了 API」判定 agent 能力。关键在 action sequence 是否随查询模式变化，而非调用次数。\nOCR 可见 Milamtic Uy, * Mae, * ae G FECECCR ipeeetiet；讨论属性图与 message passing 时段。\n多向量检索：Recall 上去，Top-1 仍可能不够 # 为什么：单段文本 embedding 会稀释标题、品牌、规格等不同粒度的信号；对节点多字段分别嵌入再聚合（multi-vector）是自然基线。\n机制/约束：AvaTaR 论文 Table 2 对比 ada-002 与 multi-ada-002（多段文本分别嵌入后聚合）：Amazon 上 Recall@20 从 53.29% 升至 55.12%，Hit@1 从 39.16% 升至 40.07%；Prime 上 Recall@20 +2.05pp、Hit@1 +2.47pp。但 MAG 子集 Hit@1 反而从 29.08% 降至 25.92%，说明「多向量单调改进」不成立。相对 AvaTaR 优化后 Amazon Hit@1 49.87%，multi-ada-002 的 40.07% 仍差约 10 个百分点——与「召回改善、首位仍不够」的表述方向一致（演讲者观点；数值来自论文，非访谈口述）。\n怎么做：把 multi-vector 当作 召回层，上层仍需要查询分解（divide-and-conquer）与关系过滤；勿用 Recall@20 单独证明产品可用。\n常见误区：增加 embedding 字段数 = 解决 relational 约束。结构条件（品牌、适应症）往往要先 定位锚点节点 再扩邻域。\nOCR 片段 po Msatle Uayy, % —_—— % @=；对应 multi-vector 与 Hit@1 讨论时段。\n工具齐全的 Agent 何时输给 Dense Retriever # 为什么：若 agent 劣于单向量检索，则 prompt 优化比堆工具更重要——这是 AvaTaR 的动机之一。\n机制/约束：\n演讲者观点（一手经历）：团队在 STaRK 上实现的 zero-shot tool agent（含 prompt engineering / ICL）曾 明显差于 simple dense retriever（~29:01 段）。 论文部分冲突：同文 Table 2 中 ReAct（Claude 3 Opus）在 Amazon / MAG / Prime 的 Hit@1 均不低于 ada-002 VSS（例如 Amazon 42.14 vs 39.16）。因此「agent 一定输给 dense」不能从已发表 ReAct 行直接推出；更可能指向 早期自研 agent（工具描述、函数边界、未优化策略）与论文基线配置差异。 定性支持：AvaTaR 引言与 Figure 2 指出手工 mega-prompt 的 ReAct 在商品查询上易产生 trivial / misleading 答案，且难跳出 LLM 先验工具模式（论文）。 怎么做：先用 最强简单基线（单向量 + 必要 metadata filter）锁定下限，再引入工具链；优化阶段用验证集上的 Recall@20 划分正负查询批（论文默认 ℓ=h=0.5，batch b=20）。\n常见误区：把 Table 2 的 ReAct 与访谈中的「失败 agent」混为同一实现。写作与复现时需写明 工具清单、模型版本、是否经过 AvaTaR 优化。\nOCR 可见 Ailaalir Hay, er, s 3 ‘a, \\ % — b) 4 . ute repre teetecsnt；agent 与 dense retriever 对比时段。\nAvaTaR：Contrastive Prompt Optimization # 为什么：标注轨迹太少时，SFT / RL 不现实；需要利用 一批查询上同一 action sequence 的成功与失败 归纳模式（如复杂查询要分解、何时走 relational tool）。\n机制/约束（AvaTaR 论文 §4，已验证）：\nActor LLM 对查询执行工具动作序列； 按 Recall@20（检索）或 Accuracy（部分 QA）将 mini-batch 分为 positive / negative； Comparator LLM 对两批做 contrastive reasoning，生成 holistic instruction，更新 actor 的策略与 tool description； 优化若干 epoch 后，取 验证集最优 action sequence / instruction 部署。 与 OPRO 的异同（演讲者观点；AvaTaR 正文未点名 OPRO）：二者都可维护多份候选并在验证集选优；OPRO 把历史解及分数写入 meta-prompt 让 LLM 生成新 prompt 文本，而 AvaTaR 的进化轴是 跨查询模式的 contrastive 批，优化对象是 可复用的 action / instruction，而非纯自然语言 prompt 种群。注意：要点文档曾误引 OPRO 为 arXiv:2306.03427，正确编号为 2309.03409。\n怎么做（概念最小闭环）：\n# 伪代码：单轮优化步 batch = sample_queries(train, b=20) for q in batch: trace[q] = actor.run(q, instruction=current_instr) pos = [q for q in batch if metric(trace[q]) \u0026gt;= threshold_high] neg = [q for q in batch if metric(trace[q]) \u0026lt; threshold_low] current_instr = comparator.contrast(pos, neg, traces={**pos, **neg}) best_instr = select_best_on_validation(candidates) 消融 AVATAR-C（去掉 comparator）在 STaRK 上 Hit@1 明显下降（论文 Takeaway 2）——comparator 不是装饰模块。\nOCR 可见 Aulus Hyp ex ry, ~ = G an =—@ =；contrastive optimization 讲解时段。\nOCR 可见 aN perks HEREC ALA Et ‘ - Ailantis Nyy；actor / comparator 模块讨论时段。\nOCR 可见 RALaOItE aypy_ ta, a %, — % EEEERSERNS TEEEER thet；与 OPRO 对照时段。*\n图遍历、GraphRAG 与一跳消息传递 # 为什么：对「Nike 鞋」查询，若只做 query–node 余弦相似度，可能命中无关 Nike 条目；先 锚定品牌节点 再沿边取邻域，精度通常更好（演讲者观点）。这与 GraphRAG「向量找种子 → 扩子图作 context」同构，可视为图上的 chain-of-thought（演讲者观点）。\n机制/约束：一层 message passing 只聚合 直接邻居；k 层对应 k-hop。演讲者用「鞋节点—价格邻居—图像节点」说明 hop=1 时价格属性可更新鞋节点表示，但不会传到无直连边的图像节点——该例为教学类比，未在 STaRK / AvaTaR 正文出现；机制本身与 PyG 文档一致。\n怎么做：把 relational tool 实现为 受限遍历（边类型、深度、节点类型过滤），而非让 LLM 自由生成 Cypher；agent 负责选「先 filter 后 expand」还是「先 semantic 后 filter」。\n常见误区：把 LLM 当 embedding 模型用；在需要分解的查询上，应优先消耗推理 token 做 查询拆分，而非加长单向量。\nOCR 可见 Milt Nyy, rg s os %, — te cet ecg (Abt tie；PrimeKG / 医学图检索讨论时段。\n约 37 分钟处嘉宾手势说明；背景仍为白板办公室，画面无流程图文字。\nMulti-agent、Workflow 与 Compound 系统的优化边界 # 为什么：同一任务可有上百种 agent 分解（researcher → writer 等），结构效应与随机性纠缠（演讲者观点）。Compound AI 中间产物多，端到端指标难以归因到单个 agent。\n机制/约束（演讲者观点）：\n反对 角色扮演式互聊；倾向 任务专精 agent（不同 prompt / 工具集）协作。 主持人路径：手工 workflow + DSPy / MIPRO 分步优化 + AvaTaR 精调 tool description——与「单 agent 自改 prompt」路线并存，无统一最优（访谈讨论）。 评测：仅看 input–output 会漏掉中间步错误；需对关键中间态加 metric 或 LLM-as-judge（AvaTaR 在部分 QA 子任务用 judge，见论文 §4）。 怎么做：compound 系统里 选择性优化 瓶颈 agent（通常是检索 / 规划），其余固定；避免同时搜索架构与 prompt。\n常见误区：agent 数量 ∝ 性能。访谈强调 兼容性：部分 agent 组合互斥，搜索空间需约束。\nOCR 可见 Ailantir 9, » ln = Op, ra, cc os ~ ‘Me —— G；workflow 与 few-shot 讨论时段。\n记忆库、kNN 轨迹与 contrastive 批 # 为什么：上下文变长是否淘汰 memory bank 尚无定论；演讲者仍保留「存成功 trajectory 的数据库式 memory」路线，因全塞进 context 成本高（演讲者观点）。\n机制/约束：\n演讲者观点：团队曾用 experience library，在测试时检索最相似轨迹做 few-shot，效果差——易学到 if Nike in query 类实例规则（过拟合式硬编码）。 论文对照：AvaTaR 正文无 “experience library” 专名；优化阶段有 memory bank 存历史最优 instruction/actions（防 actor 重复错误），不是 测试时 kNN 轨迹检索。基线 ExpeL 更接近「推理时检索成功/失败轨迹写入 context」，在 STaRK-MAG 上与 ReAct 相近、远低于 AvaTaR（论文 Takeaway 1 附近）——支持「检索相似轨迹弱于 contrastive 批优化」，但未逐字复述 Nike 案例。 怎么做：优先让 comparator 从 跨查询正负批 归纳模式；若用 few-shot，检索 workflow 模板 通常比检索完整轨迹更稳（演讲者观点），但仍需验证域外泛化。\n常见误区：把优化阶段 memory bank 与测试时 RAG 式轨迹库等同。\nOCR 可见 Atlante Ny Ss Np, rh, \u0026ldquo;a - : 4% _ Px x9! s；尾声 CollabLLM / 人机协作方向。\n约 40 分钟双人对谈；讨论 OPRO 与 compound retrieval 的时段，画面无表格。\n未收敛之处（刻意保留张力） # 主题 较一致 存在分歧或未核边界 STaRK 三域统一评测 论文 + 代码 多模态延伸为方向性表述 Multi-vector Amazon/Prime Recall↑ MAG Hit@1 反例 Zero-shot agent vs dense 引言 / 访谈定性 Table 2 ReAct ≥ ada-002 AvaTaR 机制 论文 Figure 1 / Table 2 各域提升幅度需查附录 OPRO 对照 访谈 AvaTaR 正文未点名 OPRO GNN 鞋/图示例 机制可对照 PyG 仅访谈类比 Experience library 访谈负向 ablation 论文以 ExpeL + memory bank 间接对应 若你要落地 # 先钉死基线：在同一 SKB 上跑通 ada-002 VSS + 必要属性过滤，记录 Hit@1 / Recall@20，再叠工具 agent；避免无下限地调 prompt。 拆分 textual / relational 工具契约：向量搜索与 metadata filter 的输入输出、失败码写进 tool schema，减少 zero-shot 混用（工程上可参考向量库的 hybrid 查询文档）。 用 contrastive 批优化「策略」而非堆 few-shot 轨迹：小标注集下，用验证集 Recall@20 划分正负批，迭代 instruction；慎用过拟合的测试时 kNN 轨迹库。 Compound 系统只优化检索/规划瓶颈：中间步加可观测 metric，避免端到端「看起来还行」掩盖检索失败。 公开复现：克隆 snap-stanford/stark 与 AvaTaR 附录配置，在 STaRK-Amazon 子集上复现 Table 2 一行再扩展到你自己的 SKB。 参考与延伸阅读 # STaRK 论文（arXiv:2404.13207） — 半结构化检索基准定义与三域任务 STaRK 项目站 — 数据集与 leaderboard 入口 STaRK GitHub（snap-stanford/stark） — 评测脚本与数据键 amazon / mag / prime STaRK Hugging Face 数据集 — 直接加载实验 AvaTaR 论文（arXiv:2406.11200） — contrastive prompt optimization 与 Table 2 基线 OPRO 论文（arXiv:2309.03409） — 进化式 prompt 优化对照 OPRO 代码（google-deepmind/opro） — 官方实现参考 PrimeKG 仓库 — STaRK-Prime 生物医学图源 PrimeKG 项目页（Harvard Zitnik Lab） — 整合数据源说明 PyTorch Geometric — Message Passing 教程 — k-hop 与层数约束 Weaviate 开发者文档 — 向量 + 过滤混合检索（播客生态语境） DSPy 项目 — 声明式 prompt / 程序优化（主持人实践路径） Gorilla（Berkeley 工具调用 LLM） — 工具学习早期 benchmark 参照 Dense Passage Retrieval（DPR, arXiv:2004.04906） — dense retriever 方法论背景 ReAct 论文（arXiv:2210.03629） — AvaTaR Table 2 中的 agent 基线来源 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-optimizing-retrieval-agents-with-shirley-wu-weaviate-podcast-115/","section":"文章","summary":"半结构化检索上的 Agent：STaRK 基准与 AvaTaR 优化","title":"半结构化检索上的 Agent：STaRK 基准与 AvaTaR 优化","type":"posts"},{"content":" 超大规模 Java 平台：从联邦 GraphQL 到 JVM 默认项 # Netflix 的流媒体业务把「发现内容」与「播放内容」拆成两条技术路径：前者以联邦 GraphQL 为对外数据面，后者走 Open Connect 等专用栈（演讲者观点，未在本文展开）。对 Java 工程师更有参照价值的是前者——数千个 Spring Boot 服务、开源 DGS 框架、以及把 GC、虚拟线程、框架升级做成「平台默认项」而非团队各自摸索的实践。\n本文不复述会议流程，而是把可迁移的决策点拆开：协议如何分工、平台如何在 Boot 上收敛行为、测试与迁移如何控制上下文体积、JVM 如何把尾延迟与调用超时联动、以及 Gen AI 如何嵌进已有 Spring 服务而不交出编排权。标为演讲者观点的条目（应用规模、内部 starter 坐标、Gutenberg/Hollow 运行时、IPC 毫秒级配置等）无法在公开环境复现；标为可核对的条目附官方文档、JEP 或开源仓库。\n图：开场幻灯片标题「How Netflix uses Java」与「2026 edition」字样。\n联邦 GraphQL：网关、DGS 与 gRPC 的分层 # 为什么 # 客户端需要一次请求拉齐首页（Lolomo）、影片元数据、图片等多域数据；各域团队又要独立发布。联邦构图把多个 subgraph 合成统一 supergraph，由网关做查询计划与扇出（Apollo Federation 说明）。演讲者观点：Netflix 在网关与各 DGS（Domain Graph Service） 之间用 GraphQL，DGS 再经 gRPC 扇出到订阅、推荐、观看历史等后端；播放路径不在此栈。\n机制与约束 # 对外契约：GraphQL Over HTTP 要求服务器接受 POST，客户端可用 Accept 协商 application/graphql-response+json 与 application/json。 域边界：每个 DGS 拥有自己的 schema 片段；联邦通过 _entities(representations: …) 等机制拼接字段（幻灯片示例）。 服务间：gRPC 以 .proto 的 service / rpc 建模，客户端可设 deadline（gRPC Core concepts）。 怎么做（最小示例） # POST /graphql HTTP/1.1 Content-Type: application/json Accept: application/graphql-response+json, application/json;q=0.9 {\u0026#34;query\u0026#34;:\u0026#34;query Home($id: ID!) { lolomo(profileId: $id) { rails { id } } }\u0026#34;,\u0026#34;variables\u0026#34;:{\u0026#34;id\u0026#34;:\u0026#34;...\u0026#34;}} @DgsComponent public class LolomoDatafetcher { @DgsQuery public List\u0026lt;Rail\u0026gt; lolomo(@InputArgument String profileId) { return catalogClient.railsForProfile(profileId); } } 常见误区 # 把联邦网关当成「万能 BFF」而在网关里写业务逻辑；或让 DGS 直接访问过多下游导致扇出爆炸。联邦的价值在 schema 所有权 与 可组合的查询计划，不在单点聚合所有 SQL。另一个隐蔽问题是：客户端查询深度不受控时，网关生成的查询计划可能在多个 subgraph 上重复拉取同一实体——需要在 schema 设计与 @key / @requires 使用上约束 N+1，而不是仅靠加大线程池硬扛。\n图：架构示意含「GraphQL Federated Gateway」、LOLOMO / Movie / Images DGS 及 gRPC 下游。\nPaved Road：在 Spring Boot 上叠加平台能力 # 为什么 # 演讲者观点：约 3000–4000 个 Java 应用共用开源 Spring Boot，再叠加内部 Spring Boot Netflix 模块。若每个团队自行集成安全、动态配置、可观测、gRPC 客户端，平台无法统一升级，事故面也分散。\n机制与约束 # Spring Boot Starters 约定 spring-boot-starter-* 命名；第三方应使用 thirdparty-spring-boot-starter 等形式。平台侧通过 auto-configuration 注入行为；复杂场景辅以 EnvironmentPostProcessor、BeanFactoryPostProcessor（演讲者观点，如 gRPC 客户端注册）。幻灯片中的 Paved Road 指共享的产品、实践与标准集合，供提供方与消费方对齐。\n怎么做（示意） # dependencies { // 内部坐标未公开；结构对齐官方 starter 模式 implementation(\u0026#34;com.netflix.spring:netflix-spring-boot-starter-security\u0026#34;) implementation(\u0026#34;com.netflix.spring:netflix-spring-boot-starter-observability\u0026#34;) } spring: application: name: my-service 常见误区 # 在业务模块里复制平台 starter 已提供的 Bean；或绕过 paved road 引入未审计的 HTTP/安全栈，导致升级时无法批量迁移。平台团队也常遇到反模式：把「可选扩展点」做成静默覆盖默认 Bean，使应用在升级 minor 版本时行为突变——对外应像官方 starter 一样，用条件注解与配置属性明确生效边界。\n图：幻灯片「The paved road」及对共享标准与工程杠杆的说明。\n集成测试：全栈真实性与启动成本的平衡 # 为什么 # GraphQL DataFetcher 到数据库的路径若只用 mock，回归会漏掉序列化、事务与权限组合问题。演讲者观点：主流做法是以 @SpringBootTest 做集成测试，并显式缩小加载范围。\n机制与约束 # classes 属性：只加载列出的 @Configuration / 组件（Javadoc）。 官方 test slice（如 @WebMvcTest）只引导 Web 层子集。 Netflix 侧：@EnableDgsTest、@EnableJooqTest（后者在公开仓库未检索到，视为内部切片）、Testcontainers 管外部依赖（演讲者观点）。 @SpringBootTest(classes = {IncidentsDatafetcher.class, JooqRepository.class}) @EnableDgsTest @EnableJooqTest class IncidentsDatafetcherTest { @Autowired DgsQueryExecutor queryExecutor; } 常见误区 # 每个用例 @SpringBootTest 却不写 classes，导致全量上下文缓存仍慢；或在 slice 测试里断言了未加载的安全过滤器行为。\n图：「Testing with Spring Boot」— @SpringBootTest(classes=…) 与 @EnableDgsTest 示例。\nBoot 2→3：依赖解析期的 Jakarta 改写 # 为什么 # Spring Boot 3.0 迁移到 Jakarta EE（如 Servlet 6.0）。生态里仍有仅含 javax.* 的旧 JAR，形成「库等应用、应用等库」僵局。\n机制与约束 # Gradle Artifact Transforms 在依赖解析末段改写产物。Netflix 开源的 Nebula Jakarta EE Migration Plugin（插件 ID：com.netflix.nebula.jakartaee-migration，可核对）在解析时对 legacy JAR 做包名迁移，底层使用 tomcat-jakartaee-migration。源码与构建脚本侧用 OpenRewrite UpgradeSpringBoot_3_0。演讲者观点：历时约两年基本完成全员 Boot 3；幻灯片写明「Fully on Spring Boot 3 now」。\nplugins { id(\u0026#34;com.netflix.nebula.jakartaee-migration\u0026#34;) version \u0026#34;2.0.1\u0026#34; // 版本以 Portal 为准 } 核实说明：公开插件 ID 为 com.netflix.nebula.jakartaee-migration，与部分口述中的 com.netflix.jakarta-transform 不一致，以 Gradle Plugin Portal 为准。\n常见误区 # 把字节码改写当成「可以永远不改源码」；API 语义变更、构建脚本、测试容器镜像仍须 OpenRewrite 或人工处理。仅改包名、实现未变的 JAR 才适合 transform。\n图：「Spring Boot 2 -\u0026gt; 3」— Gradle transform、OpenRewrite 与「Fully on Spring Boot 3 now」。\nAPI 形态：数据、方法与边缘 REST # 为什么 # 同一组织要同时服务「灵活取数的客户端」与「固定契约、低延迟的服务间调用」。一种协议包打天下会在某一侧过度妥协。\n机制与约束（演讲者观点 + 公开技术对照） # 场景 选型 心智模型 对外 / BFF 类 联邦 GraphQL + DGS Think in data 服务间 RPC gRPC Think methods 极简、短期暴露 REST 幻灯片以墓碑图示「REST IN PEACE」— 组织策略，非规范结论 service Catalog { rpc GetShow(GetShowRequest) returns (Show); } 常见误区 # 在 gRPC 服务上再包一层 GraphQL「方便调试」却不做 schema 治理；或用 REST 承载长期演进的领域 API 导致版本碎片。\n图：「DGS - GraphQL」对比 GraphQL 与 gRPC 心智模型，REST 标注为淘汰方向（演讲者观点）。\nDGS 与 Spring for GraphQL：执行管线合一 # 为什么 # 维护两套 GraphQL 执行栈会增加迁移成本与测试分歧。DGS 文档写明已与 Spring for GraphQL 内部整合：查询执行由 ExecutionGraphQLService 承担，DgsQueryExecutor 作为代理（DGS Spring GraphQL Integration）。\n机制与约束 # 生产：仍可用 @DgsQuery、@InputArgument 等注解（可核对 + 幻灯片 OCR）。 测试：@EnableDgsTest + DgsQueryExecutor，可 executeAndExtractJsonPath（Query Execution Testing）。演讲者观点：秒级反馈，无需完整 Boot 启动。 @DgsQuery public List\u0026lt;Show\u0026gt; search(@InputArgument SearchFilter filter) { return showsRepository.allShows().stream() .filter(s -\u0026gt; s.getTitle().toLowerCase() .startsWith(filter.getTitle().toLowerCase())) .toList(); } @SpringBootTest(classes = {LolomoDatafetcher.class, ShowsRepository.class, ArtworkService.class}) @EnableDgsTest class LolomoDatafetcherTest { @Autowired DgsQueryExecutor dgsQueryExecutor; } 常见误区 # 混用 Spring GraphQL 与 DGS 两套编程模型（官方文档明确不建议）；或在 @EnableDgsTest 中仍加载全应用却声称「轻量测试」。\n图：幻灯片「Now fully integrated with Spring for GraphQL」与 @EnableDgsTest / DgsQueryExecutor 代码。\nJDK 水位与 Generational ZGC # 为什么 # 演讲者观点：公司最低 JDK 17，多数服务跑 21 或 25（具体比例未公开）。在大型堆 + 极短 IPC 超时 下，G1 可能出现约 1.5s 级 STW，触发超时、重试与集群负载放大；切换 Generational ZGC 后 pause 与 IPC client errors 显著下降，并已成为默认 GC 策略（演讲者观点；与 OpenJDK 默认化方向一致，见下）。\n机制与约束（可核对） # JEP 439（JDK 21）：-XX:+UseZGC -XX:+ZGenerational。 JEP 474（JDK 23）：Generational ZGC 为 ZGC 默认模式。 JEP 490（JDK 24）：移除非分代 ZGC。 # JDK 21 java -XX:+UseZGC -XX:+ZGenerational -jar app.jar # JDK 23+ 仅 -XX:+UseZGC 即默认分代（见 JEP 474） java -XX:+UseZGC -jar app.jar 无法核对：幻灯片中的毫秒级 IPC 配置、Netflix 是否全量默认 ZGC。JEP 439 对比 G1 时指出 G1 pause 可从毫秒到秒级——这与「尾延迟敏感 + 短超时」叙事方向一致，但不能替代 Netflix 内部监控曲线的因果证明。\n常见误区 # 仅看平均 pause 而忽略 P99；或在未调整超时与重试策略时单独换 GC，仍把重试风暴归因于「下游不稳定」。换 ZGC 后 CPU 占用与分配速率可能变化，容量规划应连同堆大小与 -XX:SoftMaxHeapSize 等参数一并回归，而不是只盯 pause 曲线。\n图：「Generational ZGC: The new default」— G1 约 1.5s pause 与 IPC 超时、重试的关联（演讲者观点）。\n图：「More predictable GC -\u0026gt; lower error rates」— Max GC pause 与 IPC errors by endpoint 时间序列。\n虚拟线程：框架开关与 pinning 回滚 # 为什么 # 阻塞式 I/O 占主导时，平台线程池容易成为瓶颈。JEP 444 虚拟线程 用轻量线程映射大量阻塞调用。演讲者观点：曾因 thread pinning 在较早 JDK 上引发生产死锁而大规模回滚；修复后再通过 Tomcat、Spring 默认执行器、DGS DataFetcher 等框架层开启，业务代码零改动（尚无 rollout 后量化性能数据）。\n机制与约束 # Spring Boot 3.2+：spring.threads.virtual.enabled=true（DGS Virtual Threads）。 DGS：dgs.graphql.virtualthreads.enabled=true 使用户 DataFetcher 跑在新虚拟线程。 Pinning：JEP 491 在 JDK 24 交付，减轻 synchronized 导致的 pinning（可核对）。幻灯片写「JDK 25」— 与 JEP 491 的 Release 字段不一致，实施应以目标 JDK 发行说明为准。 常见误区 # 在 pinning 未缓解的 JDK 上全量打开虚拟线程；或忽视 DGS 文档 对 ThreadLocal（追踪、MDC、安全上下文）的警告。\n图：「Virtual Threads (JDK 25)」— Tomcat connectors、@DefaultExecutor、DGS data fetchers 与「Needs context propagation!」。\n结构化并发与上下文传播 # 为什么 # 虚拟线程之上若用 StructuredTaskScope 并行 fork，子任务默认不复制 Spring Security、Micrometer 等 ThreadLocal 依赖的上下文（演讲者观点）。代码看起来正确，鉴权与追踪却在子任务中丢失。\n机制与约束 # JEP 505（JDK 25 预览）：StructuredTaskScope.open() 等 API 需 --enable-preview。 JDK API 支持 Configuration.withThreadFactory(ThreadFactory)；Scoped Values 理想但难迁移存量框架（演讲者观点）。 可行方向：Micrometer Context Propagation 的 ThreadLocalAccessor 等在自定义 ThreadFactory 中复制上下文（工程方向可核对；Netflix 具体实现未公开）。 // JEP 505 风格示例；inheritContext 为演讲伪代码，非 JDK 标准 API try (var scope = StructuredTaskScope.open( StructuredTaskScope.Configuration.newBuilder() .withThreadFactory(ctxAwareFactory) .build())) { scope.fork(() -\u0026gt; downstreamClient.call()); scope.join(); } 常见误区 # 把 fork 当成普通 ExecutorService.submit 而不处理失败传播；或假设 Scoped Values 会自动替代所有 ThreadLocal 库。\n生产内嵌 Gen AI：受控的 agentic workflow # 为什么 # 「用 AI 写代码」与「线上 Java 服务调用 LLM」是不同问题。后者需要可审计、可限流、步骤可预期的编排（演讲者观点）。幻灯片 Agentic Workflows 强调：工作流由工程师掌控，LLM 只参与部分步骤，在引入 AI 的同时保持确定性与可控性（对照 Anthropic — Building effective agents）。\n机制与约束 # Spring AI 提供 ChatClient、Tool Calling、Model Context Protocol (MCP) 等构件；「agentic workflow」本身不是 Spring AI 定义的术语。\n@RestController class OpsAiController { private final ChatClient chatClient; @PostMapping(\u0026#34;/internal/ai/summarize\u0026#34;) String summarize(@RequestBody Incident incident) { return chatClient.prompt() .user(\u0026#34;Summarize mitigations for: \u0026#34; + incident) .call().content(); } } 常见误区 # 把 coding agent 直接连生产写路径；或未对 tool call 做权限边界与超时，把 LLM 不确定性扩散到数据面。\n图：「Agentic Workflows」—「You keep control of the workflow」与「Adding AI while keeping control and determinism」。\n启动剖析：从慢 Bean 到可执行的改造建议 # 为什么 # Spring 启动慢时，仅列出耗时 Bean 对第三方库往往不够 actionable。演讲者观点：内部启动分析服务在剖析数据上叠加 agentic workflow，结合库源码给出「问题类 / Maven 坐标 / 是否应异步化」等建议；实现基于 Spring + Spring AI。\n机制与约束（演讲者观点 + 幻灯片 OCR） # 控制台 UI（OCR 出现 com.netflix.gusto.Gusto，口述未明确产品名）展示 Startup Analysis Report 与 Top Recurring Bean Bottlenecks (Application Starts)。典型条目：\nevoMetadataCachedClient — Evolution Metadata Client；gutenbergSubscriber.startSubscriber() 在构造器中同步执行（演讲者观点：连接 Gutenberg 拉元数据）。 AssetArrivalDatesClientImpl — 对 Netflix Hollow / Cinder 消费者 buildAsync().join() 阻塞加载并建索引。 Hollow 公开定位为高性能只读内存数据集传播（com.netflix.hollow:hollow）。典型耗时 12s–58s（演讲者观点，outlier 更高）。\n@Bean @Lazy EvoMetadataCachedClient evoMetadataClient(EvoMetadataProperties p) { return new EvoMetadataCachedClient(p); // 避免构造器同步拉全量 } 常见误区 # 对所有慢 Bean 一律 @Lazy 而不改库内同步初始化；或忽略启动剖析与运行时热路径的差异。\n图：「Top Recurring Bean Bottlenecks (Application Starts)」— 排名第 1 的 evoMetadataCachedClient — Evolution Metadata Client。\nBoot 4 与平台侧升级编排（边界说明） # 演讲者观点：下一跳 Spring Boot 4 计划由平台集中 headless 运行 Claude Code，自建工作流引擎（多段 prompt、每步独立 subagent、checkpointing 保留 git 与对话状态）。Claude Code CLI 的 -p（print / 非交互）与 Checkpointing 文档支持「非交互」概念，不等于 Netflix 内部 netflix-upgrade-runner 等实现（未核实）。幻灯片背景可见「Boot 4.0 / Spring Fra…」字样，细节待官方发布说明为准。\n未展开：演讲仅预告 Project Leyden / AOT 由同事另场介绍，本文不臆测 Netflix 落地。\n参考与延伸阅读 # GraphQL 入门 GraphQL Over HTTP 草案（POST 与媒体类型） Apollo Federation 仓库说明（subgraph 与网关） gRPC 核心概念（service/rpc/deadline） Netflix DGS 文档 DGS 与 Spring for GraphQL 整合 Spring Boot 应用测试（含 test slice） Spring Boot 3.0 发布说明（Jakarta EE） Nebula Jakarta EE Migration Gradle 插件 OpenRewrite：升级到 Spring Boot 3.0 配方 JEP 439：Generational ZGC JEP 444：Virtual Threads JEP 491：消除虚拟线程 pinning（JDK 24） DGS：虚拟线程与 ThreadLocal 注意事项 Spring AI 参考手册（ChatClient / MCP） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-how-netflix-uses-java-2026-edition/","section":"文章","summary":"超大规模 Java 平台：从联邦 GraphQL 到 JVM 默认项","title":"超大规模 Java 平台：从联邦 GraphQL 到 JVM 默认项","type":"posts"},{"content":" 充分上下文：RAG 该测「够不够答」，而不只是「像不像相关」 # RAG 管线里，检索器把若干 chunk 拼进 prompt，生成模型再作答。工程上常把失败归咎于「没召回到」或「模型胡说」；评测侧则大量依赖 RAGAS 一类 relevance、faithfulness 指标。UC San Diego 与 Google 等合作者在 Sufficient Context: A New Lens on Retrieval Augmented Generation Systems（正文标注 Published as a conference paper at ICLR 2025）中提出：给定当前 context，模型是否应当能答这道题——与「片段与 query 多相关」不是同一维度。下文把论文可核对结论与访谈中的工程推断分开标注；数字优先对齐 Table 1 / Figure 2 / Figure 4 / Figure 6。\n播客封面：Weaviate 标识、麦克风与 #125 期号；本期主题为 sufficient context 与 RAG 评测。\n问题空间：检索质量、上下文充足性与拒答 # 关切 常见做法 本文强调的缺口 检索排名 nDCG@k、向量相似度 高相关 ≠ 信息够答题 生成忠实度 RAGAS faithfulness 忠实于错误前提的 context 仍可能答错 拒答 / 选择性生成 固定阈值、仅模型自评 单独用「不充分」门控会砍掉大量「不充分却答对」样本 生产日志里，三类失败常叠在一起：召回不足、context 充足但模型不会用（Table 4a：Gemma 在人工标为 sufficient 时仍有约 25.4% 被判为 hallucinate）、context 不充分却靠 parametric knowledge 答对（摘要写明 SOTA 模型在 insufficient context 下仍 35–62% 答对）。若只优化相似度或只训「不知道就说不知道」，容易在错误子集上优化。\n充分上下文 vs 相关性：概念与可测边界 # 为什么 # RAGAS 提供 context_precision、context_recall、faithfulness、answer_relevancy 等，但没有与论文同名的 sufficient context 二元标签。论文 §3.1 定义：实例 ((Q,C)) 为 sufficient，当且仅当存在 plausible 答案 (A\u0026rsquo;)，使得在 (C) 的信息下 (A\u0026rsquo;) 能合理回答 (Q)；允许多跳推理，不要求事先给出 ground-truth answer。这与 TRUE-NLI 式 entailment（给定答案 (A) 再判蕴含）不同。\n机制与约束 # 概念区分（访谈框架，非形式定理）：存在「相关但不充分」；嘉宾认为充分信息应相关，但论文未证明「充分 ⇒ 相关」的偏序。演讲者观点。 Table 1 中 TRUE-NLI（T5 11B）precision 高、recall 低，与「蕴含 ⇒ sufficient、逆不成立」的叙述一致（文献）。 怎么做（最小示例） # 用 LLM 作二元 autorater（论文 Table 1 用 Gemini 1.5 Pro 0/1-shot；大规模打标用 FLAMe-RM-24B）：\nGiven question Q and retrieved context C only: Does there exist an answer A\u0026#39; that Q can be reasonably answered from C alone? Reply: sufficient | insufficient 勿把「C 是否包含 GT 字符串」当作唯一规则：论文 Contains GT 准确率 0.809，仍低于 Gemini 1-shot 0.930（Table 1）。\n常见误区 # 用 embedding 相似度阈值替代 sufficiency 标注。 把 RAGAS context_precision 当作「够不够答」的代理指标。 分屏对谈：左侧 Weaviate podcast 角标，嘉宾侧讨论 sufficiency 与 relevance 的分工。\n叠字画面含 Weaviate 与 ANNI 片段；无清晰论文公式，仅作时间锚点。\n金标 autorater 与主实验数据集 # 为什么 # 要评「autorater 是否可靠」，需要小规模人工金标，再在大规模检索 context 上分析模型行为。\n机制与约束 # 金标集（§3.2）：115 条 ((query, context))，专家标 sufficient / insufficient；来源为 PopQA、FreshQA、Natural Questions、EntityQuestions——并非 HotpotQA / MuSiQue（后者用于 §4 主实验）。访谈若混述数据集名称，以论文为准。 主评估（§4.1）：FreshQA（True Premise, 452）、Musique-Ans（dev 500）、HotpotQA（dev 500）；检索管线为 FlashRAG + REPLUG + intfloat/e5-base-v2。 Table 1（金标 115 条）：Gemini 1.5 Pro 1-shot F1 0.935 / Acc 0.930；0-shot 0.878 / 0.870；FLAMe-24B 0.892 / 0.878。播客「80–90%」落在区间内但偏保守，宜写 87–93%。 怎么做（最小示例） # 分层统计前先固定 autorater（论文主分析用 Gemini 1-shot），再按 sufficient / insufficient 切分 Correct / Abstain / Hallucinate（LLMEval 语义判对错，非纯字符串匹配；见附录 B.3）。\n常见误区 # 认为金标与 HotpotQA 同分布——115 条与 500 条 dev 分析是两套构造。 用含 ground-truth answer 的 prompt 作生产默认（Table 1 显示有提升但仍弱于无答案 Gemini 1-shot）。 分屏访谈：主持人侧 Weaviate podcast 背景，讨论金标标注流程（画面无 Table 1）。\nOCR 叠字含 MINI HHH]‘ My 与 WN 等；不能代替读表，Table 1 以 PDF 为准。\n不充分仍答对：parametric knowledge 与 RAG 的耦合 # 为什么 # 若「不充分 ⇒ 应拒答或再检索」，会假设模型不会用预训练知识补洞——论文数据表明该假设不成立。\n机制与约束 # 35–62%（摘要）：SOTA LLM 在 insufficient context 下仍输出 correct（文献，§4.3）。 Table 2 定性：该现象大量来自 closed-book 本就能答对 的题——检索到的片段不够单独答题，但模型靠参数化知识过关。 反直觉（文献）：在模型无 context 本答不对时，塞入仍不充分的 context，有时反而「解锁」正确答案（访谈强调；机制为开放问题）。 怎么做（最小示例） # 对每条 query 记录四元组：(sufficient_label, rag_context, model_answer, llm_eval_correct)，单独汇报 insufficient ∧ correct 占比，勿与全量 accuracy 混报。\n常见误区 # insufficient 占比高就强制二次检索——可能删掉已靠参数化知识答对的样本。 把「答对」等同于「忠实使用了 context」。 OCR 含 Aly MI\\ Vt TZ) 等噪声；对应 insufficient-context 讨论时段，图表以论文 Figure 6 为准。\n分屏对谈中段；左侧书架与 Weaviate podcast 标识，无实验曲线。\nRAG 损害 abstention：检索越多，越不敢说「不知道」 # 为什么 # 工程直觉认为 RAG 降低幻觉；论文 §4.2 标题即 Models Abstain Less with RAG：加入 context 后模型更不愿 abstain，在 insufficient 子集上幻觉相对上升。\n机制与约束 # Gemma 2 27B（gemma-2-27b-it）在 HotpotQA 上（Figure 6，堆叠条解读）：Without RAG — Correct 65.2% / Abstain 24.8% / Hallucinate 10.0%；With RAG, insufficient — 37.9% / 11.9% / Hallucinate 50.2%（文献）。 播客口述「加检索后幻觉约 66%」在正文图表中无法精确对齐；最接近误读是把 64.1% correct（sufficient + RAG）听成幻觉率。未核实边界：除非有幻灯片，宜采用 Figure 6 数字。 Claude 等：无 RAG 时 abstain 84.1% → 有 RAG 52%（§4.2，文献）。 怎么做（最小示例） # 对比同一模型三条曲线：no_rag、rag_sufficient、rag_insufficient，分别报 abstain rate 与 hallucinate rate（论文用 LLMEval 管线）。\n常见误区 # 检索命中 GT 片段就认定风险下降——sufficient 子集上 abstain 可升高，insufficient 子集仍可能大量 hallucinate。 只训「看到 context 就答」，不训「context 不够要说不知道」。 OCR：Weaviate 与 HHA Ih HH i i 叠字；abstention 实验见论文 Figure 3/6。\nOCR：Weaviate Hh ml 与 HIM i mi iW 片段；访谈画面无 Figure 编号。\n选择性生成：不要单独用 P(sufficient) 当门控 # 为什么 # 团队曾设想用 sufficient-context rater 决定 abstain；§5.1 写明这种 heavy-handed 做法会 lower overall performance，因砍掉「insufficient 但 correct」样本。\n机制与约束 # P(True)：Kadavath et al. — 多次采样再自评（开源模型成本高）。 P(Correct)：直接要「最可能答案 + 概率」（专有模型用，文献 §5.1）。 Sufficient 信号：二值 autorater（实验用 FLAMe，1600-token chunk；任一块 sufficient 则整体 sufficient）。 组合：与自评概率做 logistic regression 预测 hallucination，再阈值化——不是单独 P(sufficient) 门控。 增益（相对仅 confidence）：HotpotQA 上 Gemma 27B 在 最高准确率区域 \u0026gt;10%；Gemini 1.5 Pro 在约 70% coverage 区域 \u0026gt;5%（文献 Figure 4）。摘要另写在实际作答子集上正确占比提升 2–10%。 Coverage = 未 abstain 的查询占比；Selective accuracy = 作答子集上的正确率——勿与 recall@k 或 pass@k 混用。 怎么做（最小示例） # # 概念：离线拟合，线上用同一阈值控制 coverage # features: [logit_p_correct, logit_p_sufficient, optional_bias] # label: hallucinate per LLMEval on (Q, C, model_answer) Musique 上 Gemma 的 sufficient 系数可为 0（增益消失）——组合信号数据集相关（文献）。\n常见误区 # P(sufficient) \u0026lt; τ 就拒答或触发再检索。 只报全量 accuracy，不画 coverage–selective accuracy 曲线。 分屏访谈；左侧可见 A THOUSAND BRAINS 书脊与 Weaviate podcast 角标。\nOCR：Hi Hi Hh N | Hh Hh 与 Weaviate；后期讨论 judge 与对抗注入时段。\nContext 长度、拼接与「工程层」上下文 # 为什么 # 长上下文窗口普及后，常见论点是「不必 RAG、一次塞全库」。论文 Figure 2 与访谈对此给出部分反证与延伸。\n机制与约束 # 本文实验（Figure 2）：检索 context 上限 2000 / 6000 / 10000 tokens；2000→6000 时 sufficient 比例温和变化（如 Musique 33.4% → 44.6%），6000→10000 几乎不变；后文固定 6000 tokens（文献）。 Lost in the middle：Liu et al., TACL 2024 被引用；本文未报告「金答案在 context 中间 vs 首尾」的对照——播客关于 chunk 拼接、metadata 量、人工标 sufficient 变难等多属 演讲者观点 / 经验延伸。 矛盾 evidence：检索片段互相矛盾时，嘉宾倾向标 insufficient；与模型 parametric knowledge 冲突则是另一层（预训练/微调）。演讲者观点。 Context engineering：在 relevance 之后，如何把碎片组成可用整体（消歧、一致性）——与 Graph RAG、重排并列，非替代召回（演讲者观点）。 怎么做（最小示例） # 在 6k token 预算下做截断实验：比较 sufficient 比例与下游 selective accuracy，而非盲目拉满窗口。\n常见误区 # 窗口越大越好，忽略无关 context 增幻觉（Related Work 引 noise 文献；本文主实验按 sufficient/insufficient 分层，非单独「无关片段」对照）。 把访谈中的位置效应实验归因于 Joren et al. 2025 正文。 OCR：\\ y Ki Mm Nak 与 Weaviate \\4 NAN；对应长 context / K 值讨论时段。\nOCR：Weaviate 与 WIA i i WY 叠字；画面无 token 曲线。\n教模型拒答：SFT/LoRA 与产品侧重排 # 为什么 # 若 selective generation 仍不足，自然会问：能否 SFT 出可靠的「我不知道」？论文 Table 3 与 Vertex 产品文档给出部分答案与边界。\n机制与约束 # Mistral-7B-Instruct-v0.3 + LoRA（rank 4, alpha 8）：混合「I don\u0026rsquo;t know」与正常答案——%Correct 可升，%Abstain 仍极低（文献）。访谈称 Mixtral；以论文模型名为准。 访谈：100% I don\u0026rsquo;t know 样本能推过去，但混合比例与 abstention 非线性（演讲者观点）；DPO/GRPO 对校准不确定性「有空间」——未在本文实验。 Vertex AI RAG Engine reranking：提供 semantic reranker 与 LLM reranker（Gemini 评估 chunk 与 query 的 relevance）。文档未出现 sufficient context 作为排序目标。 论文 §6 Future Work：细粒度 sufficient autorater 可用于 ranking after retrieval——研究方向，非已核实产品行为。嘉宾称 Google 合作中将 sufficiency 思想接入 re-ranker（演讲者观点；无法在公开文档核实）。 怎么做（最小示例） # 产品侧：在多路召回后用 LLM reranker 压缩 top-k；评测侧：用 Table 1 级 autorater 离线标 insufficient 比例，驱动 recourse（web search、人工、更强模型），而非单一 abstain 门。\n常见误区 # 假设 Vertex 默认按 sufficiency 排序。 用单次 LoRA 实验否定一切 retrieval-aware fine-tuning（论文未测 RAFT 等；主持提到的 Frankenstein RAG vs 联合训练为访谈对照，非本文结论）。 OCR 含 Q\u0026rsquo;s weaviate 与 Vy if y；赞助商/角标画面，不能证明 Vertex 架构图。\n分屏访谈后期；讨论 Mixtral/Mistral abstention 实验（画面无 Table 3）。\n嘉宾手势说明知识库对抗与注入风险；左侧 Weaviate podcast 标识。\n评测生态：与 RAGAS、ARES、主动检索的并置 # 方法 与 sufficiency 的关系 RAGAS 多维 LLM-judge；无同名 sufficient 标签 ARES 编译式 judge：context relevance、faithfulness、answer relevance FLARE 低置信 token 触发 forward-looking 再检索 FLAMe 24B 级 autorater，成本介于 Gemini 与人工之间 演讲者观点：下一步可像 RAGAS 一样为 sufficiency 建大数据集并微调 judge；生产日志标 insufficient 可触发改 corpus、人工裁定、贵检索等 recourse——与「只改模型」并列。\nOCR：Weaviate Niantic Ny) 叠字；生态延伸讨论时段。\n分屏对谈末段；左侧 A THOUSAND BRAINS 书脊，讨论 Vertex 与工程落地。\n若你要落地 # 离线：在自有日志上对 ((Q,C)) 跑 Gemini 1-shot 或 FLAMe sufficiency 标签，单独统计 insufficient ∧ correct 占比，再决定 recourse，勿用单一阈值全局 abstain。 在线选择性生成：采集 P(Correct)（或开源用 P(True)）与 P(sufficient)，用 logistic regression 拟合 hallucination，按目标 coverage 调阈值并画 selective accuracy 曲线。 检索预算：优先在 ~6k tokens 量级做截断与重排实验（对齐 Figure 2），再考虑拉满 10k+。 拒答训练：若 SFT「I don\u0026rsquo;t know」，用论文 Table 3 预期——准确率可能升而 abstain 不恢复；需另设计偏好学习或检索感知训练，并单独评 abstain。 产品重排：Vertex 等平台的 LLM reranker 文档写的是 relevance；将 sufficiency 接入排序视为 论文 Future Work + 自定义管线，上线前用金标子集验证。 参考与延伸阅读 # Sufficient Context（arXiv:2411.06037） — 定义、Table 1–4、Figure 2/4/6 arXiv PDF（2411.06037） — 表格与图形的可核对副本 RAG 原始框架（Lewis et al., 2020） RAGAS 可用指标文档 ARES: Automated Evaluation Framework P(True) / 语言模型自评校准 FLAMe: Foundational Autoraters FLARE: Active Retrieval Augmented Generation Lost in the Middle（Liu et al., 2024） HotpotQA · MuSiQue Vertex AI RAG：Retrieval and ranking RAFT: Retrieval-Augmented Fine-Tuning — 访谈对照的检索感知微调方向 Weaviate 文档：RAG 与向量检索 — 向量库侧工程背景（与论文无直接绑定） DBLP: Joren et al., ICLR 2025 — 书目信息 写作说明：论文称已发表于 ICLR 2025；若 arXiv v2 与 OpenReview 终稿不一致，以出版 PDF 为准。播客数字与图表不一致处，正文已标「文献 / 演讲者观点 / 未核实」。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-sufficient-context-with-hailey-joren-weaviate-podcast-125/","section":"文章","summary":"充分上下文：RAG 该测「够不够答」，而不只是「像不像相关」","title":"充分上下文：RAG 该测「够不够答」，而不只是「像不像相关」","type":"posts"},{"content":" 从 JDK 8 到 25：把跨 seventeen 个版本的升级当成平台工程 # 许多团队仍把「升级 JDK」理解成替换生产环境的 java 可执行文件。若编译、测试、构建三条 classpath 仍停留在 Java 8 时代的依赖与参数上，问题往往不会在切换当天爆发，而是在集成测试、灰度或数周后的缓存反序列化里集中出现。\n跨 seventeen 个主版本，断裂点可按影响面粗分为四类：进程起不来（废弃 GC/SM 参数）、跑起来就崩（模块拆分包、NoClassDefFoundError、反射封装）、编译通过但行为变了（locale、UTF-8、字符串拼接）、更晚才炸（序列化 UID、注解处理缺失）。本文把 JDK 8→25 当作全链路 Java 资产迁移：先用 JDK 自带工具摸清断裂面，再按上述分层处理，最后把「到达 25」与「保持六个月节奏」拆开——前者是项目，后者才是能力。\n迁移对象：每一条 classpath，而不是一个 runtime JAR # 为什么：传递依赖里的 sun.*、已标记 for removal 的 API、JNI/FFM 用法，很少出现在业务 src/ 里，却会在目标 JDK 上触发 NoClassDefFoundError 或启动警告。只升级生产运行时、测试仍用旧 JDK 编译，会在 CI 后期才暴露不一致。\n机制/约束：Maven/Gradle 的 compile、test、runtime 三套解析树可以不同；容器镜像里的 JAVA_OPTS 也可能与本地开发脚本分叉。JDK 25 的 System.getProperty(\u0026ldquo;java.class.path\u0026rdquo;) 只反映当前进程实际加载路径。\n怎么做：\nmvn -q dependency:tree -DoutputFile=target/deps.txt mvn -q dependency:build-classpath -Dmdep.outputFile=target/cp.txt public final class ClasspathProbe { public static void main(String[] args) { String cp = System.getProperty(\u0026#34;java.class.path\u0026#34;); if (cp == null) return; for (String e : cp.split(java.io.File.pathSeparator)) if (e.endsWith(\u0026#34;.jar\u0026#34;)) System.out.println(e); } } 常见误区：认为「主工程已升到 25」就等于全栈升级；忽略测试 scope、annotation processor、构建插件自带的 JAR。建议在升级章程里写清三条 invariant：开发机、CI agent、生产镜像的 java -version 与 JAVA_HOME 一致；每次发布前导出并归档 dependency:tree；对 fat JAR 或 shade 产物额外跑一次 jdeps -R，因为合并后的包会掩盖传递依赖来源。\n图：Practical Dependency Resolution — Maven、Gradle、SBT 与依赖复杂度\n升级前的静态侦察：jdeps、jdeprscan、jnativescan # 为什么：JDK 9 起的模块强封装、JEP 403 移除 --illegal-access、以及 JEP 472 对 native access 的约束，都会先体现在字节码与启动参数上，而不是业务逻辑 diff。\n机制/约束：\n工具 典型用途 手册 jdeps --jdk-internals 定位对 JDK 内部 API 的依赖 应对 JEP 396 jdeprscan --for-removal 扫描即将移除的 API 与 --release 25 联用 jnativescan 扫描 JNI/本地库引用 与 JEP 472 运行时策略互补 japicmp 依赖小版本升级的 API 二进制差异 第三方，非 JDK 自带 怎么做（classpath 须覆盖全部 JAR，含 ~/.m2 或 lib/*）：\njdeps --multi-release 25 --jdk-internals -R app.jar lib/*.jar jdeprscan --release 25 --for-removal --class-path \u0026#34;lib/*:app.jar\u0026#34; jnativescan --class-path \u0026#34;lib/*:app.jar\u0026#34; 常见误区：只对 target/classes 扫描；遗漏 jdeprscan 的 --for-removal，把「已废弃」与「计划移除」混为一谈。另一个隐蔽点是 JNI 与 FFM：jnativescan 给出静态画像，运行时是否还需 --enable-native-access 要以目标 JDK 的 JEP 472 阶段策略为准（JDK 24 起警告路径已产品化，未来可能收紧为 deny）。\n第一堵墙：JDK 9–17 的模块、GC 与封装 # 为什么：JDK 9 单版本就集中了模块路径、默认 GC 切换、CLDR locale、字符串拼接实现变更等多项断裂——幻灯片将其概括为「nine 到 seventeen 的第一堵墙」。若仍保留 Parallel GC 调优、CMS 标志、Java EE 内嵌 API，会在 9/14/17 等台阶叠加失败。\n机制/约束（与官方 JEP 对齐的要点）：\n拆分包 / 模块路径 → LayerInstantiationException（JEP 261 语境） 默认 GC Parallel → G1（JEP 248）→ 吞吐与暂停曲线变化，属静默类 CLDR 默认 locale（JEP 252）→ 格式化输出漂移 强封装（JEP 396，JDK 16）→ InaccessibleObjectException 移除 --illegal-access（JEP 403，JDK 17）→ 反射后门不可长期依赖 CMS / Nashorn / -XX:MaxPermSize 等 → 多为 Won\u0026rsquo;t Start 图：Breakages by Version: JDK 9 — 17 — Split packages、JEP 248/252/280 等条目\n图：JDK 9: seven breaking changes in a single release. The first wall.\n怎么做（过渡，非终点）：\njava --add-opens java.base/java.lang=ALL-UNNAMED -jar legacy.jar java -Djava.locale.providers=COMPAT,CLDR -jar app.jar 常见误区：把 --add-opens 写进永久配置而不推动库升级；在 JDK 17+ 仍假设 --illegal-access=permit 可用。\n第二堵墙：JDK 18–25 的编码、native access 与编译器行为 # 为什么：18–25 的变更常混在同一升级窗口：有的进程起不来，有的无异常但结果变了，还有的重编译后才暴露。\n机制/约束：\n主题 JEP / 依据 典型影响 默认 charset UTF-8 JEP 400 Windows 上历史文件读写静默损坏；-Dfile.encoding=COMPAT 可过渡 Native access JEP 472（JDK 24 交付） 需 --enable-native-access=ALL-UNNAMED 等 Security Manager JEP 486（JDK 24） -Djava.security.manager=... 启动即失败 注解处理默认 javac 手册 + JDK-8321314 JDK 24 起未显式配置则不运行 processor（非 JEP 477，477 为隐式类/实例 main） Compact headers JEP 519 JDK 25 产品化；JEP 明确非目标为默认 object header java.lang.IO JEP 512 新 API 与 java.io 并存，需适配 import/教学代码习惯 图：Breakages by Version: JDK 18 — 25 — Native access requires \u0026ndash;enable-native-access (JEP 472)\n怎么做：\njava --enable-native-access=ALL-UNNAMED -jar app-with-jni.jar java -Dfile.encoding=COMPAT -jar app.jar 常见误区：把 JEP 472 仅锚定在 JDK 25；把幻灯片脚注「JEP 477」当成注解处理变更（编号已过期，应以 javac 手册为准）。对 JEP 471/498 涉及的 sun.misc.Unsafe 内存访问，应把「警告」当作移除倒计时，而不是可忽略的编译器噪声；字节码工具链（ASM、ByteBuddy）须与目标 class file version 对齐，否则会在构建链更早阶段失败。\n启动期硬失败：JVM 参数审计优先于业务代码 # 为什么：过时 GC 标志（如 CMS，JEP 363）、遗留 GC 日志参数（JEP 158、JEP 271，统一为 -Xlog），以及 Security Manager 相关启动项，会让 JVM 在业务 main 之前退出。若不做 diff，团队会在「应用没改」的前提下全站不可用。\n机制/约束：java 手册 提供 GC logging 到 -Xlog 的转换说明；-XX:+PrintFlagsFinal 可导出两版 JDK 的 flag 全集做对比。\n怎么做：\n\u0026#34;$JAVA8_HOME/bin/java\u0026#34; -XX:+PrintFlagsFinal -version \u0026gt; flags-jdk8.txt \u0026#34;$JAVA25_HOME/bin/java\u0026#34; -XX:+PrintFlagsFinal -version \u0026gt; flags-jdk25.txt diff -u flags-jdk8.txt flags-jdk25.txt | rg -i \u0026#39;gc|logging|security\u0026#39; || true ENV JAVA_OPTS=\u0026#34;-XX:+UseG1GC\u0026#34; # 移除 -Xloggc:、CMS、SecurityManager 相关项 ENTRYPOINT [\u0026#34;sh\u0026#34;,\u0026#34;-c\u0026#34;,\u0026#34;exec java $JAVA_OPTS -jar /app/app.jar\u0026#34;] 常见误区：只检查应用仓库的 JAVA_OPTS，遗漏 Helm chart、systemd、CI 矩阵里的旧 flag。实践中可先对最小可启动的 java -version 与 java -jar app.jar --help 做冒烟，再逐步打开完整集成套件；若使用 Kubernetes，记得同步检查 init 容器与 sidecar 的 JDK 版本，它们往往复制了主容器的旧参数模板。\n运行时反射与「被吞掉的」封装异常 # 为什么：JEP 396/403 之后，对 JDK 内部类型的 setAccessible(true) 可能抛出 InaccessibleObjectException。框架若 catch 后忽略，线上表现为功能随机失效而非清晰栈迹。\n机制/约束：静态结果来自 jdeps --jdk-internals；运行时可用 -Xlog:exceptions=info 让 JVM 记录的异常更易在集成环境浮现（针对 JVM 抛出路径，无法替代应用层吞异常）。\n怎么做：\njava -Xlog:exceptions=info -jar build/integration-tests.jar try { var f = LegacyLib.class.getDeclaredField(\u0026#34;internal\u0026#34;); f.setAccessible(true); } catch (InaccessibleObjectException ex) { System.getLogger(\u0026#34;migrate\u0026#34;).log(System.Logger.Level.ERROR, ex.getMessage(), ex); } 常见误区：仅靠单元测试、未跑全量集成；未把 jdeps 命中与日志中的 InaccessibleObjectException 交叉验证。\n静默语义：Security Manager 移除、locale 与 UTF-8 # 为什么：这类问题属于「Runs but different」——测试只断言无异常时极易漏检。\n机制/约束：\nJEP 486（JDK 24）：Security Manager 永久禁用；六类 AccessController.doPrivileged 在 SM 不启用时立即执行 action，不再经权限裁决。 System.getSecurityManager() 恒为 null；原先依赖 SecurityException 阻断路径的代码会静默继续执行。 JEP 400：Charset.defaultCharset() 默认 UTF-8。 JEP 252：CLDR 为默认 locale 数据源。 图：Security paths silently succeed — No exception, that\u0026rsquo;s the problem\n怎么做：\njava -Djava.locale.providers=COMPAT,CLDR -Dfile.encoding=COMPAT -jar app.jar Files.writeString(path, text, StandardCharsets.UTF_8); 审计代码中 catch (SecurityException)、doPrivileged()、getSecurityManager() != null 等模式。\n常见误区：把 SM 移除仅当作「删掉启动参数」；忽略依赖库里的权限假设（风险判断，非 JEP 字面结论）。\n序列化：shipped 二进制与 freshly recompiled 必须分轨验证 # 为什么：Java 内置序列化在跨主版本时，若类未固定 serialVersionUID，重编译后运行期计算的 UID 可能变化，导致 InvalidClassException——可能在迁移数日后的缓存读取才爆发。\n机制/约束：Serializable 文档说明显式 serialVersionUID 的作用；serialver 可在仍使用 JDK 8 时为类生成 UID 常量。\n怎么做：\nserialver com.example.model.OrderDto public class OrderDto implements java.io.Serializable { private static final long serialVersionUID = 1234567890123456789L; } CI 上并行两条轨：test-shipped-jar（旧构建产物）与 test-recompiled（JDK 25 新编译）。长期更稳妥的方向是 JSON、Protobuf、Avro 等显式 schema 格式（工程建议）。\n图：Serialisation drift — Cached data from JDK 8 can\u0026rsquo;t be deserialised on JDK 25\n常见误区：「编译通过」等同于序列化兼容；测试夹具从未包含 JDK 8 时代序列化字节。\n构建陷阱：JDK 24+ 注解处理与 Lombok # 为什么：自 JDK 24 起，javac 默认不再自动运行注解处理器；Lombok 若仍依赖隐式发现，可能出现编译成功、运行期 NoSuchMethodError（getter/builder 缺失）。这与 JEP 477（隐式类）无关，跟踪实现见 JDK-8321314。\n机制/约束：Lombok changelog 在 1.18.40 声明 JDK 25 支持；1.18.42 主要为 IDE/ErrorProne 修复。幻灯片写「Since JDK 23」与当前手册不符，应以 24+ 为准。\n怎么做：\n\u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;annotationProcessorPaths\u0026gt; \u0026lt;path\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.42\u0026lt;/version\u0026gt; \u0026lt;/path\u0026gt; \u0026lt;/annotationProcessorPaths\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; 常见误区：升级 Lombok 版本却未在 annotationProcessorPaths / Gradle annotationProcessor 中显式声明；单测未调用 Lombok 生成方法。建议在 CI 增加一步「反编译或 javap -c 抽查」关键 DTO，确认 getter/builder 字节码确实存在；对 MapStruct、Micronaut 等其它 processor 适用同一规则，不要假设「以前能编过」在 JDK 24+ 仍成立。\n图：The Lombok trap — Since JDK 23, javac no longer runs annotation processors by default（版本号以 JDK 24+ 手册为准）\n机械重构与供应链现实 # 为什么：javax→jakarta、弃用 API 替换可用 OpenRewrite 批量处理，但卡点常在第三方库是否跟进。演讲者给出典型现代应用约 150 个传递依赖、仅约 22% 开源 Java 组件仍活跃维护的印象数——无法独立核实，须用 SBOM 与仓库元数据自行验证。\n怎么做：\nmvn -U org.openrewrite.maven:rewrite-maven-plugin:run \\ -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \\ -DactiveRecipes=org.openrewrite.java.migrate.UpgradeToJava25 常见误区：把升级 scope 限在自有 repo；未与维护者沟通 EOL 库替换或商业支持路径。\n图：What is a Software Supply Chain — 每个依赖自带整条供应链\n可执行的收尾清单与 JDK 25 能力画像 # 为什么：把一次性大跳拆成可重复步骤，能降低「不知道坏在哪一层」的概率。幻灯片强调：若 JVM 起不来，其余都无从谈起——应先 diff PrintFlagsFinal，再对完整 classpath 跑 jdeps/jdeprscan，开 -Xlog:exceptions=info 做集成测试，并分开验证 shipped 与 recompiled 产物。\n图：Seven things to do on Monday — Diff PrintFlagsFinal between JDK 8 and 25\n到达 JDK 25 后，平台能力包括 虚拟线程 JEP 444、现代 GC（G1/ZGC/Shenandoah）、records/sealed/pattern matching、强封装与 FFM 等。闭幕幻灯片上的「2–3× GC」「20%+ throughput」属于演讲者/幻灯片 marketing 表述，须在你的负载上用 JFR 与基准测试验证：\njava -XX:StartFlightRecording=duration=60s,filename=baseline.jfr -jar app.jar try (var ex = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) { ex.submit(() -\u0026gt; { /* IO-bound work */ }); } 图：Why JDK 25 — migration matters more than the destination — Virtual threads, 2-3x GC, 20%+ throughput（性能数字需自行基准验证）\n持续升级（演讲者观点）：OpenJDK 六个月发布节奏 下，「数年一跳」会把本文列出的多层断裂叠在同一窗口。可操作的工程习惯包括：每个发布列车对全量 classpath 重跑 jdeprscan --for-removal；依赖升级用 japicmp 做二进制兼容门；用 OpenRewrite 的 UpgradeToJava25 等菜谱消化机械变更；用 JFR 在相同负载下对比 GC 暂停与分配率；对无法升级的传递依赖建立 EOL 台账并与维护者或商业支持方对齐。分发版（Adoptium、Corretto、Liberica、Zulu 等）的补丁节奏各异，但语言与 JVM 行为以 OpenJDK 为准——不要把「我们只换运行时、不改构建」当作低风险路径。\n常见误区：把 JDK 25 当作终点；忽视构建插件、测试容器、IDE 与生产 JDK 版本漂移。若团队仍按「数年一跳」规划，宜在立项阶段就把本文字段的断裂表拆进风险登记册，并为每一类断裂指定 owner（平台、中间件、业务线），避免把所有兼容性工作压到发布前两周。\n参考与延伸阅读 # JDK 25 文档集 java 应用启动器手册（含 PrintFlagsFinal 与 GC 日志迁移） javac 编译器手册 — JDK 25（含注解处理与 -proc 选项） jdeps 手册 jdeprscan 手册 Java 对象序列化规范与 serialver JEP 396：默认强封装 JDK 内部 API JEP 400：默认 UTF-8 字符集 JEP 472：Native Access 警告与限制 JEP 486：永久禁用 Security Manager OpenRewrite Java 迁移菜谱目录 Project Lombok 变更日志 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-jdk-8-to-25-without-the-pain-engineering-a-modern-java-platform/","section":"文章","summary":"从 JDK 8 到 25：把跨 seventeen 个版本的升级当成平台工程","title":"从 JDK 8 到 25：把跨 seventeen 个版本的升级当成平台工程","type":"posts"},{"content":" 从 RAG 到 Search Agent：检索、合成数据与评测的三条张力 # 大模型产品把「联网搜索」做成默认能力之后，工程团队常面临同一组矛盾：用户要的是短答案还是长报告？训练预算是烧在 LLM token 还是 search API 次数？评测该看答对率还是看轨迹效率？本文围绕 search agent（以 search / browse 工具 在互联网上为多跳 web 问题找答案）这一工作定义，对照 BrowseComp-Plus 等公开基准、合成数据路线 WebShaper，以及嘉宾 Nandan Thakur（BEIR / MIRACL 合著者）在访谈中的工程判断——不强行收束为单一结论；凡未在论文或 leaderboard 中出现的数字，均标为演讲者观点或未核实。\n问题空间：三条轴，而不是一个「更聪明的 RAG」 # 经典 RAG 往往是「检索一次 → 拼进 prompt → 生成」。Agentic search 把检索变成多轮、可分支的过程：模型决定何时搜、搜什么、是否继续读文档。公开文献里，BrowseComp-Plus 将 Deep-Research agents 操作化为：LLM + search tools，在固定语料上测 Accuracy 与 Search Calls。\n与此同时，工业界还有 deep research 产品叙事——用户常期待报告式输出、任意工具编排、更长的 test-time compute。嘉宾的划分是：search agent 锚定「答一问」；deep research 是更宽的伞（演讲者观点）。两条产品线可以共享底层检索，但优化目标与 harness 并不相同。\nSearch agent 与 Deep research：命名即产品预期 # 为什么：同一套「会搜网的模型」，若对外称 deep research，用户会预设长文档、多工具、可下载报告；若称 search agent，更接近 web QA / BrowseComp 族（演讲者观点）。\n机制/约束：BrowseComp-Plus 在约 100K 固定文档上评测，便于复现检索栈（BM25、DiskANN 稠密索引等），与「实时全网 API」的商用 agent 不可直接数值对比。\n怎么做（心智模型）：把 harness 拆成 plan → search → read → answer；先明确交付物是 span 级短答案 还是 nugget 级长答案（后者接近 TREC RAG 的 nugget / support 评测）。\n常见误区：用报告类产品的 UX 去评 BrowseComp 式短答案任务，或用一次 RAG 的延迟 SLA 去卡多轮 agent。\n分屏访谈画面：左侧背景墙可见 Weaviate podcast 标识，右侧为嘉宾 Nandan Thakur。\n画面 OCR 可见 Weaviate podcast 品牌字样（无架构幻灯片文本）。\n评测谱系：从 BEIR 到 BrowseComp+ # 为什么：BEIR 推动社区在 18 个零样本数据集上迭代检索器；MIRACL 把异构语言纳入同一套 qrel 思维。Search agent 需要类似「社区围着 benchmark 转」的拉力，但 IR 指标与 agent 指标不是同一随机过程（演讲者观点）。\n机制/约束：\n层级 典型指标 稳定性（文献/常识） 检索器 NDCG@k、MRR 固定 qrel 重跑，方差极小 Agent 端到端 Accuracy、Search Calls 同模型多 rollout 方差大 长报告 RAG Nugget recall、Support 0–2 与 BrowseComp Accuracy 不同族 BrowseComp-Plus 论文报告：GPT-5 + BM25 55.9%，换 Qwen3-Embedding-8B 约 70.1%；项目页脚注中 GPT-5 + Google Search API 约 59.9%。嘉宾口述「头部模型已达 90–95%」与上述 2025-08 公开表不一致——可能指另一基准、私有榜或未核对的 BrowseComp（非 Plus）设置；正文不得以口述替代 leaderboard。\n嘉宾用 filter / funnel 理解 BrowseComp+ 子句：多候选约束逐条收窄（演讲者观点）；论文侧有 human-verified supporting docs + mined negatives、GPT-4o sub-query 分解管线，方向一致，但 10³–10⁵ → 个位数 的数量级未在论文中逐字出现。\n怎么做：在固定语料上复现 BrowseComp-Plus + Tevatron 再谈「换检索器是否涨点」；商用 API 轨迹另建私有 eval。\n常见误区：把 BEIR 上的 NDCG 提升直接等同于 BrowseComp 上 Accuracy 提升；Search-R1 + BM25 在 BrowseComp-Plus 仅约 3.86% 说明「关键词化查询即可」在该难基准上尚未成立。\n博士论文题名片段：RETRIEVAL AND RETRIEVAL-AUGMENTED GENERATION ON HETEROGENEOUS DOMAINS AND LANGUAGES；作者 Nandan Thakur，导师 Prof. Jimmy Lin，University of Waterloo Faculty of Mathematics。\n合成数据：Orbit、WebShaper 与「链式」多跳 # 为什么：Search-R1 等在 NQ、HotpotQA 上训练时，任务难度与 BrowseComp 族错位；不少数据集论文不释出训练数据（演讲者观点）。合成管线试图用 seed → 检索 → 抽事实 → 隐藏 → 再检索 的 intersection 思路造难例。\n机制/约束：\nWebShaper（可核对）：formalization-driven，Knowledge Projections + agentic Expander；数据集 WebShaperQA。 Orbit（未核实）：嘉宾描述约 20k BrowseComp-风格 四至五层 riddle、DeepSeek 生成 + 自验证 + 外部 search agent 验证、消费级笔记本连续跑数月（演讲者观点）。公开检索未发现署名 Orbit 的论文/仓库；在发布前应视为项目口述。 难度对比（演讲者观点）：BrowseComp+ 偏 filter（子句多候选、逐条漏斗）；Orbit 更接近 A→B→C→D 链式，自认尚未达到 Plus 的 filter 强度。 怎么做（极简管线）：\nseed_entity → web_search → extract_facts → mask_entity → repeat → QA_pair 质量门禁：每跳用独立检索结果校验可解性，而非仅信任 LLM 自洽。\n常见误区：合成题允许模型只解开一条线索就猜答案（嘉宾举 Emoji Movie 类捷径，演讲者观点）；理想 agent 应 todo-list 式逐条验证 约束。\n约 17 分钟时段分屏画面：讨论 Orbit 合成公式与例题时，背景墙 Weaviate podcast 标识仍可见。\n画面 OCR 片段含 Weaviate 与 DOQaQCcaSsS 等播客叠字（无幻灯片 API 名）。\n训练经济学：GRPO、rollout 与「Search 比 LLM 贵」 # 为什么：Search-R1 用 outcome-based reward 与 retrieved token masking，在 7 个 QA 集上让小模型相对 naive RAG 有约 +41% / +20%（Qwen2.5-7B/3B，论文表）。下一步竞争点常被表述为：同等 Accuracy 下更少的 token 与 search 调用（演讲者观点）。\n机制/约束：\nGRPO（DeepSeekMath）：每题采样 G 条输出，用组内归一化 reward 算 advantage；G 是超参，论文未写死 8。训练时「多条轨迹中少数成功即可产生正 advantage」与嘉宾「8 条里 1 条对就有信号」机制相容，但勿把训练 group size G 与推理 pass@k 混为一符号。 API 账单（演讲者观点）：若 6 turns × 8 rollouts × 1 search/turn，心算约 48 次 search / 训练样本；BrowseComp-Plus 论文称强模型平均每题 \u0026gt;20 次 search。Search 按次计费、LLM 按 token 计费时，agent 训练下 search 账单可高于 LLM——与「token 越来越便宜」的直觉相反。 顺序长视界 vs 并行：推理侧 pass@K 可行；训练侧跨 rollout 传信用 RL 太贵，常见路径是 强教师 rollout → SFT 蒸馏（演讲者观点）。 Context rot：Chroma 研究报告 在控制难度下显示输入变长则性能下降；与「评测保留全轨迹」和「工程压缩上下文」形成张力。 怎么做：先用 Search-R1 代码库 的 PPO/GRPO/reinforce 开关在小语料验证，再放大 G 与 turn 数；单独记录 search_calls 与 tokens 两列成本。\n常见误区：把 GRPO 的 G 直接写成 pass@8；在 BrowseComp+ 轨迹里重复提交同一 query仍算有效探索（嘉宾批评为浪费，演讲者观点）。\n约 20 分钟：嘉宾侧窗口自然光，左侧 Weaviate podcast 书架背景——讨论训练成本与 rollout 的时段。\n约 40 分钟：嘉宾正视镜头，背景木架与窗光；同期话题涉及 harness 与检索接口。\n检索接口：Snippet、全文与自托管栈 # 为什么：商用 search API 多返回 snippet + offset，类似经典 reader 的 span；自托管 BM25 / ANN 在十亿级语料可起步，但 agent 有时需要 snippet 级可复现 或全文工具（演讲者观点）。\n机制/约束：嘉宾倾向 search → 选 doc → document tool 拉全文 的两段式；训练时因 API 慢/贵未充分做端到端全文 RL（演讲者观点）。学术替代路径包括 FineWeb / ClueWeb 建索引 + 内部 search API；BrowseComp-Plus 与 Tevatron 提供可复现 BM25/稠密检索。「Deep Research Gym」专名在公开 URL 核实中未找到独立项目页（2026-05 核实），写作时宜指 Tevatron/BrowseComp-Plus 生态或标注未核实。\n嘉宾猜想（无实验数字）：经 SFT/RL 优化后，agent 或只需输出 BM25 关键词 配合 lexical retriever——在 BrowseComp-Plus 上尚未被公开结果支持，保留为研究假设。\n怎么做：\n# 概念 harness（非生产代码） for turn in range(max_turns): q = llm.plan_query(state) hits = retriever.search(q, top_k=10) # snippet + doc_id if need_full_text(hits): doc = corpus.fetch_full(hits[0].doc_id) state = llm.update(state, hits, doc) answer = llm.finalize(state) 常见误区：用单次向量 top-k 替代多轮 search；忽略 REPLUG 时代「检索与 LM 分工」在 agent 里以 摘要/外部文件夹记忆（如 Claude Code 式）重现，而非唯一解是 Databricks 式「压缩与检索联合端到端训练」（主持人转述，官方 benchmark 页 2026-05 未核对到）。\n画面 OCR 含 Weaviate podcast 与 ma 等叠字，对应检索与 agent 讨论时段。\n约 16 分钟分屏：左侧 Weaviate podcast 标识与书架，右侧嘉宾——合成数据与验证话题附近。\nHarness：记忆、压缩与评测闭环 # 为什么：多轮搜索使上下文长度成为第二瓶颈；三四年前 RAG 主题的 prepend 检索文档（REPLUG）、chunk 裁剪、nugget 报告评测，在 agent harness 里再次出现（演讲者观点）。\n机制/约束：\n模块化：先 search，再按需 full-document tool；与「检索 agent + 压缩器联合端到端训练」路线并存，嘉宾个人更偏模块化（演讲者观点）。 TREC RAG（可核对）：2024 指南含 Nugget、Support、Fluency、Retrieval 四指标；Support 0–2 评 grounding。嘉宾判断「流畅度权重下降、更应看 grounding」属参与者观点。 工具使用门槛：约半年前仍需 SFT+RL 才稳 tool use；现 mid/post-training 已蒸馏进大模型，竞赛转向 效率与 Pareto（演讲者观点，时间锚为录制前后口语）。 常见误区：为压长度删掉对评测可审计的 citation 轨迹；把 IR 的「单次确定性排序」心智套在 stochastic rollout 上。\nOCR 可见 Weaviate podcast por 字样，无额外架构文本。\n约 14 分钟分屏：左侧 Weaviate podcast 背景墙，讨论 GRPO 与 pass@K 的时段。\n未收敛的结论（刻意并列） # 任务定义：BrowseComp-Plus 论文的 Deep-Research agent 操作性定义与嘉宾的「search agent ⊂ deep research 伞」可并存；产品命名仍影响用户预期（演讲者观点）。 数据：WebShaper 已发表；Orbit 规模与管线待公开核实。 SOTA：公开 BrowseComp-Plus ≤70.1%（固定语料设置）与嘉宾 90–95% 口述冲突——正文只能并列，不能合并。 训练：GRPO 组内相对奖励支持「稀疏成功」；48 次 API/样本 为 lab 心算，非定理。 下一步 benchmark（演讲者观点）：多语言/多模态 riddle、FreshStack / CRAG 族加 agentic search、更难 filter——与 Omar Khattab「需要更难 benchmark」一类判断同向，具体指标未在播客展开。 若你要落地 # 先钉交付物与评测族：短答案走 BrowseComp-Plus 式 Accuracy + Search Calls；长报告走 TREC nugget/support，勿混表。 成本账本拆两列：search_calls 与 llm_tokens 分开记账；训练前用 small G、小 turn 做预算仿真（GRPO 的 G 见 DeepSeekMath）。 合成数据加硬门禁：每跳外部检索校验 + 反捷径规则（禁止单线索猜答案），参考 WebShaper 的 agentic Expander 思路。 检索栈可复现优先：能在 BrowseComp-Plus 固定语料 上复现再换商用 API；需要全文时显式加 fetch_full(doc_id) 工具，而非假设 snippet 够训。 上下文策略与评测一致：若评测要完整轨迹，训练可另做压缩/distillation；关注 context rot 文献对长度–准确率权衡的实证。 参考与延伸阅读 # BEIR：零样本信息检索基准 MIRACL：多语言检索基准 Search-R1：强化学习 + 多轮搜索 Search-R1 官方实现与实验日志 DeepSeekMath：GRPO 算法定义 BrowseComp-Plus 论文 BrowseComp-Plus 项目页与结果表 BrowseComp-Plus 代码与 Tevatron 检索栈 OpenAI BrowseComp 数据集（Hugging Face） WebShaper：形式化驱动的 web agent 数据合成 REPLUG：可微调检索 + 冻结 LM TREC RAG Track 首页 TREC 2024 RAG 评测说明（nugget / support / fluency） Chroma：Context Rot 研究报告 Meta CRAG 综合 RAG 基准仓库 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-search-agents-with-nandan-thakur-weaviate-podcast-137/","section":"文章","summary":"从 RAG 到 Search Agent：检索、合成数据与评测的三条张力","title":"从 RAG 到 Search Agent：检索、合成数据与评测的三条张力","type":"posts"},{"content":" 从 Record 到可解构类型：Amber 的解构—重建路线与语法治理 # 摘要：当 JEP 395 把不可变载体、名义元组与 record pattern 绑在一起时，任何超出其约束的演进都会同时失去紧凑语法与模式匹配侧的表达能力。Project Amber 正把「可按固定组件形状解构」提升为类型的顶层性质，并在 mail #2 中收窄为 deconstructible class 叙事；JEP 468（Candidate，预览）则长期等待更宽的类级解构路径。本文按依赖顺序说明动机、公开文档可对齐的术语，以及工程师如何阅读预览特性与即将到来的 Pattern Assignment（尚无 Preview JEP，见 Amber features 2026 邮件）。\nAmber 伞项目与预览节奏 # 为什么 # 大型语言改造（泛型、Valhalla 等）有独立项目；日常生产力改进则归在 Amber 下，以较小 JEP 切片交付。Project Amber 写明多数特性在定稿前通常经历至少两轮 preview（链至 JEP 12）。把「载体 / 解构」放在这一语境里，有助于判断它属于数据建模的渐进增强，而非重造类型系统。\n机制与约束 # GA：text blocks、Records、sealed、instanceof / switch 模式匹配、Record Patterns 等已在 Amber 交付列表中。 Candidate / 探索中：JEP 468（Derived Record Creation）；deconstructible 类尚无 Preview JEP 编号（截至 2026 年初公开邮件）。 Withdrawn：JEP 465 String Templates。 mail #1 将 records + sealed + record patterns 称为数据导向的「第一段弧」；carrier / deconstructible 为下一章——这是设计叙事（演讲者观点），不是 JEP 状态字段。\n怎么做 # 内部 RFC 模板里显式标注归属与预览轮次，避免与值类型议题混评：\n特性：\u0026lt;name\u0026gt; 归属：Project Amber 预览：是/否；附目标 JDK 与 --enable-preview（以对应 JEP 为准） 常见误区 # 把 Amber 当成「单一大特性」：实际是多个独立 JEP 的孵化器，交付节奏彼此解耦。\n广角对谈：两人坐在蓝绿拼贴方桌两侧，背景吧台上方可见发光招牌「DRAKE\u0026rsquo;S」；桌上有两杯琥珀色饮品，画面无幻灯片或 API 文本。\nRecord 的语义打包与约束边界 # 为什么 # JEP 395 将 record 描述为 nominal tuples，并由语言生成 equals / hashCode / toString、规范构造器等。模式匹配到来后，JEP 440 让 record 自动获得 record pattern。代价是行为绑定在 record 的窄约束上——演讲者称之为把多项能力「捆绑销售」；JEP 正文未使用该措辞。\n机制与约束 # record 不能 extends 任意用户类，超类恒为 java.lang.Record（JEP Description）。 需要继承层次、可变字段或打破 record 模型时，通常退回普通类并手写样板，同时失去与 record pattern 的自然对齐。Beyond Records 将这类落差称为 falling off a cliff（设计笔记用语；口语「两座悬崖叠在一起」为演讲者归纳，未在 mail #2 逐字出现）。 怎么做 # // GA：约束内紧凑 + 自动生成值语义 public record OrderId(String tenant, long seq) {} // 需子类化 / 可变状态时，常离开 record 舒适区 public abstract class LegacyOrderRef { public abstract String ref(); } 对现有代码的影响 # 已 GA 的 record 无需改动。讨论焦点在「非 record 类型何时应获得同等解构能力」，而非否定 record 本身。演讲者明确：并非 records 设计错误（演讲者观点），而是要把捆绑特性拆开给更宽的类。\n常见误区 # 把「悬崖」理解成 record 失败：规范侧 record 的 Non-Goals 写明其不是全面消灭 boilerplate 的战争；悬崖描述的是演进路径断裂，而非 record 不该存在。\n近景单机位：讲者紫色 Polo、领夹麦，背景绿植与装饰画虚化；无可辨识幻灯片，仅体现口述语境。\n从成员 deconstructor 到类型层解构 # 为什么 # 早期曾在类体内探索与构造器对称的 deconstructor（mail #1）。允许重载解构器会膨胀表达能力；复盘后认为示例库里几乎每类只需单一规范解构形状，不值得为多余自由度买单（统计结论为演讲者复盘，邮件强调转向类级 top-level property）。\n机制与约束 # 否决路径：可重载的显式成员 deconstructor。 保留方向：类头 component list 表达「可按这些组件解构」；成员级模式仍在 Towards Member Patterns 中单独讨论，文内声明示例非最终语法。 若只采用成员级解构，口述称往往仅得到「可解构」加 accessor 要求，编译器未必自动生成 equals / hashCode 等（对 mail #2 之后的 deconstructible 模型成立）。\n怎么做 # 下列草图仅表达「类型头与访问器对齐」意图，不代表已冻结关键字：\n/** 意图：外部可按 (name, version) 形状解构；equals 等属另一切片 */ public final class ModuleRef { private final String name; private final int version; public ModuleRef(String name, int version) { this.name = name; this.version = version; } public String name() { return name; } public int version() { return version; } } 常见误区 # 假设「有 deconstructor 成员」就等于 record 同等便利：第一版 carrier 曾试图在类头捆绑构造 + 完整状态描述 + Object 方法推导，后被 mail #2 自我修正（见下节）。\n中近景：讲者侧向发言、手势入画；背景饮品与墙面装饰，无代码或规范幻灯片。\n方案迭代：carrier 第一版与 deconstructible 第二版 # 为什么 # mail #1 的首轮 carrier class 让类头 component list 同时承诺：可按该形状构造与解构，且该形状是状态的 complete, canonical, nominal 描述，从而推导 equals / hashCode / toString。mail #2 承认这滑向 unprincipled concision——读者无法分辨缺失的 equals 是未写还是被推导。\n第二版收窄：deconstructible class 的类头列表 primarily 表示 API 级解构形状（class components）；若存在与组件同名、同类型、同顺序的构造器，语言将其视为 canonical constructor，从而解锁 Reconstruction (withers)。equals / hashCode / 字段↔component 映射等分阶段回补。\n文档版本：Beyond Records 设计笔记 仍对齐 mail #1；最新设计立场以 mail #2 为准。口述中的「Lombok territory」类比在 mail #2 HTML 正文未出现「Lombok」字面。\n机制与约束 # 概念 mail #2 角色 canonical deconstruction pattern 规范解构形状，支撑 pattern match canonical constructor 与组件列表同构的构造器 reconstructible 存在 canonical 构造时可做 derived / wither 风格重建 record deconstructible 的受限子集 怎么做 # 语义级重建（非最终语法）：\nModuleRef bump(ModuleRef original, int newVersion) { return new ModuleRef(original.name(), newVersion); } 对现有代码的影响 # record→普通类重构时，痛点不只在类型声明，还在所有 pattern match 该类型处。先交付类/接口级解构，可缓解模式匹配侧断裂；样板（equals 等）仍可能暂时手写。\n常见误区 # 因字段映射未解决而要求退回第一版「头部推导全套语义」：mail #2 逻辑已选定解构优先，不能以此回退强语义 carrier。\nBrian Goetz 近景：紫色 Polo、领夹麦，背景水罐与绿植虚化；无幻灯片，对应第一版 carrier 契约讨论的口述段。\n双人坐在蓝绿拼贴方桌，背景「DRAKE\u0026rsquo;S」发光招牌；桌上有饮品与墨镜，仍无规范幻灯片。\n分阶段交付、接口与 JEP 468 # 为什么 # Amber 可把历史上单个大 release driver 拆成多个 JEP。当前叙事：先 deconstruction（classes and interfaces），再处理 equals/hashCode、derived accessors 等（mail #2 — What\u0026rsquo;s left?）。carrier interfaces 在设计笔记 v1 与 mail #2 中均有接口状态描述/解构的扩展场景（Carrier interfaces）。\n机制与约束 — JEP 468 # JEP 468: Derived Record Creation (Preview) — Status: Candidate（非 Delivered）。语法：DerivedRecordCreationExpression → Expression with Block（官方示例：e with { ... }）。Non-Goals 写明：尚不为普通非 record 值提供 derived creation。\nBeyond Records — Reconstruction 称 JEP 468 on hold——这是设计笔记散文用语，非 JEP Status 枚举。理由：等待类级解构路径清晰，避免 record 特例与更宽类模型脱节。\n预览启用（以 JEP 页面为准，不等于已随某 GA JDK 发布）：\njavac --release 23 --enable-preview MyRecordDemo.java java --enable-preview MyRecordDemo 两条 reconstruction 概念模型（构造器糖 vs 「假装可变一分钟」的 with 块）在 amber-dev 上仍在权衡——演讲者观点，未在已抓取 OpenJDK 页面核实终局语法。\n怎么做 — 预览切片 # JEP 468 是否与 deconstructible 类首 preview 同捆——口述称「We might and we might not」（开放问题）。「slice the salami as thin as … we\u0026rsquo;d like」意味着生产代码可能经历多轮预览 API 调整（JEP 12）。\n常见误区 # 看到 JEP 468 页面上的 JDK 23 示例就认为生产已可用：须查具体 JDK release notes 与 --enable-preview 开关；Candidate 特性可随时调整。\nbrewpub 全景：高脚凳、吧台与「DRAKE\u0026rsquo;S」招牌；讲者手势说明，对应分阶段交付与「双悬崖」口述段。\n左侧黑衣听者前倾，右侧紫色 Polo 发言；背景「DRAKE\u0026rsquo;S」清晰，无 JEP 468 幻灯片文本。\nMarshalling 与序列化：公开文本能支撑什么 # 为什么 # 若拆装协议在语言层清晰，中间态可以是 JSON、字节等 wire 形式，不必是 Java 堆对象。Beyond Records 在 Looking ahead 列出 record 实例自动 marshalling / unmarshalling 的方向性句子。\n机制与约束 # 语言侧前提：解构形状、canonical 构造、（record 上）JEP 468 的 with。 Towards Better Serialization 开篇写明为探索性文档，不构成计划。 播客口语「plain marshalling」「Serialization 2.0」在已检索 OpenJDK HTML 中未逐字出现；对外材料应回链设计笔记，勿把口语别名当规范术语。 怎么做 # 最小 HTTP 骨架（演示用自定义头 X-Demo-Tenant；HttpServer 与 HttpClient 分属不同模块）：\nint port = 8080; var server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext(\u0026#34;/widgets\u0026#34;, ex -\u0026gt; { String tenant = ex.getRequestHeaders().getFirst(\u0026#34;X-Demo-Tenant\u0026#34;); byte[] body = ex.getRequestBody().readAllBytes(); // Widget w = mapper.readValue(body, Widget.class); ex.sendResponseHeaders(201, -1); ex.close(); }); 常见误区 # 把探索笔记当成即将 GA 的 Serialization 替换方案：Towards Better Serialization 明确否定「构成计划」。\n对谈中景：讲者手势展开，听者注视；背景酒吧陈设，对应字段↔component 映射「有信心但非阻塞」讨论段。\n语法讨论的治理：先语义、后表面形式 # 为什么 # mail #1 开篇 要求先讨论 concepts and directions rather than syntax。语法应 evocative（唤起概念），但过早、纯观点式的语法大战容易挤占语义反馈带宽——「Syntax discussions… worthless / worse than worthless」为演讲者观点，OpenJDK 页面未出现。\n机制与约束 # 规范阅读顺序：设计笔记 / 邮件 → JEP 草案 → 语法草案。 反馈入口：各 JEP Discussion 行、amber-dev；预览特性须附 JDK build 与 --enable-preview 复现（JEP 12：基于 real world use 收集反馈）。 对已 preview 特性：愿意按预览轮次重写 API；对尚不可运行的 memo，好的场景提问仍有价值（演讲者观点）。 怎么做 # 评审 checklist： 1) 用一句话写下语义不变式，不要先贴「Kotlin 更好」类偏好； 2) 预览特性：列出 JDK build、--enable-preview、最小复现； 3) 语法意见标注为次要，附在语义共识之后。 常见误区 # 只读 JEP 标题就上论坛争论关键字：JEP 12 写明预览特性「fully specified, fully implemented, and yet impermanent」——发布时社区仍在共同理解语义。\n左侧讲者双臂张开说明，右侧倾听；背景「DRAKE\u0026rsquo;S」招牌，OCR 残留「Ree DRAKE SY」与发光字区域一致。\n广角对谈：背景发光字区域 OCR 为「Frana. RORAKE SY」，与酒吧招牌位置对应；桌上饮品与墨镜仍在。\n中景对谈：OCR 行「Ree DRAKE SY」与背景招牌视觉一致；无语言规范幻灯片。\nPattern Assignment：无条件模式与命令式解构赋值 # 为什么 # 当编译器与读者都能静态判定某个 pattern 无条件匹配，再套 if (!(... instanceof ...)) 只会制造噪音分支与误导性 else。Amber features 2026 标题为 PATTERN ASSIGNMENT；口述「enhanced local variable declarations」应视为同一方向的口语称呼。\n机制与约束 # 尚无 Preview JEP；邮件称 draft JEP 将另议。 示例（邮件已核实）：ColorPoint(var x, var y, var c) = cp; 语义锚点：JLS 14.30.3 无条件模式。 「曾探索约 14 种语法、Preview incoming」——演讲者观点；邮件未给数量与 JDK 版本承诺。 怎么做 # void paint(ColorPoint cp) { ColorPoint(var x, var y, var c) = cp; // 邮件示意；以未来预览规范为准 // use x, y, c } 对现有代码的影响 # GA 后，静态可知必匹配的场景可从冗长 instanceof 链迁到赋值式解构；具体迁移工具与诊断规则待 draft JEP。\n常见误区 # 把邮件示例当成已可在当前 GA JDK 编译通过：须等待预览 JEP 与 --enable-preview 说明。\n对谈广角：桌上空杯与折叠墨镜，背景「DRAKE\u0026rsquo;S」灯饰；画面无 Pattern Assignment 幻灯片，仅作该段口述的场景参照。\nB-roll 与 OCR 噪声：如何不误读画面 # 节目主体为酒吧访谈，穿插 IDE B-roll。下列帧不应被解读为 carrier 类规范幻灯片；图注绑定 OCR 与可见像素。\n暗色 IDE：OCR 含「Sytevecter .frestrrey ines OFECIES」「eyte red = inagelpinel」；像素级可见 ByteVector、向量化循环，属 Vector API 演示，与 Amber 类型头语法无关。\n另一 IDE 帧：OCR「Sytevector ge veces = Bytetecter」「neatnagetpinel * 21」与补全列表叠印；仍为 SIMD 颜色旋转示例。\n片头过渡帧：画面近全黑，无可辨识技术内容，仅作时间轴占位。\n参考与延伸阅读 # Inside Java Podcast 第 52 期 — Carrier Classes \u0026amp; Discussing Syntax Project Amber — 目标、交付列表与 JEP 状态 JEP 395 — Records（Delivered，Release 16） JEP 440 — Record Patterns（Delivered，Release 21） JEP 468 — Derived Record Creation（Candidate，Preview） JEP 12 — Preview Features 机制说明 JEP 465 — String Templates（Withdrawn） Beyond Records 设计笔记（含 reconstruction on hold） mail #1 — Data Oriented Programming, Beyond Records（2026-01-13） mail #2 — Beyond Records 修订与 deconstructible class（2026-02-25） Amber features 2026 — PATTERN ASSIGNMENT（2026-01-09） JLS 14.30.3 — 无条件模式的判定 Towards Better Serialization — 探索性序列化设计笔记 Towards Member Patterns — 成员级模式示意（非最终语法） Beyond Records — Carrier interfaces 小节 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-carrier-classes-discussing-syntax-inside-java-podcast-52/","section":"文章","summary":"当 \u003ca\n  href=\"https://openjdk.org/jeps/395\"\n    target=\"_blank\"\n  \u003eJEP 395\u003c/a\u003e 把不可变载体、名义元组与 \u003ca\n  href=\"https://openjdk.org/jeps/440\"\n    target=\"_blank\"\n  \u003erecord pattern\u003c/a\u003e 绑在一起时，任何超出其约束的演进都会同时失去紧凑语法与模式匹配侧的表达能力。\u003ca\n  href=\"https://openjdk.org/projects/amber/\"\n    target=\"_blank\"\n  \u003eProject Amber\u003c/a\u003e 正把「可按固定组件形状解构」提升为类型的顶层性质，","title":"从 Record 到可解构类型：Amber 的解构—重建路线与语法治理","type":"posts"},{"content":" 从 REST 到 GraphQL：Spring 栈上的契约、解析器与实时推送 # 一场 DJ 控制台要把当前混音会话、曲目列表、艺人信息、观众投票和现场状态同时摆在屏幕上。用 REST 也能做——一个 GET 把整棵 JSON 树拉回来——但 DJ 屏往往只要其中一部分字段，而投票页只要另一套；实时状态若靠轮询，延迟和负载都会难看。Spring I/O 2026 上 Frederieke Scheper 与 Peter Eijgermans 用 Disc Jockey Console 演示了在 Spring GraphQL 与 Angular + Apollo 上从 REST 思路迁到 schema 驱动 API 的完整路径。下文按机制拆解，不当作现场实录；演示类名来自讲者代码，框架行为以官方文档与 GraphQL 规范（October 2021） 为准。\nDisc Jockey Console 现场演示：SETLIST 与 CROWD CHEERED 等交互构成选型与 API 设计的业务背景\n何时仍用 REST，何时值得引入 GraphQL # 为什么：GraphQL 语言规范 写明，客户端通过 selection set 只取所需字段，以避免 over-fetching 与 under-fetching。REST 在「简单 CRUD、领域稳定、消费者固定」时成本更低；讲者归纳（演讲者观点）为：单消费者、已有 OpenAPI / Swagger 投资、强依赖 HTTP 缓存时，继续 REST 往往更稳妥。另一方面，DJ 控制台需要 sessions、tracks、songs、artists 等多形态数据，且存在 live 更新——规范将 subscription 定义为 long-lived、按事件推送数据的请求，与 REST 轮询在架构上形成对照。\n机制与约束：Spring 侧 Query/Mutation 默认走 HTTP POST 到 /graphql，正文为 JSON（见 Spring GraphQL — Transports）。这与常见「REST GET + URL 级 CDN 缓存」不同——并非断言 GraphQL 完全不能缓存（客户端 normalized cache、persisted query 等另有路径），而是默认 POST 单端点下，HTTP 层缓存不如 REST GET 资源那样开箱即用。\n怎么做：在引入 GraphQL 前先写清消费者数量、字段差异、是否要推送、团队学习成本；若仅一个内部服务、契约稳定，不必为 GraphQL 而 GraphQL。\n常见误区：把「REST 能缓存、GraphQL 不能」当成绝对律；忽略 persisted query、字段级 CDN 等补充手段。把选型当成宗教战争——讲者收束为「REST isn\u0026rsquo;t wrong / GraphQL isn\u0026rsquo;t magic」（演讲者观点）。\n若你维护的是荷兰警方或铁路这类大型组织里的内部系统（讲者背景，演讲者观点），GraphQL 往往出现在「多团队、多 UI、同一后端」的接缝处；但若只有一个批处理消费者、契约十年不变，引入 schema 与 WebSocket 基础设施的边际收益可能为负。务实做法是先用一张表列出「字段差异 × 消费者数量 × 是否要推送 × 团队熟练度」，再决定是否在新边界上引入 GraphQL，而不是一次性替换所有 REST 端点。\nREST Still Works — Until It Doesn\u0026rsquo;t：单消费者、OpenAPI 与 HTTP 缓存等仍偏向 REST 的场景\n维度 REST 更顺手时 GraphQL 更值得评估时 消费者 单一、契约固定 多前端、字段需求分化 数据形状 稳定 CRUD 嵌套聚合、按需字段 实时性 轮询可接受 需要 subscription 缓存 强依赖 ETag / CDN 可接受应用层与客户端缓存策略 Schema 作为唯一契约与传输分工 # 为什么：多个微前端（讲者场景含 Module Federation 远程组件，属前端架构，Spring 文档不涵盖）若各维护一套 REST 聚合端点，契约容易漂移。GraphQL schema 是类型系统层面的单一合同；客户端可通过 introspection（生产可关闭）与工具链消费同一形状。\n机制与约束：\nQuery / Mutation：HTTP（Spring 的 GraphQlHttpHandler，POST /graphql）。 Subscription：WebSocket，Spring 基于 graphql-ws（子协议 graphql-transport-ws），旧版 subscriptions-transport-ws 已不活跃。 怎么做：在 src/main/resources/graphql/ 放置 .graphqls（Boot 默认自动加载，见 Spring Boot — GraphQL Schema），Query 字段名与 Java 方法名对齐（如 currentMixSession）。\n常见误区：口头说「GraphQL WebSocket」却不区分协议名；在 schema 未稳定前就让多团队各自扩展 mutation 而无 additive 演进与 @deprecated 策略。\n最小 schema 片段（字段名需与控制器一致）可写成：\ntype Query { currentMixSession: MixSession } type Mutation { crowdCheered(id: ID!): MixSession } type Subscription { mixSessionUpdated(id: ID!): MixSession } 配合 spring.graphql.graphiql.enabled=true（开发环境）可在 GraphiQL 中对照契约与 resolver。spring.graphql.schema.locations 与 file-extensions 可在多模块时改为 classpath*:graphql/**/（Schema Resources）。\nAct 3 — Mechanics：Angular + Apollo 经 spring-graphql 连接 Schema、Resolver 与 Service\n注解控制器：按字段映射，而非按 URL # 为什么：REST 常为每个资源配 @GetMapping / @PostMapping；GraphQL 入口通常是单一 HTTP 端点，解析按 operation 与 schema 字段 分发。Spring 用 @QueryMapping、@MutationMapping、@SubscriptionMapping 作为 @SchemaMapping 的快捷形式（Annotated Controllers）。\n机制与约束：AnnotatedControllerConfigurer 将带注解方法注册为 DataFetcher；未在注解中声明名称时，默认用 Java 方法名 映射字段名。@Argument 绑定 GraphQL 参数。\n怎么做（演示结构，类名来自现场）：\n@Controller public class DiscJockeyConsoleGraphQLController { @QueryMapping public MixSession currentMixSession() { return mixSessionService.getCurrentSession(); } @MutationMapping public MixSession crowdCheered(@Argument UUID id) { return mixSessionService.applyCrowdCheered(id); } } 常见误区：在 GraphQL 控制器里写 URL 路径思维；一个 mutation 塞满跨聚合副作用而不下沉领域服务。\nnear-prezentation：public class DiscJockeyConsoleGraphQLController 与 O2-rest-vs-graphql 文档并列\nDiscJockeyConsoleGraphQLController：@MutationMapping 与 crowdCheered 入口\n字段级 @SchemaMapping 与列表参数 # 为什么：即便客户端用 selection set 省略字段，DJ 列表仍可能过长；在 schema 上为 MixSession.tracks(last: Int) 增加参数，可在服务端只返回最后 N 条，减轻渲染与传输。\n机制与约束：@SchemaMapping 将方法绑到类型的字段 DataFetcher，方法第一个参数为 source（父对象），@Argument 注入字段参数（文档）。在方法内 subList 属于应用层切片；规范不强制此种参数化，但是合法设计。演讲未展开 DataLoader 与 N+1——生产嵌套列表应单独评估。\n怎么做：\n@SchemaMapping public List\u0026lt;SessionTrack\u0026gt; tracks(MixSession mixSession, @Argument Integer last) { List\u0026lt;SessionTrack\u0026gt; all = mixSession.tracks(); if (last == null || last \u0026gt;= all.size()) return all; return all.subList(all.size() - last, all.size()); } 常见误区：把所有列表裁剪都堆在 resolver，而不在持久层分页；嵌套字段循环查询导致 N+1（本场未演示防护）。\n10-mechanics：@SchemaMapping 与 DiscJockeyConsoleGraphQLController 同屏\nMutation、领域服务与订阅广播 # 为什么：观众点击「CROWD CHEERED」一类交互既要持久化领域状态，又要让已订阅的 DJ 屏/看板收到更新。GraphQL mutation 宜作薄编排层，状态变更与事务边界放在领域服务内。\n机制与约束：@MutationMapping 方法调用 MixSessionServiceImpl 等 Spring bean；@Transactional 与 repository.save 属演示领域模型（未能从官方文档核实类名）。保存后通过 MixSessionUpdatePublisher.publish 向 Reactor 流发射事件，供 @SubscriptionMapping 消费。Spring 文档写明 subscription 响应在 GraphQL Java 侧为 Reactive Streams Publisher（Transports）。\n怎么做：\n@Transactional public MixSession applyCrowdCheered(UUID id) { var session = getSessionById(id); var updated = session.applyEvent(new CrowdCheered(LocalDateTime.now())); var saved = repository.save(updated); mixSessionUpdatePublisher.publish(saved); return saved; } 常见误区：在 resolver 里直接操作 repository，导致测试与 ArchUnit 分层难以约束；mutation 成功但未 publish，订阅端永远收不到事件。\nMixSessionServiceImpl：repository.findById 与 MixSessionNotFoundException\nMixSessionUpdatePublisher：Sinks.many().multicast().onBackpressureBuffer()\nSubscription：Sinks、Flux 与 WebSocket # 为什么：替代对 currentMixSession 的轮询，在混音会话或投票 tally 变化时主动推送。\n机制与约束：\n@SubscriptionMapping 可返回 Flux\u0026lt;T\u0026gt;（Controllers）。 演示用 Sinks.many().multicast().onBackpressureBuffer() 与 tryEmitNext（Reactor Sinks API）在进程内广播；多实例、粘性会话、鉴权 不在 Spring GraphQL 文档范围内，属运维专题。 按 session id 过滤：sink.asFlux().filter(...)。讲者提到可用 Flux.concat 先推当前快照再跟更新流（演示技巧，非框架必选）。 怎么做：\npublic class MixSessionUpdatePublisher { private final Sinks.Many\u0026lt;MixSession\u0026gt; sink = Sinks.many().multicast().onBackpressureBuffer(); public void publish(MixSession s) { sink.tryEmitNext(s); } public Flux\u0026lt;MixSession\u0026gt; streamForSession(UUID id) { return sink.asFlux().filter(s -\u0026gt; s.id().value().equals(id)); } } @SubscriptionMapping public Flux\u0026lt;MixSession\u0026gt; mixSessionUpdated(@Argument UUID id) { return mixSessionUpdatePublisher.streamForSession(id); } 常见误区：把内存 Sinks 当成跨 Pod 广播方案；忽略 EmitResult 失败与背压；在未鉴权 WebSocket 上推送敏感会话。讲者直言生产里 subscription「difficult / you skip it」（演讲者观点）——与文档强调协议复杂度并不矛盾，但不应理解为规范禁止 subscription。若仅需「偶尔刷新」，SSE 或短周期轮询有时比维护 graphql-ws 集群更便宜——这属于工程权衡，而非规范裁决。\n@SubscriptionMapping 与 DiscJockeyConsoleGraphQLController 测试目录\n错误模型：HTTP 2xx 与 JSON errors # 为什么：查询不存在的 session 时，前端（Apollo）需要稳定解析业务错误，而不是只判断 HTTP 4xx。\n机制与约束：\nGraphQL 规范 — Errors：响应可同时含 data 与 errors；字段错误时 data 仍可能存在（partial result）。规范不规定 HTTP 状态码。 GraphQL.org — Serving over HTTP：当存在非 null data 时，即使伴随 errors，对 JSON 响应宜返回 2xx（partial success）。因此「HTTP 常为 200」是常见实践，网关若改写状态码需自行验证。 Spring：继承 DataFetcherExceptionResolverAdapter，用 GraphqlErrorBuilder 的 extensions 附加 errorCode（Exceptions）。 怎么做：\n@Component class GraphQLExceptionResolver extends DataFetcherExceptionResolverAdapter { @Override protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { if (ex instanceof DJConsoleException dce) { return GraphqlErrorBuilder.newError(env) .message(ex.getMessage()) .extensions(Map.of(\u0026#34;errorCode\u0026#34;, dce.getCode())) .build(); } return super.resolveToSingleError(ex, env); } } 服务层 orElseThrow(() -\u0026gt; new MixSessionNotFoundException(...)) 会进入上述解析链。\n常见误区：假设所有 GraphQL 错误都是 HTTP 200；解析失败、无 data 等情形仍可能 4xx。只在 REST 客户端里写 response.ok 判断，忽略 errors[0].extensions。\n用 curl 向本地 /graphql 发送查询不存在 id 的 operation（需替换为实际 schema 字段），典型响应形态类似：\ncurl -s -X POST http://localhost:8080/graphql \\ -H \u0026#39;Content-Type: application/json\u0026#39; \\ -d \u0026#39;{\u0026#34;query\u0026#34;:\u0026#34;query { currentMixSession { id } }\u0026#34;}\u0026#39; 响应体可能同时含 \u0026quot;data\u0026quot;: null 与 \u0026quot;errors\u0026quot;: [{ \u0026quot;message\u0026quot;: \u0026quot;...\u0026quot;, \u0026quot;extensions\u0026quot;: { \u0026quot;errorCode\u0026quot;: \u0026quot;...\u0026quot; } }]；HTTP 状态在 Spring Boot 默认配置下常为 200，但以你方网关与 spring.graphql 版本为准，未在本文环境复现的部署不应外推。\nDiscJockeyConsoleGraphQLControllerTests 与 GraphQLExceptionResolver 并列\n测试与分层：GraphQlTester 与 ArchUnit # 为什么：在不启动完整 HTTP/WebSocket 的情况下断言字段路径；并防止 GraphQL 类型渗入领域包。\n机制与约束：\n@GraphQlTest（spring-boot-graphql-test）切片测试控制器；GraphQlTester 的 documentName 从 classpath（如 graphql-test/）加载 .graphql 文档。 Spring for GraphQL 构建于 GraphQL Java 之上。 ArchUnit 包依赖规则为演示约束（Layer Checks），非 Spring 内置。 怎么做：\n@GraphQlTest(DiscJockeyConsoleGraphQLController.class) class DiscJockeyConsoleGraphQLControllerTests { @Autowired GraphQlTester graphQlTester; @MockitoBean MixSessionService mixSessionService; @Test void shouldGetCurrentMixSession() { given(mixSessionService.getCurrentSession()).willReturn(session); graphQlTester.documentName(\u0026#34;currentMixSession\u0026#34;).execute() .path(\u0026#34;currentMixSession.status\u0026#34;).entity(String.class) .isEqualTo(\u0026#34;WARM_UP\u0026#34;); } } 常见误区：只测 HTTP 集成不测 resolver 路径；让 service 模块 import org.springframework.graphql 导致 ArchUnit 失败被随意 @SuppressWarnings 掉。\nDiscJockeyConsoleGraphQLControllerTests 与 shouldGetcurrenthixsession\n前端：微前端、Apollo 与一次性投票流 # 为什么：观众扫码打开远程组件 CrowdVotePageComponent，需从 query param 读取 slot，先查当前 session 再发起投票 mutation；缓存中的陈旧 session 会导致投错场次。\n机制与约束：Apollo Client fetchPolicy: 'no-cache' 表示始终走网络且不写入 Apollo 缓存（与 network-only 相近但忽略外部 cache 更新）。路由 app-crowd-vote-page、mutation CAST_CROWD_VOTE、exhaustMap / takeUntilDestroyed 为演示代码（未能从 apollo-angular 单页核实与投票场景的绑定）。\n怎么做（结构示意）：\nconst slot = Number(this.route.snapshot.queryParamMap.get(\u0026#39;slot\u0026#39;)); this.apollo.query({ query: CURRENT_MIX_SESSION, fetchPolicy: \u0026#39;no-cache\u0026#39; }) .pipe( take(1), map(r =\u0026gt; r.data?.currentMixSession), filter((s): s is MixSession =\u0026gt; !!s), exhaustMap(session =\u0026gt; this.apollo.mutate({ mutation: CAST_CROWD_VOTE, variables: { id: session.id, slot }, })), takeUntilDestroyed(this.destroyRef), ).subscribe(); 常见误区：投票页用默认 cache-first 读到上一场 session；mutation 成功却不处理 GraphQL errors 数组。\napp-crowd-vote-page 路由与 CrowdVotePageComponent 模板路径\nCrowdVotePageComponent：fetchPolicy no-cache 与 CAST_CROWD_VOTE mutation\n上线前：订阅加固、分页与安全缺口 # 为什么：演示证明「能跑通」≠「能在生产存活」。讲者清单（主体为演讲者观点，部分与文档精神一致）包括：需要 HTTP 缓存偏 REST、团队无学习时间则别硬上、多消费者与 additive schema 偏 GraphQL、生产难点在 subscription / 分页 / @PreAuthorize 等——本场未演示 Spring Security。\n机制与约束（幻灯片 Before You Ship 与字幕交叉，具体条目以团队验证为准）：\nSubscription 加固：WebSocket 握手后鉴权常依赖 connectionParams 传 token；重连需配合 graphql-ws 与快照重放（如 Flux.concat），避免 UI 展示过期状态；帧级指标需显式埋点（如 Micrometer）。 分页：长列表宜用 cursor（first / after）；Spring GraphQL 提供 ScrollSubrange、Window\u0026lt;T\u0026gt; 等（见参考文档），演讲未写端到端示例。 Before You Ship：Subscription Hardening 与 Pagination Discipline 对照生产清单\n现场 Disc Jockey Console：CROWD CHEERED 与 SETLIST 驱动 mutation 与查询设计\n收束 # GraphQL 在 Spring 上的落地，核心是把 schema 合同、按字段的 DataFetcher、HTTP 与 graphql-ws 的分工 和 可测试的解析层 串成一条链；REST 并未失效，尤其在单消费者与 HTTP 缓存仍占优势的系统里。若你的场景是多个前端、嵌套聚合与实时推送，Spring GraphQL 提供的路径比「再加一个 BFF REST 聚合」更一致，但 subscription 运维、分页、安全与 N+1 仍需单独设计——这些在本场演示中刻意留白，不应误以为框架会自动解决。\n未验证边界（显式列出）：演示仓库的公开 Git URL 未在核验材料中给出，本文类名与方法链来自现场 OCR/字幕交叉，可能与最终开源分支有出入；DataLoader、cursor 分页、@PreAuthorize 与多副本 Sinks 广播均未在演讲中实现，读者若直接复制演示代码上生产，应单独做负载、安全与契约演进评审。HTTP REST 的 Accept / vendor media type 式版本策略本场未讨论，故本文不展开 REST 内容协商章节。\n参考与延伸阅读 # Spring GraphQL 参考文档 Spring Boot — Spring for GraphQL Spring GraphQL — Annotated Controllers Spring GraphQL — Transports（HTTP POST 与 WebSocket） Spring GraphQL — Exceptions 与 DataFetcherExceptionResolver Spring GraphQL — Testing 与 GraphQlTester GraphQL 规范（October 2021） GraphQL 规范 — Field Deprecation GraphQL.org — Serving over HTTP（状态码与 partial success） graphql-ws 协议说明（graphql-transport-ws） Reactor Core — Sinks API Apollo Client — Queries 与 fetchPolicy ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-spring-io-2026-spring-time-from-rest-to-graphql-by-frederieke-scheper-peter-eijgermans/","section":"文章","summary":"从 REST 到 GraphQL：Spring 栈上的契约、解析器与实时推送","title":"从 REST 到 GraphQL：Spring 栈上的契约、解析器与实时推送","type":"posts"},{"content":" 当标量 reward 不够用时：GEPA 与 compound AI 的反思式文本进化 # Compound AI 系统把检索、工具调用、多步推理和验证器串成一条 language program（DSPy 术语），却在生产里反复撞上同一堵墙：你真正想调的是 prompt / instruction / 可编辑文本组件，而主流 RL 管线仍把学习信号压成轨迹末端的一个 标量。论文 GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning（arXiv:2507.19457；全称 Genetic-Pareto）提出另一条路——用 自然语言反馈 驱动 反思式变异 与 按实例的 Pareto 保留，在 LangProBe 六任务上相对 GRPO 报告平均约 +6%、最高约 +20%，且 rollout 至多 35× 更少（以论文摘要为准）。实现见 gepa-ai/gepa，并已集成进 DSPy 的 dspy.GEPA。\n下文是面向有经验工程师的技术合成：把常见做法、论文/代码可核对机制、以及播客中 Lakshya A. Agrawal 的演讲者观点分开写；本集画面均为远程对谈，无论文架构幻灯片（配图仅作语境，不当作 Fig.3 替代品）。\n开场分屏：左侧 Weaviate podcast 品牌墙与 Florida Atlantic University 证书框，右侧嘉宾 Lakshya A. Agrawal。\n问题空间：compound AI 优化在优化什么 # 为什么：RAG、agent、PoT、ReAct 等形态在 LangProBe 里被统一成「带控制流的 LLM 程序」。架构选型与 optimizer（Bootstrap、OPRO、MIPRO、GRPO、GEPA）共同决定 cost–performance 前沿；换更大模型往往不如改 instruction 或可进化文本 划算。\n机制/约束：论文将待进化对象记为程序参数 (\\Pi_\\Phi)，冻结 权重 (\\Theta_\\Phi)。优化预算通常是 metric / rollout 调用次数（max_metric_calls），而非 GPU 训练步。适用前提是：你能对每个实例给出 可自动化的 (\\mu)（标量）和 ideally (\\mu_f)（文本反馈）——编译器报错、rubric 分项、profiler 输出、LLM-as-judge 评语等。\n怎么做（最小接口）：在 DSPy 侧，metric 可返回 Prediction(score=..., feedback=...)；GEPA 的 EvaluationResult 同样承载 score 与 feedback。\n# 概念示意：反馈不必塌缩成单一 float def metric(example, pred, trace=None): ok = pred.answer == example.answer feedback = \u0026#34;\u0026#34; if ok else f\u0026#34;expected {example.answer}, got {pred.answer}\u0026#34; return dspy.Prediction(score=float(ok), feedback=feedback) 常见误区：把 GEPA 当成「免标注的通用 RL 替代品」。论文对比的是 同一 language program 上的 prompt 进化；无可靠 verifier 的开放域任务，PUPA 以外 judge 优化仍属未系统验证边界（演讲者观点）。\n讨论自然语言反馈与 rubric 时段的对谈画面；画面无技术图表。\n标量轨迹 vs 文本反馈：学习信号密度 # 为什么：GRPO 等 Group Relative Policy Optimization 在长轨迹末尾汇总 reward；慢 rollout（编译 + 上板执行）下，大量对比轨迹成本极高。嘉宾动机（与论文摘要一致）：rubric 子项、符号名建议、长度约束 等 trace 信息密度高于单一标量。\n机制/约束：论文引入 feedback function (\\mu_f)，与标量 (\\mu) 并用。六任务之一的 PUPA 在 artifact 中实现为 PAPILLON（数据 Columbia-NLP/PUPA），由 LLM judge 产出质量分与自然语言 feedback。\n怎么做：先定义「什么算失败、失败时给模型看什么」——再交给 reflective mutation 的 LLM 读 trace 改 instruction。\n常见误区：认为「有 feedback 就不需要 score」。代码路径仍要 可比较的标量 做 Pareto 记分与接受/拒绝；文本是 提案方向，不是唯一度量。\nOCR 可见片段：@@\u0026amp; Weaviate Sef podcast（品牌 UI，非论文图表）。\nGEPA 主循环：候选池、minibatch 试探、全量记分 # 为什么：全局贪心地只 mutate「当前总分最高」的 prompt，容易 局部最优；需要维护多样候选并在验证集上 按实例 跟踪谁最好。\n机制/约束（论文 Fig.3–4、engine.py）：\n维护 candidate pool 与 Scores Matrix（每个 candidate × 每个 validation instance）。 每轮 Pareto 采样 → reflective mutation 或 merge 提出新候选。 先在 minibatch 上评估；有提升再 full eval 入库。 Reflective mutation（ReflectiveMutationProposer）：在选中父代上跑带 trace 的执行，把 input/output 与用户定义的 metric 文本交给 反思 LLM，产出新 instruction；System-aware merge 则在进化树不同 lineage 之间合并文本洞察，再全量评估是否入库。\n怎么做：gepa.optimize() 默认 reflection_minibatch_size=3 表示 3 个不同实例各 1 次带 trace 执行，而非同一实例重复 3–4 次采样（播客「3–4 次 rollout」与默认参数 弱一致，宜标为演讲者观点）。use_merge=True 时启用 merge 提案；长跑后 lineage 信息经 DspyGEPAResult.parents 等结构保留。\n常见误区：把「候选池」理解成 k-best 单标量排序；核心是 per-instance 子分数（prog_candidate_val_subscores）。另一误区是忽略 minibatch 筛选：未在子集上改进的提案不会进入全量记分，这是控制 max_metric_calls 的关键阀门。\nOCR 可见：» Weav =) 4 / Weav（对谈 UI）。\nGenetic-Pareto：按实例保留，而非只追 aggregate 冠军 # 为什么：可能存在「只在 Task 1 极好」的 instruction 片段；若每轮只改 aggregate 最高分，这些 非冠军 insight 会丢失。\n机制/约束：论文 §3.1 的 Pareto-based candidate sampling；代码默认 frontier_type=\u0026quot;instance\u0026quot;（ParetoCandidateSelector）。从「在每个 instance 上表现最好的候选集合」中 随机 选父代做 mutation；长跑后 system-aware merge（MergeProposer）尝试合并不同 lineage 的文本洞察。\n嘉宾将此举类比 MAP-Elites 的 quality-diversity 思想——GEPA 论文未引用 MAP-Elites，机制上接近「按实例精英集」，不能写成「实现了 MAP-Elites」。\n常见误区：test-time 模式下「每实例存 best prompt」与部署「单一 universal prompt」矛盾——论文 §5.1 的 inference-time 用法 允许 为一批 hard task 过拟合该批并共享 insight；是否上线单一 prompt 是产品决策，不是算法必输出。\nOCR 可见：‘ow Weaviate OF podcast（品牌 UI）。\nOCR 可见：@\u0026amp; Weaviate @f podcast _（对谈分屏）。\n相对 MIPROv2 与 GRPO：DSPy 谱系中的位置 # 为什么：团队已在 DSPy 生态里做过 Bootstrap FewShot → OPRO（prompt + score）→ MIPROv2（instruction + examples，Optuna 搜索）→ GEPA。GEPA 论文报告相对 MIPROv2 平均 10%+（摘要），AIME-2025 例 +12%；相对 GRPO（实验配置见 gepa-artifact）为六任务平均 +6%、最高约 +20%、rollout 至多 35× 更少。\n机制/约束：GEPA 用 遗传式提出 + Pareto 采样 替代 MIPRO 侧的 Bayesian/Optuna 程序搜索，并强调 单轨迹上的迭代反思（reflective mutation）。对比 GRPO 时，artifact 中 GRPO 常配 num_rollouts_per_grpo_step=12 等——对比的是 teleprompter 级 prompt 优化，非预训练 RL。\n口述冲突（须标注）：嘉宾曾称相对 GRPO 最高约 +25%；摘要写的是 up to 20%，正文另有任务级 19% 等表述。35× 与摘要一致；25% 无摘要支持。\n常见误区：把标题「Outperform Reinforcement Learning」读成「淘汰所有 RL」。论文边界是 有丰富 (\\mu_f) 的 compound LLM 系统；嘉宾亦认为未来 RL 会吸收 natural language reflection（演讲者观点，非 MMGRPO 已证结论）。\nOCR 可见：(By Weaviate @f podcast（MIPRO/GEPA 谱系讨论时段）。\nLangProBe 与 kernel 讨论附近的对谈帧；背景仍为 podcast 布景。\nOCR 可见：a7.) \\e) 8) xe)（GRPO 对比口述时段 UI）。\nLangProBe 实验与 §5.1 的 kernel / 推理时搜索 # 为什么：需要同时比较 架构（CoT、RAG、ReAct…）与 optimizer 的 Pareto；嘉宾称更好架构常 同时 提性能降成本，optimizer 再推前沿（演讲者观点 + LangProBe 设计目标）。\n机制/约束（已验证）：GEPA 主表六任务：HotpotQA、AIME、LiveBench-Math、IFBench、PUPA、HoVer（与 artifact get_benchmarks() 一致）。AppWorld 在 LangProBe 的 agent benchmark 集中，未列入 GEPA 论文六任务主表。播客提到的 Baleen 在 LangProBe 目录与 GEPA PDF 中 均未检出——疑为口述混淆，正文不依赖此名。\n§5.1（初步实验，不与主表混谈）：\nAMD XDNA2 / NPUEval 与 KernelBench + NVIDIA V100 CUDA； 论文表述：CUDA kernel 在 35 个代表任务中 \u0026gt;20% 快于 PyTorch-eager（非播客笼统「击败人工 PyTorch baseline」；须对照表格）。 Inference-time：将待解任务集同时作 (D_{\\text{train}}) 与 (D_{\\text{pareto}})，允许对该批任务「过拟合」并在相似子任务间迁移 insight（论文 §5.1；与嘉宾 test-time 叙述一致）。嘉宾举例：一批 PyTorch→CUDA 算子可共享同一进化中的 prompt 教训；也可 仅为单实例 优化后丢弃更新——与「训练一个从零泛化的 universal prompt」是不同产品模式（演讲者观点）。\n反直觉但论文支持的一点：优化改的是 (\\Pi_\\Phi)（instruction 等文本），但 test-time 下每轮实际变化的是 在该 prompt 下生成的 code/输出；compiler/runtime 报错写回 prompt，更像 自举数据 而非一步梯度（§5.1 与演讲者「改 prompt = 改解」表述一致）。\n样本效率主张的边界：摘要写 \u0026ldquo;even just a few rollouts\u0026rdquo; 可带来大幅提升；嘉宾称 as few as one 失败 rollout + 反馈即可——后者宜标演讲者观点。README 给出昂贵场景 100–500 次 metric 调用 vs GRPO 5,000–25,000+ 的量级对比，与 35× 同向，但是营销区间，落地应自建计数器。\n常见误区：用 CITATION.cff 旧文案「四任务 / +10%」——与 v2 摘要 六任务 / +6% 冲突，以 arXiv 摘要与 PDF 为准。勿把 KernelBench 段落数字并入六任务主表做「全面 SOTA」表述。\nOCR 可见：@@\u0026amp; Weaviate ager podcast。\n约 30 分钟处对谈：Weaviate podcast 分屏与 Florida Atlantic University 证书，无 kernel 性能表。\nOCR 可见：Weav ait / XQ（推理时/硬件讨论时段）。\n超越 prompt 字符串：optimize_anything 与多目标 Pareto # 为什么：HNSW 循环、CUDA 片段、任何 可文本化 的系统组件都可能比权重微调更快迭代（README / optimize_anything 文档）。嘉宾称 GEPA 是 text evolution engine（演讲者观点）；HNSW 具体实验未在 GEPA 论文 PDF 中检出。\n机制/约束：objective_scores + frontier_type 取 \u0026quot;objective\u0026quot; / \u0026quot;hybrid\u0026quot; / \u0026quot;cartesian\u0026quot; 时做 多目标 Pareto tracking；reflection 可同时看到 recall↑ 与 runtime↑ 等分项，而非只看聚合标量。\n常见误区：以为必须拆独立仓库「GEPA-as-a-text-evolution-engine」——能力已并入 gepa 主库与 PyPI gepa。\nOCR 可见：Weavi )（多目标/text evolution 讨论时段）。\nOCR 可见：@@\u0026amp; Weaviate Sgr podcast（开场动机：硬件/慢 rollout 场景）。\n仍未收敛的分歧（勿强行统一结论） # 主题 常见做法 嘉宾论点 证据边界 Agent vs 固定 workflow 产品层二选一 对 系统构建者 同为 LLM+控制流；差别在控制流谁写 演讲者观点；LangProBe 兼收 AppWorld 与固定管道 领域知识进 prompt 还是权重 微调派 vs prompt 派 先提取进 prompt，再编码进权重更高效 MMGRPO 未在 GEPA 论文出现；mmgrpo_runs 无公开 README Train-then-generalize vs test-time 单一部署 prompt 前者反馈应可迁移；后者可超专用（如具体 compiler error） §5.1 + 演讲者观点 一次失败 rollout 是否够用 RL 需大量对比轨迹 「as few as one」 论文写 \u0026ldquo;few rollouts\u0026rdquo;；「一条就够」为演讲者观点 GEPA vs JEPA 名称 对外 GEPA 团队内部曾称 JEPA 演讲者观点（Yann LeCun JEPA 命名冲突背景） 小模型 + 搜索 vs 超大模型 堆参数 350M 经搜索可超 500B（主持人转述） 本集无实验名；无法验证 多样性来源 高温 best-of-N Pareto + reflective mutation 即可探索解空间 论文强调多样性；与 best-of-N 严格等价未证 DSPy 集成状态（截至 2026-05 检索）：dspy.GEPA 已存在于 DSPy main，PyPI 包 gepa 可独立安装；播客录制时「数日内并入」的 具体日期无法反推，但当前工程上已可跟 官方教程 试用。嘉宾路线图 MMGRPO（先 prompt 再 RL 写权重）与 GEPA-like reflection for weight updates 在论文与 arXiv 检索中 均无对应条目，写作时勿与 README 提到的 BetterTogether 混为一谈。\n收束段对谈：双方分屏，无结果表 OCR。\n若你要落地 # 先写清 (\\mu) 与 (\\mu_f)：失败时模型应看到什么文本（编译错误、judge rubric、泄漏检测说明），再选 dspy.GEPA 或 gepa.optimize()；开放域仅 score、无 verifier 时预期应保守。 划清验证集角色：(D_{\\text{pareto}}) 用于按实例记分与 Pareto 采样；不要把「训练集泄漏进选择」误当成 bug——inference-time 模式 故意 让 train=pareto（§5.1）。 预算按 max_metric_calls 规划：相对 GRPO artifact 的数千 rollout，GEPA 营销/README 常提 100–500 次量级；与论文 35× 同向，但以你的 metric 成本为准。 别只 mutate aggregate 冠军：确认 frontier_type=\u0026quot;instance\u0026quot;（默认）符合你的多任务/多技能 instruction 需求；有多目标时用 objective_scores。 引用数字用论文：主表 +6% / +20% / 35×；kernel 与 NPU 写 §5.1 初步实验；嘉宾 25% 仅作口述差异脚注式说明，不作 SLA。 参考与延伸阅读 # GEPA 论文（arXiv:2507.19457） — Genetic-Pareto、六任务与相对 GRPO/MIPROv2 指标 GEPA 论文 PDF — Fig.3–4 算法与 §5.1 inference-time gepa-ai/gepa — GEPAEngine、gepa.optimize() API gepa-ai/gepa-artifact — 复现实验与 GRPO/MIPROv2 配置 DSPy 仓库 — language program 与 teleprompter 生态 DSPy GEPA 教程 — 集成用法 DSPy GEPA 源码 — GEPAFeedbackMetric、@experimental LangProBe 论文（arXiv:2502.20315） — compound AI benchmark 定义 LangProBe 仓库 — AppWorld、HoVer 等程序集 PUPA 数据集 — 隐私改写 + judge 反馈 optimize_anything 介绍 — 文本参数进化 PyPI: gepa — 安装与版本 MAP-Elites（QD 类比背景） — 非 GEPA 引用，仅供理解 Pareto-per-instance BetterTogether（arXiv:2407.10930） — GEPA README 提及的「GEPA + RL」互补方向，≠ MMGRPO MIPROv2 实现（Optuna） — 与 GEPA 对照的 Bayesian 搜索侧 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-gepa-with-lakshya-a-agrawal-weaviate-podcast-127/","section":"文章","summary":"当标量 reward 不够用时：GEPA 与 compound AI 的反思式文本进化","title":"当标量 reward 不够用时：GEPA 与 compound AI 的反思式文本进化","type":"posts"},{"content":" 当查询变成整段代码：RAG 评测与搜索型 Benchmark 的分裂 # 生产里的 RAG 管线早已不是「用户敲五个词、系统返回十条蓝链」。开发者会把报错栈、LangChain 配置片段、多仓库上下文一并塞进检索器；Agent 还会在生成过程中反复查库。与此同时，选型仍常看 BEIR 上的 nDCG@10 或 MTEB 排名——这些榜多数假设 短查询、秒级响应、排名列表质量。BEIR 共同一作 Nandan Thakur 在公开讨论中主张：搜索型 IR 评测与 长上下文、事实级覆盖 的 RAG 评测正在分裂；二者不应被单一数字合并。（下文标注 演讲者观点 处来自其访谈表述，已与论文核对处单独标明。）\nBEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models；作者含 Nandan Thakur、Nils Reimers 等，UKP-TUDA。\n问题空间：三条互不替代的评测轴 # 轴 典型代表 测什么 容易误读之处 异构零样本检索 BEIR、MIRACL 短 query → 文档排名 高分 ≠ 长代码问答可用 开发域 RAG 检索 FreshStack SO 长问题 + GitHub 语料上的 nugget 覆盖 偏 检索 test collection，非端到端生成榜 Agentic / 多跳 BrowseComp、BRIGHT 推理链 + 多次检索 延迟预算与 BEIR 不可比 演讲者观点：Wikipedia、HotpotQA 式「短问 + 已知世界知识」的 RAG 评测，模型可能不靠检索就能答；更应测 小众域、新文档、需长总结 的任务（其以 TREC RAG 中多因素人物关系类问题为例）。该判断是评测哲学，非 BEIR 论文结论。\n若你维护向量库或 RAG 网关，常见失败模式是：线上 query 分布已迁移到「粘贴半页日志」，而离线仍只跑 MS MARCO 式短 query 回归——外推误差往往体现在 召回够用但 nugget 覆盖不足，而非 nDCG 小数点后的排名抖动。\nBEIR：为什么 IR 与 NLP 需要同一套零样本榜 # 为什么 # BEIR 论文 指出：神经 IR 长期在 同质、窄域 设定下比较（例如在 MS MARCO 上训练又在同分布上评测），难以判断模型能否迁移到法律、生物医学、争议检测等 分布外 任务。BEIR 聚合 18 个公开集、9 类检索任务，统一报告 零样本 nDCG@10（pytrec_eval 的 ndcg_cut_10）。\n机制与约束 # DPR 在 BEIR 上被论文描述为少数非 MS MARCO 训练的稠密基线之一，零样本泛化在 18 集中普遍弱于在 MS MARCO 上训练的模型——说明「同源训评高分」与「跨域榜」可脱节（文献）。 主表以 nDCG@10 为主；beir 框架亦支持 MRR、Recall@k，但 论文主结果不以 MRR 为统一主轴。 演讲者观点：选 @10 是为模仿搜索结果页前若干条；论文原文仅写用户希望相关结果 靠前，未出现 Google/SERP 明文——宜视为工程类比，非 BEIR 原文表述。 怎么做（最小示例） # # 概念：beir 评测检索器在多个数据集上的 nDCG@10 from beir import util from beir.datasets.data_loader import GenericDataLoader from beir.retrieval.evaluation import EvaluateRetrieval dataset = \u0026#34;nfcorpus\u0026#34; url = f\u0026#34;https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/{dataset}.zip\u0026#34; data_path = util.download_and_unzip(url, \u0026#34;datasets\u0026#34;) corpus, queries, qrels = GenericDataLoader(data_path).load(split=\u0026#34;test\u0026#34;) # retriever.search(...) 后： # ndcg, _map, recall, precision = EvaluateRetrieval.evaluate(qrels, results, [10]) 常见误区 # 用 BEIR 榜首直接选型 长查询 RAG 产品——查询长度、标注粒度、延迟假设均不同。 把 MRR 与论文主表 nDCG@10 混为同一「BEIR 分数」。 分屏对谈画面；左侧背景可见 Weaviate podcast 标识与书架，无算法幻灯片。\nFreshStack：长查询、nugget 与 test collection 的「故意作弊」 # 为什么 # FreshStack 面向 开发者 RAG：查询来自 Stack Overflow（2024-10 dump，保留 accepted answer），筛选 2023 年起 的小众技术主题以降低 LLM 训练污染；语料为对应标签下的 GitHub 公开仓库切块（文献/ HF 数据集卡）。演讲者观点：坚持真人社区问题，批评部分 RAG benchmark 过度依赖合成 query。\n机制与约束 # 分块：论文规定语料 maximum 2048 tokens（文献）。访谈称因当时 embedding 上下文上限而折中——论文未写该因果，不宜当作官方解释。chunk 越大，单文档内事实越集中，但跨文件依赖（如 monorepo 多包引用）更依赖检索器多次命中，而非一次大块命中。 检索指标：除排名质量外，Coverage@20 直接对应「前 20 篇能否撑起全部 nugget」——比「第 11 名相关文档是否存在」更贴近 RAG 生成前的上下文充足性（文献定义；与生成端 hallucination 仍非同一指标）。 Nugget：core concept or atomic fact essential in a response；谱系可追溯至 TREC QA 的 information nugget（论文引 Voorhees 2003、Lin 2005/2006 等）。FreshStack 用 GPT-4o 从问答生成 nugget，并在子集上由研究者验证精度/覆盖（文献）。 标注：单元是 nugget–document 支持（二元），而非整段长 query 与文档的「部分相关」。池化时对多路 inference（子问题分解、HyDE 类闭卷答案等）与 oracle（答案全文、nugget 拼接）检索，每模型 top 100，分数归一化后 相加融合，再对融合结果 top 20 做 LLM 支持判断（文献；与访谈口述的 top-50 数字不一致，以论文为准）。 演讲者观点：构建 test collection 时混合 BM25、稠密向量、ColBERT 类检索器以增加标注多样性，在 test collection 语境下属于某种「cheating」——目的是减少 holes（应标未标进池的文档）。文献核对：FreshStack 池化用 BM25、BGE、E5 Mistral 7B、Voyage-large-2 及分数融合，未使用 ColBERT；MIRACL 标注阶段为 BM25 + mDPR + mColBERT（top-10 判断），融合为归一化 平均，亦非 RRF。 检索侧指标含 α-nDCG@10、Recall@50、Coverage@20（nugget 被前 20 文档覆盖的比例）等（文献），与 BEIR 主榜不可直接横比。\n怎么做（最小示例） # 从 Hugging Face 加载查询与 nugget 标注（需接受 FreshStack 许可条款）：\nfrom datasets import load_dataset queries = load_dataset(\u0026#34;freshstack/queries-oct-2024\u0026#34;, split=\u0026#34;train\u0026#34;) # 字段含 nuggets、nugget 级 relevant_corpus_ids 等；见数据集卡 自建私有集时可复用范式：分解/多路查询 → 融合扩池 → nugget 级判支持，但上线服务是否同样多路融合需单独做延迟与成本评估（演讲者观点）。\n常见误区 # 把 FreshStack 分数当作 端到端 RAG 答案质量——它主要评 检索 test collection。 假设访谈中的 RRF 即 FreshStack 默认实现——论文为 归一化分数求和；RRF 见 SIGIR 2009，与 MIRACL/FreshStack 建池写法 不同。 追求「召回越多越好」——演讲者观点 提出 minimal spanning subset：若少量文档已覆盖全部 nugget 与引用，优于把 20 篇塞进 context；非 已标准化竞赛指标。 分屏对谈；左下 Weaviate podcast 角标，右侧嘉宾发言，画面无 benchmark 表格。\n主持人分屏持笔；背景 Weaviate podcast 标识，讨论进行中的访谈画面。\n查询改写：SPLADE、分解与「理想 embedding」的张力 # 为什么 # 经典 IR 的 query expansion 对 BM25、ColBERT、SPLADE 均常有收益。演讲者观点：理想状态下，端到端 embedding 应 无需 显式 rewriting；但现实里用户查询变长变复杂后，query decomposition（FreshStack 子问题、BRIGHT 代码/数学）在现阶段仍有必要——与「理想」并存，而非二选一结论。\n机制与约束 # SPLADE（SParse Lexical And Expansion）：在词汇表维学习稀疏激活，实现 隐式 expansion，缓解 vocabulary mismatch（文献）；可部分替代手工加词，但不等于可删掉所有 LLM 改写步骤。 FLARE（arXiv:2305.06983）：生成中对 低置信 token 触发 forward-looking 再检索，属 active RAG 一族（文献；演讲者观点 将其与 multi-hop 评测动机一并提及）。 怎么做 # 对短 Web 查询：可先 A/B 无改写 vs SPLADE/稠密混合；对长开发问题：在标注或评测集上尝试 nugget 级子查询，再与 HyDE 类 oracle 路对照 FreshStack 论文的 inference/oracle 划分。\n常见误区 # 把 MTEB/BEIR 上的单向量榜首等同于「长查询无需分解」。 在生产环境照搬 test collection 的 四模型融合——演讲者观点 承认线下建池强、线上 multi-hybrid 效率差。 约 10 分钟处分屏；Weaviate podcast 背景牌与麦克风，无检索公式幻灯片。\nAgentic 检索与延迟预算的再谈判 # 为什么 # BRIGHT 等强调更长、需推理的查询；BrowseComp 类任务需要代理式浏览与多跳检索。演讲者观点：此类任务 必须 agentic retrieval，单次搜索不够；与 BEIR 短 Web 查询上「分解有帮助但非必须」形成对照。\n机制与约束 # 延迟：读 PhD 时强调检索延迟；现在 ChatGPT/推理模型用户愿为答案质量等 数分钟（演讲者观点，行为假设，无统一实验编号）。 专利检索 等：可接受 30 分钟–1 小时 离线、多级 reranker（monoT5/duoT5 等 Waterloo 系工作）——与 BrowseComp「agent 浏览 20 分钟」同属 正确性优先、延迟不敏感 族（演讲者观点）。 FLARE 与 Search-R1 等把 何时检索、检索什么 纳入训练或循环，与静态 top-k 检索评测不同轴（Search-R1 细节本稿未独立核验）。 怎么做 # 为 agent 产品单独建 多跳成功率 / 工具调用轨迹 评测，与 nDCG@10 分栏报告；引用 TREC RAG Track 的 nugget 级 A_strict 等指标时，对齐当年赛题定义而非往年口径。\n常见误区 # 用单一「检索准确率」概括 BrowseComp 与 BEIR。 忽略 streaming 带来的「可接受等待」变化，仍按 200ms SLA 设计多跳 agent（演讲者观点）。 约 20 分钟分屏；Weaviate podcast 标识，嘉宾侧纯色背景。\n分屏对谈后期画面；左侧 Weaviate podcast 角标，右侧嘉宾面向镜头。\n域专用 Embedding 与通用模型：尚无标准答案 # 演讲者观点：承认「不知道标准答案」；小模型难压缩多域知识，但 decoder/LLM 底座变大 后，通用跨域训练可能变容易；knowledge cutoff 仍可用 RAG 补新文档——需在 你自己的域 上实测，而非照搬榜一。\nAIR-Bench 等合成/模拟域 benchmark 在缺乏人工标注时有用（演讲者观点）；与 FreshStack「真人 SO」路线形成 不同取舍，不构成孰优孰劣的单一结论。\n未在本访谈字幕中展开：主持人曾问 FreshStack 的 hard negative labeling，嘉宾转向 mixture of retrievers——不可 将本集当作 hard negative 训练指南。\n结构化检索与分页：评测空白 # 房产 App「城市 + 500 条在售 + 分页聚合」类需求，涉及 text-to-SQL、过滤与 MapReduce 式并行推理，而非传统 BEIR 文本相关性。演讲者观点：个人研究重心在 纯文本检索与 embedding；承认 pagination 重要，但 未 给出替代 nDCG@10 的分页评测方案——工程上宜自建 分面检索 + 聚合正确性 指标，勿期待 BEIR 延伸覆盖。\n约 30 分钟分屏；左侧书架与 Weaviate podcast 标识，讨论结构化检索时的访谈帧。\n分屏中可见 Weaviate podcast 字样与绿色声波图形，为节目品牌角标。\n若你要落地 # 分轴建榜：短查询保留 BEIR/MTEB 类检索榜；产品若已是长上下文 + 代码块，增加 FreshStack 类或 自建 nugget 评测，勿用单一 nDCG@10 选型。 复现 FreshStack 标注时：以论文 top-100 融合、top-20 判支持 为准；融合策略写清是 分数和 还是 RRF，勿混称。 报告 agent 与搜索：BrowseComp/BRIGHT 与 BEIR 分栏；延迟 SLA 单独声明。 建池可厚、线上可薄：多检索器融合扩池在 离线标注 有效（演讲者观点 + 文献），生产路径需另做 QPS/成本测算。 争议域先 A/B：域专用 embedding（如 Voyage 域模型路线）与通用大 embedding 在你的 query 分布上实测，而非只看公开榜。 参考与延伸阅读 # BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models BEIR 评测代码仓库 beir-cellar/beir FreshStack: Building Realistic Benchmarks for RAG on Developer Documentation FreshStack 查询集（Hugging Face） FreshStack 语料（Hugging Face） MIRACL: A Multilingual Retrieval Dataset MIRACL 项目主页 SPLADE v2: Sparse Lexical and Expansion Model Reciprocal Rank Fusion（SIGIR 2009） FLARE: Active Retrieval Augmented Generation BRIGHT: A Realistic Benchmark for Retrieval-Augmented Generation TREC RAG Track 官网 MTEB Leaderboard Sentence-Transformers 训练与评测概览 RAG 原始论文（Lewis et al.） AIR-Bench: AI Retrieval Benchmark ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-rag-benchmarks-with-nandan-thakur-weaviate-podcast-124/","section":"文章","summary":"当查询变成整段代码：RAG 评测与搜索型 Benchmark 的分裂","title":"当查询变成整段代码：RAG 评测与搜索型 Benchmark 的分裂","type":"posts"},{"content":" 多阶段语言程序与自动 Prompt 优化：从 DSPy 到 MIPRO # 当 RAG、Agent 与「复合 AI 系统」叠成十几步流水线时，真正卡住团队的往往不是再换一个向量库，而是：谁在什么粒度上优化 prompt、demo 与模块组合，以及端到端 metric 如何把功劳摊到每一步。 本文围绕 DSPy 与其中的 MIPRO（Multiprompt Instruction PRoposal Optimizer）梳理机制、文献可核对部分与仍属工程判断的边界；画面素材为访谈双人镜头，未见可读架构图或结果表，定量结论以论文与官方 API 为准。\n开场画面：左侧背景墙可见 Weaviate podcast 品牌标识（OCR 片段：Weaviate i livre 1 4 AH INE \\ih）\n问题空间：多阶段程序、复合系统与评估瓶颈 # 为什么：检索增强、工具调用与多角色编排，本质都是「多次 LM 调用 + 中间状态」——网页 Agent 逐步行动、检索器改写查询、写作流水线分节生成，都依赖外部状态，很难用单次结构化输出在上下文内等价替代（演讲者观点）。\n机制/约束：业界常混用 multi-stage、compound、multi-agent；在 DSPy 主论文 叙事里，重点是可编译的多模块程序与程序级 metric，而非 UI 上有几个聊天气泡。CrewAI 式多角色团队可视为另一类多阶段 LM 系统，但营销类任务往往缺少可在线优化的延迟指标——评估设计才是瓶颈（演讲者观点）。\n常见误区：把「Agent 框架」与「prompt 优化器」对立；优化器针对的是程序内各 LM 模块的 instruction/demo 参数，中间检索、函数调用可以是黑盒，只要 metric 在程序出口可算（见后文 P09 边界）。\n若你已有 Weaviate / 其他向量库上的 RAG，可把它视为程序中的 Retrieve 或自定义 Python 模块：优化器不会替你改 embedding 模型，但会改「如何用检索结果回答」那一层的 instruction 与 demo。主持人提到的「生成式反馈环」——把生成内容写回库以供后续检索或微调——与多阶段程序正交，属于数据飞轮设计（演讲者观点），需单独定义一致性与权限策略。\n讨论 proposal / credit assignment 时段：画面仍为访谈分屏，OCR 可见 Weaviate \\ ( inetd) I) Ue\nDSPy：用程序声明替代手工 prompt 工程 # 为什么：手工 prompt 在个案上可极强（嘉宾自述 EHR 项目中优于有限数据微调——演讲者观点，不可外推），但换模型、扩任务时维护成本高。\n机制/约束：DSPy 将流水线写成 Module + Signature + Metric；teleprompter / optimizer 在训练集上搜索各模块的 instruction 与 demonstration，目标为程序级分数，而非单点 loss 反传。\n怎么做（minimal）：\nimport dspy class QA(dspy.Signature): \u0026#34;\u0026#34;\u0026#34;Answer from context.\u0026#34;\u0026#34;\u0026#34; context = dspy.InputField() question = dspy.InputField() answer = dspy.OutputField() class RAG(dspy.Module): def __init__(self): self.retrieve = dspy.Retrieve(k=3) self.generate = dspy.ChainOfThought(QA) def forward(self, question): context = self.retrieve(question).passages return self.generate(context=context, question=question) # metric + trainset → optimizer.compile(program, trainset=...) 常见误区：把 DSPy 当成「更好的 prompt 模板库」；其核心是可优化的参数化程序，与 OPRO 类「LM 当优化器」思路可组合，但多模块时信用分配更难（下节）。\n访谈中 Krista 将「用 GPT-4 写 DSPy 程序」描述为因 API 类似 PyTorch 而「还不错」（演讲者观点）。这降低的是拓扑搭建成本，不替代 metric 设计与优化预算；愿景级的「用户只说 optimize」仍依赖任务示例、评判标准与算力（演讲者观点）。工程上更稳妥的路径是：人工搭骨架 → 自动搜 prompt/demo → 再考虑 per-stage 换模型或 prompt routing（演讲者观点）。\n早期概念分野讨论：OCR 片段 Weaviate \u0026ldquo;i, if \\ HH ae a\n约 4 分钟处：主持人侧可见 Weaviate podcast 墙牌与波形装饰，无技术幻灯片\nMIPRO：提议、自举与组合搜索 # 为什么：多阶段程序中，每个模块的 prompt 含 instruction 与 few-shot，搜索空间随模块数指数级膨胀；需要专门处理「如何提议好候选」与「谁该为 metric 负责」（论文 §3.1–§3.2 术语：proposal challenge、credit assignment challenge——arXiv:2406.11695）。\n机制/约束：\n叙事来源 阶段划分 访谈口述 两阶段：先提议 instruction/demo，再组合搜索（演讲者观点） DSPy MIPROv2 文档 三阶段：bootstrapping → grounded proposal → discrete search（surrogate + BO） 二者不矛盾：文献把 bootstrap 单列；访谈将「提议+搜索」并谈。主循环可概括为：\nFew-shot 来源（P02，已核实）：对训练样本跑完整程序；若 metric 判定成功，保留逐步 trace 作为该模块 demo——BootstrapFewShot 族与 MIPRO 论文 Initialize 一致。\nInstruction 提议（P03，已核实）：GroundedProposer 可接入 dataset summary、program summary（DescribeProgram / DescribeModule）、bootstrap I/O，以及 categorical tip（源码含 high_stakes 等）。嘉宾称「让 LM 看见自己的程序结构」能改进提议——演讲者观点，与论文 grounding 设计一致。\n组合搜索（P04，部分核实）：将 instruction × demo 视为昂贵黑盒；MIPRO 用 Optuna TPE 作 surrogate，在 mini-batch 上评估以抗噪（论文 §4.3；Snoek et al. 2012）。MIPROv2 默认 minibatch_size=35（源码），勿与论文中「20–50 次 full evaluation trials」预算混为 batch=20（核验报告已标注）。\n怎么做：\ntp = dspy.MIPROv2(metric=my_metric, prompt_model=\u0026#34;gpt-4o-mini\u0026#34;, task_model=\u0026#34;llama-3-8b\u0026#34;) optimized = tp.compile(my_program, trainset=train, num_trials=30, minibatch=True) 常见误区：以为 MIPRO 只改 system 一句；它同时搜 instruction 与 demonstration 组合，且 v2 强调小 batch 上的快速试探。\n论文 §3.2 还讨论 greedy、surrogate、history-based 等信用分配策略：surrogate 与 MIPRO 主循环中的 TPE 一致，用观测过的 (组合, mini-batch 分数) 估计各候选潜力；greedy 逐模块锁定，实现简单但可能陷入局部最优。访谈未逐条对比算法名，落地时以 dspy.MIPROv2 默认策略为准，并在 dev 集上对比「只优化最后一层生成器」与「全模块一起搜」的增益差。\n反直觉（标注来源）：优化器常找到人类难以想到的 instruction 措辞（主持人引 unreasonable effectiveness of prompts 类轶事，Krista 认同）；向 proposer 强调 high_stakes 情境的 tip 在源码与论文附录中均有对应，但「为何有效」尚无机制解释（演讲者观点）。更多 few-shot 并非单调有益——嘉宾警告长 demo 可能迷惑模型，Llama-3-8B 级 task 模型有时不如只用 instruction（演讲者观点），与 industry 上的 many-shot 热潮形成张力；Google 原始 many-shot 论文 ID 勿随意引用（核验中 2402.04326 为无关论文）。\n口述 MIPRO 两阶段主流程附近：OCR 片段 Weaviate Hii ) / / Alt / Whi ie\nBootstrap few-shot 讨论：OCR 片段 Weaviate \\ Mi; / ad MAA i iM} /\nProposer 输入组成讨论：OCR 片段 Weaviate / A Hi Un \u0026rsquo; IN) i \u0026rsquo; NV ie 7\n贝叶斯优化与 MIPROv2 讨论：OCR 片段 Weaviate i } Wh yy Na de ee Nea\n信用分配：Module-level OPRO 与 Program-level OPRO # 为什么：多模块共用一个端到端 metric 时，必须判断哪一 stage 的哪类变量拉高了或拖累了分数（论文 §3.2 credit assignment）。\n机制/约束：\nModule-level OPRO：仅把单个模块的历史 instruction + 该模块相关分数交给 LM 提议下一版（扩展 OPRO 的单 prompt 设定）。 Program-level OPRO：把整程序所有 instruction 与单一 program score 交给 LM 做全局推断——论文 §4.5 实验 未带来额外增益；嘉宾称当前模型尚不足以做好全局贝叶斯式信用分配，module-level 更现实（演讲者观点，与论文结论一致）。 常见误区：与 ORPO（偏好对齐）混淆——二者缩写相近、问题无关（已核实）。\n有确定单元测试的代码生成任务，嘉宾认为「执行结果 + 自改进闭环」最强（演讲者观点）；开放域写作、营销文案仍依赖 LLM-judge 或人工抽检。DSPy 可把 metric 链也参数化，但若评判器与真实业务目标错位，优化会在错误目标上过拟合——这与 RAG 里「检索指标 ≠ 用户满意度」是同一类问题。\nModule-level vs program-level 讨论邻近：OCR 片段 Weaviate £) 7 A / } ( Oi) ] wean\nOPRO / 模块级优化讨论段：OCR 含 Weaviate \u0026ldquo;= ~ _— -—w —\nMeta-proposer、评判器与「不可微」中间件 # Meta-proposer（P08，已核实）：0-Shot MIPRO++ 用 BO 选择是否向 proposer 提供 dataset summary、program summary、tip、temperature、meta-prompt 内 demo 等离散开关（论文 §4.4）。嘉宾提醒：全量 tune meta-prompt 耗优化轮次，预算紧时可能不如直接把 trial 花在主搜索上——演讲者观点；论文 Lesson 5 亦讨论高低预算下的权衡，无统一公式。\nLLM-as-judge：无金标准的任务（摘要质量、推文风格）常依赖评判 LM；Who Validates the Validators? 讨论评判者与人类对齐。DSPy 可把 metric 本身建成可优化程序——演讲者观点，需警惕评判漂移。\nTool / 检索中间件（P09，部分核实）：优化目标仍是程序出口 metric；论文六任务以 QA/分类/检索管线为主，未提供充分 tool-use 优化基准——嘉宾称 LangProBe 将扩展 tool 场景，但公开仓库名 LangProBe 本次未在 GitHub/DSPy README 中命中（论文 §5.1 基准内容可核对，产品名未核实）。\nProposer vs task 模型（P10，部分核实）：论文 §5.2 主实验 task model 为 Llama-3-8B，proposer 为 GPT-3.5（非访谈口述的 GPT-4 默认）；访谈称 8B proposer 与 GPT-4 差距「小于预期」——演讲者观点。结构上 proposer 调用次数通常远少于 task LM 在训练集上的反复 rollout，故可把大模型预算倾斜给 proposer（演讲者观点）。\nProposer 模型与成本结构：OCR 片段 Weaviate / Mabey We A Nl i Nei i\n约 6 分 23 秒：嘉宾分屏特写，背景为室内顶灯，画面无公式或 API 列表\n基准、LangProBe 与可复现实验 # 为什么：没有对齐的任务集，就无法比较 BootstrapFewShot、MIPRO 与手工 prompt 谁更值得算力。\n机制/约束：MIPRO 论文 §5.1 给出 六个任务（HotPotQA、HotPotQA Conditional、Iris、Heart Disease、ScoNe、HoVer），各含 dataset / metric / 固定 LM program 拓扑；主表为 accuracy 或任务专属指标（如 HoVer Retrieval@21）。摘要写明在 DSPy 生态发布新 optimizer 与 benchmark。\n怎么做：复现 Table 2 时对齐论文 §5.2：Llama-3-8B 作 task model、GPT-3.5 作 proposer（特定任务 bootstrap teacher 用 GPT-4o）、optimizer budget 20–50 full eval trials，并记录 minibatch 带来的额外 trial 数。\n常见误区：把访谈中的 LangProBe 名称直接写成已验证的 PyPI/GitHub 包——本次检索 stanfordnlp/dspy 未命中该字符串（名称未核实）；以论文六任务为准更稳妥。主持人提到的「DSPy ImageNet 时刻 / ~20 点 margin」（Bo Wang 组）未验证具体 benchmark，不宜写入生产决策。\nLangProBe / 基准套件口述段：OCR 片段 Weaviate i NA Wu) Nh ipl\n与 RAG、Agent UI、微调的分野（未收敛的结论） # 主题 常见做法 嘉宾/文献张力 证据边界 单次 JSON vs 多阶段 结构化输出一次搞定 有外部状态则必须多轮 演讲者观点 Many-shot ICL demo 越多越好 长上下文可能迷惑模型；小模型或只用 instruction 嘉宾未读 Google many-shot 原文；勿引用错误 arXiv ID Chat 单入口 vs 多 Agent UI 多个专用 bot 倾向整合降低选择成本，亦承认 Slack 式协作 演讲者观点 Prompt routing 全测试集单一最优 prompt 看好 per-example 路由 演讲者观点 生成式反馈环 向量库只读检索 生成结果回写 DB 可作后续训练/检索语料 与 Weaviate 场景相关，未验证具体实现 Prompt + 权重联合优化 先 prompt 再 LoRA 同事工作探索二者同时搜（Stanford） 演讲者观点，无统一公开配方 资深 prompt 工程师 人工迭代必胜 嘉宾自述曾被自动优化「打脸」 演讲者观点 微调：在数据极少且任务分布稳定时，微调仍可能胜出；但若流水线含检索、工具、多模块，端到端 prompt 优化有时更省标注（EHR 个案为 演讲者观点）。更现实的组合是：DSPy 优化 prompt/demo → 收集高分 trace → 再决定是否蒸馏或微调小模型。\n约 24 分钟：双方对谈表情，左侧仍可见 Weaviate podcast 墙牌，无 benchmark 表\nMeta-proposer / BO 开关讨论邻近帧：OCR 片段 Weaviate 1! / rap 1 Mit ly Y Nd\n若你要落地 # 先固定程序拓扑与 metric：在 STORM 类多阶段写作或 RAG 管线上，用 dev 集定义可自动计算的 metric；模糊任务再引入 LLM-judge，并计划校对人抽验。 从 BootstrapFewShot + 小预算 MIPROv2 起步：num_trials 对齐论文 20–50 次 full eval 量级；启用 minibatch=True 时注意默认 minibatch_size 与 trial 计数方式（文档与 mipro_optimizer_v2.py）。 分模块信用分配：优先 module-level 策略；勿指望 program-level OPRO 在当前模型上稳定增益（论文 §4.5）。 分开配置 prompt_model 与 task_model：以论文为准核对 proposer 规模；用本地 8B 跑 task、云端较强模型跑 proposer 是常见省钱组合（论文为 GPT-3.5 proposer + Llama-3-8B task）。 为 tool-use 与检索单独建基准：MIPRO 论文六任务不覆盖复杂 tool 链；上线前用自有 trace + 通过/失败 metric 补洞（演讲者观点：优化器对中间件「应保持 agnostic」，但 benchmark 缺口真实存在）。 参考与延伸阅读 # DSPy 官方文档 DSPy 优化器教程（含 MIPROv2 三阶段） MIPROv2 API 参考 BootstrapFewShot API MIPRO 论文：Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programs DSPy 论文：Compiling Declarative Language Model Calls OPRO：Large Language Models as Optimizers ORPO（与 OPRO 无关，偏好对齐） Practical Bayesian Optimization（Snoek et al.） Who Validates the Validators?（LLM 评判对齐） STORM：多阶段写作流水线示例 DSPy Tools / ReAct 编程指南 GroundedProposer 源码 MIPROv2 实现（mipro_optimizer_v2.py） stanfordnlp/dspy GitHub 仓库 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-mipro-and-dspy-with-krista-opsahl-ong-weaviate-podcast-103/","section":"文章","summary":"多阶段语言程序与自动 Prompt 优化：从 DSPy 到 MIPRO","title":"多阶段语言程序与自动 Prompt 优化：从 DSPy 到 MIPRO","type":"posts"},{"content":" 多向量检索：在单向量、late interaction 与级联重排之间选型 # RAG 与 Agent 栈里，检索仍是成本与质量的分水岭：单向量 bi-encoder 快但压缩过度；cross-encoder 准但无法全库扫描；多向量 late interaction（以 ColBERT 为代表）试图在两者之间占一条可索引的缝。评测也在分裂——MTEB 上的 dense SOTA 在 BRIGHT 上可能大幅掉队，而 Agent 又把「查询」从关键词扩展成工具 trace 与推理链。\nLightOn 的 Amélie Chatelain、Antoine Chaffin 从训练动力学、代码语义 grep、推理密集型基准，谈到 PLAID、Muvera 与 ColBERT-Zero。双方对命名（multi-vector / late interaction / ColBERT 家族）并不纠结，但对「该不该上近似索引」「RAG 是否已被 grep 取代」分歧明显。工程上更像按规模与预算选层，而非押注单一银弹。下文按机制拆解，并标明文献可核对处与演讲者观点。\n三分屏访谈开场：主持方背景可见 Weaviate podcast 标识，嘉宾 Antoine Chaffin（左上是）与 Amélie Chatelain（右侧主画面）\n问题从哪来：压缩、交互与 Agent 查询形态 # 为什么：生产 RAG 要同时满足延迟、召回与可解释性。单向量把整段文本压成一个点，词法变体与细粒度对齐在压缩中丢失；cross-encoder 对 query–document 做全交互，只适合短列表重排。Agent 又把查询从关键词拉成长 reasoning trace、工具调用上下文，与经典「短 query + 长文档」假设进一步错位。\n机制/约束：信息检索里长期存在 retrieve-then-rerank；多向量方案把「交互」推迟到检索阶段之后，用 MaxSim（对每个 query token 取与文档 token 的最大相似度再聚合）在独立编码的 query/document token 嵌入之间做软匹配。ColBERT 官方实现中，late interaction 即 query 与 document 分别编码后再做细粒度打分（score() 路径为 token 矩阵上的 max 再 sum）。\n怎么做（概念）：\n# MaxSim 核心（与 ColBERT score 一致，示意） # Q: [q_len, dim], D: [doc_len, dim] scores = (Q @ D.T).max(dim=1).values.sum() 常见误区：把「多向量」等同于「多路 embedding 字段混搜」；或以为 MaxSim 的计算量级等于「全库 cross-encoder」——实际瓶颈往往在存储每个 token 的向量（演讲者观点：计算约为 query 长度 × 维度的矩阵乘，可接受；索引体积才是痛点）。视频级 token 数若全精度落盘，存储与 IO 会先于算力成为瓶颈（演讲者观点）。\nLate interaction 相对 dense：结构换训练，而非单纯更大模型 # 为什么：Amélie 将 dense 描述为对语义的有损压缩，late interaction 保留 token 粒度却在学习空间里做软匹配；Antoine 的博士动机则是 bi-encoder 速度 + cross-encoder 表达力——ColBERT 是路径之一，不是终点。\n机制/约束：ColBERT-Zero 在公开 Nomic Embed 混合上，用同 backbone 对比 dense 与 multi-vector：BEIR 平均 nDCG@10 = 55.43（\u0026lt;150M 档，模型卡可核对）。演讲者观点：早期用约 2M 样本训练的 late interaction 对比「整网预训练」的 dense 并不公平；在可比数据上，文本侧 late interaction 仍常更强。另一组演讲者观点：维数足够时 dense 理论上可逼近 cross-encoder，但 bi-encoder 训练噪声大；MaxSim 的 max 只强化最佳 token 对齐，动力学更「干净」——Antoine 引用的具体论文未在对话中给出题名，无法独立核实。\n怎么做：级联而非单跳——演讲者观点倾向 dense → late interaction → cross-encoder，逐层收窄候选；ColBERT 若只做 reranker，前级 recall 不足会浪费其能力（与两阶段 IR 常识一致）。\n讨论级联重排与候选池 M 时：Amélie 手势说明，画面仍为访谈三分屏、无技术幻灯片\n常见误区：用极小 dense 候选池直接接 ColBERT rerank，以为能「补救」召回；或忽视 PLAID 的 centroid pruning——候选来自聚类中心时，中心池本身必须先保证 recall（演讲者观点）。\n规模分层：精确 MaxSim、PLAID、Muvera # 为什么：百万级语料无法对每篇文档做全精度 token–token 打分；千级代码库或客服 KB 则可能整机内存内暴力算。\n机制/约束（文献侧）：\n组件 可核对要点 访谈补充 ColBERTv2 残差压缩，索引约为 vanilla 的 6–10× 缩减 — PLAID centroid interaction / pruning；相对 ColBERTv2 最高 7×（GPU）/ 45×（CPU）；实验至 140M passages 嘉宾口语「IVF-PQ」：PLAID 摘要未出现 IVF-PQ，PQ/残差更贴近 ColBERTv2；宜写 centroid + 压缩组合 Muvera FDE 将多向量压成固定维单向量，内积近似多向量相似度；摘要称 ε-approximation Antoine：未自行验证证明；效果因模型而异 MS MARCO BEIR 载明 8.84M passages 口语「880 万」为约数 演讲者观点（Antoine）：语料很小（数千文档）时，精确 MaxSim 往往优于 Plaid 近似；小模型上 Plaid 甚至可能差于暴力搜索——「Occam\u0026rsquo;s razor，别优化不必优化的东西」。Connor 转述 Weaviate 产品方向：Muvera 单向量召回 + 全精度 MaxSim rerank；本次未能从可抓取的 Weaviate 开发者文档核实 Muvera 集成细节，以官方发布为准。\n谈 MaxSim 计算与「每 token 存一向量」的存储痛点时画面（OCR 片段：© // WE / wg /）\n讲解 PLAID / 索引压缩时段的访谈画面（OCR 可见 Weaviate）\n接续 Fast Plaid 与大规模索引讨论（OCR：Weaviate podca:）\n怎么做（选型）：\n\u0026lt; ~10⁴ 文档：优先精确 MaxSim 或全量 token 存储；跳过近似索引。 10⁶+ 文档：PLAID 类 centroid 候选 + 全精度 MaxSim rerank；LightOn 提供 Fast Plaid（Rust）与 PyLate 的 indexes.PLAID。 候选生成：Muvera FDE 可作 Plaid centroid 的替代（论文 BEIR 上平均约 10% recall 提升、90% 更低延迟）；是否银弹——演讲者观点：「when it works, it works very well」，跨模型不一致。 常见误区：百万级语料仍上 Plaid；或在 Muvera 近似后直接喂 LLM、跳过全精度 MaxSim rerank。\n与向量库集成的边界：Connor 提到 Weaviate 侧探索 Muvera 与 IVF-PQ 类能力——产品事实应以 Weaviate 向量索引文档 当前版本为准；本场为录制时口头说法，且本次未能从可抓取页面核实 Muvera 段落。若你自建栈，逻辑上仍是：近似召回负责广度，MaxSim 负责 late interaction 精度。\n大规模索引与 Fast Plaid 讨论：三分屏，左下 Weaviate podcast 与 FAU 文凭背景\n代码检索：grep、语义扩展与「RAG 已死」 # 为什么：编码 Agent 默认用 grep；多轮关键词试探延迟低但易漏语义相关实现。LightOn 产品线为 ColGrep + LateOn-Code（访谈口语「CodeGrep」），提供类 grep API 的语义检索。\n机制/约束（产品文档可核对）：LateOn-Code-edge（17M）、LateOn-Code（约 130–149M）；预训练数据管线 CoRNStack 2412.01007，微调面向 MTEB Code v1。README 在 Code v1 子任务上对比 17M multi-vector 与更大 dense（如 GTE-ModernBERT）——非泛指所有 Gemini API。演讲者观点：千级文件仓库可 GPU 内精确 MaxSim；「RAG 死了」指 Agent 用 grep + 大上下文即可，但语义 grep 能一次命中需多轮 grep 的代码；人类感知延迟 grep ≈ ColGrep。\n代码检索与 Agent 工具链讨论：三分屏，左下 Weaviate podcast 背景\n语义 grep 对比多轮关键词查询时段（OCR：Weaviate (fi aE）\n常见误区：把访谈中的「70M」当作官方型号（应为 17M edge）；用 MTEB 通用榜替代 MTEB Code v1 子任务口径。\n推理密集型检索：BRIGHT、ReasonIR 与超长 query # 为什么：BRIGHT 测的是推理密集型相似（如不同题面同一定理），MTEB 榜首类 dense 在 BRIGHT 上可从 59.0 nDCG@10（MTEB） 跌至 18.3 nDCG@10。Agent 把 chain-of-thought、工具 trace 当作 query，进一步拉长查询侧 token。\n机制/约束：ReasonIR（Meta ReasonIR-8B）在 BRIGHT 上 29.9 nDCG@10（无 reranker）、36.9 nDCG@10（有 reranker）。演讲者观点：Antoine 用 PyLate 在 ReasonIR 公开数据上微调 130M ModernBERT-ColBERT，称优于至 7B、接近同数据 8B，而同 backbone dense 明显更差——未在 ReasonIR 官方仓库找到 130M 数字，需独立复现；BrowseComp+、API embedding 对比同为演讲者观点。\nReasonIR / dense vs ColBERT 对比讨论：Amélie 主画面手势说明\n同主题时段访谈画面（OCR：Weaviate ,odcast）\n常见误区：用 MTEB 分数推断 BRIGHT 表现；把 agent trace 当 query 却不换训练分布（ReasonIR 路线是合成推理数据 + 专用微调）。\n长文档与长 query：ColBERT 原文强调 document 可离线编码、query 在线交互，适合长文检索形态。演讲者观点：ColBERT 对长文档泛化优于 dense 是「已知事实」，但超长 query（人类问句 vs 拼接 LLM reasoning trace）Antoine 仍部分存疑；在其实验中，reasoning-tuned 模型加 trace 有提升——属实验判断，非 BRIGHT 论文定理。\nBRIGHT 与推理密集型任务讨论：主持方发言，背景 Weaviate podcast 标识\n训练与工具链：ColBERT-Zero、PyLate、prompt # 为什么：多向量预训练贵；产品化需要 Sentence-Transformers 式 API 与大 batch 对比学习。\n机制/约束（可核对）：ColBERT-Zero 三阶段（无监督对比 → 监督 hard negative → KD）；跳过最贵无监督阶段：~40 vs ~408 GH200-hours（约 10×），保留 99.4% 性能（55.12 vs 55.43 nDCG@10）。HF 提供 ColBERT-Zero-noprompts 等变体；演讲者观点：简单 query/document prompt（非 LLM 指令）有提升，机制或未明（类似 query expansion 的猜测）。\nPyLate：GradCache 2101.06983 的 CachedContrastiveLoss、gather_across_devices=True，降低多向量对比学习显存门槛。\nColBERT Zero / prompt 消融讨论时段（OCR：a Weaviate Ne podcast）\n训练配方与公开数据混合讨论：左下书架可见 The Worlds I See 等书目，Weaviate podcast 标识\n常见误区：认为「dense + KD」即足够（ColBERT-Zero 论文主张明显欠训）；把 prompt 当成 ChatGPT 式 system prompt。\n微调产品化（演讲者观点）：ColBERT 类模型「不易 collapse」，适合 embedding 微调产品线——与 P01 的 MaxSim 机制叙述一致，但「梯度只更新匹配 token」仍是训练直觉，需对照 PyLate 具体 loss 实现，非 ColBERT 原文定理。\n收束段访谈画面（背景 OCR 片段含 THE YORLDS，疑为 THE WORLD\u0026rsquo;S 误识别）\n多模态与混合信号 # 为什么：图像/视频 token 密度远高于文本段落，单向量压缩损失更大（演讲者观点，信息论式论证，无定量定理）。\n机制/约束：文本侧 ColBERT-Zero 已在可比训练下对齐 dense vs multi-vector；多模态公平对比仍受预训练规模失衡影响（演讲者观点）。\n混合检索：双方同意保留 BM25 与 dense、ColBERT 的互补失败模式；调参重点是各阶段候选 M 与 rerank 深度。演讲者观点。\n多模态压缩与 token 存储讨论（OCR 含 Weaviate）\n混合检索与 rerank 池大小讨论（OCR：Weaviate »odcast）\nrerank 争议（未完全核对）：Connor 提及 Databricks《Drowning in Documents》中增大候选 M 会出现 phantom hits——该 URL 2026-05-17 抓取为 404，无法核对正文。Antoine 提出另一叙事：reranker 在分布尾部差，因训练只用 hard negatives；修复需加 tail 样本——是否为同一研究，未核对。\nBenchmark 与训练公平性讨论：三分屏访谈，无结果表幻灯片\n常见误区：去掉 BM25 只留最强语义模型；或为降延迟把 dense 候选 M 压到过小再抱怨 ColBERT rerank 无效。\n若你要落地 # 先量纲再选索引：文档数、平均 token 数、QPS、能否全内存 MaxSim——小库直接精确算，大库再 PLAID / Muvera + 全精度 rerank。 固定指标口径：检索写 nDCG@10 / MRR@10；代码用 MTEB Code v1；推理密集型用 BRIGHT，勿与 MTEB 通用榜混读。 级联写进 SLA：dense recall → multi-vector MaxSim →（可选）cross-encoder；每一级记录 recall@M，避免 phantom hit 或 tail 崩塌时盲目增大 M。 Agent 代码路径：保留 grep；语义层试 ColGrep / LateOn-Code，在自有仓库做 A/B（演讲者观点称延迟接近 grep）。 训练预算紧：参考 ColBERT-Zero 跳过最贵无监督阶段；用 PyLate + GradCache 复现，公开数据从 Nomic Embed 混合起步。 参考与延伸阅读 # ColBERT：高效检索的 late interaction ColBERTv2：残差压缩与去噪监督 PLAID：面向性能的 late interaction 驱动 Muvera：固定维编码的多向量检索 BRIGHT：推理密集型检索基准 ReasonIR：Meta 推理检索器与合成数据 ColBERT-Zero：多向量预训练配方与 BEIR 结果 ColBERT-Zero 模型卡与训练成本说明 Nomic Embed：公开训练数据混合 GradCache：大 batch 对比学习 PyLate：ColBERT 训练与检索库 Fast Plaid：Rust 多向量索引引擎 ColGrep 与 LateOn-Code 发布说明 MTEB Leaderboard Weaviate 向量索引概念文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-multi-vector-search-with-ame-lie-chatelain-and-antoine-chaffin-weaviate/","section":"文章","summary":"多向量检索：在单向量、late interaction 与级联重排之间选型","title":"多向量检索：在单向量、late interaction 与级联重排之间选型","type":"posts"},{"content":" 多向量检索的索引悖论：MUVERA 如何用单向量 ANN 逼近 Chamfer # ColBERT、ColPali 一类模型把文档表示成 token 级向量集，查询时用 late interaction（对每个 query token 在 document token 上取最大相似度再聚合，业界多称 MaxSim，论文常写 Chamfer）换取比单向量 bi-encoder 更细的语义对齐。代价也直接：索引条数从「每文档一条」膨胀到「每文档数百条」，查询侧还要做矩阵式交互。Google Research 的 MUVERA（Multi-vector Retrieval via Fixed Dimensional Encoding）提出 FDE（fixed dimensional encoding）：用确定性、非学习的空间划分把多向量集压成 一条 高维单向量，在 HNSW / MIPS 上 一次 近似最近邻，再对 top-K 做真实 Chamfer 重排；Weaviate v1.31 已集成该编码路径。\n下文是面向有经验工程师的独立技术综合：把 可核对论文/文档 与 访谈观点 分开，不把 BEIR 上的 nDCG 与论文里的 Recall@N 混读，也不暗示「单向量启发式」与 MUVERA 在所有数据集上已分出绝对胜负。\nWeaviate 教程页：Multi-vector embeddings (ColBERT, ColPali, etc.)；Prerequisites 要求实例 v1.29+，并说明 late interaction 逐段匹配。\n问题空间：三种检索范式与一条被忽视的相似度 # 为什么 late interaction 会回到「索引条数」 # RAG 与 Agent 的检索层通常在三档之间取舍（演讲者观点，与 ColBERT 社区叙述一致）：\n范式 文档表示 查询成本 典型瓶颈 Cross-encoder 无 query 无关的独立 doc 向量 全对全 attention 无法规模化 ANN Single-vector dual encoder 一条 embedding 一次 ANN 丢失 token 级交互 Multi-vector 固定 doc token 向量集 查询时 late interaction 存储 + 多次检索/重排 MUVERA 论文 §1.1 将多向量相似度标准写为\n[ \\textsc{Chamfer}(Q,P)=\\sum_{q\\in Q}\\max_{p\\in P}\\langle q,p\\rangle ]\n并说明其与 MaxSim 同义，distance 变体亦称 relaxed earth mover distance。Rajesh Jayaram（MUVERA 一作）在访谈中把多向量相似度与 Earthmover / 最优传输 类比，差别在于去掉 token 一对一匹配约束（演讲者观点；形式化同构未在播客中给出定理编号）。\n机制与约束 # ColBERTv2 实验设定：d=128，|Q|=32 个 query token（MUVERA §3）。 Chamfer 对 document 侧重复 token 不敏感（max 对 P 中重复项），对 query 侧重复敏感——形式化上成立；Rajesh 将其类比集合包含的非对称性（演讲者观点）。 带 schema 约束（标题不得匹配正文等）的检索，Chamfer 不适用，需改损失或掩码（演讲者观点；ColBERT/MUVERA 正文未展开）。 怎么做（最小示例） # 概念上：索引阶段保存 P 的全部 token 向量；查询阶段算 Q，再算 Chamfer 或调用引擎的 MaxSim 算子（Weaviate 教程）。\n常见误区 # 默认「平均后再 max」与论文 求和 等价——实现可能除以 |Q|，不能默认与论文符号一致。 把 Chamfer 当作任意结构化字段检索的默认度量。 远程三方分屏讨论；左下可见 Weaviate podcast 标识，画面无算法幻灯片。\n单向量启发式：工程上可行、理论上无保证 # 为什么 ColBERTv2 / PLAID 仍把 token 摊进 ANN # 在 MUVERA 之前，社区主流 single-vector heuristic（论文用语）：把每个 query/document token embedding 当作独立向量 编入 MIPS/ANN 索引；对每个 query token 检索，合并候选后用 Chamfer 重排。PLAID 在此基础上加 centroid interaction、剪枝等，相对 vanilla ColBERTv2 报告显著延迟下降（论文数据，非播客 QPS 表）。\n访谈中的口述流程（与论文相容，K 因实现而异）：约 32 次 ANN → 至多 32×K 候选 → 去重 → MaxSim 重排。Roberto Esposito（Weaviate Applied Research）强调候选规模与重排成本。\n机制与失效模式 # MUVERA 明确指出：该启发式 可能找不到真实 Chamfer 近邻，且需对每个 query embedding 查询更大的索引（引言）。\n演讲者观点 的反例：某文档仅在 一个 query token 上极高相似、其余 token 不对齐，仍可能被召回；全局 80% 对齐的文档反而输给「一词完全匹配」的文档——与「整体查询语义」的直觉相悖。论文从理论上强调 SV 代理 无 ε-近似保证，但未用播客式反例编号。\n怎么做 # 若已部署 PLAID/ColBERTv2 引擎，短期不必拆掉；评估时应记录 recall@K（Chamfer 真值） 与 延迟，而非只盯端到端 nDCG。\n常见误区 # 把 PLAID 的 centroid 剪枝等同于「裸 token 摊平 32 次 ANN」——实现细节以 PLAID 原文 为准。 认为「重排总能纠正召回错误」——重排只能重排 已进候选集 的文档。 对谈分屏；左下 Weaviate podcast 角标，右侧为嘉宾画面。\nMUVERA / FDE：一次 ANN + 可证明代理 # 为什么「回到单向量」不是另训 bi-encoder # MUVERA 的核心是 非对称 确定性编码 (\\mathbf{F}{\\text{q}}(Q))、\\mathbf{F}{\\text{doc}}(P))，使内积 ε-近似 Chamfer（Theorem 2.1 / 2.2，§2）。这与「单独训练一个单向量模型 + ColBERT 重排」不同：后者 无 单向量分数与多向量分数之间的保证（访谈观点，与论文「首个有证明的单向量 Chamfer 代理」一致）。\n离线：对文档（及查询）算 FDE → 单次 MIPS/ANN（可用现成求解器；产品侧多为 HNSW）→ 对 top 候选用 真实 Chamfer 重排。Roberto 将候选从「32×K」量级缩到「K」量级（方向一致；严格 K 仍取决于 over-retrieve 配置）。\n论文相对 PLAID 的摘要级结论（BEIR 子集实验，非全库普适）：平均 recall 高约 10%、延迟低约 90%；达到相同 recall 时 FDE 可比 SV heuristic 少检索约 2–5× 候选（摘要）。\n机制：不能查询前拼接 token # MaxSim 的匹配是 查询依赖 的：在查询到达前，document 侧「该与哪个 query token 对齐」未知（演讲者观点 + 算法必要性）。简单拼接 token 向量不能构成 FDE；穷举排列在 |Q|=32 时不可行。\n怎么做（参数心智模型） # Weaviate 文档给出编码维公式（已核实）：\n[ \\text{dimension} = \\texttt{repetitions} \\times 2^{\\texttt{ksim}} \\times \\texttt{dprojections} ]\n示例 ksim: 4, dprojections: 16, repetitions: 20 → 5120 维。论文实验 d_FDE ∈ {2460, 5120, 10240}；口述「5k–10k」宜理解为区间而非单点。配置入口见 Multi-vector encodings。\n常见误区 # 把 FDE 内积分数当作最终排序分——生产仍应 Chamfer 重排 top-K。 假设论文声称生产环境 K=1 足够——那是 FDE 理想极限的 访谈推论，论文讨论的是 recall–latency Pareto。 画面可见 tal ) Weaviate 字样，为播客品牌角标而非论文架构图。\n约 20 分钟处三方讨论；左下 Weaviate podcast 标识，无幻灯片 API 文本。\nFDE 构造：分桶、非对称聚合与空桶 # 为什么用 LSH 而不是「32 个 k-means 中心」 # 空间划分 (\\bm{\\varphi}) 默认 SimHash（LSH）；亦可用 k-means（§2）。对每个桶 (k)：\nquery 侧：桶内 token 向量 求和； document 侧：桶内 token 向量 取平均（避免 doc 重复 token 抬高分数；Rajesh 称对 average 或任取桶内一点证明均成立）。 fill_empty_clusters：仅 document FDE；query 侧 never for queries（论文原文）。若在 query 侧填充空桶，会人为抬高无对应 query token 的桶分数（访谈解释与算法一致）。\n可 多次独立 SimHash 重复 后拼接降方差。论文对照：k-means 替代 SimHash 在 Pareto 上 通常无收益且常更差，且丧失 data-oblivious 性质。\nRajesh 的 演讲者观点：分区数宜与 query token 数同量级（约 32–64）；在千万～亿级 token 上，极少中心的 k-means 欠拟合；LSH 的「对所有点有保证」在此 编码阶段 优于欠拟合聚类。这与「LSH 在一般 ANN benchmark 上不如 HNSW」 不矛盾——LSH 用于 桶划分，HNSW 用于 FDE 的 ANN（访谈 + 论文分工）。\n怎么做 # 调参时优先扫 ksim（桶数 (2^{ksim})）、repetitions、dprojections；Weaviate 默认示例 ksim: 4 → 16 桶，非 论文强制 32–64。\n常见误区 # 在 子空间 PQ 上破坏同桶语义——Roberto 警告在空间划分上做 PQ 式切分可能破坏保证（工程警告，未 在论文中逐条定理化）。 把 doc 侧平均简单等同于重排阶段的 ball carving——后者是另一阶段的 token 合并（见下节）。 约 10 分钟分屏；左下 Weaviate podcast 水印，讨论单向量启发式与 PLAID。\nOCR 噪声帧；可见 2 33 \u0026gt; 0 63 = 等叠字，无可用公式，仅作时间轴锚点。\n维度、PQ 与重排：膨胀之后如何仍省内存 # 为什么 FDE 维数暴涨仍可能更省 # 索引 条数 从「每 doc 上百条 token 向量」变为 1 条 FDE。ColBERTv2 上平均每 doc 约 10,087 floats（≈ token×128，§3）。FDE 可达 10240 维，再经 product quantization 约 32× 压缩（例：10240 维 → 1280 bytes，摘要）。访谈称 PQ 为论文与产品路径的关键之一；Weaviate v1.31 可将 MUVERA 与 PQ/BQ/RQ/SQ 等叠加（compression 文档）。\nFDE 越大 → Recall@N 越高 → 可减小 retrieval K；FDE 越小则需更大 K 与更重排（论文 Pareto + 访谈 trade-off）。\n重排加速：论文 ball carving vs 播客数字 # 附录 ball carving（§C.3）：重排前对 query token 聚类，MS MARCO 上 query embedding 数减少约 5.4×（τ=0.7 等）。播客口述 「4× 剪枝、矩阵乘 16×」 —— 未在 MUVERA 原文找到字面表述；写作时应标为 访谈观点 或引用 ball carving 论文数据。\nWeaviate v1.31 实验中 PQ 与 scalar quantization 均显著提升 QPS（产品实验/访谈）；官方未给出 与播客对等的绝对 QPS 对照表。\n常见误区 # 把 Recall@N（FDE 检索） 与 nDCG@10（BEIR 端到端） 当作同一指标。 float32→1bit 标量量化理论约 32× 上限，便认为一定优于 PQ——取决于实现与 recall 损失，需自测。 约 30 分钟处；主持持杯，三方继续对谈，无技术幻灯片。\n约 21 分钟；左下 Weaviate podcast 标识，讨论评测与 benchmark 方法论。\n评测与产品边界：BEIR、CRISP 与不可比的 SOTA # 为什么多向量不宜直接「打榜」单向量 SOTA # MUVERA 实验使用 BEIR 6 个子集（含 MS MARCO、HotpotQA、NQ、Quora、SciDocs、ArguAna）。Rajesh 的 方法论观点：应在同一 IR 数据上比，但分数 只应与多向量模型互比；不宜宣称击败多年 hill-climb 的单向量 SOTA。长查询、复杂文档需要 新 benchmark（访谈；MTEB 主榜以单向量任务为主，未找到 官方等价 multi-vector 榜）。\nCRISP（Clustering Multi-Vector Representations for Denoising and Pruning，共同作者含 Rajesh Jayaram）在 BEIR 上报告向量削减与质量权衡；摘要示例 ArguAna 上 C4×8 相对未剪枝 +5.5% 等。「播客推动 CRISP 发布」 —— 无法核验（论文提交日期 2025-05-16）。\n工程集成（已核实） # 能力 版本 Multi-vector + late interaction v1.29+ Multi-vector GA v1.30（博客） MUVERA 编码 v1.31 教程模型示例：ColBERT、ColPali、ColQwen；配置 Encoding.muvera(...) 见 官方文档。\n常见误区 # 在 ArguAna 等敏感子集上不调 剪枝/聚类（CRISP 方向）便断定「多向量不行」。 混读 v1.29 多向量与 v1.31 MUVERA 要求。 OCR：a, / M Weaviate @ podcast — 播客角标帧。\n约 40 分钟分屏；仍为远程对谈画面，无架构幻灯片。\n学科文化：NN 理论与向量数据库为何长期各说各话 # Rajesh 提出：近邻搜索理论（sketching、LSH、ε-近似）与 向量数据库实践（HNSW、IVF、DiskANN、产品量化）二十年来交流有限（演讲者观点）。MUVERA 可视为一次把 可证明代理 嵌进 MIPS 索引链路的尝试；是否成为主流，取决于多向量模型在 RAG/Agent 中的占比、以及 FDE+PQ 在你数据上的 Pareto 是否优于 PLAID 工程栈。\n未统一结论：PLAID 仍是强基线；MUVERA 在论文子集上报告更高 recall 与更低延迟，但是否覆盖你的语料、schema 约束与 SLO，只能自测。高维 FDE 是否可被 学习压缩表示 直接替代，Rajesh 承认是开放问题（访谈）。\n若你要落地 # 先定度量：确认业务是否真需要 Chamfer/MaxSim；有字段级匹配约束时，先改损失或掩码，再选索引结构。 建立双指标：FDE 阶段的 Recall@N（对 Chamfer 真近邻） 与端到端 nDCG/MRR 分开报告；勿与单向量 MTEB 榜直接比。 对比两条召回链：在同一 ColBERTv2 索引上跑 SV heuristic/PLAID 与 MUVERA FDE（Weaviate ≥1.31 或论文参考实现），扫 d_FDE 与 retrieval K 的 Pareto。 编码参数：从论文附近的 5120/10240 与 Weaviate 公式出发扫 ksim、repetitions；启用 PQ 时单独测 recall 损失；空桶填充仅 doc 侧。 重排预算：在 top-K Chamfer 上叠加 ball carving（论文 ~5.4× query token 削减）而非盲信播客 4×/16×；对 ArguAna 类任务评估 CRISP 式剪枝 是否必要。 参考与延伸阅读 # MUVERA 论文（arXiv:2405.19504） MUVERA HTML 全文（§1 Chamfer / §2 FDE） ColBERT（SIGIR'20） ColBERT 官方 README（MaxSim 图示） ColBERTv2 PLAID：Performance-optimized Late Interaction Driver BEIR 基准论文 BEIR GitHub 套件 CRISP：多向量聚类剪枝与去噪 Weaviate 教程：Multi-vector embeddings Weaviate：Multi-vector encodings / MUVERA 配置 Weaviate 1.31 发布说明（MUVERA） Weaviate GitHub Release v1.31.0 ColPali：视觉文档 late interaction RAG 原始论文（检索增强生成框架） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-muvera-with-rajesh-jayaram-and-roberto-esposito-weaviate-podcast-123/","section":"文章","summary":"多向量检索的索引悖论：MUVERA 如何用单向量 ANN 逼近 Chamfer","title":"多向量检索的索引悖论：MUVERA 如何用单向量 ANN 逼近 Chamfer","type":"posts"},{"content":" 泛型代码在 JVM 上如何变快：擦除、剖析与「坠崖」之后的攀爬 # Java 泛型在语言层经过类型擦除后，运行时仍是一份共享字节码；性能能否逼近 C++ 为每种 (容器, Comparator) 静态展开的机器码，取决于 HotSpot 能否在稳定剖面下完成去虚化、内联与常量传播。本文以 median-of-three QuickSort 为标尺，梳理「好日子」与 megamorphic「悬崖」之间的机制，并说明 Hidden Class 克隆与 MethodHandles 强制内联等研究性修复思路——其中 µs 与倍数若无本地 JMH 复现，均标为讲演者观点；Project Valhalla / Project Leyden 相关表述为路线图愿景，非当前 JDK 产品承诺。\n图：JavaOne 2026 / Redwood City 现场——泛型与 JVM 专用化主题 session 开场\n单份字节码 vs 模板展开：性能问题从哪来 # 为什么：业务需要可共享的库实现（一份 sort(T[], Comparator)），又希望在热路径上接近手写 int[] 或 C++ vector\u0026lt;int\u0026gt; 全静态特化的吞吐。语言规范只保证编译期擦除，不承诺零虚调用；C++ 则走异构翻译——vector\u0026lt;int\u0026gt; 与 vector\u0026lt;float\u0026gt; 是不同类型，可各自生成代码。\n机制/约束：擦除后 JVM 仍通过 invokeinterface、aastore 等字节码做接收者类型剖析；与「编译期已展开」的 C++ 模板相比，专用化发生在运行中的 C2，且受剖面宽度、内联预算约束（见后文 TypeProfileWidth）。\n怎么做（概念对照）：\n// Java：一份泛型入口 public static \u0026lt;T\u0026gt; void sort(T[] a, Comparator\u0026lt;? super T\u0026gt; c) { /* ... */ } // C++：每种 comparator 可独立实例化（机制见 cppreference Templates） template\u0026lt;typename Cmp\u0026gt; void quick_sort(int* a, int n, Cmp cmp); 常见误区：把「擦除」等同于「运行时一定慢」——慢的是未形成稳定单态剖面时的虚调用与装箱路径，而非擦除本身。\n基准矩阵：同一算法，不同数据与调用形态 # 为什么：要把「算法相同」与「表示 + 调用形态」拆开，才能解释后续 2×–5× 乃至更大跨度的性能落差。\n机制/约束：讲演者在 JDK 25 + JMH + AArch64 下，对 1000 个随机 int 排序给出近似锚点（讲演者观点）：手写 IntQS/int[] ~10.5µs；QS/Integer[] 装箱泛型约慢近 3×；反射统一路径 ReflQS 在 flat int[] 上可回到 ~10µs 量级，而饱和剖面可拖到 ~70µs。C++ vector\u0026lt;int\u0026gt; 全静态特化（clang -O3）与 Java 手写 primitive 同量级；若 comparator 退化为 std::function，则类似 JVM megamorphic（讲演对比论点，未用单一规范页核实）。\n图：HAND-SPECIALIZED / MONO·PRIMITIVE——baseline performance profile-guided devirtualization flat array, fully inlined\n怎么做（JMH 骨架）：\n@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) public class SortBench { @Benchmark public void intQS(int[] a) { IntQuickSort.sort(a); } @Benchmark public void genericQS(Integer[] a) { GenericQuickSort.sort(a, Integer::compare); } } 常见误区：只比较「是否泛型」，忽略 Integer[] 装箱带宽、以及 Comparator 实现类是否在调用点稳定。\n反射数组：为 flat / boxed 统一铺路 # 为什么：Project Valhalla 方向下，同一套 API 可能面对多种布局（扁平 value 数组 vs 装箱引用数组）；讲演用 ReflectiveQuickSort 通过 Array.get / Array.set 统一访问，探测「一种算法、多种布局」时优化器还能走多远。\n机制/约束：装箱用 4 字节 int 换 16–24 字节对象表示（指针 + 头 + 载荷）——讲演者观点，对象头与压缩指针因 VM 而异；装箱换来完整多态，但提高 footprint 与数组遍历带宽成本。primitive 数组与现行泛型擦除仍难混用（「yet\u0026hellip;」）。\n图：Why the reflective case is important——Remember the overheads of boxing: 4 bytes of int swaddled in 16-24 bytes of stuff\n图：装箱 footprint 与带宽代价——primitive arrays don\u0026rsquo;t mix well with generics (yet\u0026hellip;)\n怎么做：\nstatic void reflectiveSort(Object a, int lo, int hi, Comparator\u0026lt;?\u0026gt; cmp) { Object pivot = Array.get(a, hi); for (int i = lo; i \u0026lt; hi; i++) if (cmp.compare(Array.get(a, i), pivot) \u0026lt; 0) swapReflect(a, i, /* ... */); } 常见误区：以为「反射一定慢一个数量级」——在 flat int[] 上，若 C2 能把 Array.get 专用成与 iaload 等价的机器码，仍可能接近手写版（讲演测量，需本地验证）。\nHotSpot 的 JIT 闭环：剖析 → 去虚化 → 内联 # 为什么：擦除后的泛型方法在字节码层仍是虚调用；要把 Comparator.compare 与数组元素访问一起常量化，通常必须先有可信的类型剖面，再内联进单一 IR 做寄存器分配与指令选择。\n机制/约束：PerformanceTechniques 描述：解释器/C1 记录调用点接收者类型（常与 TypeProfileWidth 相关，JDK 25 默认宽度为 2）；C2 在「历史上一种或两种类型」时做乐观检查，失败则 deoptimize 或走 vtable/itable。GraphKit::uncommon_trap 注释写明：中途退回解释器——与汇编里 b.ne uncommon_trap 对应。对泛型排序而言，数组组件类型与 Comparator 接收者类型是两条独立剖面通道：只有二者在 IR 中同时收窄，aastore/aaload 上的 store-check 消除与 compare 内联才会在同一轮优化里「对齐」；任一通道饱和，另一端即使仍单态，也可能拖住整段循环（讲演归纳，属实现层观察而非 JLS 条款）。\n怎么做（利于形成单态剖面）：\nComparator\u0026lt;Integer\u0026gt; cmp = Integer::compare; GenericQuickSort.sort(keys, cmp); 常见误区：在微基准里每次换一个新的 lambda 类，却期望达到 Integer::compare 的稳定优化——接收者类不稳定会直接打断闭环。\n「好日子」汇编：循环外 guard，循环内直比较 # 为什么：读懂生成的 AArch64（或本机 ISA）能判断性能卡在 guard 还是热循环体。\n机制/约束：当 comparator 在方法历史上仅见 1–2 种类型时，C2 可在循环外加载 cmp.class 与常量元数据比较，失败则 uncommon_trap；成功则在循环内把 compare 内联为对数组元素的 ldr/cmp（讲演汇编解读，需本机 PrintAssembly + hsdis 核对）。\n图：Disassembly 1——b.ne uncommon_trap ;if not, bail out to interpreter\n怎么做：\njava -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly \\ -XX:CompileCommand=print,*QuickSort* -jar bench.jar 常见误区：把 uncommon_trap 当成「异常」——它是投机失效时的正常去优化路径，频繁触发才会拖垮吞吐。\nMegamorphic 悬崖：剖面饱和之后发生什么 # 为什么：插件式 comparator、多租户共用排序内核、或基准里轮换多种 Comparator 实现，会让同一调用点见到多种接收者类型；优化器失去「该赌哪一种」的信号。\n机制/约束：讲演将「≥3 种 comparator 类型」描述为饱和剖面，热循环退化为间接调用与大量 spill/setup（幻灯称可比良性路径多 10×+ 指令/迭代——讲演者观点）。相对干净单态，幻灯给出约 2×–5× 的 megamorphic 惩罚区间（讲演者观点，未独立验证）。HotSpot Wiki 同时记载：inline cache 在见到第二种 receiver 时也可能进入 megamorphic 状态并使用 vtable/itable stub——与「第三种饱和」的口语可能指不同子系统（TypeProfileWidth vs IC 状态机），阅读汇编时宜以本机剖面为准。关闭 -XX:UseTypeProfile 会让 C2 更依赖其它路径收集类型信息，讲演用它证明「调用方常量传播」可以部分替代剖面，但该开关默认开启，产品环境不应随意关闭。\n图：Disassembly 2——© megamorphic call site - happens many times / More than 10x instructions per iteration\n图：Diagnosis——megamorphic code is 2x to 5x slower (from saturated profiles) than clean monomorphic code\n怎么做（实验上刻意制造饱和，非生产写法）：\nComparator\u0026lt;Integer\u0026gt;[] variants = { Integer::compare, Comparator.reverseOrder(), (a, b) -\u0026gt; a - b }; for (var c : variants) GenericQuickSort.sort(copy(arr), c); 常见误区：认为 int[] 一定比 Integer[] 更安全——讲演指出在 bimorphic 布局剖面下，flat primitive 数组可能出现比饱和更陡的 cliff（「flat data is Valhalla challenge」——讲演者观点），Valhalla 时代需要 per-layout 的 code species，而不能假设 primitive 恒赢。\n结构性约束：一份优化包与未来的 species # 为什么：若同一 Java 方法在任意时刻只有roughly 一份优化机器码（讲演对现状的描述，未能找到逐字官方文档），则 int/long/Short 等多类型剖面会挤进同一份 nmethod，放大污染。\n机制/约束：Valhalla 项目页列出 Parametric JVM——在运行时对泛型类/方法参数化做专用化与优化；讲演用语 species 指「QuickSort::\u0026lt;Integer\u0026gt;sort 路由到仅服务 Integer + 特定 comparator 的算法副本」，属愿景，无现行 Java 语法。JEP 515 与 JEP 483 则让 Leyden 训练运行可把剖析与代码写入 AOT cache——方向与「保留动态编译成果」一致，但不等价于任意 C++ 式模板均持久化。\n图：Prescription——Project Valhalla envisions specialized generics / each distinct use gets specialized code\n图：species 概念——To climb back up the cliff, each distinct use gets specialized code\n常见误区：把 Leyden AOT 当作「一次训练，永久 C++ 速度」——仍受训练覆盖路径、缓存失效与后续去优化策略约束。\n修复原型 A：Hidden Class 克隆换干净剖析 # 为什么：在不能立刻改 VM 支持 per-callsite species 时，库层可用新类身份隔离已被污染的 MethodData。\n机制/约束：JEP 371 的 Lookup.defineHiddenClass 从 classfile 字节派生不可被常规发现加载的类；讲演实验：读取模板 ReflectiveQuickSort.class → 定义 hidden class → MethodHandle 调用，使优化器对新类重新积累单态剖面，称可将 ~70µs 拉回 ~12µs 量级（讲演实验，Javadoc 未承诺剖析隔离）。限制：嵌套类/多 classfile 难克隆、浪费 code cache（讲演坦诚）。\n图：The whole journey——@ REPAIR: HC-CLONE / fresh profile via hidden class / monomorphism restored\n怎么做：\nbyte[] tpl = Sort.class.getResourceAsStream(\u0026#34;ReflectiveQuickSort.class\u0026#34;).readAllBytes(); Class\u0026lt;?\u0026gt; species = MethodHandles.lookup().defineHiddenClass(tpl, true).lookupClass(); MethodHandle sort = MethodHandles.privateLookupIn(species, MethodHandles.lookup()) .findStatic(species, \u0026#34;sort\u0026#34;, MethodType.methodType(void.class, Object.class, Comparator.class)); sort.invokeExact(array, comparator); 常见误区：把演示当成框架默认能力——hidden class 不可作普通 API 类型链接，且克隆不能解决所有嵌套/helper 类依赖。\n修复原型 B：MethodHandles 强制内联与类型传播 # 为什么：当 callee 剖面已饱和，可在调用方用更窄的 MethodType 与常量 comparator 驱动 C2 做类型传播，弱化对 UseTypeProfile 的依赖（讲演实验）。\n机制/约束：asType 收窄为 Integer[]、insertArguments 绑定 comparator、dropArguments 保持签名；讲演称配合 hidden class 触发的 MH customization（forceCustomize——非公开 API，本地 JDK 未找到该符号）与 -XX:InlineSmallCode=1g（java 手册 条目：控制可被内联的已编译方法体积，默认 2500，演示值生产慎用）可在 -XX:-UseTypeProfile 下仍达 ~24–28µs（IterQS/Integer[]，讲演观点）。\n图：Alternative repair for megamorphism——Type propagation replaces type profiling / mh.asType / insertArguments\n怎么做：\nClass\u0026lt;?\u0026gt; ac = Integer[].class; MethodHandle mh = baseMh.asType(methodType(void.class, ac, ac, Comparator.class)); mh = MethodHandles.insertArguments(mh, 1, comparator); mh = MethodHandles.dropArguments(mh, 1, Comparator.class); // forceCustomize(mh); // 讲演实验 helper java -XX:-UseTypeProfile -XX:InlineSmallCode=1g -jar force-inline-demo.jar java -XX:+PrintFlagsFinal -version 2\u0026gt;\u0026amp;1 | grep -E \u0026#39;InlineSmallCode|UseTypeProfile\u0026#39; 常见误区：在生产环境关闭 UseTypeProfile 或把 InlineSmallCode 拉到极大——会改变全局内联启发式，与讲演可控微基准不是同一回事。\n旅程总览：坠崖、攀爬与 C++ 的「不完整动态」 # 为什么：架构选型要在「静态瀑布构建」与「静态—动态反馈环」之间权衡；JVM 保留 classfile 主副本 + 在线 JIT，C++ 则在 comparator 类型不可见时缺乏等价的重编译与剖析框架（讲演论点）。\n机制/约束：在剖析完整时，Java 可达 HAND-SPECIALIZED / MONO 区（~10.5µs，对标 C++ custom template）；沿 BIMORPHIC → SATURATED 下滑出现约 1.5–6× cliff；HC-CLONE 与 FORCE-INLINE 为讲演两种「爬回」原型。作者材料 Dynamic/Static interplay PDF 可访问（HTTP 200），但图中 µs 仍应以本地 JMH 为准。\n图：The whole journey——down the cliff and back / With intact profiles and dynamic compilation, Java has great performance\n图：C++ 对比——When C++ QuickSort templates use std::function, the comparator is untyped, just like megamorphic code in a JVM\n区域（讲演标签） 典型条件 近似耗时（讲演） HAND-SPECIALIZED int[] + 稳定比较器 ~10.5µs MONO / PRIMITIVE 反射 + flat int[] ~10–12µs MONO / BOXED Integer[] + 单态剖面 ~20µs SATURATED 多种 comparator 类 ~70µs 量级 REPAIR: HC-CLONE hidden class 刷新剖面 ~12µs（反射路径） REPAIR: FORCE-INLINE MH 类型传播 ~24–28µs 常见误区：用单次微基准结论指导多租户线上排序——应用层拆分 comparator 策略、隔离热路径类加载器，往往比依赖 VM 自动「猜对」更可靠。\n工程可操作的诊断顺序（不依赖讲演专用开关）：先用 -XX:+PrintCompilation 确认热点是否由 C2 编译；若吞吐异常，再对可疑方法打开 PrintAssembly 查看循环外是否存在类检查 guard、循环内是否仍为 invokeinterface。混合多种 Comparator 实现类做 A/B 时，应固定预热轮次与堆大小，避免把尚未稳定的剖面当成回归——这与 JMH 要求 fork、预热的原因一致，但线上服务还需关注类加载顺序与 JIT 队列延迟，二者不能简单等同。\n参考与延伸阅读 # JLS §4.6 类型擦除 JLS §4.8 原始类型与擦除补充 OpenJDK Project Valhalla（含 Parametric JVM 方向） Valhalla 设计笔记：同构 vs 异构翻译 OpenJDK Project Leyden JEP 483：Ahead-of-Time Class Loading \u0026amp; Linking JEP 515：Ahead-of-Time Method Profiling JEP 371：Hidden Classes JEP 401：Value Classes and Objects (Preview) HotSpot Wiki — PerformanceTechniques（剖析、内联、去优化） HotSpot Wiki — PrintAssembly（汇编诊断） HotSpot Wiki — RangeCheckElimination（数组范围检查优化） java.lang.reflect.Array API 文档 java.lang.invoke 包概览 OpenJDK JMH README John Rose — Dynamic/Static interplay（PDF，2025-08） cppreference — C++ 模板 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-how-the-jvm-optimizes-generic-code-a-deep-dive/","section":"文章","summary":"泛型代码在 JVM 上如何变快：擦除、剖析与「坠崖」之后的攀爬","title":"泛型代码在 JVM 上如何变快：擦除、剖析与「坠崖」之后的攀爬","type":"posts"},{"content":" 格式约束何时伤害 LLM：从 Agent 流水线到基准评测的分叉 # 向量检索、工具调用与多步 Agent 把 LLM 嵌进 Compound AI 流水线：中间态要可解析、可路由、可重试。工程上于是广泛采用 JSON 在步骤间传参；公开榜单与学术基准却仍以自然语言（NL）终答案为主，用 exact match 或 accuracy 打分。Appier AI Research 的 Zhi Rui Tam 在论文 Let Me Speak Freely? 中把这一分裂做成可复现实验：同一套「结构化输出」技术，在推理任务上常降分，在离散分类上常涨分。下文按机制拆解，不给出单一结论——任务符号、API 代际（JSON-mode vs JSON-schema）与 schema 深度会改写符号。\n分屏访谈开场：主持侧书架可见 Florida Atlantic University 文凭，嘉宾侧显示器为代码编辑器界面。\n生产与评测为何脱节 # 为什么：编排框架（LangGraph、CrewAI 等）需要可解析的中间态；运维与 eval harness 却习惯用 exact match / accuracy 对 NL 终答案打分。若线上全程 JSON mode 或 Structured Outputs，而榜单只报 NL 分数，部署表现与公开排名可能系统性错位（论文 Introduction 与 Compound AI Systems 动机一致）。\n机制：格式限制改变 decoding 的 token 空间——不仅是后处理，而是生成轨迹本身被裁剪或偏置。\n怎么做：在自家 dev set 上并行报告 NL、FRI、JSON-mode 三条曲线；若使用 OpenAI Structured Outputs，单独成第四条——其与 JSON-mode 在 GSM8K 上可差近 5 pp（Table 2: 91.71 vs 86.95）。Agent 基准应显式包含「步骤间 JSON」条件（论文呼吁；PlanBench 可作为规划类补充，主实验未使用）。RAG 场景同理：检索片段可结构化，但 答案推理链 是否应受 JSON 约束，需与任务符号一起 A/B，而非默认全程 schema。\n误区：把「榜单 SOTA」直接等同于「JSON 流水线 SOTA」，未做格式对齐对照。\n讨论 Agent 工作流与 JSON 编排时段的画面（OCR：po Dilamllr My, y te,）。\n约束生成谱系：FRI、JSON mode、两阶段、Function calling # 路径 约束位置 典型保证 FRI（Format-Restricting Instructions） Prompt 内嵌 schema 无硬解码保证；易出现格式违规 JSON-mode 提供商约束解码 Valid JSON；论文称 OpenAI/Gemini 侧与 function calling API 实现相关 NL-to-Format 两次调用 先 NL 推理，再转 JSON/XML/YAML JSON-schema / Structured Outputs Schema + strict 强于旧 JSON-mode；不自动解决 reasoning 语义顺序 为什么：工程师常把上述名词混为「结构化输出」，但 合法语法 ≠ 正确推理 ≠ 正确字段顺序。\n机制（论文）：越 strict，推理集上 performance degradation 越大（摘要）；分类集上 JSON-mode 因 answer space 裁剪 常优于纯文本（§5.2）。从解码视角看，约束等价于在每一步缩小 logits 支撑集：对 49 选 1 的诊断标签这是助力；对需要多步算术符号展开的 GSM8K 则可能在早期就剪掉正确推理路径。论文 Figure 1 给出典型案例：GPT-3.5-turbo 在 GSM8K 上 NL 正确，加格式限制后失败——说明损伤发生在 生成过程，而非单纯解析失败。\n怎么做（最小 pipeline）：\n# 推理向：保留 NL 中间态（示意，非论文超参） reasoning = client.chat.completions.create( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: problem}] ) structured = client.chat.completions.create( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: f\u0026#34;将下列推理转为 JSON schema:\\n{reasoning}\u0026#34;}], response_format={\u0026#34;type\u0026#34;: \u0026#34;json_schema\u0026#34;, \u0026#34;json_schema\u0026#34;: SCHEMA}, ) 误区：\n单次 prompt「请只输出 JSON」下结论——论文用 9 组 prompt 组合（3 任务描述 × 3 格式变体）取平均，并报告加 schema 后 敏感性上升（附录 G.2）。 认为 XML/YAML 在 Table 1 中偶发优于 JSON 即应全面替换——论文将波动归因 prompt variance，非格式本质。 谈及 JSON mode 与 benchmark 分数时段（OCR：if Ahh = 2 = = q 3 4）。\n开源栈 Outlines / TGI 讨论附近（OCR：po Nilamtl Mays, Cs ra,）。\n约 10 分钟处：嘉宾闭目思考，背景 IDE 仍为深色代码界面。\n任务类型分叉：推理 ↓，分类 ↑ # 为什么：推理需要长链中间符号（算术、字母拼接）；分类答案往往已在预训练分布的 离散支撑 上。\n机制（已核实，论文 HTML v3）：\n推理：GSM8K、Last Letter、Shuffled Objects；指标 exact match。gpt-4o-mini 上 GSM8K：NL 94.57 → FRI 87.17 → JSON-Mode 86.95（Table 2）。 分类：DDXPlus（49 种疾病诊断）、Sports Understanding、NI Task 280、MultiFin；指标 accuracy。Gemini-1.5-Flash / DDXPlus：Text 41.6 vs JSON 60.3（Table 10）。 怎么做：上线前用 任务符号 分流——数学/多跳 agent 慎用全程 JSON-mode；医疗/金融式 有限标签集 可试 constrained decoding（Outlines 等同系）。\n误区：\n将播客口语「输出 1–49」泛化为任意 49 类——论文对象是 DDXPlus 的 49 diseases。 认为 JSON-schema 必然挽回推理分——同表 GSM8K JSON-Schema 91.71 仍低于 NL 94.57。 主持曾假设 JSON 分布强烈拉高分类；论文与讨论均承认 分类侧机理解释仍不完整（演讲者观点 @ 分类直觉段）。 49 类分类与 JSON mode 讨论（OCR：goo Milaatte May, ta,）。\n推理 vs 分类分叉讨论（OCR：Lily Hiro / fal al ii Ahh 3）。\n合法 JSON 与任务成功脱钩 # 为什么：工程 KPI 常先盯解析成功率。\n机制：JSON-mode 保证 valid JSON（产品叙述）；论文另设 Perfect Text Parser 等，区分 format errors 与 final metrics。\n证据边界：嘉宾口述 约 99% 场景下 5–10 次采样 可得合法 JSON，但 final result 另论——演讲者经验，论文未给 99% 统计（P06 无法核实定量）。\n怎么做：监控 parse_ok 与 task_correct 两条 SLO；重采样优先换更强模型，其次 3–10 次同 prompt（嘉宾优先级；未验证 同温 10 次 vs 9 种 prompt 何者更优）。\n误区：json.loads 成功即宣告 Agent 成功。\n重采样与 prompt 敏感性（OCR：= any Milani 7, 1 ? ~ a）。\n两阶段与「先自由说，再格式化」 # 为什么：Strict JSON 在生成阶段即绑定 schema，可能压缩推理所需的 中间 token 分布（嘉宾假设：解码域在 JSON / NL / LaTeX 风格间切换带来混乱——演讲者假设，非定理）。\n机制（部分核实）：论文 NL-to-Format 相对 NL 在多数模型上 nearly identical；相对 JSON-mode / FRI 在推理集上明显更优。播客中「much better」的对比对象是 strict JSON，不是相对纯 NL 的巨幅提升——勿过度解读。\n怎么做：复杂 agent 将 规划 / CoT 留在 NL，末端单次 format_to_json()；嵌套 schema 按层拆分（见下节）。\n误区：把所有步骤都塞进一个巨型 JSON——嘉宾口述 ~10 层嵌套 单步质量 明显退化（访谈观点，论文无 10 层对照实验）。\n复杂 JSON 与两阶段策略（OCR：) — ———_ xxii ¥ 2 —_—）。\n嵌套 schema 讨论续（OCR：Nilunt go Muni Nar, a ,）。\n约 16–17 分钟：主持面向镜头，嘉宾低头组织表述。\nFunction calling、字段顺序与 RFC 语义 # 为什么：Tool call 被 marketed 为「现代 structured output」，但 JSON 对象无序（RFC 8259）；OpenAPI 中 function.arguments 为字符串，properties 无顺序语义。\n机制（部分核实）：论文 §5.4 将 OpenAI JSON-mode 与 function calling API 关联，并记录推理任务中 reasoning 应先于 answer 的违背；结论强调 key order 与 reasoning–format decoupling。嘉宾建议：仅需 enum 级工具名 时 function calling 足够；顺序敏感 时改用可控 JSON-mode / schema——条件性建议，播客未做跨厂商 API 实测。\n怎么做：若业务逻辑依赖 reasoning → answer 的呈现顺序，在协议层用 数组或分字段两次调用，勿假设 object key 顺序。\n误区：把 function calling 与 JSON-schema 混为一谈——后者强化合法性，不自动 保证语义顺序（GSM8K JSON-Schema 91.71 仍低于 NL）。\nFunction calling 与字段顺序（OCR：iti JHE erry rai \u0026lt;）。\n同主题续（OCR：HHEIIT Hy RAEN TIER TD）。\nRAG、工作流步骤顺序与 test-time compute # 为什么：检索增强与多步 workflow 常在步骤间传递结构化载荷；与「Let me speak freely」形成张力——中间步是否需要 strict JSON？\n机制：论文未专门评测 RAG；讨论延伸至 步骤顺序（先检索后生成 vs 颠倒）与 test-time compute（o1 类模型更多 internal reasoning）。嘉宾自认公开 benchmark 「a bit limited」，难找「既需要 structured output 又能靠结构提分」的数据集（演讲者观点）。\n怎么做：RAG 管道对 检索结果 用结构化、对 模型推理链 用 NL 的混合策略值得 A/B；规划类任务可跟踪 PlanBench。\n误区：因 o1 内部链不可见，便认为外部 JSON 约束「无关紧要」——外部格式仍影响 可观测步骤 的解析与工具链接。\nRAG 与步骤顺序（OCR：1 { Hl alt \\. an —— ad）。\n工作流顺序讨论（OCR：rip il if Hi hth）。\nTest-time compute / o1 方向（OCR：waite Ei? \u0026quot;7 ‘gn）。\n约 19 分钟：嘉宾低头，背景仍为办公椅与显示器。\n开源约束解码：Outlines、TGI、Llama 3 8B # 为什么：数据不能出域时需要自建 mask logits 式解码，而非仅依赖黑盒 API flag。\n机制：论文引用 Willard \u0026amp; Louf (2023) 与 Text Generation Inference 的 Guidance；Outlines 提供 Guaranteed valid structure。TGI 文档描述 grammar 在 /chat/completions 与 tools 上的映射。\n证据边界：嘉宾称 Llama 3 8B + TGI JSON mode 已「pretty good」——主观访谈；AI Engineer Summit 上 SQL 语法 100% 为 会场转述，播客未复现，Python/C++ 嘉宾持保留。\n怎么做：隐私场景优先评估 Outlines / TGI Guidance；与 API JSON-mode 结果交叉验证。\n误区：微调单一 DSL（如 GraphQL）后模型 只会 该 DSL，与多工具 Agent 接口不兼容（主持经验，嘉宾未系统反驳）。\n片头姓名条区域（OCR：Nilenth ae * Hayy, We te,）。\n约 10 分钟：主持注视嘉宾，麦克风入画。\nPrompt 优化、微调与评测外推 # 短期： DSPy、OPRO、TextGrad 等可缓解 prompt 敏感性（嘉宾点名，未与本文实验对比）。在资源有限、尚未决定微调前，先用九套 prompt 方差估计（论文附录 G.2 方法论）可避免被单次 FRI 误导。\n规模：百万用户级仍可能需要 fine-tune 压长尾（嘉宾）；与格式约束正交。主持提到微调 GraphQL 后模型生成面变窄，与多工具 Agent 的开放接口存在张力——若你走微调路线，需单独验证 格式遵守 与 工具泛化 是否同时成立。\n格式多样性：论文在部分 模型 × 数据集 × prompt 上 XML/YAML 优于 JSON/NL——归因于 prompt 敏感性，非格式本质优越（Table 1/9）。实验模型含 gemini-1.5-flash、claude-3-haiku、gpt-3.5-turbo、LLaMA-3-8B-Instruct（§3.3），与播客录制时口述的「最新 frontier」清单 不完全一致，外推需重跑。\nXML/YAML 条件性结论已核实；勿写成「永远换 XML」。\n分类任务 JSON 偏置直觉（OCR：a dat th H BS ' i ok）。\n约 20 分钟：嘉宾张口发言，笔记本 Framework 徽标可见。\n证据边界（读完全文前可先扫一眼） # 主张 状态 推理 strict 降分 / 分类 JSON 涨分 论文 Table 2、10 已核实 NL-to-Format 相对 strict JSON 更优 部分核实；相对纯 NL「几乎相同」 99% 五次采样合法 JSON 访谈观点，论文无此定量 ~10 层嵌套单步退化 访谈观点，论文无对照实验 Function calling 键序 RFC 无序已核实；推理顺序见论文 JSON-mode 段 若你要落地 # 按任务符号选格式：推理/数学/agent 多跳 → NL 或 NL-to-Format；离散分类（如 DDXPlus 式标签集）→ JSON-mode / enum 约束。用自家数据复现 Table 2 方向，勿照搬单点分数。 报告格式条件下的 eval：线上若步骤间 JSON，基准应含同条件分数，否则与 Compound AI 部署脱节。 拆分 KPI：valid_json_rate 与 task_accuracy 分开告警；重采样修语法不替代模型升级。 顺序敏感 schema：勿依赖 object key 顺序；考虑分步调用或数组字段（RFC 8259 + 论文 reasoning-order 观察）。 深嵌套 schema 分步生成：单 API 吐出「十层嵌套」成功率与表象能力分裂（访谈 ~10 层为量级比喻）；先 NL 计划再逐层填表。 参考与延伸阅读 # Let Me Speak Freely? — arXiv:2408.02442 — 格式限制对 LLM 影响的系统研究（Tam et al., 2024） 论文 HTML v3（含 Table 2 / DDXPlus） OpenAI Structured outputs 指南 — JSON-schema 与 strict 模式产品语义 OpenAI OpenAPI 规范仓库 — json_schema、ChatCompletionMessageToolCall 字段定义 RFC 8259 — JSON 数据格式 — 对象无序集合的 normative 定义 GSM8K — grade-school-math — 推理类 exact match 基准 Compound AI Systems — Berkeley BAIR — 多组件 AI 系统架构语境 PlanBench — gpt-plan-benchmark — 规划与 agentic 评测扩展方向 Hugging Face TGI — Guidance 概念 — grammar / JSON 约束解码 Text Generation Inference GitHub — 自托管推理与 guided generation Outlines（dottxt-ai/outlines） — 约束解码与 guaranteed structure Willard \u0026amp; Louf — Efficient Guided Generation（Outlines 论文系） — 约束解码理论基础（论文引用链） DSPy — stanfordnlp/dspy — 声明式 prompt / 权重优化框架 Weaviate Podcast #108 视频检索 — 官方 YouTube 发布页（检索入口，具体 watch URL 以频道为准） DDXPlus 数据集背景（论文引用 StreamBench 子集） — 49 类医学诊断实验设定 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-let-me-speak-freely-with-zhi-rui-tam-weaviate-podcast-108/","section":"文章","summary":"格式约束何时伤害 LLM：从 Agent 流水线到基准评测的分叉","title":"格式约束何时伤害 LLM：从 Agent 流水线到基准评测的分叉","type":"posts"},{"content":" 规模化 DataFrame：当 notebook 习惯撞上分布式执行 # RAG 管线、评测脚本与 agent 工具链里，pandas 仍是默认的「把表拉进内存再算」接口：特征表、检索日志、标注结果往往先变成 DataFrame，再进 embedding、重排或指标计算。瓶颈通常不在「会不会写 SQL」，而在 单进程 GIL、全表进内存 与 人类可读文件格式（CSV）的并行语义。Snowflake 侧的 pandas on Snowflake、开源 Modin 与已并入 Snowflake 生态的 Ponder，代表两条路线：要么把 pandas 调用编译到仓库引擎，要么在 Ray / Dask 上做任务并行 DataFrame 引擎。\n下文围绕 Modin 作者 Devin Petersohn（Snowflake；Ponder 联合创始人）在公开对话中的技术主张，与官方文档、论文及可核对源码并置。不合并为单一结论；口述因果与性能判断标为「演讲者观点」，可核对处给出链接或可复现线索。\nDataFrame 与 SQL：不是「谁更快」，而是语义契约不同 # 为什么 # 数据科学家在 notebook 里迭代时，默认 行序可重复（head() 每次一致）、操作可任意组合；仓库 SQL 则常为 逻辑关系 优化，物理顺序不承诺。把两者硬比延迟，会掩盖 交互式探索自由度 与 引擎内结构化执行 的分工。\n机制与约束 # 维度 常见实践 嘉宾立场（演讲者观点） 心智模型 SQL = 仓库批处理；pandas = 本地 EDA DataFrame 像「美食广场」可混搭操作；SQL 像「米其林」菜单受限但一致 迭代方式 SQL 写一段跑一段 DataFrame 更 incremental；SQL 交互未必更「自然」（与流行叙事部分相反） 采纳驱动 追求更低延迟 基因组学等场景周/月级实验节奏下，更快未必被关心；生产力与保持原工作流优先 Snowflake SELECT 文档写明：无 ORDER BY 时结果为 unordered set，重复执行可能 每次顺序不同。这与 pandas 用户对 head() 的稳定预期形成张力（见下一节）。\n怎么做（最小示例） # 在仓库侧若只关心集合语义，显式排序或键：\nSELECT * FROM events ORDER BY ts, id LIMIT 10; 在 pandas 侧勿假设 read_sql 无 ORDER BY 时行序稳定。\n常见误区 # 用单次 benchmark 证明「SQL 比 DataFrame 更适合探索」。 忽视 逻辑有序（DataFrame）vs 物理无序（关系引擎） 的产品缺口。 「pandas 封装」还是「编译器」：Modin 的中间表示 # 为什么 # 用户常把 Modin 理解为「能在 Ray 上跑的 pandas」；若只改一行 import，不理解 Query Planner → Dataframe Algebra → 执行后端，会在 Snowflake 路径与 Ray 路径之间选错运维模型。\n机制与约束 # Modin 架构文档 描述：Query Planner 将 pandas API 译为 Dataframe Algebra DAG；Query Executor 优化并编译为执行序列；文案使用 compiler 一词。论文 Towards scalable dataframe systems（Petersohn、Lee、Parameswaran 等）提出 dataframe data model 与 algebra，讨论 flexible schema、ordering、行列等价等——未使用嘉宾口述的「关系代数 + 线性代数婚姻」字面表述，该比喻宜作直觉（演讲者观点）。\n嘉宾强调：相对 Dask DataFrame / Ray Datasets，Modin 在 Ray/Dask 上主要用 task scheduling，自研 dataframe engine 与代数算子（部分核验：RayWrapper / DaskWrapper 为 @ray.remote 与 client.submit 包装任意 callable；公开文档未逐字写「零引用 Ray Datasets」）。\n怎么做 # import modin.pandas as pd # drop-in 叙事见 Modin README df = pd.read_parquet(\u0026#34;s3://bucket/large.parquet\u0026#34;) df = df[df[\u0026#34;country\u0026#34;] == \u0026#34;US\u0026#34;] # 规划器可下推 filter（视后端） print(df.head()) 常见误区 # 把「compiler」等同于已公开的形式化 IR 规范（播客未给出 PDF 级规范）。 在 Snowflake 后端期待与 Ray 路径相同的 控制面（嘉宾：SQL 侧更像 transpiler，优化器为黑箱——演讲者观点）。 行序、物化视图与 Snowflake 语义鸿沟 # 为什么 # 交互式 notebook 依赖 稳定 head；MPP 仓库在无 ORDER BY 时本就不保证顺序。桥接层若不做额外语义，用户会以为「库坏了」。\n机制与约束 # 已核验：Snowflake SELECT 无 ORDER BY → 无序（见上节文档）。 演讲者观点：Modin 对 Snowflake 等后端 模拟有序；开源路径有 cached materialized views；Snowflake 路径强调 guaranteed order。未核验：Modin 主仓库公开文档/源码中 Snowflake 物化视图实现细节。 怎么做 # 需要稳定预览时，在 SQL 层加键序，或接受桥接层的物化/缓存成本（产品行为以 Snowflake pandas 文档为准）。\n常见误区 # 认为 SELECT * LIMIT 10 在任意仓库上等于 pandas head() 语义。 把嘉宾物化视图叙述当作 Modin 开源默认行为（边界未证实）。 查询优化：先过滤，再谈「最聪明」的优化器 # 为什么 # 分布式 agent 若先 read_parquet 全表再 filter，会把 网络与解码 成本放大到不可接受；数据库课的经典策略 filter-first 同样适用于 DataFrame 编译栈。\n机制与约束 # Apache Parquet 通过 row group 统计、Page Index 等支持 谓词下推；Modin 的 Parquet 读路径在 engine=\u0026quot;pyarrow\u0026quot; 时使用 pyarrow.dataset 与 filters_to_expression（源码 parsers.py）。 演讲者观点：拥有全栈 task graph 时可做更细 bypass，部分想法 未进开源；嘉宾自述 Modin 没有最 sophisticated 的优化器——与论文「相对 pandas/Dask DataFrame 的加速」是不同维度，不宜写成矛盾。 怎么做 # # PyArrow 系读取常支持 filters 参数（具体以 Modin/pandas 版本为准） df = pd.read_parquet(\u0026#34;data.parquet\u0026#34;, filters=[(\u0026#34;year\u0026#34;, \u0026#34;\u0026gt;\u0026#34;, 2020)]) 常见误区 # 在 CSV 巨文件上期待与 Parquet 同等的 存储层下推（CSV 无列式统计）。 因嘉宾自谦优化器而否定 planner + executor 两层优化 的存在（文档已写明）。 并行读 CSV：引号内的逗号不是分隔符 # 为什么 # RAG 清洗、评测导出、爬虫落盘仍大量 CSV；按行数或朴素字节切分会在 quoted field 中间断开，导致静默错行——比「多 worker 读不同行」难一个数量级。\n机制与约束 # 已核验：Modin TextFileDispatcher.offset() 根据 quotechar 奇偶判断切分点是否在引号外，否则读到行终止符（text_file_dispatcher.py）。 演讲者观点：瓶颈常在 parse 而非裸 read；Amazon reviews 类数据中逗号、换行可出现在引号内。可与 Python csv 的 quotechar 行为对照。 怎么做 # # 概念：driver 分配 byte offset，worker 从 quote-safe 边界开始 # Modin 内部分区示意 — 勿对 giant CSV 做朴素 bytes//n_workers df = pd.read_csv(\u0026#34;huge.csv\u0026#34;) # Modin 在 Ray 上并行 quote-aware 切分 常见误区 # 按固定行数分片而不处理 RFC 式引号。 假设「更多 worker」线性加速而忽略 解析 CPU 主导（播客未给 benchmark 数字）。 数据移动：Arrow、Ray 对象存储与「只传 handle」 # 为什么 # Agent 多步链、特征流水线里 最贵 的往往是 shuffle 与跨节点拷贝，而非单次 groupby 算子。\n机制与约束 # 主张 核验 嘉宾：worker 间序列化 generally Apache Arrow 部分核验：依赖含 pyarrow；Ray 分区 put 注释指向 Plasma store；Ray Serialization 主叙事为 pickle/cloudpickle + 对象存储 嘉宾：大文件优先 共享存储（NFS/S3/GCS），传 handles 而非文件块 部分核验：实验性 CSVGlobDispatcher 使用 fsspec URL；无 Modin 官方「禁止 over-the-wire 分片」逐字句 Ray：运行时 pickle 函数，非 600 个 pandas API 各一种 task 已核验：RayWrapper._deploy_ray_func 包装任意 callable（engine_wrapper.py） Apache Arrow 列式内存 与 Parquet/Feather I/O 强相关；不宜写成「官方规定 worker 间仅 Arrow IPC」。\n怎么做 # 部署时让各 worker 能直接读同一 s3:// 前缀；减少 driver 聚合中间结果。\nimport ray @ray.remote def process_shard(path: str, offset: int, limit: int): # worker 本地 open(path) — 示意 ... 常见误区 # 把小文件也走分布式 read，支付调度开销。 把 Plasma 历史组件等同于当前所有 Ray 版本的唯一序列化路径。 GPU DataFrame：cuDF 是后端之一，不是万能替换 # 为什么 # 向量检索、embedding 批处理推动 GPU 算子；但 DataFrame 上的「怪异」操作在 GPU 库上常缺实现或回退慢。\n机制与约束 # 部分核验：Modin 仓库存在 cudf_on_ray 实验路径（cudf_on_ray）；RAPIDS cuDF 提供 pandas-like API。 演讲者观点：与 Georgia Tech 的分布式 GPU DataFrame POC 可行；cuDF 对 wacky 操作更难；无播客内性能数字。 怎么做 # 对规整数值列、可 GPU 化的算子评估 cudf.pandas 或 Modin+cuDF 路径；对字符串/对象列密集 workload 先 profiling 再决定。\n常见误区 # 默认 entire pipeline GPU 化而不测 PCIe 传输与回退。 把 POC 叙述当作生产默认配置。 LLM、Copilot 与 API 设计：未验证的采用加速器 # 为什么 # 做 RAG/agent 的工程师关心：工具 schema 与训练语料分布 是否一致。若 agent 生成 import pandas as pd 而运行时却是近似 API，会放大修复成本。\n机制与约束 # 演讲者观点：镜像 pandas API 使 Copilot 建议更可用，成为 Modin 采用的 事后意外；「接近 pandas」在 LLM 建议下可能不如「就是 pandas」；客户反馈「match API」越来越重要——无法核验因果与幅度（无 A/B、无 Copilot 官方研究）。 可核对：Modin README 的 drop-in 与 API 覆盖率表（如 DataFrame 覆盖比例）——应与 LLM 叙事 分轨：性能可测，Copilot 未测。 主持延伸：Snowflake SQL 扩展少 → 训练 DSL 成本低；或 LLM 把 Ponder 当 tool（产品方向，本对话未展开 schema/评测）。 与 AI 栈的弱相关线程：Lux（规则/推荐式可视化，非 LLM 画图）；Weaviate × Arrow × GPU 向量索引为 主持设想，嘉宾未给实现级回应。 怎么做 # Agent 工具定义优先绑定 文档齐全、语料高频 的 API；若用 Modin，在 prompt 中显式 import modin.pandas as pd 并锁定版本。\n常见误区 # 把 Copilot 轶事当作下载量增长的 proved cause。 在评测 agent 时只测 SQL pass@k，忽略 DataFrame 工具轨迹 与多库清洗（参见 Data Agent Benchmark 等不同 workload）。 张力小结：不必强行统一 # 主题 文档/论文 演讲者观点 建议表述 Compiler 强度 有 Dataframe Algebra + compiler 文案 接近查询编译器 一致，但缺公开 IR 规范 Arrow 序列化 pyarrow 依赖、Plasma 注释 generally Arrow 降级为「列式 I/O + 对象存储」，非唯一 IPC Snowflake 顺序 SELECT 无序已证实 物化视图保序 产品行为需查 Snowflake pandas 文档 LLM 采用 README 性能叙事 Copilot 助推 分轨：未验证因果 若你要落地 # 先定语义：需要稳定 head() 还是在仓库侧接受无序集合？再选 pandas on Snowflake、Modin+Ray 或本地 pandas。 格式优先 Parquet：谓词下推有格式规范支撑；巨 CSV 则确认 quote-aware 并行读实现，勿朴素按字节切分。 减少数据移动：共享对象存储 + worker 本地读；避免 driver 收集全表再广播（与 Ray object store 设计一致）。 优化器预期：默认 filter-first 与 Parquet 统计；勿假设开源 Modin 等同仓库级 CBO（嘉宾自谦——演讲者观点）。 Agent/RAG 分轨：工具 API 与训练分布对齐可单独设计实验；勿把 Copilot 轶事与延迟 benchmark 混为同一 KPI。 参考与延伸阅读 # Modin 文档首页 Modin 系统架构 — Dataframe Algebra Modin README — 引擎与 API 覆盖 Towards scalable dataframe systems（VLDB 2020） Flexible rule-based decomposition in Modin（VLDB） pandas 性能增强指南 Ray Core 概念 Ray Tasks 与 remote functions Ray 对象序列化 Apache Parquet 文件格式与元数据 Apache Arrow 列式内存规范 Snowflake SELECT 语义 Snowflake 存储与计算分离 pandas on Snowflake 开发者指南 RAPIDS cuDF 文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-scaling-pandas-with-devin-petersohn-weaviate-podcast-101/","section":"文章","summary":"规模化 DataFrame：当 notebook 习惯撞上分布式执行","title":"规模化 DataFrame：当 notebook 习惯撞上分布式执行","type":"posts"},{"content":" 合成数据：RAG、Agent 与评测里的「造数」边界 # 向量库、检索 Agent 与离线评测集，最终都绕不开同一类工程问题：在真实标注稀缺、隐私受限或长尾分布稀疏时，用什么机制补数据，以及补出来的分布是否值得信任。 Hugging Face / Argilla 生态里的 distilabel、Synthetic Data Generator（SDG）与 Persona、DEITA 等研究线，讨论的是 LLM 驱动的数据合成 pipeline——从 instruction 扩增到 preference、再到 Hub 上的可查询数据集。画面素材为三人远程访谈，未见可读架构图或结果表；下文定量与 API 行为以论文、数据集卡片与官方文档为准，嘉宾口述标为演讲者观点。\n约 10 分钟处：三分屏访谈布局，主持人侧背景墙可见 Weaviate podcast 标识，无技术幻灯片\n问题空间：合成数据落在哪条链路上 # 为什么：RAG 需要 query–passage–answer 或 chunk 级标注；Agent 需要 tool trace、多轮对话、失败恢复 样本；评测需要 held-out、可复现 的输入变体。真实日志往往带 PII、分布偏斜，或根本不存在「标准答案」。合成数据常被当作 data augmentation 的 LLM 版（Ben 开场表述——演讲者观点），但工程上至少要分清：你是在 扩增训练集、改写评测输入、还是 蒸馏另一模型的行为。\n机制/约束：David 提出四层 taxonomy——augmentation、effective prompting、reformation/rewriting、distillation/synthesis（对模型做 completion 或领域蒸馏）——嘉宾框架，非行业标准术语表。与 Llama 3.1 Model Card 中「微调阶段 25M+ synthetically generated」及 LLM-based classifiers 用于数据筛选的叙述可对照：合成 + 过滤 已进入主流发布流程，但 未 意味着预训练主语料由合成构成（部分可核对）。\n常见误区：把「合成」等同于「用 GPT 多生成几条」；未定义 目标分布（SFT / DPO / RAG 检索评测）就堆行数，容易在规模化时撞上质量墙——David 称 10 条试跑正常、100 条常需改 pipeline 或换更大模型（演讲者观点，非普适阈值）。对 RAG 而言，合成 query 而不动 chunk 边界与 citation 约束，评测分数可能虚高：模型学到的是「问法分布」，而非索引是否召回正确段落——宜把 检索命中率 与 生成忠实度 拆开度量。\n讨论 Hub 与 prompt-to-model 闭环时段：画面可见 Weaviate podcast 品牌字样\nPost-training 形态：instruction、preference 与 critique # 为什么：Self-Instruct 用 175 条种子指令经改写扩到约 52k instructions，再用 GPT-3 API（论文用语；口语常称 ChatGPT）生成 completion 后 SFT——规模数字与 README 一致。后续 UltraFeedback 从既有 prompt 出发，多模型各生成多条 response，再由 GPT-4 做细粒度反馈——流程 verified。\n机制/约束：David 将 post-training 数据归纳为 instructions → preferences → critiques 流水线（演讲者归纳）。UltraFeedback 官方四维为 instruction-following, truthfulness, honesty, helpfulness；嘉宾曾列举 helpfulness, conciseness, effectiveness——helpfulness 重合，后两者无官方一一对应，写作时宜以 UltraFeedback README 为准。\n怎么做（minimal）：固定 seed prompt 集 → 多模型采样 completion → 用 judge（GPT-4 或本地分类器）打维度分 → 导出 preference 对供 TRL DPO/ORPO。\n常见误区：preference 数据 越多越好；David 建议对两 completion 的 helpfulness 等分数 求差，差值过小则 不宜纳入 DPO（演讲者启发式，非 UltraFeedback 官方规则）——模型难以学习的「近似平局」会浪费预算。\nPersona 与文档 seed 讨论段：OCR 片段 %) Weaviate »dcgst，画面为谈话镜头\n约 5 分钟处：主持人背景墙 Weaviate podcast 标识，嘉宾分屏聆听\nPersona 驱动合成：多样性，而非知识载体 # 为什么：Persona Hub（正确 ID：2406.20094，非误链的 2407.17308）从网页等抽 Text-to-Persona，再 Persona-to-Persona 生成关系人（如店员 → 顾客），并条件生成 instruction–response；论文称可达 10 亿级 persona 规模（摘要自述）。Ben 强调：约 50 词的 persona 描述不能替代可抽取的领域知识，主要带来 variety；同一长文档 + 不同 persona 可改变 instruction/response 分布（演讲者观点）。\n机制/约束：论文将 persona 视为 分布式知识视角载体；嘉宾则强调 variety 与覆盖文档 低曝光章节（同一文档直接要 QA 易卡在热门段落——演讲者观点）。二者张力宜并列写明，而非二选一。去重：Persona Hub 论文 写明 embedding cosine similarity \u0026gt; 0.9 过滤，并辅以 MinHash threshold 0.9——verified。\nPersona-to-persona 非「向量杂交」：主持人曾类比 batch 内两 persona 交叉；David 澄清为 单 persona 为 seed，由 LLM 生成互动对象，与论文 Persona-to-Persona 中 interpersonal relationships 一致——文献/访谈一致。\n怎么做（minimal）：选 corpus（如 RedPajama v2 或 fineweb-edu）→ distilabel 跑 Text-to-Persona → 条件生成 QA → 嵌入去重 → 推 Hub。Ben 举例：对 Weaviate 文档 可直接让 LM 综合非结构化内容为 schema 等结构（演讲者观点）——这与「persona 不提供领域知识」并不矛盾：知识仍来自文档，persona 只改写 谁在用、问什么角度。\n常见误区：认为 persona-guided 将 一统 所有合成路径。Ben 否认；paraphrase、keyword 注入、原始 seed 仍并存（演讲者观点）。Paraphrase 评测集更偏 鲁棒性测试；训练侧多样性可来自 Persona Hub 等机制，而非仅改写 eval 输入（演讲者观点）。\nPersona Hub 去重阈值讨论附近：OCR 片段 a 3 = 5 © =，无幻灯片文本\n接续讨论：OCR 片段 2 te} \u0026gt; 3 =，画面仍为三分屏访谈\nargilla/FinePersonas-v0.1 在 fineweb-edu 上生成约 2100 万 条 persona，标签含 distilabel；聚类子集 FinePersonas-v0.1-clustering-100k 提供 177 个 cluster 供企业按主题选 persona——数据集卡片可核对；「工程类簇」为使用场景举例（演讲者观点）。\nPipeline 工程：distilabel、缓存与有状态执行 # 为什么：合成 pipeline 常是 DAG：生成 → 打分 → 过滤 → 写 Hub；中途失败若从头重跑，成本不可接受。\n机制/约束：distilabel 文档支持 Ray 调度 vLLM（含 tensor_parallel_size）、PushToHub 边生成边落盘——组件存在 verified。导航含 Pipeline cache；「按 pipeline 配置参数键控缓存、失败回退上一步」 为嘉宾描述，参数键控措辞本次未在可访问文档正文定位，宜对照源码或版本更新说明。Weaviate Transformation Agent 强调 workflow 持久化、第 N 步失败从 N 续跑（主持人产品叙述——演讲者/主持人观点），与 distilabel 的 DAG 缓存是 不同 durability 模型，选型取决于你是否需要跨会话、跨服务的编排。\n怎么做（minimal）：\n# 概念示意：distilabel Step 链 + PushToHub（API 以官方文档为准） from distilabel.pipeline import Pipeline # pipeline.add_step(...).add_step(PushToHub(repo_id=\u0026#34;org/synth-rag-v1\u0026#34;)) # pipeline.run(parameters={\u0026#34;num_rows\u0026#34;: 1000}) SDG README 默认 MAX_NUM_ROWS=1000；任务类型含 Text Classification、SFT、RAG——verified。嘉宾称 500–1000 条 即可起步试跑（演讲者观点；500 非文档默认值）。\n常见误区：小规模 demo 顺利即认定 万级可线性扩展（见上节 10 vs 100 条经验）。另： SDG 仓库 注明维护重心已转向 aisheets——产品生命周期，不改变「曾基于 distilabel + Gradio」事实。\nDSPy 与 distilabel 集成讨论段：OCR 片段 2 os \u0026gt; 0 Ss =\n约 20 分钟处：三分屏，主持人侧 Weaviate podcast 墙牌，无架构图\nBen 的实践：DSPy 离线优化 prompt，再固化进 distilabel；库内深度集成「轻到不必要」（演讲者工程取舍）。distilabel 强调 可复现的固定 prompt；DSPy 适合多 API、多模型下 一致性 优化——与 DSPy 程序级优化叙事正交，可组合而非互斥。\n质量与多样性：DEITA、分类器与「坏榜好零件」 # 为什么：合成行数膨胀后，质量、复杂度、多样性 三者常冲突；全量喂入 SFT/DPO 未必最优。\n机制/约束：DEITA（正确 ID：2312.15685）在 complexity × quality 标量（论文 evol score (s = q \\times c)）排序后，用 Repr Filter 在表征空间保多样性（阈值 τ 附录约 0.8–0.9）——verified；访谈中的「2D 映射」为 语义近似、表述不严格。Ben 称 WizardLM 端到端榜样不算强，但 prompt 进化、嵌入多样性筛选 等步骤被后续工作复用（演讲者评价）；DEITA README 在 6K SFT 数据预算 下 DEITA-7B 可优于 WizardLM-13B 部分指标——「不强」需结合基准与数据量理解，非全面落后。\nHF 为 SmolLM2 等训练 教育内容分类器（0=商业等，1=教材级）——David 称比纯 entropy/diversity 更重（演讲者观点）。SDG 迭代中用 下游 fine-tune 是否涨点 验证 pipeline 是否有意义（演讲者实践），因果需对照实验设计。\n常见误区：以单一 benchmark 分数否定整条方法链；忽视 逐步技巧 的可拆卸复用。图像 preference 管线（LMSYS 类 prompt → 复杂度进化 → FLUX 双图 → Argilla 二选一 → ~15k 行 → DPO/ORPO）为 嘉宾项目叙述；当前可见 Hub 仓库元数据为 n\u0026lt;1K、README 占位——~15k 未能用公开卡片证实，引用时宜标注未验证边界。中期 NSFW 渗透 prompt 与生成图，需 preference + 安全分类 + 人工扫尾（演讲者项目教训）。\nDEITA 与优化信号讨论：OCR 可见 ty Weaviate\nSDG 与分类器迭代讨论：OCR 可见 fey Weaviate\n约 8 分钟处：三分屏访谈，左侧 Weaviate podcast 墙牌，无幻灯片\nHub 数据栈：从生成到 SQL 筛选 # 为什么：合成数据若不能 版本化、可查询、可导出训练格式，就只是临时 JSON。\n机制/约束：HF Datasets Viewer SQL Console 由 DuckDB WASM 驱动，可从 Data Studio 过滤并导出 Parquet/CSV——verified。David 描述在 约 5 万–10 万 行规模用 SQL 做 近似近邻 式向量检索；远程 ANN 索引能力边界 以当时 Hub 为准（演讲者观点，2026 是否变化未复核）。\n怎么做：生成后 PushToHub → Data Studio 打开 → SQL 过滤（如 score_chosen - score_rejected \u0026gt; 0.3）→ 导出供 TRL / transformers / sentence-transformers 训练。\n常见误区：把 SQL Console 当作生产级 向量数据库；它擅长 批处理筛选与探索，不等价于在线 ANN 服务。\nData Studio / SQL 讨论段：OCR 片段 [jw / Weaviate ee cast\n约 30 分钟处：三分屏，主持人侧证书与 Weaviate podcast 标识\n约 40 分钟处：右侧嘉宾特写，暖色台灯背景，无技术图表\n收尾讨论：OCR 片段 e ) Weaviate € podcast\n未收敛的结论：几条仍开放的分歧 # 主题 常见做法 嘉宾强调 证据边界 Persona 是否承载知识 把 persona 当「角色扮演」增广 variety \u0026gt; 知识；文档才是知识 seed 与 Persona Hub 论文表述有张力 Paraphrase 评测集改写测鲁棒性 训练多样性另有 Persona/keyword 路径 演讲者观点 Prompt 优化产品化 Agent 内在线改 prompt HF 侧更重 自训模型 + 自有数据；Weaviate 关注 RAG optimization as a service 主持人/嘉宾观点 DSPy × distilabel 库内集成 离线优化 + 版本化注入即可 演讲者工程取舍 合成用于 pre-train 仅 post-train Llama 3、SmolLM2 等已用合成+分类过滤 Llama 3.1 卡片 partially verified 不必强行统一为一条「最佳实践」；目标函数（涨点、覆盖、安全、成本）不同，pipeline 组件就应不同。Agent 轨迹合成若缺少 工具名、参数与观测结果 的结构化字段，后期很难做 step-level 归因；评测侧若只 paraphrase 用户问句而不变 工具可用集与环境状态，测到的多是措辞鲁棒性，而非规划能力——两类需求应分 dataset schema 设计，而非共用一个 JSONL 模板。\n若你要落地 # 先写清 seed 类型：知识来自 文档/RAG corpus 还是 persona/paraphrase 只负责 分布打散；二者混用时在 metadata 里标注来源，便于失败回溯。 用正确论文与官方维度：UltraFeedback 以 四维反馈 为准；Persona Hub、DEITA 分别引用 2406.20094、2312.15685。 小规模验证再放大：500–1000 行（SDG 默认上限 1000）跑通 distilabel DAG + Hub 导出，再用 下游任务 metric（分类、RAG、SFT loss）决定是否加预算；preference 删除 分差过小 的样本。 Pipeline 耐久性单独选型：要跨失败续跑选 有状态 workflow（如 Transformation Agent 路线）；要 可复现、可缓存的批处理 选 distilabel + Ray/vLLM，并对照 Pipeline cache 文档与版本。 发布前安全与去重：图像/开放域文本合成假设 NSFW 与近重复 会出现；Persona 0.9 阈值与 DEITA Repr Filter 可作为默认起点，而非终点。 参考与延伸阅读 # Self-Instruct: Aligning Language Models with Self-Generated Instructions (arXiv:2212.10560) UltraFeedback: Boosting Language Models with High-Quality Feedback (arXiv:2310.01377) UltraFeedback 官方 README（GPT-4 四维标注说明） Scaling Synthetic Data Creation with 1,000,000,000 Personas — Persona Hub (arXiv:2406.20094) What Makes Good Data for Alignment? — DEITA (arXiv:2312.15685) DEITA 项目 README（含与 WizardLM 对比表） distilabel 文档首页 distilabel：Ray 与 vLLM 扩展指南 Synthetic Data Generator 公告博客 argilla/synthetic-data-generator Space FinePersonas v0.1 数据集卡片 Hugging Face Hub：Datasets SQL Console（DuckDB WASM） Llama 3.1 Model Card（合成微调数据与分类器筛选） ModernBERT 论文 (arXiv:2412.13663) TRL：偏好优化训练文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-synthetic-data-with-david-berenstein-and-ben-burtenshaw-weaviate-podcast/","section":"文章","summary":"合成数据：RAG、Agent 与评测里的「造数」边界","title":"合成数据：RAG、Agent 与评测里的「造数」边界","type":"posts"},{"content":" 检索列表多样化：几何后处理、评测裂缝与 RAG 上下文预算 # 向量检索把「相关」召回到一个候选池里，但相关不等于列表有用。电商要避免同款刷屏，学术搜索要覆盖子领域，RAG 要把有限 context window 分给互补段落——这些需求都指向同一类问题：在已有相关性分数下，如何从 Top-N 里选出内联差异足够大的子集。Pyversity 把 MMR、MSD、DPP、Cover、SSD 等贪心多样化算法封装为 NumPy 后处理层，挂在任意 Python 检索栈之后。本文按工程主题梳理机制与边界：文献与仓库实现可核对处给出链接；Springer Nature 生产路径、默认策略变迁、YouTube+DPP 等口述标为「演讲者观点」，并写明未核实处。\n分屏画面：主持侧墙饰 @@\u0026amp; Weaviate ABF podcast 标识；嘉宾 Thomas van Dongen；无算法幻灯片\n问题空间：多样化站在检索管线的哪一步 # 为什么：典型 RAG / 搜索管线是 query → 稠密或混合召回 →（可选）cross-encoder 重排 → 列表构造 → LLM。前段优化的是 query–document 相关性；多样化优化的是已选列表内部的冗余。若跳过这一步，Top-10 可能全是同一主题的近邻 chunk，agent 多跳检索也会反复读到语义重叠的段落（演讲者观点：冗余上下文会浪费 token 并干扰推理；与 Lost in the Middle 类「中间信息被忽略」现象属于不同机制，本期无对照实验）。\n机制/约束：多样化是 O(k) 步贪心，每步只对照已选子集与候选全集，不做全排列最优。输入为 embeddings[N,d] 与 scores[N]（相关性，可来自向量距离或 cross-encoder）；输出为长度 k 的索引重排。与离线 k-means 聚类不同：聚类要刻画全集结构，在线千 QPS 场景下全集二次扫描往往不现实（演讲者观点；k-means 单轮通常为 O(n·k·d)，非必然 O(n²)，但嘉宾用复杂度对比强调检索后处理的轻量性）。\n怎么做（最小示例）：\nimport numpy as np from pyversity import diversify, Strategy embeddings = np.random.randn(500, 256).astype(np.float32) scores = np.random.rand(500) # 可与 embeddings 异源 idx = diversify( embeddings, scores, k=10, strategy=Strategy.DPP, diversity=0.5, # [0,1]，越大越偏多样性；内部 λ = 1 - diversity ) 常见误区：把多样化当成「换 embedding 模型」。主流做法是先用最强相关性嵌入召回，再廉价 diversify（演讲者观点）；是否在训练目标里注入「天然分散近邻」尚无清晰路径（嘉宾称开放问题）。另一误区是把多样化与 query-time 聚类 / 语义 group-by 混为一谈：二者在「把相似文档归组」上直觉相近，但聚类通常面向全集结构发现，多样化面向已有分数的列表重排；嘉宾用延迟与是否只需已选子集增量来划界，而非否认概念相似（演讲者观点）。\n约 21 分钟段：画面 OCR 片段 le) O 10) fe) Q . Dy；双人谈话，讨论嵌入与 RAG 上下文\nMMR 与 MSD：边际距离 vs 全局散开 # 为什么：Maximal Marginal Relevance（Carbonell \u0026amp; Goldstein, SIGIR 1998）是工业界最熟悉的基线：每步在候选中选使「相关性 − 与已选最大相似度」最大的文档。参数在原文记为 λ（相关性权重）；Pyversity API 用 diversity ∈ [0,1]，内部 λ = 1 − diversity，写作时勿与原文符号混用。\n机制/约束：MMR 用 max 相似度惩罚，倾向推开与最近已选项；Max-Sum Diversification（MSD）对全部已选项累加距离，列表往往更「均匀散开」。README 给出二者同为 O(k·n·d)；合成基准下 10k 候选、k=10 时 MMR/MSD/DPP 均在 15ms 量级（benchmarks/README，硬件未固定，非生产 SLA）。\n怎么做：strategy=Strategy.MMR 或 Strategy.MSD，diversity 从 0.3–0.5 扫一遍；Weaviate Query Agent 的 Search 模式内置 MMR 重排，通过 diversity_weight（0–1）调节。\n常见误区：（1）以为 MMR 与 MRR（Mean Reciprocal Rank）是同一缩写。（2）播客录制时嘉宾称「包默认 MMR、处方先试 DPP」——当前 main 分支默认已是 Strategy.DPP（2026-05 核对）；写落地文档应以仓库版本为准。（3）把 diversity=0 当成「关闭多样化」后仍期望列表顺序与纯分数排序完全一致——实现上仍走贪心框架，应以对照实验确认行为。\n画面含 g®\u0026amp; Weaviate 8@f podcast 品牌条；约 22 分钟算法对比讨论段\nDPP：体积最大化与默认策略之争 # 为什么：Determinantal Point Process 在嵌入空间里最大化选中集合的「体积」（核矩阵行列式 / 贪心 residual variance），同时受向量长度与夹角约束，比单一余弦距离惩罚更有几何解释。Chen et al. 给出快速贪心 MAP，摘要写明在推荐场景做过 online A/B——支持「多样化可用产品实验验证」这一方法论，不等于 已公开证实「YouTube 首页」采用 DPP（嘉宾口述 Google YouTube 案例，本期未给出论文题名，未核实）。\n机制/约束：Pyversity 复杂度 O(k·n·d + n·k²)，n 很大时常数高于 MMR/MSD。作者 README 将 DPP 标为 Recommended default；合成 benchmark 上 DPP 在 ILAD/ILMD 上通常优于 MMR/MSD（指标定义见下节）。\n怎么做：无先验时 strategy=Strategy.DPP，diversity=0.4 起调；与 MMR 并排 A/B 看业务 CTR / 满意度，而非只看离线 nDCG。运行时仅依赖 NumPy，可嵌在现有 Python rerank 服务中（仓库 pyproject.toml 已核实）。\n常见误区：把 DPP 当成「比 MMR 慢一个数量级」——同阶贪心下差异主要在 n·k² 项；在 n≈数百、云向量库 RTT 占主导时，嘉宾与主持人倾向认为 +1ms 级多样化不如优化网络与初召回（演讲者观点）。\n约 8 分钟：Weaviate podcast 分屏，嘉宾手势说明 DPP 与 MMR 差异；无公式画面\nCover 与 SSD：慢路径覆盖与会话序列 # 为什么：科学文献搜索需要子领域覆盖（CV / NLP / 传统 ML 等），而不只是去掉重复标题（演讲者观点）。Coverage-based diversity（RecSys 2016）与 Pyversity 的 Cover（facility location）在「代表全集」目标上与 k-medoids 直觉相近，但实现名是 coverage 贪心而非字面 k-medoids。\n机制/约束：Cover 先算 pairwise 相似度，复杂度 O(k·n²)；README 建议 n≈10k 以上考虑 GPU/异语言重写。嘉宾称 ~10k 结果规模时优化 Cover 才值得，日常搜索 fast path 用 DPP/MMR/MSD（演讲者观点）。\nSSD：Sliding Spectrum Decomposition 为序列感知贪心（滑动窗口 Gram-Schmidt），适合 feed 或带 recent_embeddings 的会话 RAG；benchmark 中列为 niche，本期口述未展开机制细节。\n怎么做：文献探索、目录整理类任务 strategy=Strategy.COVER，缩小候选池再跑；会话场景试 Strategy.SSD 并传入近期已读向量。\n常见误区：用 Cover 处理百万级实时召回全集——应先截断到 rerank 窗口（如 Top-200）再 Cover。\n约 32 分钟：OCR 含 Ga \u0026gt; 0 ore) Do cae fol；谈话镜头，对应 Cover / k-medoids 讨论\n评测：BEIR、ILAD 与 FreshStack 量的是不同东西 # 为什么：选型若只扫 MTEB / BEIR 的 nDCG@k、Recall@k，衡量的是相关性排序，不是列表内差异。在 BEIR 式任务上对 diversify 后列表跑同一指标，往往难涨分——因为优化目标已从「单点相关」变为「列表结构」（演讲者观点；Pyversity 自有 CF benchmark 显示 DPP 可 +nDCG，属特定合成设定，不可外推为 BEIR 普适结论）。\n机制/约束：\n指标 来源 含义 边界 ILAD Pyversity metrics.py 列表内配对平均 (1 − cos sim) 基准套件内标准名；更广 IR 文献统一性未逐篇核对 ILMD 同上 列表内最小 (1 − cos sim) 同上 coverage / α-nDCG FreshStack nugget 覆盖、带权 nDCG 需主题/nugget 标注；不是 ILAD nDCG@k BEIR/MTEB 相关性 不评 diversity FreshStack 面向技术文档 RAG：社区问答 → nugget 抽取 → 评检索是否覆盖答案块；与 MTEB 上同名 FreshStackRetrieval 代码检索任务不是同一套框架（核对报告已标注）。\n怎么做：离线先用 ILAD/ILMD 做算法对比与 diversity 扫参；有 nugget 标注时加 FreshStack coverage；生产以 A/B（有/无 diversify）测 CTR 或满意度为准（演讲者观点；Chen et al. 2017 摘要支持 A/B 方法论，Springer 管线尚未对多样化做 A/B，统一 benchmark suite 未发布）。\n常见误区：用 BEIR 涨分证明多样化无效；或把 FreshStack coverage 写成 ILAD。嘉宾称「漂亮的 recall / nDCG / ILAD 曲线」仍是代理，唯一可靠验证是线上 A/B（演讲者观点）——这与推荐系统多样性文献的一般做法一致，但不意味着离线指标无用：离线用于筛算法族与超参，线上用于验证业务假设。另：BEIR 各子集 qrels 可含多条相关文档，更准确的说法是「主流榜不衡量列表内差异」，而非「每条 query 只有一个 relevant」。\n约 16 分钟：Weaviate podcast 分屏，评测与 FreshStack 讨论时段\n约 41 分钟：OCR 含 Time \u0026gt; 0 re) Go 23；A/B 与 benchmark 计划谈话段\nRAG、Agent 与「几何 vs LLM」多样化 # 为什么：RAG 与 multi-hop agent 的上下文窗口有限；Pyversity README 将动机写明为 Avoid feeding the model near-duplicate passages。首轮检索若广覆盖、低重复，后续 hop 才有信息增益空间（主持人场景 + 嘉宾认同用例方向）。\n机制/约束：多样化仍是检索列表后处理，不等于 agent 工作流本身。Weaviate Query Agent 提供 Ask 与 Search；播客口语中的 think 模式在 2026-05 核对的公开文档中未出现（可能为口语或旧称）。Search 的 diversity_weight 即内置 MMR。\n用 LLM 直接挑选「多样文档集」缺乏几何保证、延迟与成本高（演讲者观点），类比「用 LLM 做 rerank 可以，但专用 cross-encoder 往往够用」。embeddings 与 scores 可来自不同模型——scores 须准确反映 query–doc 相关性（演讲者观点）；multi-vector（ColBERT / MaxSim）可投影为单向量再套用算法，专门多样化 multi-vector 的文献嘉宾称未找到（边界未验证）。\n怎么做：召回 Top-50 → cross-encoder 打分 → Pyversity DPP 取 Top-10 → 拼 prompt；与 Query Agent 内置 MMR 二选一，避免重复 diversify。\n常见误区：在 agent 每一跳都堆相似 chunk「提高召回」；或把 Voyage-3-large（偏相关）与 Arctic 2.0（偏多样）分工当成稳定规律——嘉宾称未观察到稳定模式（演讲者观点）。无多样化时加随机扰动有时也能满足探索需求，但方差大、不可复现；多样化是更可控的替代（演讲者观点）。电商场景「多样」常指去同款重复、同色可并存；科学搜索则更接近 serendipitous discovery（主持人转述 Pierce 概念）——同一套算法参数很难同时最优，应按产品语义选策略族。\n画面 OCR：@@\u0026amp; Weaviate ABF podcast；嘉宾介绍 Pyversity 与 Springer 背景时段\n约 56 分钟：ec » Weaviate 6B podcast；收尾讨论生产验证与学术搜索\nOCR：\u0026gt; Weaviate Sf podcast w, Nilatlte tay；电商 vs 文献搜索「多样」语义差异段\n若你要落地 # 默认路径：初召回用当前最强相关性嵌入 + reranker；在 Top-200 上跑 DPP（diversity 0.35–0.55 网格），别跳过扫参直接沿用 MMR 习惯。 参数与符号：统一使用 Pyversity 的 diversity（越大越多样），勿与 MMR 原文 λ 方向搞反。 评测分层：离线 ILAD/ILMD 筛算法；有 nugget 时加 FreshStack coverage；上线用 A/B 看业务指标，BEIR nDCG 仅作相关性 sanity check。 场景分流：商品去重 → MMR/MSD 往往够用；文献/探索式搜索 → 缩小池后试 Cover；会话 feed → 评估 SSD。 性能预算：n≤1e4、d≤768 时 Pyversity 纯 NumPy 通常不是瓶颈；先压网络与初检索，再考虑 Rust/GPU（演讲者观点）。 参考与延伸阅读 # MMR 原论文 — SIGIR 1998 Max-Sum Diversification — arXiv:1203.6397 DPP 机器学习教程 — arXiv:1207.6083 快速贪心 DPP MAP 与在线实验 — arXiv:1709.05135 Coverage-based 推荐多样化 — RecSys 2016 SSD 滑动谱分解 — arXiv:2107.05204 Pyversity 仓库与 API Pyversity PyPI 包 Pyversity 合成延迟说明 FreshStack 论文 — arXiv:2504.13128 FreshStack 代码与评测 BEIR 基准与评测指标 MTEB 排行榜 Weaviate Query Agent 文档 Query Agent Search 与 diversity_weight Lost in the Middle — 长上下文位置偏差 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-pyversity-with-thomas-van-dongen-weaviate-podcast-132/","section":"文章","summary":"检索列表多样化：几何后处理、评测裂缝与 RAG 上下文预算","title":"检索列表多样化：几何后处理、评测裂缝与 RAG 上下文预算","type":"posts"},{"content":" 检索嵌入的工程取舍：从 Arctic Embed 看榜单、训练与生产约束 # 向量检索的质量，往往在「换 embedding」这一环就被决定大半——至少 Snowflake 与 Weaviate 两侧工程师在公开讨论里都把话说得很直。本文围绕 Snowflake Arctic Embed 系列（含 Arctic Embed 2.0 技术报告）梳理：榜单该怎么读、对比学习预训练与 fine-tune 各自解决什么、Matryoshka 表征学习（MRL）如何落到十亿级索引，以及 多语基准与专有分布之间的裂缝。数字优先对齐 arXiv 与官方 README；内部基准、成本估算与部分体感结论标为「演讲者观点」，并标明无法独立复现的边界。\n分屏画面：Luke Merrick, Snowflake；Puxuan Yu, Snowflake；Connor Shorten, Weaviate；Charles Pierse, Weaviate（无技术幻灯片，仅为讨论现场）\n问题空间：RAG 里 embedding 站在哪一环 # 为什么：检索增强生成（RAG）与 agent 工具链里，常见路径是 query → 稠密（或混合）检索 → 重排 → 上下文拼装 → LLM。embedding 把文本压成固定维向量，索引侧用 HNSW 等近似最近邻结构做召回。一旦向量语义与业务问法错位，后面的 cross-encoder 重排只能「在错误候选里排序」。\n机制/约束：Snowflake 将 Arctic Embed 用于自研 Cortex Search 并与开源权重对齐；Weaviate 在 Cloud Embeddings 模型列表 中提供 Snowflake/snowflake-arctic-embed-m-v1.5、Snowflake/snowflake-arctic-embed-l-v2.0 等托管选项。产品侧动机是：第三方 embedding API 的限流、维度与计费模型，与向量库索引参数强耦合。\n怎么做（最小示例）：在 Weaviate 中指定 vectorizer 为托管 Arctic 模型时，collection 的 vectorIndexConfig 维度须与模型输出一致；若计划用 MRL 截断，需在入库与查询两侧约定同一前缀维（见下文）。\n常见误区：把「换更大 LLM」当成检索质量主杠杆。演讲者观点（Snowflake 内部早期搜索调参）：调 synonym、融合、reranker 有收益，但 换 embedding 的影响往往最大——该主张 无法 用公开 Cortex Search A/B 表核实，宜作假设而非定理。\n约 6 分钟处：Luke Merrick, Snowflake / Puxuan Yu, Snowflake / Weaviate podcast 分屏，讨论预训练范式\n榜单素养：MTEB、BEIR 与 MIRACL 各量什么 # 为什么：选型时工程师常扫 MTEB leaderboard。MTEB 覆盖分类、聚类、检索等八类任务；全任务平均分 与「你的系统是否只做 passage retrieval」并不等价。Snowflake 团队反复强调：若业务是 BEIR 式同任务检索，应盯住 MTEB Retrieval（常记为 MTEB-R）上的 nDCG@10，而非平均分。\n机制/约束：\n基准 典型指标 分布特点 读榜时注意 BEIR nDCG@10 等 异质零样本 IR，跨域 最初为证跨域泛化；社区现常混用 MS MARCO、NQ 等训练集（演讲者批评：易变成「在测集上拟合」） MTEB-R nDCG@10 与 BEIR 有重叠的检索子集 Arctic 论文主报此列 MIRACL nDCG@10，18 语 Wikipedia 段落 2.0 作者自述训练亦用到 MIRACL 相关数据；不宜 当唯一多语真理 怎么做：评估脚本对齐论文口径——例如 Arctic Embed 2.0 Large 全维 MTEB-R 0.556，256 维截断 0.547（Table 1，约 98.4% 保留）。口语里的「98% recall」在文献中实为 MTEB-R nDCG@10 相对全维分数的保留率，不是 recall@k。\n常见误区：\n用 MIRACL 18 语均分选模型，却部署在新闻、工单、代码库等 域外 语料——2.0 在 CLEF 等域外集上相对 MIRACL 的差距，论文用语是 suggesting potential overfitting，非指控作弊。 1–2 分的榜单差距未核对 prompt 模板、池化、归一化 是否与论文一致（演讲者观点：apples-to-oranges 很常见）。 画面字幕条：Luke Merrick, Snowflake；Puxuan Yu, Snowflake；Weaviate（嘉宾标识，非结果表）\n预训练：E5 式对比学习与 batch 工程 # 为什么：通用句向量若只做 token-level MLM，检索对齐不足。E5 路线用弱监督 InfoNCE + in-batch negatives 在 query–document 对上预训练；Arctic 1.0/2.0 自述 closely replicates 该范式，README 称约 4 亿 预训练样本对。\n机制/约束（可核对部分）：\nBatch size：2.0 报告 32,768，32×H100 数据并行（§2 / Appendix）；访谈口语「约 32k」与文献一致。 Source stratification（1.0 §4.4）：每个 minibatch 仅含单一数据源；Table 5 在 batch 16,384 下 stratify Yes → 46.96 vs No → 43.74 nDCG@10（MTEB-R 子集）。2.0 将 不同语言子集 也视为 source。文献引用 prior work 亦有 single-source minibatch——宜称「Arctic 系统 ablation」，而非全球首创。 负样本：预训练主要靠 大 batch 随机 in-batch negatives（演讲者观点）；精细 hard negative 策展主要在 fine-tune 阶段才关键。 怎么做（概念级）：复现实验时先固定 stratify 开关与 batch 规模，再谈换 backbone；聚类式预训练另有 预印本 2407.18887，但主报告认为收益小于 stratify。\n常见误区：把访谈中「4k→8k→16k 持续提升」直接当作 2.0 正文曲线——1.0 Table 5 有 4,096 vs 16,384 两点；完整三点曲线 未 在已公开正文找到，更可能来自内部或对 E5/BGE 的转述。Modal 整段预训练 约 2,000 美元 为 访谈估算，无法复核。\n约 10 分钟：Luke Merrick, Snowflake / Puxuan Yu, Snowflake / Connor Shorten, Weaviate 分屏\nFine-tune 2.0：Teacher、假负样本与 curriculum 的负结果 # 为什么：预训练给出通用几何；领域检索常靠 fine-tune 拉近 query–正例、推远 hard negatives。\n机制/约束（2412.04506 §2.4，已核实）：\nTeacher 模型：英语用 stella-en-1.5B-v5（HF: dunzhang/stella_en_1.5B_v5），对比较弱 gte-large-en-v1.5；多语侧用 multilingual-e5-large 等。 假负样本过滤：沿用 NV-Retriever（勿与 NV-Embed 混淆）——候选负例若得分 高于已知正例的某百分比阈值（Figure 2 扫描 95%–99%）则丢弃，避免把真相关文档当负例训练。 Curriculum learning：正文写明 random data ordering produced comparable or better results；与 1.0/1.5 时代「课程式 hard negatives 约 +0.5 MTEB-R」形成反差。 演讲者观点：换更强 单一 teacher 的收益，可比拟 fine-tune 阶段「10× 缩放定律」量级；相对 NV-Embed 式多 teacher ensemble，团队选 运维简单的单 teacher（ensemble 增益在对方论文中亦有限——对比性陈述，非本文复现）。\n怎么做：自建 fine-tune 管道时，至少实现 (1) teacher 打分挖掘 negatives，(2) 按 known-positive 分数比例过滤假负例，(3) 用固定随机序作 ablation baseline，再考虑 curriculum。\n常见误区：在 2.0 上继续堆 curriculum，却未同步升级 teacher 与阈值——可能白做工。引用硬负样本流程时请链 NV-Retriever (2407.15831)。\n字幕：Luke Merrick, Snowflake；Puxuan Yu, Snowflake（约 49 分钟 hard negative 讨论段）\nMRL 与生产：维度、内存与指标口径 # 为什么：Matryoshka Representation Learning (MRL) 训练时强制向量 前缀子向量 在各截断维下仍可用，以便在索引侧用 256 维 换 约 4× 存储与 HNSW 内存，而尽量保留排序质量。\n机制/约束：\n2.0：MRL 贯穿 contrastive pretraining 与 finetuning 优于仅在 fine-tune 加 MRL；后者又优于「无 MRL 直接截断」（§2.5）。Large 模型 768→256 维保留约 98% MTEB-R nDCG@10；对比 Google text-embedding-004 截断后约 94%（2.0 正文引用第三方，非直接复刻 Gecko 全文口径）。 Weaviate Resource Planning：HNSW 索引须驻内存；vectorCacheMaxObjects 限制缓存向量数。十亿规模下，用 7B 级 embedding 换 2× 质量、却吃掉内存预算，往往不如 MRL 降维 + 可选 PQ 重打分（演讲者观点：全精度向量有时仍须保留，因不知查询将用哪段前缀维）。 演讲者观点（未公开表）：MRL 截断后 recall@k 掉得慢、nDCG@10 掉得多——「低维仍找得到，但排序更噪」。论文 未 用 recall@k 对照，写作时应分开表述。 怎么做：索引维数 d_index=256，查询与文档 同一截断；若启用量化索引，可对候选集做磁盘重打分，再回全维向量精排（产品实现因栈而异）。\n常见误区：把「98% 保留」理解成 recall@10 不变。另：在 仅完成约 30% 计划预训练 后就做 MRL fine-tune 会变差——属 Puxuan 开发经历（访谈观点）；文献方向一致但无「30%」公开表。\n约 20 分钟：Luke Merrick, Snowflake / Puxuan Yu, Snowflake / Weaviate podcast（MRL 与维度讨论时段）\n字幕：Luke Merrick, Snowflake；Puxuan Yu, Snowflake；Weaviate podcast；Charles Pierse, Weaviate\n多语：迁移、过训练与「omnilingual」幻觉 # 为什么：企业语料常混合英、中、日、西语；单模型多语可避免「英语一套、多语一套」的运维分裂。行业默认多语会 牺牲英语 MTEB；Arctic Embed 2.0 报告英语 MTEB-R 与强英语基线接近（Table 1：2.0-M 0.554 vs 1.0-M 0.549），但团队自述 「为何单模型不伤英语」仍未完全弄清（访谈观点）。\n机制/约束（部分可核对）：\nFigure 3 / §4.1：对比预训练步数增加时，未出现在预训练语料的语言（如 zh、ja）在 MIRACL 上可出现 负向跨语迁移（例：zh finetune 后 nDCG@10 相对变化 -13.3% 等，以论文图注为准）。结论句：prolonged contrastive pretraining does not always enhance cross-lingual transfer。 Figure 4：向预训练 加入中文数据可提升英语 MTEB-R——针对的是「非目标语 over-training」风险，而非「中文必然伤害英语」。 MIRACL vs 专有集：Puxuan 称团队有 未公开 训练外评测集，观察到多种开源多语模型 MIRACL 很强、专有集明显变差（不可验证）。Luke 缓和：不宜直接指控过拟合，但 Wikipedia 分布与 Gecko 等商业模型对比时，~100M 开源档在 MIRACL 均分上可显著更高，而英语 MTEB-R 可能落后 2–5 分（访谈 + 第三方表，未在本环境复算 Gecko 全表）。 怎么做：客户仅中英时，应用 客户域 hold-out 集评估，而非只报 MIRACL；若预训练语料不含日语，应监控日语 query 是否因 过长预训练 退化。演讲者估计「预训练时长可减半仍 pretty decent」——无文献比例结论。\n常见误区：把 multilingual 当成 omnilingual；MIRACL 18 语均分不能代表工单、法规、代码注释等分布。\n约 28 分钟：Luke Merrick, Snowflake / Puxuan Yu, Snowflake / Charles Pierse, Weaviate（多语与评测讨论）\n约 24 分钟：Weaviate podcast 分屏，Charles Pierse, Weaviate / Connor Shorten, Weaviate\n架构分岔：单向量 HNSW vs ColBERT / SPLADE / 重排 # 为什么：多向量（ColBERT）、稀疏（SPLADE）与 cross-encoder 重排在理论上可抬高上限，但带来存储、延迟与工程复杂度。\n机制/约束（多为访谈观点，无 Arctic 公开消融表）：\nSnowflake Cortex Search 面向 大规模单向量 + HNSW；Luke 称 ColBERT 更强但 实现与存储成本高。 在已有强 dense 神经检索后，再叠 SPLADE 的边际收益 常不明显（立项前内部实验，未公开）。 Weaviate 路线：模块化 向量库 + 可选托管 embedding，规避单一黑盒搜索栈。 怎么做：默认路径选 单向量 + 可选 cross-encoder 重排；仅在召回率瓶颈且团队能承担索引复杂度时引入 ColBERT 类方案。\n常见误区：榜单 7B 级 embedding 直接上生产——eval 泄漏、推理成本与 HNSW 内存可能不划算。演讲者「hot take」：若预训练就能达到 fine-tune 级 hard negative 质量，7B 训练作业可能显得浪费——强烈主观，勿当共识。\n字幕：Weaviate podcast；Luke Merrick, Snowflake；Puxuan Yu, Snowflake；Charles Pierse, Weaviate\n约 40 分钟：Luke Merrick, Snowflake / Puxuan Yu, Snowflake / Connor Shorten, Weaviate 分屏\n信任边界：「懂模型」还是「懂提供方」 # 为什么：开源权重与托管 API 是否一致，影响 RAG 供应链审计。\n机制/约束：Snowflake 宣称 Cortex Search 与开源 Arctic Embed 共用同一模型族；Weaviate Cloud Embeddings 列表含 Arctic 型号。Charles 一侧论点：提供方 自用同一 embedding 做生产检索，比单看 MTEB 分数更能建立信任——属 产品伦理与组织行为，非可复现定理。\n常见误区：只看 leaderboard 选模型，不读 训练数据与评测集重叠（2.0 对 MIRACL 的自述即一例）。\n字幕：Luke Merrick, Snowflake；Puxuan Yu, Snowflake；Weaviate podcast；Connor Shorten, Weaviate\n若你要落地 # 评测对齐业务：以 MTEB-R / 自有域 nDCG@10 为主指标；若有多语，加 域外 集（新闻、工单），勿仅用 MIRACL Wikipedia。 维度与索引一并设计：若采用 Arctic 2.0 Large 的 256 维 MRL 前缀，在 Weaviate（或其它 HNSW）入库前固定 d，并核对 HNSW 内存规划；口语「recall 保留」请改写为 MTEB-R nDCG@10 保留率。 Fine-tune 抄作业顺序：teacher 挖掘 → NV-Retriever 式假负例过滤 → 随机序 baseline；在 2.0 设定下 不必 优先 curriculum。 多语预训练监控：对 未进预训练语料的语言 画学习曲线，防止「训越久越好」；必要时缩短 contrastive pretrain（访谈建议）并以客户域验证。 托管 vs 自托管：用 Weaviate Embeddings 模型页 或 Snowflake-Labs/arctic-embed 锁定版本与维度；变更模型时 全量重嵌入，勿假设旧向量可混用。 参考与延伸阅读 # Arctic-Embed（arXiv:2405.05374） — 1.0 技术报告：对比预训练、source stratification、batch ablation Arctic-Embed 2.0（arXiv:2412.04506） — MRL、多语迁移、teacher 与假负例、curriculum 负结果 Snowflake-Labs/arctic-embed（GitHub） — 权重、推理示例与 README 训练规模 MTEB: Massive Text Embedding Benchmark（arXiv:2210.07316） — 任务族与 leaderboard 定义 BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation（GitHub） — 零样本 IR 基准包 MIRACL 多语检索项目站 — 18 语 Wikipedia 检索说明 Text Embeddings by Weakly-Supervised Contrastive Pre-training（E5, arXiv:2212.03533） — Arctic 预训练范式来源 Matryoshka Representation Learning（arXiv:2205.13147） — MRL 原始方法 NV-Retriever: Improving Text Embedding Models with Effective Hard-Negative Mining（arXiv:2407.15831） — 假负例阈值策略 NV-Embed: Improved Techniques for Training LLMs as Generalist Embedding Models（arXiv:2405.17428） — 与 Retriever 论文区分阅读 Gecko: Versatile Text Embeddings Distilled from Large Language Models（arXiv:2403.20327） — 商业嵌入与截断对比语境 Embedding And Clustering Your Data（arXiv:2407.18887） — 聚类式预训练探索（非主路径） Weaviate 向量索引概念 — HNSW 与索引类型 Weaviate 资源与 HNSW 内存 — 「索引须驻内存」原文 Weaviate Cloud Embeddings 模型列表 — 含 Snowflake Arctic 型号 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-arctic-embed-with-luke-merrick-puxuan-yu-and-charles-pierse-weaviate-pod/","section":"文章","summary":"检索嵌入的工程取舍：从 Arctic Embed 看榜单、训练与生产约束","title":"检索嵌入的工程取舍：从 Arctic Embed 看榜单、训练与生产约束","type":"posts"},{"content":" 结构化输出：从「能解析的 JSON」到 logit 级约束生成 # RAG 管道、工具调用、评测脚本和 compound agent 都有一个共同痛点：下游代码需要可验证的结构，而 LLM 默认输出是开放文本。常见补救是 prompt 里写「只返回 JSON」、再用正则或二次模型抽取——这在 demo 里够用，在生产里会把失败推迟到 json.loads 或业务校验层。另一条路径是在解码每一步限制合法 token，使输出在生成过程中就落在 grammar / schema 定义的集合里；OpenAI Structured Outputs 与 Outlines 代表的产品层与开源实现层，常被混称为同一概念，但机制并不相同。\n下文把 structured outputs（应用目标：可解析、可入库）与 structured generation（实现：logit 掩码 + 有限状态机）分开讨论；机制段以 Efficient Guided Generation（arXiv:2307.09702） 与 Outlines Core 公开设计为准，性能与评测争议处标注可核对边界与演讲者观点（dottxt 联合创始人 Will Kurt、Cameron Pfiffer 在 Weaviate Podcast 上的讨论，未在本环境复现其内部 benchmark）。\n图：节目标题卡可见 Structured Outputs、#119、Will Kurt / Cameron Pfiffer / Connor Shorten 与 .TXT / Weaviate 标识\n为什么需要「生成时」约束，而不只靠事后解析 # 为什么：Agent 编排、向量库写入、函数调用参数传递，都假设字段名、类型、嵌套关系稳定。若只在生成后解析，模型仍可能输出合法自然语言但非法 JSON；修复往往再调一次 LLM 或手写 parser，延迟与成本叠加。OpenAI Cookbook 对 Structured Outputs 的说明强调 API 层对 JSON Schema 的遵守；自托管栈则多在推理引擎内挂 logits processor（演讲者观点：Cameron 将最松情形仍视为 regex .* 级别的约束，哲学上不存在真正的「无结构生成」——此说法未形式化证明，宜作概念框架）。\n机制/约束：约束解码在每一步根据已生成前缀计算允许 token 集，对其余 logits 置 (-\\infty) 再采样；Outlines Logits Processors 文档描述为对 logits 施加 mask。与「生成完再校验」相比，失败模式从「解析异常」前移到「无法进入非法分支」——但若 schema 与任务语义不匹配，仍可能得到语法合法、语义错误的输出。\n怎么做（最小示例，概念层）：\n# 伪代码：每步 allowed = index.allowed_tokens(state); logits[~allowed] = -inf 常见误区：把「模型说了 JSON」等同于 structured generation；prompt 约束不保证 token 级合法。另一误区是认为约束一定更慢——见后文 coalescence 与 Willard et al. 2023 的 little overhead 定性表述（无统一微秒级对照表）。\n图：约 8 分钟处三路视频画面，右侧嘉宾佩戴耳机发言，左下可见 Weaviate podcast 背景字\n有限状态机：regex、grammar 与 Index # 为什么：JSON Schema、工具参数、分类标签都可编译为可判定的 token 序列集合；在 BPE 词表上跟踪「当前处于 automaton 哪一状态」，才能高效算 allowed_tokens。\n机制/约束：公开实现路径大致为：正则或 grammar → Index（outlines-core README 称 finite-state automation / DFA）+ Vocabulary（tokenizer 与词表对齐）；每步 next_state 后取允许 token。vLLM backend_outlines.py 通过 Guide.write_mask_into 写入 mask。演讲者观点（Cameron）：实现上可能经 NFA 再确定化；当前 README 未逐步写出 NFA→DFA，标为 partially verified。CFG 表达力高于正则；Outlines Output Types 支持 Lark CFG，工程上常对 JSON 等结构做 regex 近似或有界 unroll（递归深度上界节目未给出）。\n怎么做：用库侧 regex / json_schema / CFG 构造 Index，在自托管引擎注册 logits processor；勿手写「禁止出现 }」类 ad-hoc 规则替代 FSM。\n常见误区：以为禁止某个字符（如数字 0-9）等于禁止数值能力——演讲者观点（Cameron，演示级）：模型可能改用 Unicode 上标等路径，无系统级 benchmark 佐证（P09，unable to verify）。另一误区：CFG 等于「任意 JSON Schema 精确表达」——Coalescence 博客指出并非所有合法 JSON schema 都能用单一正则表示，需工程折衷。\n图：画面 OCR 可见 ast podc 与 Weaviate 播客角标，无技术幻灯片 API 名\n图：约 7:57 主持人持杯、左下 Weaviate podcast 标识，嘉宾分屏讨论约束解码\nSchema 设计：先固定字段，还是让模型推断 # 为什么：生产系统要可测试、可版本化的契约；RAG 抽取、工单分类、10-K 字段映射通常业务方先知道列名。\n机制/约束：固定 JSON Schema 或 Pydantic 模型 → 编译为 FSM；动态 function calling 可从 Python 签名生成运行时 schema（演讲者观点，Will）——介于 rigid 与 fully inferred 之间。演讲者观点（Cameron）：多数场景应先定 schema 再写下游；「先看数据再让模型提议 schema」可行但波动大，强用例未见。Will 补充可用强模型生成 regex 再喂 Outlines——实验性。\n怎么做：\nfrom pydantic import BaseModel class Ticket(BaseModel): summary: str department: str ticket_id: str # outlines / vLLM: response_format 或 grammar 绑定该 schema 常见误区：schema 越细越好。Last-letter 类任务上，演讲者观点（Will）：把 chain-of-thought 模板硬编码进 schema 优于完全无结构，但劣于只约束最终答案、思考字段放开——说明「结构强度」本身是超参。另一误区：忽略字段间依赖；单次 JSON 内多字段可让后续键「看见」前文（见下文复合任务），与「每个字段一次 API」不同。\nCoalescence：确定性片段与加速声称 # 为什么：JSON 中大量 token 高度可预测（如 { 后接 \u0026quot;）；若每步都调用完整采样，算力花在低熵片段上。\n机制/约束：Coalescence: making LLM inference 5x faster（Will Kurt）描述：在 FSM 判定为确定性的片段跳过采样，直接推进状态；基于 regex 与 FSM 等价性。演讲者观点（Will）：约 2–3×+；博客标题与正文写 约 5×——二者未在本环境对照 benchmark，正文应并列引用并标边界。\n怎么做：依赖支持 coalescence 的 Outlines / 推理栈版本；不可假定所有 vLLM 构建默认开启。\n常见误区：把 coalescence 与「约束必然降延迟」划等号；加速取决于 schema 可预测片段占比。另一误区：与 token 粒度假说混谈——演讲者观点（P10）：dog 单 token 与 D+O+G 路径概率可能不同，偏小 token 偏好「有论文」但篇名未给出（unable to verify）；coalescence 偏「更长路径」的预研未发表。\n图：OCR 片段 ast © 2 Re} = 8 = podca ®，画面为播客分屏无架构幻灯片\n推理栈集成：vLLM、xgrammar、Outlines Core # 为什么：约束解码必须在与词表对齐的推理进程内执行；否则 mask 与 tokenizer 不一致会导致非法或死锁。\n机制/约束：vLLM Structured Outputs 支持多后端；StructuredOutputsConfig.backend 含 auto、xgrammar、guidance、outlines 等。sampling_params.py 中 auto 常先尝试 xgrammar，失败时回退 guidance 或 outlines（非一律 Outlines）。演讲者观点（Cameron）：需要 inline regex 等中等特性时回退 Outlines；vLLM 捆绑「旧 Python Outlines」——与当前 main 使用 outlines_core（Rust） 的叙述部分冲突，以部署版本 README 为准。\n怎么做：生产环境显式指定 backend 并锁定 vLLM / outlines-core 版本；对 DeepSeek-R1 等推理模型，vLLM Reasoning Outputs 表明可对答案段做 json/regex 结构化——演讲者观点（Cameron）：整段 JSON 封死 think 块会损害推理，宜保留 think 自由文本、仅约束最终答案（机制合理，gist URL 未验证）。\n常见误区：假设 API 托管 Structured Outputs 与自托管 logits 约束可互换评测。另一误区：忽略 xgrammar 与 Outlines 的 schema 特性差异导致 silent fallback。\n图：约 24 分钟三路讨论，左下 Weaviate podcast 字牌与书架背景\n图：OCR 可见 a. 25 qg2 G5 Oo SS or ‘\u0026amp;] 等叠字，画面仍为播客访谈\n质量争议：格式约束是否损害推理 # 为什么：选型需要回答「加结构会不会掉点」；若结论依赖非对等评测，工程决策会偏。\n机制/约束：Let Me Speak Freely?（arXiv:2408.02442）（Tam et al.）摘要写明：格式限制下 LLM 推理能力显著下降；GSM8K 等任务上自由文本优于受限格式（文献/文档支持）。论文 §3.3 对非结构化分支用 LLM 抽取最终答案（附录选 Claude 等作抽取模型）——文献/文档支持。演讲者观点（Will）：与 dottxt 在 GSM8K 上的 eval 不符，怀疑 prompt、解析路径、二次 LLM 成本不对等；认可论文「手写 parser vs 约束解码」工程角度。同一模型「请输出 JSON」vs 不用 Outlines：有的崩盘、有的变好（演讲者观点）——说明评测应固定 prompt × 模型 × 解析器 三元组。\n怎么做：复现争议时同时报告：约束解码栈、schema 字符/ token 预算、是否用外部 LLM 解析。演讲者观点（Will，P07）：GSM8K 结构化推理步字符上限过紧时先逊于 baseline，放宽后可超 baseline——无公开表格，unable to verify。\n常见误区：用单一论文否定所有 constrained decoding。另一误区：用 Berkeley Function Calling Leaderboard 的格式细节（如 float 必须 .0）作为主论据——演讲者观点称存在不公平，本轮未在 BFCL 源码定位该规则（标为访谈观点直至定位）。\n图：约 40 分钟嘉宾正视镜头，深色背景分屏，讨论评测与分布不匹配\n图：OCR 可见 p@\u0026amp; Weaviate See podcast 播客角标\n分布不匹配与「一次生成多字段」 # 为什么：直觉认为互联网语料不像 JSON，加约束会 OOD；嘉宾反驳称邮件、推文、标签等本身有结构，问题在约束强度与任务匹配（演讲者观点，共识级）。\n机制/约束：真正风险是 mask 掉高概率 token 路径导致局部分布偏移——研究缺口（演讲者观点）；DeepLearning.AI 课程示例称强制 { \u0026quot;name\u0026quot;: 后条件分布与无约束时局部对齐（未在本环境复现）。复合任务上，演讲者观点（Will/Cameron）：一个 structured JSON 同时完成摘要、部门、工单号等，常优于多步流水线——理由包括字段间自上下文、更少 prefill；Connor 提及 multi-task inference 论文，字幕未给题名（unable to verify）。\n常见误区：把软件工程「拆小函数」直接套到 LLM——模块化人类代码的论据在这里常不成立（演讲者观点）。另一误区：为多轮对话加长 context 而忽视可靠性与 streaming 提前下游（Cameron 强调 agent 作为可组合工具，而非聊天轮数竞赛——演讲者观点）。\n图：OCR 可见 p@® Weaviate Bf podcast 角标\n图：OCR 可见 ec )) Weaviate Sf podcast 播客画面\nAgent 与 compound 系统：JSON 作为 RPC # 为什么：Orchestrator 调度 specialist 时，需要稳定消息格式（与 Weaviate transformation agent 等协议同构的讨论方向）。\n机制/约束：结构化 JSON/RPC 使多 agent 组合成更大程序（演讲者观点，Cameron）；现场所见交互多为 \u0026lt;10 turns（演讲者观点）。Will 设想 LLM 系统或形成新抽象层——未来设想，非产品承诺。\n怎么做：协议层定义 schema 版本；推理层用 constrained decoding 保证可解析；评测层分离「格式分」与「任务分」。\n常见误区：把 agent 等同于长聊天。另一误区：未区分 API structured outputs 与 logit 级 generation 在合规与可审计上的差异。\n若你要落地 # 先写业务 schema 与失败语义，再选 OpenAI Structured Outputs 或自托管 Outlines / vLLM structured output；锁定 backend 与版本，记录 auto 实际 fallback。 推理模型（R1 类）分区约束：思考段自由文本，答案段单独 schema（参考 vLLM Reasoning Outputs）。 评测协议写清：是否用二次 LLM 解析、schema 字符预算、解析器实现；对标 arXiv:2408.02442 时核对附录 prompt 变体。 **优先尝试「单次多字段 JSON」**替代多步链式调用——若任务字段强相关；用集成测试验证，勿仅凭模块化直觉拆分。 监控非法绕过（非常规 Unicode、工具参数边缘类型）与 coalescence 版本；性能声称以你方 schema 实测为准，勿直接采用 5× 或 2–3× 口述数字。 参考与延伸阅读 # Outlines（GitHub） — 结构化生成主库与文档入口 Outlines Core（GitHub） — Rust Vocabulary / Index / Guide Efficient Guided Generation（arXiv:2307.09702） — 约束解码与 FSM 早期论文 Let Me Speak Freely?（arXiv:2408.02442） — 格式约束与推理质量对立研究 Coalescence 博客 — Will Kurt — 跳过确定性片段与加速声称 Outlines — Logits Processors — mask 机制说明 Outlines — Output Types / CFG — regex、JSON schema、CFG vLLM Structured Outputs 文档 — 多后端与配置 vLLM sampling_params.py（auto 路由） — xgrammar / guidance / outlines 回退 vLLM backend_outlines.py — outlines_core 集成 vLLM Reasoning Outputs — DeepSeek R1 等推理解析 OpenAI Structured Outputs 开发者文档 — API 层 JSON Schema 保证 OpenAI Cookbook — Structured Outputs Intro — 托管 API 示例 DeepSeek-R1（Hugging Face） — 推理模板与 think 段说明 Berkeley Function Calling Leaderboard — 工具调用评测（格式细节需自行核对） dottxt.ai — 商业支持与博客索引 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-structured-outputs-with-will-kurt-and-cameron-pfiffer-weaviate-podcast-1/","section":"文章","summary":"结构化输出：从「能解析的 JSON」到 logit 级约束生成","title":"结构化输出：从「能解析的 JSON」到 logit 级约束生成","type":"posts"},{"content":" 金融研究语料上的企业 RAG：向量库、Agent 与 eval 的工程取舍 # 金融机构把分析师笔记、基金/股票研究、社论等半结构化文本做成可查询的 GenAI 能力时，瓶颈往往不在「能不能接一个大模型」，而在 摄入吞吐、检索粒度、权限边界、以及 agent 引入后的延迟与可测性。Morningstar 内部平台 Morningstar Intelligence Engine（MIE） 的公开产品规格几乎查不到；下文把可核对的一手文档与 演讲者观点 分开写，供你在同类场景里做架构对照，而非当作 Morningstar 官方白皮书。\n问题空间：API 平台，而不只是聊天窗 # 为什么：内部产品团队与外部客户都需要同一套 RAG / agent 能力，但技能栈差异大；若每个应用自建管线，embedding、分块、eval 会重复且不可比。\n机制/约束：演讲者将 MIE 描述为 API 驱动、偏低/无代码 的平台：可配置 RAG 管线、GenAI agent、自带工具，并在路线中扩展 text-to-SQL（演讲者观点；Morningstar Developer 门户存在，但未载明 MIE 能力清单）。首条生产语料域为 研究/社论/分析师笔记（演讲者观点），典型链路与 Weaviate 文档中的 RAG 叙述一致：嵌入 → 向量库语义检索 → 上下文送入 LLM。\n怎么做（最小示意）：\n文档事件 → 队列 worker → chunk + embed → Weaviate upsert 用户问题 → filter(entitlement) → hybrid/semantic search → rerank → LLM 常见误区：把「平台」等同于单一 Chat UI；忽略 Deploy 后统一 API 与多租户 eval 的需求（工具市场、eval UI 为 演讲者观点，无公开文档）。\n分屏访谈：左侧 Weaviate podcast 标识与波形装饰，右侧嘉宾夹克红色 MORNINGSTAR 字样——本期无架构幻灯片，技术主张均来自口述。\n向量库选型：FAISS 验证，Weaviate 扛复制与运维边界 # 为什么：FAISS 适合 POC 级稠密检索（\u0026ldquo;efficient similarity search … of dense vectors\u0026rdquo;），但 HA、复制、跨 AZ 扩缩若自建，团队时间会从 内容质量 漂移到基础设施（演讲者观点）。\n机制/约束：Weaviate Replication 文档写明复制可 \u0026ldquo;improve availability\u0026rdquo; 并支持 replicationFactor；这与「用托管/成熟向量库换工程焦点」的叙述同向。演讲者观点：GPT 出现后先 FAISS，后在容器内 数日 试验开源 Weaviate 并进入合作——时间线无公开案例可核。\n怎么做：POC 用本地/文件索引；生产前明确 RPO/RTO、只读副本读扩展、升级策略，再决定是否自建 FAISS 集群还是向量库。\n常见误区：默认「FAISS 不能上生产」——FAISS README 未否定生产，缺的是 复制与多副本语义；选型是成本曲线，不是道德判断。\n画面 OCR 可见片段：Weaviate ap Niantic Mn, \\dcas . =? ap, —— 品牌噪点帧，无技术图表。\n摄入：SNS/SQS/Celery 削峰，而非 Kafka 叙事 # 为什么：分析师内容经 CMS 等多源涌入，embedding 调用（录制时点为 Azure 上的 text-embedding-ada-002，1536 维、8192 token 上限）在高峰会成为瓶颈。\n机制/约束：Amazon SNS 做 topic fan-out，SQS 缓冲，Celery worker 在 K8s 上弹性伸缩——演讲者观点 明确 未采用 Kafka（与「大规模摄入必 Kafka」的默认剧本相反）。组件能力有一手文档；MIE 是否仅此组合 无法核实。\n怎么做（示意）：\n# 伪代码：事件驱动摄入，与访谈栈对齐的方向性示例 sns.publish(TopicArn=DOC_TOPIC, Message=json.dumps({\u0026#34;doc_id\u0026#34;: id})) # SQS → Celery task: fetch → chunk → embed → weaviate batch import 常见误区：为「看起来更大数据」强上 Kafka，却缺少跨团队流处理运维；Python 栈下 队列 + worker 往往足够（演讲者观点）。\n约 10 分钟段：嘉宾着 MORNINGSTAR 夹克讨论 pipeline；背景仍为分屏访谈，未见 AWS 架构图。\n分块、合成问句与「小 embed、大 retrieve」 # 为什么：页界/固定长度初版块常切断语义；用户问的是 问题，块却是 陈述，向量空间不对齐会降低召回。\n机制/约束：\n演讲者观点：块间重叠、多次 全量 re-ingest、检索后 rerank；CMS pub/sub 处理 PDF/文本/JSON，部分需外部映射。 合成问句嵌入：用 LLM 为块生成假设问题再 embed，因 question–question 相似度优于 question–document（演讲者观点，无 Morningstar 论文）。 文献侧：Anthropic Contextual Retrieval 用块附上下文再嵌入，宣称与 rerank 组合可降低失败检索（其内部评测，≠ MIE 数字）；LlamaIndex Auto Merging Retriever 体现 小块检索、合并父块 的「小索引、大上下文」模式，与嘉宾口述同构，但 「embed small, retrieve big」字面口号未在 LlamaIndex 官方命中。 怎么做：\nchunk → LLM(synthetic_questions[]) → embed each → store metadata: parent_doc_id, offsets query → retrieve top-k synthetic hits → expand to parent span → rerank → prompt 常见误区：把摄入阶段合成问句 直接当 golden eval 或微调集——演讲者观点 称合成题主要用于检索，不默认进入 eval；若未来做合成 eval，需换模型/加噪声防 同源偏差。\nOCR 片段：Weaviate— podcas . go Nias May, —— 讨论 contextual chunk 时段的品牌帧。*\nOCR 片段：SaAdcCoct podcast \u0026gt; as Nilantte Nay, —— 合成问句与 eval 边界讨论附近。\n分屏：主持人侧 Weaviate podcast 墙牌与 FAU 证书，嘉宾侧 MORNINGSTAR 标识——嵌入与分块策略为口述内容。\n生产 RAG 保留 ReAct：用延迟换答案质量 # 为什么：单轮 retrieve-augment-generate 在复杂金融问题上易漏工具步骤或误选语料库。\n机制/约束：ReAct（Yao et al., 2022）要求 \u0026ldquo;reasoning traces and task-specific actions in an interleaved manner\u0026rdquo;，每轮多一次 LLM 调用 → 延迟上升 可预期。演讲者观点：MIE 由无 agent 起步，为质量 在生产保留 ReAct；平台内 RAG 管线本身是 ReAct，而 function calling / programmatic action calling 主要服务 消费 MIE API 的下游——下游的一个 function 可再调平台 ReAct，形成嵌套。\n怎么做：为 ReAct 设 max_steps、超时、工具白名单；对延迟敏感路径保留 vanilla RAG 降级开关（访谈未称已实现，属工程建议）。\n常见误区：因 benchmark 上 agent 更炫就全量 agent 化；忽视 逐步 eval 成本 随步骤数爆炸（嘉宾对比 LangGraph 式工作流的 StateGraph 可观测性，自身更偏 工具描述 + ReAct 选型，演讲者观点）。\n约 20 分钟：嘉宾讨论 agent 与延迟；画面仍为双主持人分屏，无延迟曲线图。\n分屏访谈帧：左侧 Weaviate podcast 标识，右侧 MORNINGSTAR 夹克——多 agent / AutoGen 讨论时段。\n延伸（未上线边界）：Microsoft AutoGen 仅 POC，适合 后台多轮 + 缓存 insight；Reflexion 论文在 HumanEval 报 pass@1 提升——与嘉宾口述 SQL「约 80–83%」不同任务、不同指标，不可混用。\n工具市场与「多 Agent」：路由优于组织隐喻 # 为什么：金融场景工具多（RAG、SQL、自定义 API），需要 discoverability 与隔离，而非给 LLM 编「CEO/市场部」角色剧。\n机制/约束：演讲者观点——Tool marketplace 发布容器内运行的自定义 API；ReAct 读 工具描述 + prompt 选工具；Deploy 后统一 API 暴露。上线形态更像 共享 Morningstar RAG tool + 按 agent 附加 SQL 等工具的路由，而非 multi-agent 组织隐喻（演讲者观点）。\n常见误区：用 AutoGen 多角色直接扛终端用户请求——嘉宾称 延迟与 eval 难度 使其更适合批处理（演讲者观点）。\nOCR：Weaviate DOC 1 oa Ailantir Unt, —— tool marketplace 讨论段的品牌帧。\nEval：golden Q\u0026amp;A 与 meta-evaluation，而非摄入合成题 # 为什么：换模型、temperature 或灌入大批量新文档时，需要可重复对比，否则无法向业务方证明「变好了」。\n机制/约束：演讲者观点——内置 golden Q\u0026amp;A；指标含 accuracy、conciseness、groundedness 及 meta-evaluation；非工程师可用 UI 上传 eval。无法核实 MIE 指标公式与是否实现 pass@k。公开 RAG eval 常配合 LLM-as-judge，但 与 Spider/BIRD 等 SQL leaderboard 不是同一套度量。\n怎么做：固定 问题集版本 + 检索快照 + 模型版本 三维；变更 embedding 模型时按 Azure 文档 对 ada-002 → text-embedding-3-* 全量重嵌。\n常见误区：用摄入合成问句当 eval——易 过拟合检索器 且与真实用户问法分布脱节（演讲者观点）。\n约 14 分钟 eval 讨论：分屏画面，无指标仪表盘截图。\ntext-to-SQL：schema 工程与口述准确率的上限 # 为什么：结构化持仓、评级、指标查询若全靠向量检索，成本高且难保证聚合正确。\n机制/约束：演讲者观点——早期即有 text-to-SQL POC；在 少量表（\u0026lt;10）且列名自解释 时口述 约 80–83%（无 benchmark 名、无 pass@1/execution accuracy 定义，无法核实）。工程手段：为列提供 rating_id 整数 避免对文本 rating 误 AVG；用 数据库视图 减 join；只读 SQL 角色、LIMIT、聚合尽量下推 SQL；错误时 SQL agent 重试。公开对照仅能用 Spider 2.0 等社区套件说明行业基准存在，不能证实或反驳 80–83%。\n怎么做：\n-- 只读角色 + 行数上限（示意） SET ROLE mie_readonly; SELECT rating_id, AVG(metric) FROM v_fund_ratings GROUP BY 1 LIMIT 100; 常见误区：表一多、列名缩写就期待通用模型「接近可用」；或把 Reflexion 论文 91% pass@1 HumanEval 误读为 SQL 成绩。\n约 36 分钟 text-to-SQL 段：嘉宾 MORNINGSTAR 夹克、分屏访谈，无 SQL 准确率图表。\n权限与合规：源头不 ingest，检索前 filter # 为什么：生成后屏蔽单句无法挽回已泄露的向量；金融数据还有 domicile、entitlement 维度。\n机制/约束：演讲者观点——非公开敏感内容 尽量不进入向量库；已入库对象带 metadata，在请求到达 Weaviate/LLM 之前 按 entitlement 收窄或拒绝。库侧能力：Weaviate Filters 支持属性过滤；Weaviate v1.28.0 引入 RBAC Preview（\u0026ldquo;roles and permissions … Isolation is at the collection level\u0026rdquo;，preview API 可能变更）——主持人提及，可与业务 entitlement 叠加强度未知。\n常见误区：只靠 RAG 后处理 redact；忽视 Python 工具必须沙箱，否则退回 限定 function 集（演讲者观点：SQL 默认可用只读角色，Python 风险更高）。\nGuardrails 与模型栈：外购 vs 自建、微调 vs prompt # 为什么：模板化合规（免责声明、禁止投资建议措辞）与输入输出双侧检查，外包服务在金融场景常不够用。\n机制/约束：演讲者观点——试过多家 guardrails-as-a-service 后倾向 in-house；平台将支持客户 自带 guardrail（规则/小模型/LLM prompt）。文风上小规模 Llama 微调「response was okay」，短文章 GPT-4o + prompt 已够用（gpt-4o 在 Azure 模型表可查）；大规模研报仍靠分析师。演讲者观点。\n常见误区：认为金融文风必须微调；或 overnight 自动生成研报可无人值守——嘉宾称仍属 研究产物、需人工审阅，多模态图表风险更高（演讲者观点）。\n嘉宾红色 MORNINGSTAR 字样与左侧 Weaviate podcast 墙牌——收尾段身份标识帧。\n若你要落地 # 先画权限与数据分级：敏感未公开材料默认不进向量库；检索链路上 filter 早于 embed 查询，并对照向量库 复制/RBAC 能力做威胁建模。 摄入与 eval 解耦：合成问句服务召回；golden Q\u0026amp;A 单独维护，换 embedding 模型时计划 全量重嵌 + eval 重跑。 Agent 分层：终端路径控制 ReAct 步数；批处理/insights 缓存可考虑 AutoGen 类框架，但别与前台抢同一 eval 矩阵。 text-to-SQL 先缩 schema：视图 + 只读角色 + LIMIT；口述准确率区间需 自建 benchmark，勿引用无关论文指标。 录制时点模型已老化：ada-002 与 GPT-4 系列在 2025+ 部署前查 Azure/OpenAI deprecations，避免维度变化导致静默检索退化。 参考与延伸阅读 # ReAct: Synergizing Reasoning and Acting in Language Models（arXiv:2210.03629） Reflexion: Language Agents with Verbal Reinforcement Learning（arXiv:2303.11366） FAISS — Facebook AI Similarity Search Weaviate 数据库与 RAG 工作流介绍 Weaviate — Replication 配置 Weaviate — Filters（属性过滤） Weaviate v1.28.0 Release — RBAC Preview Anthropic — Introducing Contextual Retrieval LlamaIndex — Auto Merging Retriever LangGraph overview — 状态图与工作流 Microsoft AutoGen — agentic AI 框架 Azure OpenAI — 模型与嵌入（text-embedding-ada-002、gpt-4o） Amazon SNS 与 SQS — AWS 异步消息组件 Celery — 分布式任务队列入门 Spider 2.0 — text-to-SQL 社区 benchmark（对照用） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-morningstar-intelligence-engine-with-aravind-kesiraju-weaviate-podcast-1/","section":"文章","summary":"金融研究语料上的企业 RAG：向量库、Agent 与 eval 的工程取舍","title":"金融研究语料上的企业 RAG：向量库、Agent 与 eval 的工程取舍","type":"posts"},{"content":" 开放模型上生产：Java 团队的 LangChain4j 集成路径 # 把生成式 AI 放进已有 Spring 服务，难点往往不在「能不能调通一个 ChatCompletion」，而在能否用同一套 Java 代码在本地 Ollama、团队自托管的 OpenAI 兼容端点，以及 Azure AI Foundry 托管推理之间切换，同时把工具调用、RAG 与基本质量门禁做成可观测、可回滚的管线。下文按工程能力块组织：集成层、本地运行时、演示服务、RAG 与评测、容量延迟、托管迁移与安全——论断尽量绑定 LangChain4j 文档、Ollama API、JEP 444 与可复现命令；未在公开文档中复核的数字与案例标注为演讲者观点。\n演示源码可通过 aka.ms/opengenai 获取（HTTP 301 至 gen-ai-with-open-models）。仓库按 Demo 1（本地推理 + 工具调用）与 Demo 2（RAG + 评测）拆分脚本，便于在 JavaOne 现场逐段复现；生产落地时则应把「能跑通」与「能上线」拆开：前者验证集成面，后者补齐持久化、鉴权、护栏与 SLO。\n图：幻灯「Selecting the Right Model for Your Use Case」——按任务、延迟与内存约束选型，而非追逐榜单第一名（演讲者观点）。\n统一集成层：LangChain4j 与 OpenAI 兼容端点 # 为什么 # Java 团队若沿用「每个模型一个 SDK」，换端点就要改业务层。演讲中的做法是：用 LangChain4j 的 OllamaChatModel、OpenAiChatModel 与同一套 AiServices、@Tool、EmbeddingModel / EmbeddingStore，把 Python 样例里的 base_url + api_key 迁到 Bean 配置。演讲者观点：切换 Ollama、vLLM 或云端时「尽量不改业务 Java」——实际仍需改 baseUrl、apiKey、modelName 等 Bean，而非零 diff。\n机制与约束 # Ollama 集成 默认 http://localhost:11434；OpenAI 兼容路径 为 /v1/chat/completions。 @Tool 属于 LangChain4j function calling（Tools 教程），与 MCP 是不同协议层；混用概念会导致架构图画错边界。 演示仓库 OllamaConfig 中 temperature 为 0.7；要点草案写 0.2 以利 grounded 回答——二者不冲突，属可调超参。 图：幻灯「LangChain4j: Code Patterns for Open Models」——OllamaChatModel.builder()、@Tool 与 AiServices.builder。\n怎么做 # OllamaChatModel chat = OllamaChatModel.builder() .baseUrl(\u0026#34;http://localhost:11434\u0026#34;) .modelName(\u0026#34;llama3.1:8b\u0026#34;) .temperature(0.7) .build(); curl http://localhost:11434/v1/chat/completions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;model\u0026#34;:\u0026#34;llama3.1:8b\u0026#34;,\u0026#34;messages\u0026#34;:[{\u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;hello\u0026#34;}]}\u0026#39; 常见误区 # 把 @Tool 当成 MCP Server 暴露——二者可并存，但语义与运维边界不同。 假设换云厂商时 Java 业务类完全不动：至少配置类与鉴权头会变。 在 AiServices 接口上堆过长 @SystemMessage，却不用 MessageWindowChatMemory 控制窗口——长对话会挤占留给 RAG context 的 token（AiServices 教程 中的 memory 组件可按需启用）。 本地运行时：Ollama 与演示模型组合 # 为什么 # 在接云端账单与合规评审之前，用本地 Ollama 复现延迟、量化与工具调用行为，成本可控。演示固定组合：llama3.1:8b（对话）、mistral:7b（可切换）、nomic-embed-text（嵌入），与 演示 README 一致。\n机制与约束 # CLI ollama pull / ollama list 对应 REST POST /api/pull、GET /api/tags（Ollama API）。 Linux 官方安装为 curl -fsSL https://ollama.com/install.sh | sh；字幕中的 apt-get install ollama 未在官网核实。 GPU/显存由 Ollama 调度属演讲者观点；量化标签（如 Q4）见各模型卡片，无统一「质量-内存」对照表。 图：IDE 中 ## Demo 1: Local Inference + Tool Calling 与 Production-Ready GenAI with Open Models for Java Teams。\n怎么做 # ollama pull llama3.1:8b ollama pull mistral:7b ollama pull nomic-embed-text ollama list application.yml 中 ollama.base-url: http://localhost:11434，chat-model / embedding-model 与上一致（application.yml）。\n常见误区 # 未 pull 嵌入模型就调 /ingest——RAG 链路会在嵌入步骤失败。 把演示 mistral:7b 当成生产默认——应按自有 benchmark 重选（见选型幻灯）。 在笔记本上同时跑 70B 全精度与大型 Spring 堆——即使 JVM 未 OOM，节点仍可能因 Ollama 常驻权重而 swap（演讲者观点：模型内存多在 heap 外）。 选型幻灯还强调六步：定义任务类型（chat / RAG / code 等）→ 设定延迟与内存上限 → 用自家数据 benchmark → 评估 GGUF Q4/Q5/Q8 量化 → 比较「7B @ 50 tok/s」与「70B @ 5 tok/s」谁更符合 SLA → 固定模型版本并准备回滚配置。公开榜单只能作初筛，不能替代领域数据上的回归。\n图：幻灯「Open Model Terms」——量化、GGUF、MoE 与 TTFT。\nSpring Boot 最小可运行集成 # 为什么 # Java 21 + Maven 的 Spring Boot 应用给团队一条可复制的联调入口：构建、启动、用 curl 打 REST，再叠工具与 RAG。\n机制与约束 # pom.xml：java.version 21，Spring Boot 3.4.x。 server.port: 8080，入口 OpenModelsDemoApplication。 OCR 中 808@ 为识别噪声，实际端口 8080。 图：SOURCE CONTROL 与 README 中 mvn clean package -DskipTests、mvn spring-boot:run。\n怎么做 # mvn -q -DskipTests clean package mvn -q spring-boot:run curl -sS \u0026#34;http://localhost:8080/chat?message=ping\u0026#34; 常见误区 # 跳过 ollama serve 只起 Spring——LLM 调用会连不上 11434。 在 CI 里跑完整 Demo 却不预留 GPU/CPU 与模型拉取时间。 把 -DskipTests 当作长期策略——演示可以跳过测试，上线前应恢复针对 Controller 与 ingest 路径的契约测试。 Tomcat 在 8080 监听、context path '/' 与 OCR 中 Started OpenModelsDemoApplication 日志一致，说明这是一个标准 Spring MVC 外壳，而非独立 CLI；团队可把同一 JAR 部署到 K8s，只需把 ollama.base-url 换成集群内 Service 名称。\n对话与工具调用：从 /chat 到 /chat/tools # 为什么 # 先验证纯生成（/chat），再让模型在需要时调用确定性后端（/chat/tools），把「必查库存」从幻觉风险里拆出去。\n机制与约束 # ChatController：@GetMapping(\u0026quot;/chat\u0026quot;)，@Qualifier(\u0026quot;chatAssistant\u0026quot;) 注入，chatAssistant.chat(message)。 工具路径：@GetMapping(\u0026quot;/chat/tools\u0026quot;) → toolAssistant；InventoryTools 用 @Tool + @P(\u0026quot;Product SKU\u0026quot;)，Map 模拟库存（JDK-21 → 150），非外部 ERP。 图：public class ChatController 与 @GetMapping(\u0026quot;/chat\u0026quot;)。\n图：InventoryTools 中 String checkStock(@P(\u0026quot;Product SKU\u0026quot;)。\n怎么做 # curl \u0026#34;http://localhost:8080/chat?message=What+is+the+Java+record+keyword?\u0026#34; curl \u0026#34;http://localhost:8080/chat/tools?message=How+many+units+of+JDK-21+in+stock?\u0026#34; @Tool(\u0026#34;Look up current stock level for a product by SKU\u0026#34;) String checkStock(@P(\u0026#34;Product SKU\u0026#34;) String sku) { return Map.of(\u0026#34;JDK-21\u0026#34;, 150).getOrDefault(sku, 0) + \u0026#34; units\u0026#34;; } 常见误区 # 工具返回自由文本却不校验 SKU——模型仍可能编造未查询的商品。 在 system prompt 里塞用户可控指令而不隔离——增加 prompt injection 面（生产需输入护栏，见后文）。 为「省事」只暴露 /chat 而不做工具路由——库存、工单、定价类问题应走确定性后端；演示中 /chat/tools 与 /chat 分离正是为了对比幻觉与 grounded 工具结果。 InventoryTools 返回形如 \u0026quot;SKU %s: %d units in stock\u0026quot;.formatted(sku, qty) 的字符串（OCR 可见 formatted 片段），便于模型把工具输出复述给用户，同时保持数据源在 Java 侧可控。\nRAG：摄取、问答与启发式评测 # 为什么 # 企业文档型问答需要「答案有据可查」：先 chunk + 嵌入 + 检索，再生成；更换模型或分块策略后，用自有 golden set 做回归，避免只靠肉眼试 prompt。\n机制与约束 # DocumentIngestor：FileSystemDocumentLoader、DocumentSplitters.recursive，rag.chunk-size: 500、chunk-overlap: 50，EmbeddingStoreIngestor 写入 in-memory store。 POST /ingest 返回 documents_ingested、embedding_model、store（演示为 3 份 docs/ 样例）。 GET /ask 经 RagConfig 的 EmbeddingStoreContentRetriever + ragAssistant；样例问虚拟线程时，java21-features.txt 与 JEP 444 一致——事实来自文档，表述由 LLM 组织。 RagEvaluator：3 条 EvalCase，computeFaithfulness 等为关键词重叠启发式（源码注释 Simple keyword overlap metric），非 RAGAS 等行业基准；OCR 中 context_precision≈0.27 等为单次演示跑分，不可外推。 图：## Demo 2: RAG Pipeline + Evaluation 与 ### Ask questions grounded in your docume。\n图：DocumentIngestor.java 与 import dev.langchain4j.model.embedding.EmbeddingModel。\n图：public class RagEvaluator 与 faithfulness / context precision 相关逻辑。\n怎么做 # curl -X POST \u0026#34;http://localhost:8080/ingest\u0026#34; curl \u0026#34;http://localhost:8080/ask?question=What+are+virtual+threads+in+Java+21?\u0026#34; curl -X POST \u0026#34;http://localhost:8080/evaluate\u0026#34; 常见误区 # 把演示 /evaluate 分数当作 SLA 合同指标。 chunk 过大导致 context 稀释，过小导致语义破碎——需按文档类型调 chunk-size / overlap。 生产仍用 in-memory store——重启即失索引；应换持久化向量库（演示未覆盖）。 混淆 faithfulness（答案是否忠于检索片段）与 answer relevance（答案是否切题）——RagEvaluator 对二者分别用启发式打分；若要对外报告，应改用 RAGAS、DeepEval 等框架并固定数据集版本。 POST /evaluate 返回的 average_latency_ms、source_accuracy 等字段适合作为变更前后对比的门禁，而非绝对质量承诺。更换 llama3.1:8b 为其他开放权重后，应重新 ingest 并跑一遍 evaluate，观察 faithfulness 与 context precision 是否同向变化。\n容量、延迟与节点规划 # 为什么 # 「JVM 调优完成」不等于节点稳定：Ollama 模型权重常驻 RAM，通常在 heap 外，仍与 -Xmx 争用同一台机器；K8s 缩容到零会换来冷启动 TTFT（演讲者观点）。\n机制与约束 # Ollama Modelfile 参数 num_ctx、keep_alive；API 支持 stream（Ollama API）。 幻灯列举：Streaming、Prompt caching、KV-cache / num_ctx、Batch、Model warmup、Right-size context、Hardware matching（量化 CPU vs 全精度 GPU）。 图：幻灯「Response-Time Tuning Strategies」——Streaming、Prompt caching、Model warmup 等七项。\n怎么做 # # 容量草表（单侧运维，演讲者观点） 节点 RAM 128Gi − JVM -Xmx 24Gi − Ollama resident ~60Gi − OS/cache ≈ 余量 @Component class LlmWarmup implements ApplicationRunner { @Override public void run(ApplicationArguments args) { chatAssistant.chat(\u0026#34;ping\u0026#34;); } } 常见误区 # 只看 tokens/s 不看 TTFT——交互式聊天首 token 更敏感。 盲目增大 num_ctx——上下文越长，单次推理越慢（官方参数表可证存在 trade-off）。 未开 streaming 却让前端干等整段 JSON——用户感知的「卡顿」往往来自首 token，而非总耗时；LangChain4j Ollama 集成文档含 streaming 示例，可与 Spring SseEmitter 或 WebFlux 组合。 MoE（Mixture of Experts）类模型在幻灯中与量化、TTFT 并列介绍：总参数量大但每步只激活子集专家，适合在延迟预算内追求能力上限。演讲者观点：托管侧（Foundry NIM）与自建 vLLM 的 MoE 运维复杂度高于稠密 7B/8B，选型时应把弹性与回滚算进总成本。\n托管迁移：Azure AI Foundry 与 OpenAI 兼容 REST # 为什么 # 本地验证后，可把同一 LangChain4j 调用面指向 Azure AI Foundry 上托管的开放权重部署（演示幻灯为 NVIDIA Nemotron-3-Super-NIM-microservice），由平台承担 GPU 与弹性（ACA/AKS/KEDA 等为 Azure 通用能力，未与演示仓库绑定验证）。\n机制与约束 # Azure 聊天补全形态：POST https://{endpoint}/openai/deployments/{deployment-id}/chat/completions?api-version=...（Azure OpenAI REST）；api-version 必须以部署页为准——草案中的 2024-05-01-preview 与当前 Learn 示例（如 2024-10-21）可能不一致。 Java 侧可用 OpenAiChatModel（自定义 baseUrl）或 AzureOpenAiChatModel（deploymentName）。 幻灯/OCR 描述 Nemotron 120B 总参 / 12B active、LatentMoE、MTP、NVFP4——本次未能从 Microsoft Learn 全文复核；正文引用门户模型卡为准。Hugging Face 上同名仓库链接在核验时返回 404，写死参数量有风险。 图：Foundry 目录页「Nemotron-3-Super-120B-A12B is a large language model (LLM) trained by NVIDIA」。\n怎么做 # curl \u0026#34;$FOUNDRY_ENDPOINT/openai/deployments/$DEPLOYMENT/chat/completions?api-version=$API_VERSION\u0026#34; \\ -H \u0026#34;api-key: $AZURE_API_KEY\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;messages\u0026#34;:[{\u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;,\u0026#34;content\u0026#34;:\u0026#34;hello\u0026#34;}]}\u0026#39; OpenAiChatModel.builder() .baseUrl(System.getenv(\u0026#34;FOUNDRY_BASE_URL\u0026#34;)) .apiKey(System.getenv(\u0026#34;AZURE_API_KEY\u0026#34;)) .modelName(System.getenv(\u0026#34;DEPLOYMENT_NAME\u0026#34;)) // 以部署名为准 .build(); 常见误区 # 把 Foundry deployment name 写成 Hugging Face repo id。 忽略 api-version 查询参数——Azure 与裸 Ollama 本地 API 的版本策略不同（见下表）。 认为「上云就不用关心开放权重合规」——Foundry 强调模型溯源（演讲者观点：相对 Hugging Face 自选权重需自评 license 与安全），法务与数据驻留仍需单独评审。 演示未包含 Foundry 分支的 Spring Profile；落地时常见做法是 application-local.yml 指向 Ollama，application-prod.yml 注入 FOUNDRY_BASE_URL 与 AZURE_API_KEY，并对同一 Assistant 接口做金丝雀流量对比延迟与成本。\n端点族 版本机制 Azure OpenAI / Foundry URL 查询 api-version Ollama 原生 /api/* 文档未要求 api-version Ollama OpenAI 兼容 /v1/* 客户端形态兼容 OpenAI 开放模型的安全层 # 为什么 # 从 Hugging Face 等渠道拉取的权重通常不带商业 API 那套强制内容过滤；把未审查模型直接暴露给终端用户，合规与品牌风险由应用方承担。\n机制与约束 # 幻灯建议：Input guardrails（注入、PII、话题黑名单）→ Model → Output guardrails（毒性、事实性、格式）→ Human review。 LangChain4j 提供 ModerationModel 与 OpenAiModerationModel；演示仓库未实现护栏 Controller——属生产扩展。 Llama Guard 等为可选本地方案，演示代码未验证。 图：幻灯「Safety Layers for Open Models」——input filter → model → output filter → human review。\n怎么做 # public String safeChat(String user) { if (moderation.isViolating(user)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST); } String out = llm.generate(user); return moderation.isViolating(out) ? \u0026#34;[blocked]\u0026#34; : out; } 常见误区 # 只在输出侧做 moderation——越狱 prompt 已进入模型上下文。 无 flagged 日志与人工升级队列——安全层不可运营。 把 OpenAI Moderation API 当作唯一方案——离线或主权云场景可改用 ModerationModel 的其他实现或 Llama Guard 类本地分类器，但需自行维护模型版本与误杀率。 开放权重与托管 API 的差异在于：过滤责任默认在应用方。幻灯中的 input/output guardrails 与 LangChain4j ModerationModel 是同一思路的工程化——先拦截再生成，再拦截再返回，可疑样本进入人工复核队列，而不是仅在前端展示免责声明。\n收尾：资料入口与证据边界 # 图：收尾页 https://aka.ms/opengenai 与议题「Production-Ready GenAI with Open Models for Java Teams」。\n已核实（可对照仓库与官方文档）：LangChain4j + Ollama 集成模式；演示端点 /chat、/chat/tools、/ingest、/ask、/evaluate；JEP 444 与样例文档一致性；Azure REST 形状与 LangChain4j 双集成类。\n演讲者观点或未复核：AT\u0026amp;T 等降本案例、Foundry 目录模型数量、128Gi 容量草表、Nemotron 精确架构参数在 Learn 上的正文、护栏在生产环境的具体 SLA。\n若你已在本地跑通 Demo 1/2，下一步通常是：持久化向量库、用真实 benchmark 替换 RagEvaluator 启发式、按节点 RAM 预算选定量化档，并在 Foundry 部署上用门户给出的 api-version 做金丝雀。整条路径的核心收益，是把「模型端点」变成可替换的配置，而不是散落在各 Controller 里的硬编码 URL。\n参考与延伸阅读 # 开放 GenAI 演示资料（aka.ms） gen-ai-with-open-models 演示仓库 LangChain4j 文档首页 LangChain4j Ollama 集成 LangChain4j Tools（Function Calling） LangChain4j RAG 教程 LangChain4j Azure OpenAI 集成 Ollama 模型库 Ollama OpenAI 兼容说明 Ollama HTTP API（GitHub） Ollama Modelfile 参数 JEP 444: Virtual Threads Microsoft Foundry 文档 Azure OpenAI REST API 参考 Spring Boot 3.4 参考文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-production-ready-genai-with-open-models-for-java-teams/","section":"文章","summary":"开放模型上生产：Java 团队的 LangChain4j 集成路径","title":"开放模型上生产：Java 团队的 LangChain4j 集成路径","type":"posts"},{"content":" 企业 RAG 的边界：托管流水线、向量库与「可写回」检索 # 当基础模型跨过「能答」的门槛，企业真正卡住的多半不是再训一个 7B，而是：私有数据如何进索引、检索如何选对库、生成如何只信该信的源。Google 把这条链收成 Vertex AI RAG Engine 的托管流水线；Weaviate 则长期站在向量存储与数据建模一侧。二者在 2024 年底至 2025 年初的集成，把争论从「要不要 RAG」推回到更硬的问题：单 corpus 文档索引是否够、解析与分块谁优先、RAG 何时该变成可写回的双向回路。\n下文按工程主题组织：常见做法 → 嘉宾主张 → 可核对证据与未证边界。不强行给出单一结论。\n开场分屏：左侧 Weaviate podcast 标牌与三位嘉宾视频画面，无架构幻灯片。\n托管 RAG 流水线：为什么存在、边界在哪 # 为什么 # 自研 RAG 往往重复实现：数据源接入、分块、嵌入、索引生命周期、检索 API、与生成模型的上下文拼接。团队规模小时可行；一旦 corpus 变大、合规与 IAM 变严，运维与版本锁定（尤其 embedding 与 corpus 绑定）会吃掉大量精力。官方文档将 RAG Engine 描述为六步：ingestion → transformation（含 chunking）→ embedding → indexing（corpus）→ retrieval → generation；默认向量后端为托管 RagManagedDb（基于 Spanner），也可接 Vector Search、Feature Store、Weaviate、Pinecone 等。\n机制与约束 # 不自研向量库：产品定位是编排层，而非替代 Weaviate/Pinecone 的数据库。（文献/文档支持） Corpus 与嵌入模型生命周期绑定：文档写明 \u0026ldquo;association between your embedding model and the RAG corpus remains fixed for the lifetime of your RAG corpus\u0026rdquo;——换嵌入通常意味着重建索引。（文献/文档支持） GA 时间线：发布说明记载 2024-12-20 RAG Engine GA；介绍博客 datePublished 为 2025-01-10。嘉宾称团队约「2024 年初」启动——官方未见对应表述。（访谈观点 vs 发布说明） 怎么做（最小路径） # 快速入门见 RAG quickstart。若已有 Weaviate 实例，专页 将 Weaviate 与 RAG 搭配使用 要求 CreateRagCorpus / UpdateRagCorpus 与 collection 一对一，并通过 Secret Manager 配置 API Key；支持 hybrid search 与 dense/sparse 权重调节。\n常见误区 # 把 RAG Engine 当成「又一个聊天机器人」——发布博客 强调其作为 Gemini API 的 tool 嵌入现有应用。（文献/文档支持；「非 chatbot」为产品定位表述） 假设换 embedding 是改配置即可——需按文档规划 corpus 重建。 将「几分钟跑通 notebook」等同于生产最优：嘉宾与文档均指向 手动调参（chunk_size、chunk_overlap 等），尚无 AutoML 式自动搜参。（见后文 P05） 约 10 分钟处：三人分屏讨论托管 RAG 与向量库分工，画面仍为访谈镜头。\n外部向量库：Weaviate 在流水线中的位置 # 为什么 # 客户已在用开源或自管 Weaviate 时，迁移到云厂商托管索引的切换成本高。集成诉求是：少改应用逻辑、保留 schema 与 hybrid 能力、在 Vertex 侧扩展 ingestion/检索。（访谈观点；与 Google 专页方向一致）\n机制与约束 # 维度 文档事实 访谈张力 集成形态 RAG corpus ↔ Weaviate collection 1:1；客户自管实例 + Secret 「少改代码」仍为体验描述，非零配置 成熟度 向量库对比表中 Weaviate 为 Preview；更新非即时同步 嘉宾称「已发布集成」 GA 当日向量库 2024-12-20 发布说明列 Vector Search + Pinecone，未列 Weaviate 时间差需在方案评审中写明 怎么做 # 按 use-weaviate-db 配置 schema 字段（如 fileId、corpusId、chunkId、chunkData）。Marketplace 上的 Weaviate Shared Cloud 解决的是部署订阅，不等于 RAG Engine 连接器本身。\n常见误区 # 把 Preview 当 GA 做 SLA 承诺。 忽略 ingestion 在 Google 侧、向量在客户侧 的双运维边界。 访谈 UI 局部 OCR 碎片：; 2 3 oO =（无幻灯片技术内容）。\n解析与分块：杠杆在 transformation，而非只堆 rerank # 为什么 # 检索失败常源于 chunk 语义断裂、表格被切碎、标题层级丢失。嘉宾（Vertex PM）认为当前 RAG 质量提升的 最大杠杆在 parsing/chunking，rerank 有帮助但次之；另一位嘉宾同意 parsing 优先，并补充 re-index 与 embedding 推理成本 的优化空间。（访谈观点；Google 未书面排序「parsing \u0026gt; rerank」）\n机制与约束 # Document AI layout parser：按 layout entity（标题、表格、列表）生成 context-aware chunks。 Reranking：semantic-ranker-default@latest、LLM reranker（Gemini 相关性打分）并存——两者皆为官方一等能力。 可调参数：嵌入模型页 默认推荐 text-embedding-005；fine-tune transformations 给出默认 chunk_size 1024、overlap 256（token）。嘉宾所称「90–95% 质量、先上手后极致」——无官方数值。（访谈观点） Anthropic Contextual Retrieval（2024-09-19）报告 failed retrievals 降 49%，叠加 rerank 降 67%——指标定义与 Vertex layout parser 不可直接移植数值；Vertex 文档 未 宣称采用 Anthropic 同名方案。\n怎么做 # 对 PDF/复杂版式启用 layout parser；对纯文本可先 fixed-size chunking（GA 发布说明提及 fixed-size + overlap）。评估时分别记录 检索失败率 与 生成 grounded 率，勿混用 Anthropic 与 Google 栈的 benchmark。\n常见误区 # 先上 rerank、后补解析——与嘉宾优先级相反，且可能放大错误 chunk 的分数。 把 contextual retrieval 当作 Vertex 默认实现——未证实。 讨论 parsing 优先级时段，OCR：wy is] 8 ® s。\nOCR：oO 3 $ fe} oO =。\n单 corpus 文档模型 vs 企业多源现实 # 为什么 # Connor 提出：RAG Engine 的隐含模型接近 「一个搜索索引 + 文档块」。企业侧却是营销表、博客、多个 Weaviate collection、数据仓库多表 schema 并存。（访谈观点）\n机制与约束 # 语义冲突（如德/奥对「湖/海」定义不同）在传统本体工程里靠 schema 硬约束；Bob 认为 prompt + 向量检索 可缓解部分歧义，Lewis 反驳规则式 prompt 会堆积成「无数条 regex」——最终精确控制仍可能回到 SQL / 形式化查询。（均为演讲者观点）\nLewis 强调：多源 RAG 需要中间 reasoning stack / agentic flow 做语义映射与 pipeline 选择（全发 vs 选择性调用）。这与文档侧「单 corpus 编排」形成 架构留白：需自建路由层。\n怎么做 # 为每个 业务域 建独立 corpus/collection；在应用层实现 意图分类 + corpus 路由（或 LangGraph/自研 agent）。ontology 与数仓 schema 的对齐仍是开放问题，勿假设 RAG Engine 自动理解「表即本体」。\n常见误区 # 把所有表塞进一个 corpus 指望模型自己懂 join。 用 prompt 替代数据治理——短期有效，长期维护成本可能不低于规则引擎。 单向 RAG 与「可写回」：generative feedback 的命名差 # 为什么 # 经典 RAG：query → retrieve → generate → 展示，数据流单向。Bob 提出 generative feedback loop：生成结果可 update/delete/校验并写回 向量库；collection 级 instruction 可在 ingest 时拒绝、警告或存修正版；Lewis 将其与 GAN 式双模型互评、agent 架构联系。（演讲者观点）\n机制与约束 # Weaviate 官方相近能力为 Transformation Agent（Technical Preview，文档写明 \u0026ldquo;Do not use in production\u0026rdquo;）：fetch → transform/enrich → write back，原地更新对象属性。与 Bob 所称 ingest 闸门式 generative feedback 机制不完全一致；术语 generative feedback loops 在 docs.weaviate.io 未检到同名专页。\nVertex RAG Engine 文档侧重检索增强生成，未 描述将 LLM 输出标准写回 corpus 的路径。MDM（主数据管理）场景：嘉宾称模型自动化约 70% 即重大进展，100% 不现实——无论文编号。（访谈观点）\n怎么做 # 若需写回：在 Weaviate 侧评估 Transformation Agent 或自研 post-generate pipeline；在 Vertex 侧仍将 RAG Engine 视为 读路径，写路径单独设计审计与版本。\n常见误区 # 把营销用语 generative feedback loops 直接等同于 GA 产品能力。 在生产环境启用 Preview Agent 且无回滚策略。 feedback loop 讨论段，OCR：Y je} = re} ® =。\nOCR：2 a. = fe] © =。\nGrounding：Search、企业 corpus 与 parametric knowledge # 为什么 # 企业 RAG 要 保守、可审计：事实来自外部源，而非可能过时的参数记忆。消费端先例是 Gemini 的 Grounding with Google Search——需要公开、最新 Web 知识时启用。\n机制与约束 # RAG Engine 与 Gemini：原生 tool 集成（博客原文 \u0026ldquo;natively integrated with Gemini API as a tool\u0026rdquo;）。（文献/文档支持） Lewis 愿景：模型像知道自己何时该 Search 一样，路由到 sales corpus / HR index——无 Google 公开训练卡或企业多 corpus 自动路由规范。（访谈观点） 抑制 parametric knowledge：举例「Alphabet Q4 2023 营收」——公开信息或在权重中，但企业要求 只信内部上下文；嘉宾提出 system instruction + 对比样本的 SFT 式行为，并自述 naive、非最终实现。（无法核实为官方能力） 怎么做 # 组合：RAG corpus 检索 + grounding 配置 + 明确 system instruction（禁止未引用断言）。多源时显式实现 router，勿假设模型自发选对库。\n常见误区 # 仅外挂 RAG、不处理模型「以为自己知道」——检索到了仍可能掺入参数知识。 将 Lewis 的训练设想写成已发布 Gemini 功能。 grounding 讨论，OCR：a ie} = lo} ® =。\n约 30 分钟：Lewis 手势说明中，画面仍为访谈，无 API 清单幻灯片。\n图、本体与 multi-vector：概念桥接，非默认架构 # 为什么 # 语义网长期投入本体与 KGM；嘉宾（含前 KG 从业者）倾向 向量库 + embedding + LLM 在大规模场景绕开严格实体关系，Lewis 仍保留「可有图结构，但遍历可由 LLM 革新」。（演讲者观点）\n机制与约束 # Weaviate named vectors：Collections can have multiple named vectors，查询需指定 target vector。Connor 类比「每对象多向量 ≈ 图中多条边」——为概念类比，非官方定义。向量库对比表写 Weaviate 具 built-in graph capabilities（cross-references），与 named vectors 是不同机制。\n怎么做 # 关系密集域可试验 多向量 + cross-reference；勿在未验证检索收益前全盘放弃数仓 schema 治理。\n常见误区 # 用 multi-vector 替代 join 与血缘文档。 把 schema.org 级本体实施难度低估为「embedding 即可」。 OCR：oO is} \u0026gt; 5 ® =。\n调参、成本与「类 AutoML」空白 # 为什么 # 生产上 token 成本推动 fine-tune / distillation；嘉宾亦转述第三方趋势：RAG 采用上升，prompt engineering 与 fine-tuning 热度下降——Menlo 报告原文本环境未核对（403）。（访谈转述，无法核实）\nBob 观点：基础模型「够好」后，fine-tuning 商业价值下降，企业首要问题是 用自己的数据怎么做。（演讲者观点）\n机制与约束 # Lewis 愿景：LLM 在 corpus 上建议 chunking/embedding，批量试跑（如 10 组）选最优——类似 AutoML，但 RAG Engine 今日无此 API。（访谈 + 文档否定项一致）匿名案例：百万级手册改阈值重跑 pipeline 约 一周——无第三方佐证。\nConnor 提及 DSPy 作优化框架——非 Google 内置。\n怎么做 # 用手动 fine-tune-rag-transformations 与离线 eval 迭代；蒸馏留给 高频、固定模板 查询。Bob 提议 20% 文档采样 推断其余 80% 重索引质量——研究向想法，无论文。\n常见误区 # 等待官方 AutoML for RAG 再上线——产品路线图未承诺。 为省检索成本跳过 eval 集建设。 OCR：© 5 = 5 ® =。\nOCR：© 8 = 5 ® =。\nAgent、工作流与评估：何时不必「全能 agent」 # 为什么 # 多 pipeline、多 corpus 时，需要推理 选哪条流水线 或并行调用。MDM 菜单式自动化：嘉宾称模型按人类菜单执行约 70%，余下靠经验。（访谈观点）Lewis 称 MDM 仍是 GenAI 浪潮约 2.5 年后 尚未被颠覆 的硬骨头。\n机制与约束 # Agent（开放工具、循环决策）vs Workflow（固定 DAG）的选择取决于：失败成本、可解释性、延迟预算。RAG Engine 提供的是 可嵌入能力块，不是替代 BPM 的全自动 MDM。\n评估应覆盖：检索命中率、引用忠实度、写回一致性（若有）、成本 per query——而非仅生成流畅度。\n怎么做 # 低歧义、单 corpus：workflow + RAG tool 往往足够。 多源、需消歧：agent + 显式 router + human-in-the-loop。 写回场景：单独定义 审计日志与批准门。 常见误区 # 为「显得先进」上 agent，却无工具边界与终止条件。 用 thumbs up/down 代替检索层失败分析。 约 31 分钟：三人分屏，讨论 enterprise grounding 与反馈，无指标表画面。\nOCR：2 He = 5 ® =。\n若你要落地 # 先钉死数据模型：按业务域划分 corpus/collection；接受 embedding–corpus 生命周期绑定，规划重建窗口而非假设热切换。 把预算花在 transformation：优先 layout/context-aware chunking 与注入质量；rerank 作为第二层；用与栈匹配的指标（勿照搬 Anthropic 49%/67%）。 多源必须自建 router：RAG Engine + Weaviate（Preview 集成）解决的是单链编排，不会自动替你完成 sea/lake 级语义映射或 MDM。 写回与 MDM 分路径设计：读用 RAG Engine；写用 Weaviate Transformation Agent（或自研）并视为实验特性，直到走出 Preview。 grounding 拆两层：检索上下文 + 禁止未引用断言的 instruction；若模型「已知」 públic 事实却与内部政策冲突，需产品层策略而非仅加检索——Lewis 所述训练抑制 未获官方文档支持，应做 POC 验证。 参考与延伸阅读 # Vertex AI RAG Engine 概览 向量数据库选项（含 Weaviate Preview） 将 Weaviate 与 RAG 搭配使用 RAG 快速入门 数据注入 使用嵌入模型（text-embedding-005 等） 微调 RAG 转换参数 Document AI 布局解析器 RAG 检索与重排 Introducing Vertex AI RAG Engine（产品博客） Generative AI 发布说明（2024-12-20 GA） 依托 Google 搜索进行接地 Weaviate 数据概念：named vectors Weaviate Transformation Agent Anthropic：Contextual Retrieval DSPy（提示与权重优化框架） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-vertex-ai-rag-engine-with-lewis-liu-and-bob-van-luijt-weaviate-podcast-1/","section":"文章","summary":"企业 RAG 的边界：托管流水线、向量库与「可写回」检索","title":"企业 RAG 的边界：托管流水线、向量库与「可写回」检索","type":"posts"},{"content":" 企业 RAG 与 Agent：当向量库遇上四十年分析软件 # 受监管行业里的 builder 常面对同一组张力：非结构化数据占多数、模型迭代快于合规审批、POC 密度远高于可运维生产。SAS 的 Saurabh Mishra 与 Weaviate 联合创始人 Bob van Luijt 在一期技术对谈里，分别从企业分析软件与开源向量数据库两侧拆解这些张力——下文按工程主题重组，不按时序复述；可核对主张绑定 RAG 原始论文、SAS Retrieval Agent Manager（RAM）、Weaviate 文档 等一手来源，其余标为演讲者观点。\n问题空间：三条并行曲线 # 企业 AI 落地很少卡在「能不能调 LLM」，而卡在三条曲线是否对齐：\n数据曲线：文档、手册、工单、扫描件持续变更；SAS 博客 称企业数据 \u0026gt;80% 为非结构化——该比例出自 SAS 二次陈述，未在文中给出可复现调研链接，宜作方向性判断而非审计数字。 模型曲线：BYO LLM、嵌入模型、量化与 ONNX 加速（RAM Features List 有提及）使推理侧可选型，但知识截止日期与合规边界不会随 API 版本自动解决。 组织曲线：B2C 产品（如 ChatGPT）的周活规模拉高高管对「AI 战略 / agent 数量」的焦虑（演讲者观点），与「企业 RAG 其实仍处早期」的工程判断并存——两条叙事不必收敛为同一结论。 开场段三分屏：左下背景标牌可见 Q Weaviate 与播客标识；画面为远程访谈，无可读架构幻灯。\n能力演进：Retrieval、RAG 与 Agent # 为什么 # 同一团队里「RAG」可能指检索增强生成模式，也可能指含分块、嵌入、入库的整条数据管道（SAS 侧强调后者权重更大，演讲者观点）。若不先对齐语义，排期与验收会错位。\n机制与约束 # Bob 将企业能力概括为 retrieval → RAG → agents 三阶段（演讲者观点）。学术上，Lewis et al., 2020 将 parametric memory（预训练 seq2seq）与 non-parametric memory（dense vector index）组合——索引可更新而无需重训全部生成参数，即「解耦」在架构层的含义；论文仍对联合组件做 fine-tune，不等于生产上永远零协调。\nSaurabh 补充时间尺度：行业「历史性」叙事往往只回溯一年；严肃 RAG 投入约自 2024 上半年起，且 「R 之前」 的数据准备被长期低估（演讲者观点）。\n怎么做（最小示例） # 概念上把三类 SLA 分开验收：索引新鲜度（小时/天）、检索 recall@K、生成 groundedness——勿用单一「聊天好评率」替代。\ningest_job → chunk_store + vector_index query → retrieve(k=8, filter=tenant_acl) → prompt → answer + citations 常见误区 # 把「上了向量库」等同于「完成了 RAG」。 用 Agent 编排掩盖劣质分块（garbage in, agent out）。 约 4 分钟：Saurabh 手势说明 RAG 史观与数据准备；三分屏访谈，无幻灯 API 名。\n同一主题前后帧：OCR 可见 @ Weaviate 角标片段；技术结论以口述与文档为准。\n「R 之前」：嵌入管道才是隐性主战场 # 为什么 # SAS RAM 产品页 定位处理 unstructured enterprise data（含 PDF、扫描件）。若 ingest 质量差，再强的生成模型也只能放大噪声。\n机制与约束 # Saurabh 称 SAS 在既有 复杂 AI 管道引擎 上验证文档→嵌入流程，可接 SAS 自训或外部模型（演讲者观点；引擎 SKU 未在公开 RAM 页点名，待架构白皮书核实）。访谈称 no-code UI 可在分钟级完成 document→embedding（演讲者观点，无公开 benchmark）。\n制造场景叙事（个案 / 演讲者观点）：遥测 ML 告警 → 文档/RAG 生成可执行维修计划；SAS 预测性维护博客 主张在 ML 检测之上叠加 RAG 编排，且 doesn\u0026rsquo;t replace your existing ML systems——与「预测 + 检索 + 生成」分层一致，但未披露具名客户与量化指标。\n怎么做（最小示例） # 为每个文档版本保留 doc_id、chunk_id、embedding_model_id，便于回溯与重嵌：\n{ \u0026#34;doc_id\u0026#34;: \u0026#34;manual-v3\u0026#34;, \u0026#34;chunk_id\u0026#34;: \u0026#34;manual-v3#p12\u0026#34;, \u0026#34;embedding_model\u0026#34;: \u0026#34;text-embedding-3-large\u0026#34;, \u0026#34;vector_ref\u0026#34;: \u0026#34;weaviate://Class/uuid\u0026#34; } 常见误区 # 只调 prompt，不版本化 chunk 与嵌入模型。 忽视扫描件 OCR 错误在向量空间的系统性偏差。 约 9–10 分钟：讨论 fine-tune 与 RAG 关系；主持方背景 Weaviate podcast 标识清晰。\n微调与 RAG：叠加，不是二选一 # 为什么 # 「已经 fine-tune 就不做知识库」在数据持续变更的场景下往往不成立。\n机制与约束 # Saurabh：微调模型仍是时间点快照；除非更新频率跟上业务数据，否则会 stale；RAG 用于注入新鲜事实（演讲者观点）。这与 RAG 论文 用显式非参数记忆缓解 updating world knowledge 的方向一致，但企业侧还可叠加领域微调——二者是模块组合，非互斥替换。\n怎么做 # domain_finetuned_llm + fresh_index(query) → grounded_answer 上线前用固定评测集对比：仅微调、仅 RAG、微调+RAG 三臂。\n常见误区 # 把微调当作「一次训练永久正确」。 在 regulated 场景用微调替代可审计的 citation 链。 框架 POC 与企业软件：两条速度曲线 # 为什么 # 企业 builder 常用 LangChain 等快速搭 demo；访谈称框架版本漂移使 POC→生产 困难（演讲者观点）。Bob 补充企业里长期存在「阻止你做某事的人」与「做新东西的人」——下文聚焦 builder，不代表治理侧多余（演讲者观点）。\n机制与约束 # Saurabh 归纳：框架「擅长 tinkering，不一定是 production-level software」（演讲者观点）。可核对事实：LangChain 仓库存在且活跃；对 POC→生产难度的评价无第三方 benchmark。\nRAM 侧对照：内置 automated / user-driven evaluations、plugin 式集成（Features List）——属产品承诺，非 LangChain 替代品的客观排名。\n怎么做 # POC 阶段就记录：依赖版本锁、检索指标、合规检查清单；生产迁移时优先换可运维边界（索引服务、eval 流水线、权限中间件），而非逐行搬运 notebook。\n常见误区 # 把 demo 延迟当作生产 SLA。 忽视「成功者一只手数得过来」与展会密度之间的反差（演讲者观点）。 约 12 分钟：谈框架与生产鸿沟；OCR 含 @ Weaviate pod 角标。\n权限、合规与 Agent：RBAC 并未换新范式 # 为什么 # AI 没有减轻「数据难」：治理、隐私 rigor 未放松（演讲者观点）。regulated 行业仍质疑把数据送进 OpenAI/Anthropic 的 prompt（演讲者观点）。\n机制与约束 # Bob 以 Weaviate Query Agent 讨论：对 agent 而言传统 RBAC「基本未变」（演讲者观点）。可核对：Weaviate RBAC 定义 roles → permissions → resources（collections、tenants 等）；Query Agent 为 Cloud 上自然语言查询服务，文档未写「Agent 不改变安全范式」——该句保留为访谈判断，与 RBAC 机制相容但不等同官方表述。\nSaurabh 同意：数据与安全 overlay 仍是老问题；AI 只是新入口。\n怎么做 # 检索前硬过滤 collection / tenant；LLM 调用走企业批准的 endpoint；日志中分离 prompt 内容 与 审计元数据 的保留策略。\n常见误区 # 认为「Agent 自主」即可绕过内容 ACL。 在多租户向量库只做应用层 filter，不同步权限撤销事件。 约 15 分钟：Query Agent 与 RBAC；OCR les] Weaviate Foc 为角标碎片。\n约 17 分钟：数据治理与安全 overlay；OCR fos] Weaviate podca。\n准确度契约：chat、eval 与 citation # 为什么 # RAG 场景下，用准确度交换业务价值，无法要求 100% 准确（Bob，演讲者观点）。LLM 在 workflow 内引入不可完全消除的非确定性（Saurabh，演讲者观点）。\n机制与约束 # 缓解路径（产品层，部分可核对）：\n手段 文档支持 访谈补充 自动化 / 用户驱动 eval Features List：optimal configuration champion–challenger 多路嵌入管道（术语未出现在官网） citation Auto-generates citations linking answers to source documents 点击 citation 滚动至 PDF 段落（UI 细节未在文档验证） HITL human-in-the-loop oversight mission-critical 分岔（演讲者观点） 官网营销句 Improve content accuracy by up to 40% 未给出基线、数据集与 metric——不可与 MRR、pass@k 等学术指标对齐。\n怎么做 # offline: golden_qa → score(retrieval, answer, citation_match) online: thumbs + sample human audit → champion config promotion 常见误区 # 用聊天「感觉对」代替 citation 是否覆盖关键句。 把 40% 提升当作可复现实验结论。 约 19 分钟：准确度与 eval；OCR 为 a fe Bay 3 = 等噪声，画面仍为访谈三分屏。\nAgent 边界：自治、人在回路与确定性混合 # 为什么 # 自治（autonomy） 被视作 agent 关键特征；mission-critical 场景下，完全自治与企业可接受风险冲突（演讲者观点）。自主 agent 的障碍大于 chat RAG：前者要求系统替人决策，后者允许人读完后行动（演讲者观点）。\n机制与约束 # 可并存模式（Saurabh，演讲者观点）：\nHuman-in-the-loop agent：执行到关键步骤暂停待批。 混合工作流：规则 / 传统 ML 处理裁决点，RAG/LLM 处理文书、摘要等非裁决环节（房贷类示例）。 Bob 侧 Query Agent 代表「自然语言 → 自主检索」；企业若上自治编排，仍需在策略层定义工具白名单与副作用上限。\n怎么做 # 为每个 agent 能力标注 autonomy_level（read-only / draft / execute-with-approval）并在编排器强制执行。\n常见误区 # 「上了 RAG 就能上 autonomous agent」的线性假设。 用单一聊天界面承载高后果操作且无审批链。 约 21 分钟：Agent 与 HITL；OCR we os =u 8. =。\n接续讨论混合工作流；OCR Oe. Be oe 3 =。\nRAM 与向量库：开放栈里的首选集成 # 为什么 # 企业需要 BYO LLM / BYO 向量库；但若一切皆可选，决策成本爆炸（演讲者观点）。合理解法是 开放架构 + first-class integration。\n机制与约束 # 可核对（RAM Features List）：\n向量库：PGVector 与 Weaviate 并列集成；plugin-based integrations、no vendor lock-in。 LLM：OpenAI、Azure OpenAI、Amazon Bedrock、Ollama 等。 citation、evaluation、ONNX / quantization 与 diverse hardware。 访谈独有 / 待官方确认：多嵌入模型、将支持 BYO embedding、显式 champion–challenger 流程；「RAM 已 GA」——产品页有 Get started today，抓取文本中未出现 GA 字样，宜写「已公开发布 / 可采购」。\nBob：Weaviate Python client（文档，PyPI weaviate-client，v4 默认 gRPC）更像 library 而非传统 DB driver（演讲者观点）——心智模型差异，非 API 规范术语。SAS 栈 Python 后端、React 前端、公司内亦有 Go（演讲者观点，RAM 实现语言未在官网证实）。\n访谈称 RAM 可在无 GPU 基础设施上通过架构选择加速（演讲者观点）；官网只写硬件多样性与 ONNX，未声明「无需 GPU」。\n怎么做 # # 概念：RAM 编排层 + Weaviate 作向量后端（伪代码） import weaviate client = weaviate.connect_to_local() # 或 Cloud；见官方 quickstart # ingest / hybrid_search / tenant 配置按 RBAC 与 schema 文档实施 选型时并行 PoC：同一 corpus 下对比 Weaviate 与 PGVector 的 hybrid、多租户、运维成本——勿仅凭品牌合作叙事拍板（SAS×Weaviate 合作动机来自访谈，官网未记载选型过程）。\n常见误区 # 把「first-class」理解成「唯一绑定」——文档并列 PGVector。 忽视 gRPC 50051 等网络策略（见 Python 客户端文档）。 约 27–28 分钟：RAM 与 Weaviate 集成；OCR MS Weaviate es podcast。\nPython 客户端讨论；OCR Py Weaviate OP podcast。\n同段：主持方背景 Weaviate podcast 完整字样。\n成熟度、TCO 与采购对话 # 为什么 # Builder 关注嵌入模型与框架；客户 demo 优先问部署成本与 TCO（演讲者观点）——关注点错位会直接导致 POC 无法立项。\n机制与约束 # Bob 类比 AI 企业落地可能比从业者想象的更早还要早——像互联网早于 Google/Amazon 上市（演讲者观点，未与渗透率数据对应）。Saurabh 类比零售被线上替代前夕：无人能预测破坏力，且 B2C 采用速度使组织难以吸收「其实还早」（演讲者观点）。\nRAM 入口重定向至产品页；FAQ 行业含 banking, insurance, manufacturing, health care, public sector——与访谈行业列举一致。GA 时间点、roadmap 细节以 SAS 发布说明为准。\n怎么做 # 商务材料准备：索引规模 → 存储与重嵌成本、查询 QPS → 向量库节点、合规 → 数据驻留与 LLM 路由，与技术指标并列。\n常见误区 # 只展示「更聪明的回答」，不展示 $/1M tokens 与 重索引周期。 用 consumer ChatGPT 体验推导 enterprise SLA。 约 34 分钟：RAM 发布与 TCO；OCR oD 2 irs S tyre =。\n约 28 分钟：Bob 抬手分点说明；三分屏 Weaviate podcast 标识。\n约 20 分钟：准确度与 citation 讨论前后；Saurabh 手势说明，无产品 UI 截图。\n若你要落地 # 先钉清「RAG」边界：验收 ingest（分块、嵌入版本、ACL）与 query（recall、citation）分开，再谈 Agent 自治级别。 向量库 PoC 用同一 corpus：对照 Weaviate 与 PGVector 的 hybrid、租户与运维；RAM 仅作参考集成列表，不替代你自己的 SLA。 把 RBAC 放在检索之前：对齐 Weaviate RBAC 或等价模型；regulated 场景单独评审 LLM 出站数据流。 用 eval + citation 建信任链：offline golden set 驱动配置晋升；线上 citation 至少到文档级，段落级滚动需在产品中实测。 高后果场景默认 HITL 或规则裁决：LLM 负责文书与摘要，不把信贷、安全等裁决点交给端到端自治（演讲者观点，与 RAM HITL 营销方向一致）。 参考与延伸阅读 # Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks（arXiv:2005.11401） RAG 论文 PDF SAS Retrieval Agent Manager 产品页 RAM Features List（Weaviate / PGVector、eval、citation、LLM 集成） SAS 预测性维护为何不止检索（制造 + RAG 叙事） Weaviate 开发者文档入口 Weaviate 文档 llms.txt（Query Agent、hybrid search 等索引） Weaviate 概念：Data structure 与 vector / embedding Weaviate Python 客户端（weaviate-client、gRPC） Weaviate RBAC 概述 Weaviate Query Agent 概述 LangChain GitHub 仓库 SAS Viya 平台页（模型治理与部署生态，非 RAM 架构白皮书） SAS 产品文档入口 ONNX 项目主页（RAM 加速相关技术背景） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-saurabh-mishra-and-bob-van-luijt-on-weaviate-and-sas-weaviate-podcast-12/","section":"文章","summary":"企业 RAG 与 Agent：当向量库遇上四十年分析软件","title":"企业 RAG 与 Agent：当向量库遇上四十年分析软件","type":"posts"},{"content":" 企业级 RAG 与 Agent：从拼接管线到可优化的整机 # ChatGPT 之后，大量团队把「检索 + 大模型」推上生产，却很快撞上同一堵墙：prompt 在场景漂移时脆，修 A 坏 B；向量库、reranker、生成器各自调参，没有人对最终用户是否满意负责。Contextual AI 联合创始人兼 CTO Amanpreet Singh 的论述（下文未单独标注处为演讲者观点）把问题框在 RAG 2.0、主动检索、偏好学习（KTO/APO） 与 LMUnit 式评估 四条轴上——与 Weaviate 向量基础设施形成生态互补，但本文只讨论可迁移的工程判断，不复述对谈时间线。\n公开材料与播客表述并不总一致：例如官方评测产品名为 LMUnit（字幕常误听为「LLLLM Unit」），开源 checkpoint 为 70B 级专用打分模型，与口语里的「小模型」有出入；BIRD text-to-SQL 在 官方博客 中写为曾 overall #1、当前 top 5（dev 完整配方约 73% execution accuracy），不宜笼统写成「现行 SOTA」。下文在冲突处会标明证据边界。\n问题空间：为什么「能 demo」不等于「能扛流量」 # 为什么：企业知识往往远在 context 窗口之外（嘉宾举例约 40 亿 token 量级、生产约 10k queries/min——无法从公开文档核实，仅作规模直觉）。在此量级下，5–10 条 few-shot 的 agentic RAG 演示可以工作，但 QPS 与偏好分布一拉开，失败模式无法全部写进 prompt。\n机制/约束：系统瓶颈常在 分布漂移（新文档、新术语、新合规规则）与 反馈稀疏（生产 thumbs 往往极度偏负——嘉宾称约 90% 负 / 10% 正，亦为演讲者经验，无公开统计）。纯 test-time 多轮检索不更新权重，每次请求仍是 cold start，域内缩写（如企业内 SAT 非 Scholastic Aptitude Test）难以靠 in-context 纠正持久生效（演讲者观点）。\n怎么做：把优化目标从「单次回答像不像训练集」改为「轨迹 + 终局反馈」——简单 RAG 也可视为 query → retrieval → generation → feedback 四元组；复杂 Agent 则是工具调用链上的同一闭环。\n常见误区：把「能调用检索工具」等同于生产就绪；把 latency 优化理解成「少检索几轮就好」——多轮时错误会在长上下文中复合（演讲者观点），与 DeepSeek-R1 所强调的可验证奖励 + 轨迹筛选是不同杠杆。\nWeaviate 播客双嘉宾画面，左侧 Weaviate podcast 标识、嘉宾身着 contextual-ai 品牌 T 恤（约 0:02）\n开场分屏：主持人侧 Weaviate podcast 背板，嘉宾侧 contextual.ai 标识，无技术幻灯片\nRAG 1.0 与 RAG 2.0：拼接 Frankenstein 还是联合优化 # 为什么：原始 RAG 论文（Lewis et al., 2020）已用 differentiable access 与可微调神经检索器做知识密集型任务；工业界常见做法却是 frozen embedding + 向量库 + 黑盒 LM，检索器收不到生成错误的梯度——嘉宾称之为 RAG 1.0 的「Frankenstein」拼接（与 Contextual AI 对 RAG 2.0 的定义 一致：pretrain / fine-tune / align 全组件，并对 LM 与 retriever backprop）。\n机制/约束：端到端不等于处处反传：可用领域 proxy 任务 或环境奖励近似终局目标（演讲者观点）。与只改 generator system prompt 相比，联合优化让 retriever 从「答错题」中学习该拉哪段语料——但需可扩展的数据与算力，且 discrete 超参（chunk size、top-k）组合空间巨大，嘉宾称内部 R\u0026amp;D 有应对，本期未公开方法。\n怎么做（概念层）：\n# 反馈闭环（示意，非 Contextual 专有 API） for (query, user_label) in production_logs: trace = rag_pipeline.run(query) # retrieval + generation (+ tools) loss = alignment_objective(trace, desirable=(user_label == \u0026#34;up\u0026#34;)) update(retriever, generator, optional_reranker_head) 常见误区：把 2020 论文直接贴上「RAG 2.0」商标；把「用户 thumbs」与 RAG 2.0 官方叙事混为一谈——文档强调 benchmark 级联合训练，生产 thumbs 闭环更多靠 KTO 等平台能力（见后文）。\n讨论 GA 与 text-to-SQL 时段画面，可见 Weaviate 与 contextual-ai 品牌条（约 0:04）\n检索范式讨论片段，OCR 含 Nilamtle Hayy 与 ctOoNtextual-ai 字样（约 0:07）\n检索范式：retrieve-then-read、multi-hop 与主动检索 # 为什么：静态「查一次、读一次」在财年/日历歧义、工具返回后再改查询等场景会失败——嘉宾以金融文档为例：需先看语料再决定下一轮 query（演讲者观点）。\n机制/约束（文献可核对部分）：\n范式 代表工作 行为摘要 retrieve-then-read RETRO、Fusion-in-Decoder (FiD) 检索 chunks / 多 passage 后一次条件生成；FiD 增加 passage 数可提分，但非 agentic 多轮工具调用 condensed multi-hop Baleen 潜在 hop 排序 + condensed retrieval，仍不同于「见文档后改 query」 active / orchestrated 平台 GA 文案 agent 按对话上下文编排检索与生成——产品表述，无统一论文定义 怎么做：在置信度低或冲突检测触发时发起第二轮检索/SQL；把 tool 输出写入 trace 供后续优化。structured 数据路径上，Contextual 宣称 text-to-SQL 在 BIRD 上为 best fully-local，完整 dev 配方约 73% EX（execution accuracy，见 BIRD 论文）——与嘉宾口语「sort of state-of-the-art」相比，写作应加 时间戳与 local/API 区分。\n常见误区：认为 multi-hop 并行拆 query 就够；忽视非参数工具上限（如 Notion API 只搜 title）——系统只能在工具能力内学「更少犯错地使用」，不能魔法超越 API（演讲者观点）。\n约 8 分钟分屏讨论：左侧 Weaviate podcast 标识，右侧嘉宾 contextual.ai T 恤，画面无架构幻灯片\n内部术语 hill climbing 讨论时段，OCR 含 contextual-ai 品牌条（约 0:11）\nAgentic RAG、test-time 计算与权重专精 # 为什么：Agentic 卖点（多轮检索+推理）在演示中靠 few-shot 即可惊艳；生产则要面对异构用户偏好，无法把海量失败案例全部塞进 prompt（演讲者观点）。\n机制/约束：test-time 增加 compute（更多检索/反思）不改变权重；企业术语无法沉淀进参数，则每次仍是通用先验 + 临时 context。嘉宾主张更稳妥路径是先把语料知识蒸馏进权重，再服务（演讲者观点），与 DeepSeek-R1-Zero「可跳过 SFT、用 RL 激励推理」的叙事形成对照——R1 侧强调 verifiable tasks 与强基座，嘉宾归纳的长文本 ~50k vocab 动作空间与 rejection sampling 非 R1 摘要逐句结论。\n怎么做：记录完整 trajectory（工具调用、检索片段、中间草稿），用验证信号（见下节 LMUnit）筛轨迹再更新；16 条人工偏好 → unroll 大量轨迹的方向，与 OpenAI RL fine-tuning API 类能力同构（演讲者对未来样本效率的判断）。\n常见误区：为降延迟盲目减少检索轮次，或为覆盖盲目增加轮次而不做轨迹级 eval；完全去掉 parametric knowledge 使模型退成「只复读检索片段的壳」——仍需 in-house groundedness / conflict resolution 微调（演讲者观点）。\nKTO 与整机优化讨论，OCR 含 contextual.ai 字样（约 0:11）\nalex 相关讨论帧，Milánttr Unie 与 contextual-ai 品牌条（约 0:23）\n评估：从字符串相等、LLM 评委到 LMUnit # 为什么：生产若达千级 QPM，用 GPT-4 级评委评每条回答，成本可与生成相当（演讲者观点）。非 ML 客户更需要「全局单元测试」：风格、离题、guardrails、groundedness、attribution——而非单一 BLEU 式字符串相等。\n机制/约束：LMUnit 论文 提出 natural language unit tests：输入 prompt + response + unit_test(自然语言准则)，由 LMUnit scoring model 输出 1–5 连续分（官方表述），可阈值化为 pass/fail，但主接口非二元。研究页称相对 GPT-4o / Claude 3.5 Sonnet 在 unit-test scoring 上高约 9%，RewardBench 93.5%；开源 ContextualAI/LMUnit 提供 Llama 3.1-70B 等 checkpoint——与「小模型、低延迟」口语冲突，写作宜用「专用评测模型」。\n怎么做：\n# 概念示例：LMUnit 范式（见官方 pip install lmunit） score = lmunit.evaluate( prompt=user_query, response=answer, unit_test=\u0026#34;回答必须引用文档 ID；不得编造未检索到的政策条款。\u0026#34;, ) pass_ = score \u0026gt;= threshold # 阈值由业务校准 多维「信用报告」：groundedness、attribution、style 等轴分别挂 rubric。主持人提及 Who Validates the Validators?（Shreya Shankar 等）——嘉宾立场是修 broken metric，而非无限叠评委（对话引用，原文本期未核对）。\n常见误区：把 LMUnit 分数当作通用「人类偏好」代理；忽视评测器自身 70B 部署成本；合成 QA 在极偏域连合理问句都写不出（嘉宾 SAT 域术语例，演讲者观点）——但上线 真实 user query 分布 仍可作为 query 侧 proxy。\n约 20 分钟 LMUnit 话题分屏：Weaviate podcast 与 contextual.ai 品牌同框\nLMUnit 讨论时段，OCR 含 SOntextugl.oi 等品牌识别噪声（约 0:22）\n约 19 分钟分屏：嘉宾讲解评测哲学，背景无技术 API 幻灯片\n评测管线讨论，OCR 含 Ailantir Naty 与 Sontextual-ai（约 0:24）\n偏好学习：KTO、APO 与「改 prompt 的上限」 # 为什么：RLHF 把句子级偏好压成标量 reward，信息损失大，却「居然有效」（演讲者观点）。企业生产常见只有 thumbs，且极度不平衡；KTO（Ethayarajh et al.）明确可从 desirable / undesirable 二元信号 学习，无需 pairwise「A 优于 B」。\n机制/约束：APO 论文（Anchored Preference Optimization and Contrastive Revisions，作者含 Amanpreet Singh）提出更可控的对齐目标；锚定与 KL / 参考模型 相关——嘉宾口语 「anchor gap 归一化间距」非论文标准术语，且 CLAIR 实验仍用 preference pairs。与 DSPy/MIPRO 等「用 LLM 改 prompt」相比，嘉宾认为多用户分布难以用自然语言概括，迟早要动权重，且单独 fine-tune 某模块不够，要调「整机旋钮」（演讲者观点）。\n怎么做：将 LMUnit 二元/阈值信号或用户 thumbs 接入 KTO/APO 目标；对非参数工具（Python 沙箱、只读 API）学习「在约束内更少犯错」而非幻想反传进 Postgres。\n常见误区：在 90/10 负样本下仍按平衡集采样；把 APO 当成纯 unpaired thumbs 算法——论文实验与 CLAIR_and_APO 代码 仍以 pairs 为主；只优化 generator 而让 retriever 永远 frozen。\n约 34 分钟 RLHF/R1/偏好优化话题：双嘉宾分屏，无公式幻灯片\nAPO/KTO 讨论帧，OCR 含 Sontextual.gj 字样（约 0:38）\n约 34 分钟 srtcue 帧：嘉宾阐述 specialization 路径\n可信、冲突与管线离散参数 # 为什么：企业场景要 audit trail / citations（嘉宾类比 Perplexity 式 enterprise 需求，演讲者观点）、uncertainty 触发人工升级，以及文档间冲突（recency、内网 vs 外搜、分析师偏好规则）。\n机制/约束：reranker 被描述为降低生成器难度的约束——与深度学习史上「先加结构再放松」同构（演讲者观点）。向量库侧可加 recency 表达式，与端到端学习并存，但受组件 API 限制。Chunk size、top-k、抽取策略可 A/B，但组合爆炸时需 分布匹配 + 限缩 action space（演讲者观点）。\n怎么做：声明冲突解析策略（显式规则 + 可选学习）；保留适度 parametric 推理能力，避免模型只会拼接 snippets；对 Llama 3.1 等基座采用 prompt + fine-tune + 整机优化 混合（演讲者观点；平台 benchmark 将其作基线见 2025 benchmarks 博客）。\n常见误区：认为加一个 GPT-4 评委就完成治理；在 Notion 只搜 title 的 API 上期待全文级召回；把 black-box DB 完全绕开优化——省事但能力封顶。\nchunk 与优化讨论，OCR 含 SOntextual-ai（约 0:50）\nreranker/Notion 讨论，OCR 含 contextUal.ai（约 0:53）\n结语分屏，OCR 含 Sontextual-ai 与 podcast 字样（约 0:57）\n两种工程信条（不必强行统一） # 信条 常见实践 嘉宾/官方强化 证据强度 System over models 单 foundation model 包办 检索、生成、验证、冲突解析分工 演讲者观点 + 产品叙事 Enterprise specialization 通用模型 + RAG 轨迹 + 偏好 + 可验证信号更新权重 KTO/APO/RAG2 部分有论文；规模数字 不可核实 约束先于端到端 端到端黑盒一切 reranker、专用模块降优化难度 演讲者观点 评测驱动训练 离线 benchmark 至上 LMUnit 信号作 reward LMUnit 文档 部分支持 Test-time compute 今日主流实现不改权重；若未来推理时亦更新参数，问题又回到 training 栈（演讲者观点）。与向量数据库（如 Weaviate）的分工是：存储与检索基础设施 vs 端到端 RAG/Agent 优化层——互补，非替代。\n若你要落地 # 先画轨迹，再选优化杠杆：记录 query → retrieval/tool → generation → feedback；区分「多轮 test-time」与「把失败蒸馏进权重」两条路径，别只用 latency 砍轮次。 评测产品化早于堆评委：用 LMUnit 式自然语言 rubric + 阈值校准，承认 1–5 分 + 专用 70B 评测器 的成本模型，勿假设「小模型免费」。 偏好信号按生产分布设计：优先 KTO 类 unpaired thumbs；若用 APO，阅读论文中的 paired / KL anchor 设定，勿照搬口语「anchor gap」。 检索范式与工具 API 对齐：核对是否为 retrieve-then-read；财年/术语类问题预留 语料反馈后再查询；BIRD 类 SQL 目标写明 local vs API、EX vs pass@k、dev 榜单时间。 保留 parametric 推理 + 显式冲突规则：reranker/recency/人工升级是约束，不是落后设计；在工具能力上限内做端到端「更好用法」学习。 参考与延伸阅读 # Contextual AI 官网 Introducing RAG 2.0 — 端到端联合优化 Retrieval-Augmented Generation for Knowledge-Intensive NLP (Lewis et al., 2020) RETRO: Improving language models by retrieving trillions of tokens Fusion-in-Decoder (Izacard \u0026amp; Grave, 2020) Baleen: Robust Multi-Hop Reasoning at Scale LMUnit 研究页与论文 arXiv:2412.13091 LMUnit 开源仓库 GitHub KTO: Model Alignment as Prospect Theoretic Optimization APO: Anchored Preference Optimization arXiv:2408.06266 BIRD Text-to-SQL Benchmark Contextual-SQL 与 BIRD 成绩说明（2025 博客） Platform GA 新闻稿（2025-01-15） DeepSeek-R1 技术报告 Weaviate 开发者文档 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-contextual-ai-with-amanpreet-singh-weaviate-podcast-114/","section":"文章","summary":"企业级 RAG 与 Agent：从拼接管线到可优化的整机","title":"企业级 RAG 与 Agent：从拼接管线到可优化的整机","type":"posts"},{"content":" 死后 JVM 崩溃分析：用 jcmd 读 core，而不是重学一套工具 # HotSpot 进程一旦以 SIGSEGV 等方式退出，运维手里通常有两类工件：JVM 写的 hs_err_pid*.log，以及操作系统写的 core dump（Windows 上常见为 minidump）。前者像「案发现场摘要」，后者才是完整内存镜像。多年来，要在 core 里看到 Java 线程、堆、类元数据，往往得走 Serviceability Agent（SA） 或 jhsdb 这条独立链路；而线上排障早已习惯 jcmd 的子命令。 JEP 528: Post-Mortem Crash Analysis with jcmd 的目标，是把 同一套诊断命令 延伸到 Linux core / Windows 死后环境，通过 process revival 映射崩溃时的 VM 状态，而不是再维护一套 Java 层的镜像实现。\n截至 JDK 24/25 的 Oracle jcmd 手册，Synopsis 仍只描述对 存活 PID 的附着；jcmd core.1234 …、-L、-c 等语法以 JEP 正文为准，落地前应以你实际使用的 JDK 构建验证。JEP 状态为 Candidate，目标 Release 27——下文能力边界以 JEP 与可复现命令为主，演讲中的分支名、平台细目等单独标注。\n在线排障里，JMX（JConsole、JMC）、JFR、JDI/JVMTI 与统一 JVM 日志各自覆盖监控、录制与断点场景；它们依赖 进程仍存活或可连。一旦 JVM 已死，上述通道大多失效，剩下 hs_err + core + 原生调试器 + SA/jhsdb。JEP 528 要补的，是其中 Java 级视图 这一段与 jcmd 对齐，而不是再发明第四种 CLI 方言。\n图：JDK 可服务性工具谱系中，jcmd 承担本地 attach 与 Thread.print / GC.heap_dump 等子命令——死后扩展意在延续这一入口。\n图：会议标题与 JEP 528 入口——死后诊断被定位为 jcmd 的扩展，而非全新 CLI。\n崩溃工件：文本摘要与内存镜像各管什么 # 为什么：生产事故里最先需要的是「能转发、能全文检索」的线索；但 hs_err 给不出可遍历的堆图，也无法替代「某对象在崩溃瞬间是否仍被引用」这类问题。\n机制/约束：JEP 528 Motivation 将分工写得很清楚：HotSpot 在致命错误时写出 hs_err_pidXXX.log（失败线程栈、已加载库、VM 版本等）；OS 将进程地址空间保存为 core dump。Windows 上演讲常用 minidump 一词；JEP 正文仍以 core dump 统称，未规定 Windows 文件格式细节（属 OS/运维范畴）。\n怎么做：\n# 收集（Linux 示例） cp hs_err_pid*.log /case/run-20260517/ cp core.* /case/run-20260517/ ls -la /case/run-20260517/ # 目标侧：允许生成 core（依发行版固化到 runbook） ulimit -c unlimited 常见误区：只留 hs_err 就关闭工单——许多 Metaspace / 堆 / 锁 问题需要 core 上的 GC.* / VM.*；反过来，只拷 core 却不留 hs_err，会丢掉 JVM 自报的 错误类型、编译线程、VM 参数 等上下文。\n图：hs_err 为文本摘要；core/minidump 为 OS 级内存镜像——死后分析通常两者都要。\n原生调试器够不到 Java 语义 # 为什么：gdb / WinDbg 擅长 库、符号、原生栈；崩溃若发生在 JIT 代码、解释器、GC 屏障 附近，工程师更需要 哪条 Java 线程、锁谁持有、堆顶是什么。\n机制/约束：JEP 明确：用 gdb 分析 core 时，无法把 JVM 内部结构直接解释成应用级 Java 状态；解码某个 oop 的 class 需要手工读对象头（参见 JEP 450 对象头）。JEP Alternatives 同时强调：原生调试器仍是 JDK 排障的必要组成部分——jcmd 不是要取代 gdb 的任意地址检视。\n怎么做：\ngdb /path/to/java core.1852094 # 同一 core，JEP 目标语法（GA 可能尚未支持）： /path/to/jdk/bin/jcmd core.1852094 Thread.print 常见误区：在 ?? () 帧上纠结「缺符号」却忽略 Java 栈；或期望 WinDbg 单独给出 java.lang.Thread.State——那是 JVM 诊断层的事。\n图：原生栈里 JVM C++ 符号可见，JIT/解释路径常解析为 ?? ()，无法直接对应 Java 方法。\n从 SA 双轨维护到单一 jcmd 入口 # 为什么： HotSpot Serviceability 中的 SA 能在 运行进程与 core 上暴露 Java 堆与 HotSpot 结构，但 JEP Motivation 指出其实现 brittle、dated，需随 VM 演进持续维护。在线场景里，jcmd 经 Attach API 已承载 Thread.print、GC.heap_dump 等；死后若再维护 jhsdb + SA 平行实现，成本高且易漂移。\n机制/约束（部分为演讲者观点）：演讲提到 SA 与 HotSpot 源码 约 13 万行 级镜像同步、ZGC 支持不足等——JEP 未给出行数或 ZGC 细节。JEP Goals 是聚焦 jcmd、降低对 jhsdb/SA 的维护投入，并非 立即移除现有工具。\n怎么做（统一入口示意）：\n# 存活 JVM（GA 已支持） jcmd \u0026lt;pid\u0026gt; Thread.print # 死后（JEP 528 目标） jcmd core.1852094 Thread.print 常见误区：在 JEP 落地前假设 jstack/jmap 已废弃——它们仍广泛存在于旧 runbook；死后分析应规划迁移到 jcmd + core，而非混用多套脚本且版本不一致。\nCore revival：映射内存，复用既有 DCmd # 为什么：若死后诊断重写一套「Java 镜像」，将与 HotSpot 内部结构强耦合；JEP 选择 子进程 revival：把 core 按原虚拟地址 映射回来，使 指针仍有效，再调用与 live Attach 相同的 native 诊断实现。\n机制/约束：Reviving a core dump 规定：\njcmd 启动子进程，mmap core，恢复 Java heap、元空间、线程栈 等； 在原地址加载崩溃时的 libjvm.so（Windows 为 jvm.dll，演讲用语）； 不执行 Java 代码、不触发 GC；可调用解释数据结构的 native JVM 函数； 事后分析只需 core + 崩溃 JVM 二进制；不必 加载业务 .so 全集（跨机搬运友好）。 OpenJDK jmm.h 中的 ExecuteDiagnosticCommand 与 live 路径一致；幻灯中的 DCmd::parse_and_execute 为实现层说法，非 JEP 正文符号。\n怎么做（sandbox 试验构建，分支名未在公开 openjdk/jdk 验证，属演讲者口径）：\n# 需 JEP 528 实现所在的 JDK 构建 build/*/images/jdk/bin/jcmd -L /prod/jdk/lib/server/libjvm.so ./core.1852094 Thread.print 常见误区：以为 revival 会 继续跑业务线程——子进程只是 只读重演内存布局；在 core 上执行 GC.run、JFR.start 等 运行态 命令不符合 JEP Non-Goals / Future Work（Java 实现的 DCmd 与 revival 不兼容）。\n图：live 路径上 jcmd → Attach API → DCmd::parse_and_execute；死后改为对 revival 子进程中的 VM 映像发同类命令。\n图：helper 从 core 映射 Java Heap、线程栈 等于崩溃 VA；helper 自身 libc 可与生产不同地址，不影响 JVM 结构解读。\nThread.print：锁与 safepoint 的可信度 # 为什么：死后最先要回答的往往是 谁阻塞谁、崩溃线程卡在何处。\n机制/约束：JEP 将 Thread.print 列入死后 26 个可用命令之一（相对 live 约 57 个）。输出含 java.lang.Thread.State、BLOCKED (on object monitor)、_at_safepoint 等，与 live 线程转储 同类。\n怎么做：\njcmd core.1852094 Thread.print \u0026gt; threads-at-crash.txt # 与事故前 live 抓取对比（若有） diff threads-live.txt threads-at-crash.txt 常见误区：把死后输出里的 cpu=0.95ms 当作性能数据——演讲者观点：对已死进程 CPU 时间无意义；JEP 示例行仍含 cpu= 字段，规范未定义 core 上该字段语义，分析时应忽略。\n图：jcmd core.1852094 Thread.print 中 Thread-2 为 BLOCKED (on object monitor)，HotSpot 侧 _at_safepoint，栈顶 Threads$ThreadRunnable.runnableMethod2。\n堆诊断：GC.heap_info、GC.heap_dump、GC.class_histogram # 堆布局与 hprof 导出 # 为什么：hs_err 里堆地址需要人工拼接的时代，导出 .hprof 往往依赖 SA 脚本；统一 jcmd 可降低 runbook 碎片。\n机制/约束：JEP 死后列表含 GC.heap_info、GC.heap_dump。GA 手册对 live 的语义： GC.heap_info 报告布局；GC.heap_dump 生成 .hprof（filename、-all 等选项）。core 上是否支持这些选项的组合，以实现为准。\n怎么做：\njcmd ./core.1852094 GC.heap_info jcmd ./core.1852094 GC.heap_dump /tmp/postmortem.hprof 常见误区：认为 GC.heap_dump 会触发 GC——在死后 revival 中 不发生 GC；导出的是 崩溃瞬间冻结的堆图。\n类直方图 # 为什么：需要快速判断 byte[]、某业务类 是否在崩溃时异常膨胀，而不立刻开 MAT。\n机制/约束：GC.class_histogram 在 GA 中标记 Impact: High（与堆大小相关）；死后输出为 crash site 快照，#instances / #bytes 列与 live 格式一致。模块后缀如 (java.base@27-internal) 来自演示 JDK，非规范固定字符串。\n怎么做：\njcmd core.1852094 GC.class_histogram | head -40 常见误区：用直方图做 引用链分析——它只有 按类聚合；引用链仍需 heap dump 或 JEP Future Work 中的 任意对象检视（演讲称规划中的 VM.inspect，JEP 未使用该符号名）。\n![GC.class_histogram：8802 个 B 实例约占 392760 字节，java.lang.String 等紧随其后\n图：GC.class_histogram 前几行：[B 8802 实例、392760 字节；java.lang.Class、java.lang.String 等带 java.base@27-internal 模块标注。\n类与元空间：VM.classes、VM.classloader_stats、VM.metaspace # 已加载类与初始化状态 # 为什么：崩溃若发生在 类初始化、\u0026lt;clinit\u0026gt;、复杂 ClassLoader 树 上，需要知道类是否 fully_initialized。\n机制/约束：JEP 死后命令含 VM.classes、VM.classloader_stats（注意官方全名，不是 演讲口语 ClassLoader.stats）。 VM.classes 支持 -verbose 输出更细字段；幻灯 OCR 中的 KlassAddr、State、Flags 手册未逐列保证，以目标构建输出为准。\n怎么做：\njcmd core.1852094 VM.classes | rg \u0026#39;MyApp|fully_initialized\u0026#39; jcmd core.1852094 VM.classloader_stats 常见误区：脚本里写 ClassLoader.stats 导致 命令不存在——应改为 VM.classloader_stats。\n图：VM.classes 表头含 KlassAddr Size State Flags ClassName；示例行 fully_initialized、java.lang.Shutdown、jdk.internal.misc.Blocker 等。\nMetaspace 用量 # 为什么：Metaspace OOME 或 class space 压力类崩溃，需要在死后核对 used / committed / reserved 与 shared classes。\n机制/约束：JEP 含 VM.metaspace；手册列有 basic、show-loaders、by-chunktype 等选项。默认输出粒度 以实现为准。\n怎么做：\njcmd core.1852094 VM.metaspace \u0026gt; meta-at-crash.txt 常见误区：只盯 used 忽视 reserved——与 hs_err 对照时，两者可能讲述 提交 vs 预留 的不同侧面；应以 VM.metaspace 易扫读行为主、hs_err 为交叉验证。\n图：Metaspace used 72K, committed 192K, reserved 1114112K；497 classes (492 shared) 及 chunk 级 Non-Class / Class 分段。\nCLI：-L、-c 与 help 的匹配规则 # 为什么：core 常被拷到笔记本分析；jcmd 版本 与 崩溃 JVM 版本 可能不同，但 libjvm.so 必须对齐。\n机制/约束（JEP post-mortem environments）：\n-L /path/to/libjvm.so：指定崩溃 JVM 的二进制； -c ./core：参数为 core 文件，避免与 main class 名 冲突； 分析用 jcmd 可与崩溃 JDK 不同版本，但须能加载 崩溃时的 JVM 二进制； 死后环境须与崩溃点 相同 OS 与 CPU 架构。 演讲者观点：jcmd \u0026lt;core\u0026gt; help 只列出 core 可用子集（JEP 用「57 中有 26 个」描述范围，未单独定义 help 过滤算法）；OL9/RHEL9 上与生产一致的 libc.so 映射可降低 revival 失败率——非 JEP 条文。\n怎么做：\njcmd -c ./core.1852094 help jcmd -L /opt/jdk-prod/lib/server/libjvm.so ./core.1852094 Thread.print 常见误区：笔记本装了 JDK 21 的 jcmd，却用 JDK 17 的 core 却不传 -L 指向 17 的 libjvm.so；或把 core 拷到 不同架构 的机器上分析。\n图：死后接口从 jcmd \u0026lt;pid\u0026gt; 扩展为对 $COREFILE 执行 help 与子命令。\n能力边界与交付节奏 # 为什么：避免对 未 GA 能力过度承诺，也避免用 jcmd 替代 所有 原生调试。\n机制/约束：\n主题 JEP / 事实 演讲者补充 与 gdb 关系 Non-Goals：不能替代原生调试器 WinDbg 场景类比 运行态命令 死后 无 JFR.start/JFR.stop 等 GC.run 亦不在死后列表 任意对象检视 Future Work：inspect arbitrary Java objects 口语 VM.inspect，无 JDK 27 排期承诺 平台 Goals：Linux + Windows；Future Work：macOS 细化为 x64 / AArch64 三元组 状态 Candidate，Release 27，JDK-8328351 sandbox 分支 jcmd_core_process_revival 未在公开 GitHub 检出 怎么做（边界自检）：\njcmd core.1852094 help | rg -i jfr # 预期无 JFR.start/stop 常见误区：在 core 上找 JFR.start；或认为 jcmd 能 inspect 任意地址的对象——当前应使用 gdb/jhsdb，直至 Future Work 落地。\n图：JEP 528 将 broad selection of jcmd commands 带到 Linux x64 / Linux aarch64 / Windows x64；工程状态为向 code review 推进（幻灯口径）。\n建议的死后排查顺序 # 若你维护的是 HotSpot 崩溃 runbook，在 JEP 528 可用后，可按「先廉价、后沉重」收敛证据链：\n读 hs_err：信号、问题线程、VM 版本、-XX: 参数。 Thread.print：锁、Java 栈、safepoint。 VM.metaspace / VM.classes / VM.classloader_stats：元空间与类加载器。 GC.class_histogram：对象组成是否异常。 需要 MAT 时 GC.heap_dump。 VM 原生缺陷或 jcmd 无法覆盖的地址，再用 gdb。 参考与延伸阅读 # JEP 528: Post-Mortem Crash Analysis with jcmd — 死后 jcmd 的权威范围、revival 机制与 26 个命令列表 JEP 528 — Reviving a core dump — 子进程映射、禁止执行 Java/GC JEP 528 — jcmd in post-mortem environments — -L、-c、跨版本与 OS/CPU 约束 JEP 528 — Non-Goals — 不替代 gdb、不运行 Java 代码 JEP 528 — Future Work — 任意对象检视、class dumping、与 JFR 的边界 JDK-8328351 — JEP 528 跟踪 Issue Oracle JDK 24 jcmd 手册 — live 子命令语义（Thread.print、GC.*、VM.*） Oracle JDK 24 jhsdb 手册 — 现有 core 分析入口对照 HotSpot Serviceability — Serviceability Agent 与工具生态 HotSpot Serviceability — Serviceability Agent — SA 能力与维护背景 JEP 450: Compact Object Headers — 原生调试器手工解码 oop 时的对象头参考 OpenJDK jmm.h — ExecuteDiagnosticCommand — Attach 路径上的诊断命令派发 Wikipedia: Core dump — OS 级 core 概念 Java SE Troubleshooting Guide — Oracle 官方排障总览 Unified JVM Logging — 与死后分析互补的运行期日志 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-post-mortem-jvm-crash-analysis-with-jcmd/","section":"文章","summary":"死后 JVM 崩溃分析：用 jcmd 读 core，而不是重学一套工具","title":"死后 JVM 崩溃分析：用 jcmd 读 core，而不是重学一套工具","type":"posts"},{"content":" 向量库上的 Query Agent：可审计检索与两种「问数据」模式 # 团队把 RAG 接到业务库之后，很快会遇到一类重复劳动：把自然语言拆成 collection 路由、hybrid 检索、属性 filter、聚合，再决定要不要生成带引用的答案。通用 Agent 框架能编排这些步骤，但每一步都要自己接 Weaviate API、处理 schema 漂移与空结果重试。Weaviate Query Agent 走的是另一条路——把「会调用 Weaviate 的 agent」做成数据库侧能力，对外暴露 Ask Mode 与 Search Mode 两种入口。本文按可核对文档与仍属产品/访谈主张分层讨论，不预设「一种模式替代全部自研 RAG」。\n画面标题：Weaviate Query Agent Charles Pierse #128（节目标识帧）。\n章节提纲：Podcast Chapters 含 From Alpha to GA、Schema Introspection、Multi-Collection Routing、Search Mode 等主题。\n问题空间：Chat RAG、通用 Agent 与「数据库的自然语言接口」 # 为什么单独谈 Query Agent # 常见做法是：向量检索 + LLM 生成，或 n8n 式 DAG 把多步串起来。前者往往只暴露「最终段落」，审计时看不到实际 filter 与 limit；后者灵活，但 Weaviate 特有算子（多 collection、聚合、hybrid 参数）都要自己维护。Query Agent 的定位更接近 agent-first data access（演讲者观点）：专长是读懂 schema、下发可复现的 Weaviate 查询，而不是通用任务规划。这与 Compound AI 里「多模型 + 检索 + 工具」的宏观图景相容，但实现边界更窄——未验证其能否替代跨 Slack、数仓的异构编排。\n机制与约束 # 运行环境：文档写明面向 Weaviate Cloud（Sandbox 可试用）；自托管是否等价支持需查当前版本说明（未在本文环境复现）。 计费粒度（产品页，2026-05 核实）：Ask 约 4 requests/query，Search 约 1 request/query——集成 Search 作「检索 API」在成本上往往更省，但不等于检索质量自动优于自写 hybrid（见下文评测边界）。 GA 时间线：weaviate-agents v1.0.0 发布于 2025-09-16（GitHub Release）；首包 v0.3.3 为 2025-02-28，与访谈中「约 2025 年 3 月 preview、约六个月反馈」大致一致，非精确日历。 怎么做（最小示例） # from weaviate.agents.query import QueryAgent from weaviate.classes.agents.query import QueryAgentCollectionConfig qa = QueryAgent( client=client, collections=[ QueryAgentCollectionConfig(name=\u0026#34;Jeopardy\u0026#34;, views=[\u0026#34;default\u0026#34;]), ], ) # Ask：答案 + 溯源 + 审计字段 ask_resp = qa.ask(\u0026#34;Which category appears most often?\u0026#34;) # Search：仅检索，无 final_answer search_resp = qa.search(\u0026#34;questions about European history\u0026#34;, limit=10) （完整连接、Auth、推理 API Key 见 Usage — Instantiate。）\nCloud 控制台与 SDK 的分工 # 已核实方向：Weaviate Cloud 提供 Agents 页，可选 collection、向量化模块与 system prompt，并用自然语言框试用（见图 ocr_pick_004）。这对产品/数据同学是零代码冒烟路径；工程落地仍应走 weaviate-agents SDK，把 同一问句 在控制台与 ask/search 的 searches 输出对齐，避免「演示可用、流水线不一致」。控制台展示的聚合 JSON（如 TOP_OCCURRENCES）与 SDK 的 aggregations 字段应对同一套语义，但 UI 字段名 未 与 OpenAPI 逐字段核对。\n常见误区 # 把「两行调用」当成零配置：collection 列表、向量化模块、Cloud 凭据仍要显式准备（演讲者观点中的「两行」是营销式简化）。 默认 Query Agent 等于「通用自主 Agent」——超出 Weaviate 查询/聚合的能力不在承诺范围内。 只在控制台验收、未把 searches/aggregations 纳入 CI——回归时无法发现路由或 filter 漂移。 Weaviate Cloud：Sandbox 集群 query-agent-demo，Database version 1.32.0，REST @ gRPC Endpoint 可见。\n控制台侧栏：Clusters、Query、Collections 与 Agents 入口；气泡标注 The Query Agent。\nAsk Mode 与 Search Mode：同一 NL，不同目标函数 # 为什么 GA 要拆两种模式 # 早期 run 路径偏 端到端：路由 → 多路检索 → 扩展 → 写答案（演讲者观点）。集成方常见做法是 丢弃生成答案，只用 sources / search 结果 喂给自有 agent——产品反馈直接催生了 Search Mode（Usage 写明 retrieval only, no answer generation）。Ask 面向「要给终端用户一段话 + 引用」；Search 面向「我要高质量 IR，生成在自己栈里做」。\n维度 Ask Mode Search Mode 输出 final_answer + sources + 审计字段 search_results（QueryReturn） 文档定义 含答案生成 无答案生成 典型集成 聊天、报告摘要 自有 LLM、排序、重排后再生成 质量主张（访谈） Grounded 答案 + 对象级引用 须相对裸 hybrid 可感知提升（无公开 uplift 表，见 P09） 机制：Search 流水线里有什么 # 已核实（文档示例 JSON）：Search 响应可含 filters（如 price \u0026lt; 100）、metadata 中的 rerank_score，说明存在重排信号。访谈补充、文档未逐步列出：query decomposition、term expansion、overfetch 再 cross-encoder/listwise rerank（演讲者观点）。因此 Search Mode 的增益更可能来自 filter 缩小候选空间 + 复合 IR 流水线，而非新的 BM25/向量核函数——这与「只靠更大 embedding 模型」的叙事并不相同。\n怎么做 # 多轮对话在 GA 用 ChatMessage 列表（Conversational queries）；v1.0.0 起 run 已弃用，指向 V2 API（Release v1.0.0）。访谈称 alpha 未原生支持 chat、用户自行 workaround——无官方 changelog 逐条对照，属时间线叙述。\n常见误区 # 认为 Search 一定比 Ask「更强」——文档只区分职责，未保证 Search 在所有数据集上优于你调参后的 hybrid。 在 Search 路径仍期待 final_answer——应改读 SearchModeResponse.search_results。 Agents 页：Jeopardy collection、text2vec-weaviate，输入框 Ask me something about your data\u0026hellip;\n控制台展开 Aggregations Executed：collection Jeopardy，metrics TOP_OCCURRENCES，topOccurrencesLimit 1。\n可审计响应：queries、aggregations 与「部分答案」 # 为什么审计轨迹比「一段 Markdown」重要 # 合规与调试场景需要回答：模型到底查了哪张表、用了什么 filter、有没有跑聚合。Weaviate 把 agent 实际下发的 searches 与 aggregations 放进响应（Inspect responses），便于用 REST/客户端 手工复现同一查询。Ask 模式另有 is_partial_answer、missing_information（AskModeResponse 源码）——显式标记覆盖不全，比静默幻觉略可控。\n机制与约束 # sources：object_id + collection（文档 Sources 段）——对象级溯源，不是全文逐句 citation 协议。 聚合指标名（如 TOP_OCCURRENCES）与控制台演示 JSON 一致；字段全集以运行时 OpenAPI/JSON 为准（未逐字段对照 OpenAPI）。 两阶段引用（先 final_answer，再独立 citation 步骤）：Charles Pierse 演讲者观点；公开文档只承诺有 sources，未描述步骤数或独立 citation agent（客户端源码亦无 citation 符号）。 怎么做 # print(ask_resp.searches) # 含 queries、filters、collection print(ask_resp.aggregations) # 无聚合时文档示例为 No Aggregations Run print(ask_resp.is_partial_answer, ask_resp.missing_information) for s in ask_resp.sources or []: print(s.collection, s.object_id) Citations 与生成解耦：工程含义 # 若两阶段流水线属实（演讲者观点），产品行为更接近：先在检索结果上合成答案，再把对象绑定到 sources，而不是在单步生成里「边写边贴脚注」。这对评估的影响是：应分别测 检索召回（searches 是否覆盖真值对象）与 归因准确率（sources 是否支持 final_answer 中的关键断言）。医疗、合规等场景，访谈建议 heavier citation 子 agent 或多轮校验——未在 GA 文档中作为内置模式提供。\n常见误区 # 有 sources 就等于答案正确——访谈明确 citations 非银弹；可能出现「引用牵强但存在」。 把 sources 等同于论文里的 attribution metric——未报告 nDCG、faithfulness 等（评测缺口，见下节）。 在 Ask 路径忽略 is_partial_answer——用户会看到流畅文本，但系统已声明信息不足。 Schema 内省、property description 与结构化 filter # 为什么 schema 文档突然变「便宜杠杆」 # 纯语义检索擅长「夏天沙滩鞋」这类描述；价格区间、日期、枚举 更适合显式 filter（对话中的共识，与 Weaviate Filters 能力一致）。Query Agent 启动时会 分析 collection 与 property descriptions（Overview — Query Agent context）。多年未被重视的 description 字段，在 agent 路由与 filter 生成里变成 zero-shot 提示：例如国家字段注明 ISO 3166-1 alpha-2，可减少 filter 写成 Ireland 而非 IE 的失败（演讲者观点 + 文档方向一致）。\n机制 # 客户端模型暴露按类型的 filter：INTEGER、TEXT、BOOLEAN、TEXT_ARRAY、DATE、UUID 等（KnownFilterType）。访谈称结合 structured output 在执行前拒绝无效算子组合，以降低 retry；文档写 agent 生成 filter，未承诺「零 retry」或「执行前必拒绝」。另一点访谈强调：无法预先知道带 filter 的查询是否非空——空结果仍需运行时处理。\n怎么做 # 在 schema 中为关键属性写清语义与编码：\n{ \u0026#34;name\u0026#34;: \u0026#34;country_code\u0026#34;, \u0026#34;dataType\u0026#34;: [\u0026#34;text\u0026#34;], \u0026#34;description\u0026#34;: \u0026#34;ISO 3166-1 alpha-2 country code, e.g. IE for Ireland\u0026#34; } 常见误区 # 空 collection 或缺 description 仍期待稳定路由——应用测试覆盖「冷启动」与「字段歧义」。 把 text-to-SQL 经验直接套用：向量库的 filter 与 SQL 优化器假设不同。 远程访谈分屏：左侧主持带 Weaviate podcast 背板，右侧嘉宾 Charles Pierse。\n讨论 Schema Introspection 与 filter 时的双人对谈画面。\n多 Collection：路由、联邦检索与「语义 join」 # 为什么「选一个 collection」不够 # 产品页与 README 提到 cross-collection routing（产品页）。访谈观察到：contracts / customers 等 语义相关但无显式 FK 的 collection，需要 多库查询、结果交错（interleaved）返回（演讲者观点）；官方文档 未出现 interleaved 一词。嘉宾用语 semantic joins 指运行时依 schema 元数据关联意图——与 SQL join 互补而非替代（未验证与具体 GraphQL 查询一一对应）。\n机制 # 构造时：QueryAgent(client, collections=[...])；运行时可在 ask / search 传入 collections 或 QueryAgentCollectionConfig（含 views）。 若只需单表，仍应显式收窄 collection 列表，避免路由漂移。 怎么做 # qa = QueryAgent( client=client, collections=[ QueryAgentCollectionConfig(name=\u0026#34;Meals\u0026#34;, views=[\u0026#34;default\u0026#34;]), QueryAgentCollectionConfig(name=\u0026#34;RecoveryArticles\u0026#34;, views=[\u0026#34;default\u0026#34;]), ], ) qa.ask(\u0026#34;High-protein dinners under 600 kcal last week and recovery tips\u0026#34;) 常见误区 # 多 collection 等于自动 ER 建模——无 schema 描述时，联邦结果可能杂乱。 期待 SQL 式确定性 join——agent 路由是概率性的，需 eval。 谈 Multi-Collection Routing 时的嘉宾手势帧（画面无 API 幻灯）。\n同期分屏：左侧可见 Florida Atlantic University 证书框与 Weaviate podcast 标识。\n评测、BEIR 与尚未公开的 uplift # 为什么「优于 pure hybrid」不能写死数字 # 访谈称 Search Mode 相对 pure hybrid 有 published 提升，且 Connor Shorten 提到在 BEIR 等基准上做过实验（演讲者观点）。截至 2026-05 对 docs.weaviate.io/agents/**、产品页与常见博客的检索，未找到含 BEIR 子集、nDCG@k、Recall@k 或相对 hybrid（α、limit、是否 rerank）对照的公开表。BEIR 论文本身强调 零样本、异质集合；指标口径（nDCG@10、MRR 等）与 Weaviate hybrid API 参数 未对齐 前，只能引用口述，不能写「提升 X%」。\n部分核实：流水线组件（filter、decomposition、expansion、rerank）属于业界常见组合；无法核实：具体 uplift 与实验配置链接。\n建议的自测清单（可复现） # 在你方 collection 上固定 20–50 条「金标问句」，每条记录：期望 collection、是否应出现 filter、期望 top-k 对象 id。对比三条路径：(1) 手写 hybrid；(2) QueryAgent.search；(3) 可选 Ask。记录 searches 中的 filters 与 rerank_score 分布，而不是只评 LLM 答案 BLEU。若 Search 仅在「带价格/日期约束」子集上胜出，说明收益来自 约束检索 而非向量语义本身——这与访谈中对 Search Mode 机制的判断一致（演讲者观点）。\n常见误区 # 把营销句「better than hybrid」当成你数据集上的保证——应在自有 schema 上做 A/B，并固定 alpha、limit、targetVectors。 用 BEIR 总分横向对比不同厂商幻灯片——子集与预处理不一致时无意义。 只评端到端问答 F1，不保存 searches 日志——出问题时无法区分「路由错了」还是「生成胡说」。 MetaBuddy 与边界：案例、租户、未来方向 # MetaBuddy（健身/营养结构化数据：meals、nutrition、exercises）被 Charles Pierse 称为早期用户，用于压测 filter、date filter、aggregations 与跨 collection 问句（演讲者观点；无第三方案例稿或审计数据，无法核实业务成效）。租户（tenants）在客户端口述中被提及，无实现细节。未来方向包括更长时的 Research / reSearch 与 memory（访谈；客户端 v1.2.0 Research mode 已存在，与口述名称不完全一致）——非 GA 承诺。\n访谈中「Agent 入门易、约八成时间在 edge case」与「一周上线生产 agent」的市场话术形成张力（演讲者观点）。若你方 eval 文化薄弱，Query Agent 只能缩短 Weaviate 查询编写，不能替代 任务级回归测试。\n另：主持人曾预告结尾 eval hot take，成片在 MetaBuddy / 未来方向处结束，无独立 eval 专节——上文 BEIR 与自测清单是为弥补该缺口而写的工程建议，非节目结论。\n若你要落地 # 先选模式：终端用户要可读答案 → Ask + 检查 sources 与 is_partial_answer；已有生成栈 → Search，把 searches 当日志。 投资 schema：为 filter 字段写 description 与合法值域；在 Sandbox（Usage）用控制台「Ask me something about your data」做冒烟，再迁 SDK。 把响应当契约测试：对关键问句断言 filters / aggregations 形状，而非只断言 final_answer 文本。 自研 hybrid 基线：固定参数做对照，勿依赖未链接的 BEIR 数字。 规划成本与轮次：Ask 4× 请求计费；多轮 ChatMessage 会放大调用次数——在网关设预算与超时。 参考与延伸阅读 # Query Agent 概述（docs.weaviate.io） Query Agent 使用说明：Ask、Search、会话、Inspect responses Query Agent 产品页与定价 weaviate-agents-python-client README v1.0.0 GA：弃用 run、V2 API、会话上下文 AskModeResponse / SearchModeResponse 源码 Weaviate in 2025（Query Agent GA 表述） Weaviate hybrid 检索 Weaviate 属性 filter Weaviate 聚合 RAG 原始论文（Lewis et al.） BEIR 基准论文 Compound AI Systems（BAIR） 检索质量与 RAG 总览（Weaviate 博客） PyPI：weaviate-agents 包历史 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-weaviate-s-query-agent-with-charles-pierse-weaviate-podcast-128/","section":"文章","summary":"向量库上的 Query Agent：可审计检索与两种「问数据」模式","title":"向量库上的 Query Agent：可审计检索与两种「问数据」模式","type":"posts"},{"content":" 信息安全简报：教育 SaaS 二次打击、集群 RCE 与浏览器静默 AI # 本期聚焦 2026 年 5 月上旬几条对蓝队有直接操作意义的新闻线：Canvas 母公司 Instructure 遭 ShinyHunters 再袭、Wazuh 集群同步路径遍历、Chrome 端侧 AI 的磁盘与隐私争议，以及 Trellix 源码库未授权访问。下文按「事件—含义—建议」展开；凡未获厂商或监管背书的数字，均标明来源边界。\n教育 SaaS：维护窗口、Free-For-Teacher 与逐校勒索压力 # 发生了什么。 勒索团伙 ShinyHunters 再度针对 Instructure（Canvas LMS 运营方）。KrebsOnSecurity 报道称，攻击方在大量租户登录界面投放告示（正文可见 「ShinyHunters has breached Instructure (again)」），并敦促各校自行与团伙谈赎金，即便总部不付款；勒索信息声称波及约 2.75 亿 用户、近 9,000 所机构——均为团伙单方面声称，尚无独立取证公开。\nInstructure 官方 FAQ 给出更具体的技术叙事：2026-04-29 检测到 Canvas 未授权活动；05-07 威胁方通过「第二个 Canvas 漏洞」再次获得访问，约 10 分钟内被禁用；两轮活动均通过 Free-For-Teacher 账户实施，并确认 support tickets 相关漏洞被利用，已计划关停该免费档。平台曾整体进入 maintenance mode；部分用户所见为 「Canvas is currently undergoing scheduled maintenance」。第三方 Dipan Mann（Cloudskope）批评将 outage 写作计划维护；宾大 2025 年 9 月泄露与 2026 年 5 月时间线见其分析文——第三方分析，非 Instructure 立场。报道曾引匿名调查源称多所高校已接触团伙；而 Instructure 后期称已与攻击方达成协议、收到数据销毁确认（shred logs），客户无需单独接触攻击者——两条公开叙事存在时间差，勿混读。\n技术含义。 若厂商归因成立，攻击面集中在低权限免费档与支持工单通道，而非「大客户直接被攻破」的经典叙事；跨租户从 FFT 横向至付费租户仍为嘉宾推测，官方未承认。SaaS 场景下「contained」后复发在工程上 plausible，但节目中「残留 API token 导致二次爆发」为演讲者虚构推演，无来源。团伙声称「数十亿条」私信：未独立核实；官方 5 月 6 日声明称含姓名、邮箱、学号与用户消息，否认密码、DOB、政府标识与金融信息。事件落在北美期末周（Krebs 确认），运营损害被放大；攻击方是否故意选时点未证实。\n工程师应对。 对照官方 FAQ 与状态事件页 做通知与日志留存；按法务评估潜在未成年人数据（COPPA 仅作合规框架参考，本案无监管认定）。威胁建模：把「业务最差的一天」（期末、报税季）写入演练，而非只盯零售节假日。监控 SaaS 侧的 mass download / API 异常（Salesforce 历史教训为他案，非本案 CVE）。\nKrebsOnSecurity：可见「ShinyHunters has breached Instructure (again)」及 extortion 引语。\nKrebs：Canvas 全国范围冲击 Instructure 安全事件更新 Wazuh：已认证集群节点的路径遍历 → RCE # 发生了什么。 GitHub 安全公告 GHSA-m8rw-v4f6-8787（CVE-2026-30893，CVSS 9.0）披露：decompress_files() 在集群同步解压时未校验路径，framework/wazuh/core/cluster/cluster.py 第 454–465 行将攻击者控制的 filepath 直接传入 os.path.join()，已认证的 cluster peer 可在其他节点任意路径写文件，覆盖 Python 模块后实现 RCE。受影响 wazuh-manager \u0026gt;= 4.4.0；补丁 \u0026gt;= 4.14.4（以 GitHub Advisory 为准；节目屏上 OCR 曾误识 CVE/GHSA 与补丁号）。\n工程师应对。 版本低于 4.14.4 的集群优先升级；收紧集群互认证与网络分段——顾问标注 PR:H，不可因「无公网直连」忽视集群面。\nGitHub：Wazuh cluster sync path traversal in decompress_files()… from authenticated cluster peer，Critical 9.0。\nGitHub GHSA-m8rw-v4f6-8787 Chrome：Built-in AI 下载义务与「删了又下」 # 发生了什么。 Chrome 开发者文档 确认 Built-in AI 需下载底层模型，并建议告知用户下载耗时。独立调查 That Privacy Guy 报告称，用户删除约 4GB 的 Gemini Nano 相关文件后，Chrome 在 Windows 上会反复重新下载；持久禁用需 chrome://flags、企业策略或卸载。作者在 macOS 用 fseventsd 独立记录下载循环——行为级断言来自第三方实验，非 Google 对「删后必重下」的同等官方措辞。\n工程师应对。 企业用 Chrome Enterprise policy 管控 AI 功能；个人用户勿仅靠手动删文件——需策略或 flags。端侧 AI 是否比全云更私密：有条件判断；数据仍可能回传 Google。\nthatprivacyguy.com：「The cycle of deletion and re-download has been documented…」及 macOS fseventsd 验证段落。\nChrome Built-in AI That Privacy Guy：静默 4GB 安装 Trellix 源码库未授权访问 # 发生了什么。 Trellix 向媒体声明称，「部分源代码仓库」遭未授权访问，已引入外部取证并通知执法部门；截至调查，未发现发布流程受影响或源码已被利用（引语见 BleepingComputer 转载）。RansomHouse 曾声称掌握源码，与厂商声明尚未交叉和解；厂商声明页 本轮抓取 HTTP 403，正文以转载为准。节目假设社工入口——厂商未指明向量。\n工程师应对。 关注厂商后续 IOC 与补丁；源码泄露场景下加强供应链监控与密钥/签名轮换假设。\nBleepingComputer：Trellix 源码库事件 旁线：外包 Help Desk 与社工面（泛化） # ShinyHunters 在 ADT 等案件中曾通过 Okta voice phishing 进入 Salesforce（Krebs 转述他案报道）——非 Canvas 已证实入侵链。外包一线常持 IdP 高权限却偏重体验，与 MSSP 降本激励类似；Canvas 本案是否 vishing，节目与厂商均未证实，Instructure 归因 FFT/support tickets。此为行业模式讨论，非本周单一 CVE。\n成稿基于公开主源核对；节目嘉宾观点、匿名调查源与团伙声称已在文中区分。Rose Acre Farms / Lynx 勒索仅节目一句带过，本轮未找到可抓取主源，故未展开。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-bhis-2026-bhis-talkin-bout-infosec-news-2026-05-11/","section":"文章","summary":"信息安全简报：教育 SaaS 二次打击、集群 RCE 与浏览器静默 AI","title":"信息安全简报：教育 SaaS 二次打击、集群 RCE 与浏览器静默 AI","type":"posts"},{"content":" 用 Spring Debugger 拆穿 Spring Boot「魔法」：属性、Bean 与事务的真实链路 # Spring Boot 把对象生命周期、配置合并、条件装配和 AOP 代理叠在一起，日志里往往只剩一行「started successfully」。有经验的工程师知道问题多半出在启动链的某一环，但缺的是：在不往业务类里塞 ApplicationContext 的前提下，把「文件里写的值」和「容器里真的用的值」对齐，并把 Bean 定义、条件评估和代理边界看清楚。\nIntelliJ 的 Spring Debugger 把这类检查收进断点会话：Evaluate Expression 旁路 Spring Properties；Beans 页签区分已加载与未加载定义；配置 inlay 提示运行时覆盖。下文按机制组织——属性在 Environment 阶段定型，Bean 在定义与实例化阶段分叉，事务在代理边界上生效——并用 Terminator 主题样例里的类名作锚点（演讲者观点：谜题用于教学，不宜直接当作团队规范）。若你维护的是 Spring Boot 3.x，EPP 注册键包名可能是 org.springframework.boot.env.EnvironmentPostProcessor；Boot 4 则迁移到 org.springframework.boot.EnvironmentPostProcessor，以所用版本 Javadoc 为准。\n调试入口：Spring Properties 与「谁改写了这个键」 # 为什么 # application.properties、application-prod.properties、命令行参数和环境变量可以并存。若团队只比对 YAML 字面量，很容易误判「prod 一定覆盖 base」或「命令行永远最高」——而 Externalized Configuration 还允许在上下文刷新之前用 EnvironmentPostProcessor 插入更高优先级的 PropertySource。\n机制与约束 # Boot 文档写明：基于文件的源有固定顺序，且 command line properties always take precedence over file-based property sources。但 MutablePropertySources.addFirst(...) 会把新源放在最高优先级（Framework Javadoc）。因此「最终值」必须看运行时 Environment，不能只看投票时选中的那一层文件。\nIntelliJ 在断点处通过 Evaluate Expression → Spring Properties 显示属性的实际解析值；Review the current configuration 说明 inlay 可展示覆盖关系，并 Navigate to the property redefinition——跳转到改写该值的代码，不限于某个 application-*.properties。\n怎么做 # 在 SpringApplication.run 返回后或任意已暂停的断点：\n// Evaluate Expression → 右侧菜单选 Spring Properties → 输入键名 // 例如 terminator.main.mission 多 profile 文件布局示例（与演示 OCR 一致的多环境命名）：\n图：application-dev.properties、application-prod.properties 与 terminator-validators.json 等同屏出现，说明配置源不止一份 base 文件。\n图：断点停在 JavaonedemoApplication 的 main，spring.factories 注册 EnvironmentPostProcessor，Debug 窗口可见 Beans / Environment 等 Spring 专用页签。\n属性谜题（terminator.main.mission 同时出现在 application.properties、application-prod.properties、环境变量与命令行）在投票环节容易选错层：不能单凭「prod 文件」或「命令行」猜答案；演示运行日志出现 Sarah Connor 时，应立刻怀疑 EPP 而非 profile 覆盖失败（见下一节）。\n常见误区 # 用「常识优先级」代替 Spring Properties 里的 result。 以为 Navigate 只会打开 properties 文件；自定义 EPP 的 postProcessEnvironment 才是跳转目标。 只打开 Actuator /env 端点却未在最早断点观察 EPP 执行前的中间态（若未启用 Actuator，Spring Properties 是更轻量的替代）。 EnvironmentPostProcessor：在 Bean 工厂之前就定型的 Environment # 为什么 # 「配置写在 A，日志却是 B」类问题，时间线要前移到 ApplicationContext refresh 之前。Boot 4.x 的 EnvironmentPostProcessor 在 prior to the application context being refreshed 被调用，用于合并 property source 或编程式改写。\n机制与约束 # 内置与自定义 EPP 按 Ordered / @Order 排序。演示中的 TerminatorAcronymEpp 读取 terminator.main.mission，做首字母缩写并应用特殊规则（如 acronym 为 Sarah 时变为 Sarah Connor），再通过 environment.getPropertySources().addFirst(propertySource) 写回——这与官方 addFirst 语义一致。\n注册方式（Boot 3.x 常见键名；Boot 4 包名可能迁移，以所用版本 Javadoc 为准）：\n# META-INF/spring.factories org.springframework.boot.env.EnvironmentPostProcessor=\\ more.riddles.infra.TerminatorAcronymEpp public class TerminatorAcronymEpp implements EnvironmentPostProcessor { private static final String PROPERTY_NAME = \u0026#34;terminator.main.mission\u0026#34;; @Override public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) { String mission = env.getProperty(PROPERTY_NAME); // 演示逻辑：缩写 + 规则 → 新 PropertySource → addFirst } } 图：TerminatorAcronymEpp implements EnvironmentPostProcessor，PROPERTY_NAME = \u0026quot;terminator.main.mission\u0026quot;。\n图：注释说明 acronym 等于 Sarah（大小写不敏感）时的分支逻辑。\n排错时建议对照三条证据链：spring.factories（或 imports）里的 FQCN → EPP 源码里的 addFirst / 改写规则 → Spring Properties 中的最终字符串。演示注释写「Environment post-processor that transforms the terminator.main.mission property」，与字幕中「built-in EPP 先跑、自定义在后」的口述一致（演讲者观点：内置处理器数量约十余种，精确列表随 Boot 版本变化，不必背表，只需知道还有一层）。\n常见误区 # 在 Bean 已创建后再去改 Environment，为时已晚。 忽略 EPP 后仍坚持「命令行一定赢」——在 addFirst 之后，命令行也可能被压过（机制并存，非文档矛盾）。 把 TerminatorAcronymEpp 的演示输出 Sarah Connor 当成框架默认行为。 组件扫描、条件装配与 IDE 里的 Bean 状态 # 为什么 # @ComponentScan 决定候选组件；Classpath Scanning 写明：候选类须匹配过滤器且有对应 bean definition 注册——不等于全部会实例化。@Conditional / @ConditionalOnProperty 在定义注册前就必须 match。\n机制与约束 # 切换 @ComponentScan(basePackages = \u0026quot;puzzler2\u0026quot;) 会改变扫描列表，但 profile 或条件不满足时，IDE 可能列出类型却没有运行时 Bean（演讲者观点：静态导航与运行时装配可能脱节）。\nIntelliJ — Review loaded beans：green = 已加载，transparent = 未加载，yellow = mock。这与「猜 Bean」相比，更可靠的是断点下看容器。\n@SpringBootApplication @ComponentScan(basePackages = \u0026#34;puzzler2\u0026#34;) // 演示中在 puzzler1/2/3 间切换 public class JavaonedemoApplication { } 图：SpringApplication.run 与 === Application started successfully === 同屏，说明在改扫描包后验证的是运行结果而非仅项目树。\n常见误区 # 把 Project 视图里看到的 @Component 类等同于「一定有一个单例 Bean」。 不结合 Spring Properties 查看条件属性是否真为 true。 @ConditionalOnProperty 与 legacy XML primary：两条定义路径 # 为什么 # 排查「为什么注入的不是带 @ConditionalOnProperty 的那个实现」时，必须同时查条件注解和 XML / @ImportResource 是否重复声明了同名 bean。\n机制与约束 # @ConditionalOnProperty：默认 missing attributes do not match；havingValue = \u0026quot;true\u0026quot; 时属性须存在且匹配。条件不满足则不注册该组件定义。\n演示中 IneedYourClothesTerminatorValidator 带：\n@ConditionalOnProperty( name = \u0026#34;terminator.model.ineedyourcloses.enabled\u0026#34;, havingValue = \u0026#34;true\u0026#34;) @Component public class IneedYourClothesTerminatorValidator implements TerminatorValidator { } Spring Properties 里没有该启用键，故扫描路径不应产生该 BeanDefinition；但若 legacy XML 仍声明同名 bean 且 primary=\u0026quot;true\u0026quot;，注入点会选 primary 候选——与类上的 @Conditional 无关（机制与官方条件语义一致；XML 细节以演示仓库为准）。\n图：@ConditionalOnProperty 与 @Component 出现在 puzzler2.service 包下的 validator 类上。\n图：调试 Paused 时同屏可见 application-prod.properties 与上述 validator 源码。\n常见误区 # 只看注解，不打开 Beans 视图或 getBeanDefinition 查第二来源。 以为 @Component 上的条件会「关掉」XML 里已注册的 bean。 Evaluate Expression：在调试会话里读 BeanDefinition # 为什么 # 临时 @Autowired ApplicationContext 或打日志会污染代码。Spring Debugger 允许在表达式里访问 all properties and beans，even when not in the current execution context。\n机制与约束 # 从可见的 ConfigurableApplicationContext context 可向下取 BeanFactory，查看 depends-on、bean 名、注入字段实际类型。IntelliJ 文档未逐字列出 getBeanDefinition(\u0026quot;skynet\u0026quot;)，但该 API 属于 ConfigurableListableBeanFactory 常规用法（合理推断，非 IntelliJ 逐字保证）。\n// 断点处（需 context 在作用域内）： context.getBeanFactory().getBeanDefinition(\u0026#34;skynet\u0026#34;); // 或对字段：skynet.getTerminatorValidator().getClass().getName() 图：Debug 中 main 线程 RUNNING，项目树列出多个 TerminatorValidator 实现供对照注入结果。\n图：编辑器上下文菜单含 Evaluate Expression...，与查看 SpringApplication 导入同屏。\n常见误区 # 只在 Variables 窗格看字段快照，不查 定义阶段 的 primary / depends-on。 在未暂停或未进入含 context 的栈帧时求值失败却归咎于 Spring。 List\u0026lt;T\u0026gt; 注入：显式 @Bean List 与按类型收集 # 为什么 # 同时存在 @Bean List\u0026lt;TerminatorValidator\u0026gt; 和多个 TerminatorValidator 实现时，Skynet 构造函数里的 List 内容并不直观。Framework 对集合注入的默认语义是：向 List\u0026lt;T\u0026gt; 注入时聚合容器中该类型的 bean（Using @Autowired）。\n机制与约束 # 演讲中的谜题选项包括：仅 Rev9（显式 List bean）、T800+AI（个体 bean）、三者皆有、或异常。演讲者称 Spring Boot 3.3 之前行为与部分观众直觉不同，3.3 之后演示走向「按类型收集 validators」（演讲者观点；本次未能从 Boot 3.3 Release Notes 独立核实 changelog，升级务必用集成测试验证）。\n图：Variables 中 context = {AnnotationConfigApplicationContext@...}，与 SpringApplication.run(JavaonedemoApplication.class, ..args 同屏。\n图：幻灯片提问 List\u0026lt;TerminatorValidator\u0026gt; 的内容，选项含 Rev9、T800+AI、全部三者或 Exception。\n怎么做 # 断点停在 Skynet 构造后：\nterminatorValidator.size(); terminatorValidator.stream().map(Object::getClass).toList(); 常见误区 # 假设 List.of(new Rev9...) 的 @Bean 一定会原样注入。 不区分 Boot 小版本 就照搬旧谜题答案。 Spring Framework 7 的 BeanRegistrar：定义阶段动态注册 # 为什么 # validator 清单来自 JSON、类名由运维配置携带时，用 Class.forName + 程序化注册比手写 dozens of @Bean 更贴近现实；包名重构会导致启动期 ClassNotFoundException。\n机制与约束 # Programmatic Bean Registration（Framework 7）：通过 @Import 导入实现 BeanRegistrar 的类，在 尚无 bean 实例 时调用 register(BeanRegistry, Environment)。BeanRegistry.Spec 支持 order、primary、prototype、lazyInit、description 等；当前 Javadoc 无 qualifier(...)——演讲者称 qualifier 仍依赖类上的 @Qualifier（与 API 面一致，属演讲者归纳）。\npublic class TerminatorValidatorRegistrar implements BeanRegistrar { @Override public void register(BeanRegistry registry, Environment env) { for (var v : loadConfiguration().getValidators()) { Class\u0026lt;?\u0026gt; clazz = Class.forName(v.getClassName()); // 拼写错误 → 启动失败 registry.registerBean(v.getBeanName(), clazz, spec -\u0026gt; { if (\u0026#34;prototype\u0026#34;.equalsIgnoreCase(v.getScope())) spec.prototype(); spec.order(v.getOrder()); }); } } } 图：TerminatorValidatorRegistrar implements BeanRegistrar，loadConfiguration() 从外部配置加载。\n图：registerValidator(BeanRegistry registry, ValidatorConfigDTO.ValidatorDefinition ...) 方法可见。\n启动失败时可加 --debug 查看 condition evaluation report（Boot 常规手段；演示控制台 OCR 含 Beans / Health / Mappings / Environment 页签）。\n常见误区 # 在 Bean 已实例化后试图用 Registrar「补注册」同名单例。 认为 JSON 里的 FQCN 会随 IDE 重构自动更新。 启动时间线：把问题钉在正确的阶段 # 为什么 # 用 BeanPostProcessor 解释属性来源、或用 EPP 解释事务代理，都会错位。教学上可把启动粗分为四段（综合模型；Spring 无与下述完全同序的单页官方示意图，各段均有独立文档支撑——见 P09 核实结论）。\n机制与约束 # 阶段 典型动作 权威锚点 EnvironmentPostProcessors 合并/改写 PropertySources Boot EnvironmentPostProcessor Javadoc BeanRegistrar / 定义注册 注册 BeanDefinition，无实例 Framework 7 programmatic registration BeanFactory 实例化 singleton / prototype 创建 Bean lifecycle 文档 BeanPostProcessor 初始化前后增强、AOP 代理 factory-extension 图：幻灯片标题 BeanFactoryPostProcessor，说明在 bean 实例化之前可修改 bean definition（与 BPP 阶段区分）。\n常见误区 # 混淆 BeanFactoryPostProcessor（改定义）与 BeanPostProcessor（改实例）。 在已 refresh 的上下文里用调试器反推 EPP 顺序却不看 spring.factories 注册列表。 Prototype 与销毁回调：@PreDestroy 别用在错误 scope 上 # 为什么 # 四选一谜题常考「prototype 上的 @PreDestroy 会不会在 context.close() 时执行」。若误以为所有 scope 都会走统一销毁链，prototype 资源可能泄漏。\n机制与约束 # Prototype scope：Spring does not manage the complete lifecycle；configured destruction lifecycle callbacks are not called；client code must clean up。@PostConstruct 仍可在创建时调用；@PreDestroy 主要可靠场景是 singleton（与演讲结论一致）。\n@Component @Scope(\u0026#34;prototype\u0026#34;) class T800 { @PostConstruct void init() { } @PreDestroy void shutdown() { /* 勿指望容器在 close 时调用 */ } } 常见误区 # 用 context.close() 验证 prototype 的 @PreDestroy。 把电影梗当成容器行为。 事务与自调用：REQUIRES_NEW 经不起 this.save() # 为什么 # @JavaOneTransactionalService 组合了 @Service 与类级 @Transactional(REQUIRED, timeout=10000)；save 方法标 REQUIRES_NEW。若 saveAll 内部直接调用 save，不经代理，则方法级传播不生效。\n机制与约束 # Spring 文档明确：self-invocation does not lead to an actual transaction——even if the invoked method is @Transactional。外部经代理进入 saveAll 时外层 REQUIRED 事务存在；内部 this.save() 无新事务；若外层因异常回滚，演示结论为 Nothing will be saved（演讲简化场景，完整分析还须看 rollbackFor 与异常传播）。\n@Retention(RetentionPolicy.RUNTIME) @Service @Transactional(propagation = Propagation.REQUIRED, timeout = 10_000) public @interface JavaOneTransactionalService {} @JavaOneTransactionalService public class TerminatorTalkingModule { public void saveAll() { save(record); // 自调用 → 无 REQUIRES_NEW 代理边界 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(Record r) { } } 图：package com.jb.terminator_transactions.annotations 与 @Retention 元注解组合事务服务接口。\n图：spring.datasource.url=jdbc:postgresql://localhost:5432/terminator_db 与 spring.application.name=terminator_transactions 同屏。\n深入理解代理边界见 Understanding AOP Proxies。\n若必须在内层使用独立事务，应通过注入自身代理、ApplicationContext.getBean 获取当前 bean，或拆到另一个 Spring bean——而不是在同类里直接 this.save()。演示工程里 PostgreSQL 与 spring.jpa.hibernate.ddl-auto=update 只为让「是否落库」可见；生产环境仍应以事务日志与集成测试验证回滚路径，而非只记谜题口号「Nothing will be saved」。\n常见误区 # 认为方法上的 REQUIRES_NEW 一定能「救」内层调用。 不区分 注入的代理 与 this 引用。 把组合元注解 @JavaOneTransactionalService 当成会自动修复自调用的语法糖。 可复现的排错清单 # 属性：断点 → Spring Properties → 查键 → Navigate 到覆盖逻辑 → 查 META-INF/spring.factories 中的 EPP。 Bean 是否存在：Beans 页签 / getBeanDefinition / 条件属性与 XML primary。 集合注入：对 List 字段求 size() 与 getClass()，勿信旧版本谜题记忆。 动态注册：核对 JSON FQCN、--debug、Registrar 的 scope / order。 事务：对外部调用单步进入代理；警惕同类内部调用。 版本：EPP 注册键、Boot 3.3 前后 List 行为、Framework 7 BeanRegistrar 均以当前依赖版本文档为准，勿跨版本套用谜题答案。 掌握调试器并不能替代阅读 Reference，但能把「魔法」还原为可点击的源码与定义——这正是 Spring Debugger 的设计目标：在断点处回答「这个值从哪来、这个 Bean 为何存在、这次调用有没有经过代理」。\n参考与延伸阅读 # IntelliJ IDEA — Spring debugger（Evaluate / Spring Properties / Loaded beans） Spring Boot Reference — Externalized Configuration Spring Boot 4.0.6 — EnvironmentPostProcessor API Spring Framework — Classpath Scanning Spring Boot — @ConditionalOnProperty Spring Framework — Using @Autowired（集合注入） Spring Framework 7.0.7 — BeanRegistrar Spring Framework — Programmatic Bean Registration Spring Framework — Container Extension Points（BFPP vs BPP） Spring Framework — Bean Scopes（prototype 销毁） Spring Framework — Declarative Transaction Management（self-invocation） Spring Framework — Understanding AOP Proxies Spring Framework — MutablePropertySources.addFirst Spring Framework — @Conditional Spring Boot — SpringApplication run 与调试启动（IDE 运行配置文档入口） ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-spring-debugger-new-power-where-should-i-click-to-demystify-spring-boot/","section":"文章","summary":"用 Spring Debugger 拆穿 Spring Boot「魔法」：属性、Bean 与事务的真实链路","title":"用 Spring Debugger 拆穿 Spring Boot「魔法」：属性、Bean 与事务的真实链路","type":"posts"},{"content":" 用代码反射把 Java 内核送到 GPU：HAT 与 Project Babylon 的工程切面 # 要在 JVM 里写并行计算，常见路径是 parallelStream 或结构化并发——它们仍受 CPU 线程数约束。若要把同一份业务逻辑落到 GPU 的数万线程上，传统做法是手写 CUDA/OpenCL，再用 JNI 粘合；这对 Java 团队意味着第二套语言、第二套构建，且平台绑定明显。Project Babylon 的 Code Reflection 与 HAT（Heterogeneous Accelerator Toolkit） 走另一条路：保留 Java 写法，在编译期生成可变换的 SSA 代码模型，经多阶段 IR 变换后交给可插拔后端（OpenCL、CUDA、纯 Java 顺序执行等）。\n本文从工程视角拆解两条主线：（1）语义保真——在 AST 与字节码之间取得可变换的 IR；（2）设备可消费——把数组访问、过程间调用、buffer 读写模式降到后端与 FFI 能理解的 invoke 与访问类型。文中性能数字、IR 等价性等若无官方基准，一律标明边界；API 以 OpenJDK 孵化稿与 openjdk/babylon 的 code-reflection 分支为准。\nReflecting on HAT: A Project Babylon Case Study — Ruby Chen，Java Platform Group\n为什么需要介于 AST 与字节码之间的代码模型 # AST 保留过多句法噪声，难以做跨语言 lowering；字节码则为 JVM 执行优化，丢失大量源级语义（循环变 goto、窄类型提升等）。JEP draft 8361105 与 Code Models 文章 将 code model 定位为「保真度介于 AST 与 bytecode 之间」的不可变树：元素为 operation / body / block，采用 SSA，设计受 MLIR/LLVM 风格编译器影响。仅标注 @Reflect（孵化模块 jdk.incubator.code）的方法或 lambda 会由 javac 把模型写入 class file，运行时通过 Op.ofMethod 等 API 取出根节点 CoreOp.FuncOp，再 transform 生成新模型并可回写为字节码。\n机制与约束：每个 block 必须以终止 op（return、分支等）结束；操作用 %n 引用 SSA 结果。变换是「遍历旧模型 + 用 block builder 构造新模型」，模型本身不可变。\n最小示例（与 JEP 8361105 示例 同族）：\nimport jdk.incubator.code.Op; import jdk.incubator.code.Reflect; import jdk.incubator.code.dialect.core.CoreOp; @Reflect static int squareKernel(int i, int[] array) { array[i] = array[i] * array[i]; return 0; } // 运行时：Optional\u0026lt;CoreOp.FuncOp\u0026gt; m = Op.ofMethod(Square.class.getDeclaredMethod(\u0026#34;squareKernel\u0026#34;, ...)); // m.ifPresent(f -\u0026gt; f.transform(transformer)); 遍历与构造：对已有 FuncOp 可 elements() 流式访问 block/body/op；新建逻辑时用 Block.Builder 追加 op，并保证每个 block 以终止 op 结束。打印 SSA 文本（func @\u0026quot;squareit\u0026quot; (%0 : ...) -\u0026gt; { %1 = var.load ...; return %7; }）是调试变换最可读的方式，与 IDE 里看到的 Java 源码并非一一逐行对应。\n常见误区：把 code model 当成 AST 或反汇编 bytecode 的替代品；未标 @Reflect 时 Op.ofMethod 返回 empty，后续图构建会直接失败；在 transform 中直接修改旧节点——应始终产出新 FuncOp。\nCode Reflection: Purpose of Code Models — AST 过高、字节码过低，Code model 为 goldilocks spot\nDissecting the Code Model: Bodies and Blocks — func、var.load、invoke、return 的 SSA 文本形态\nHAT: Code Model — @Reflect 标注的 squareKernel 与宿主 square 分发结构\nHAT 栈：Panama 内存、代码反射与可插拔后端 # HAT 用 Panama Foreign Function \u0026amp; Memory API（MemorySegment、MemoryLayout）描述堆外 buffer，用 Babylon 代码反射读取/变换 kernel 的 FuncOp，再通过 Accelerator / ComputeContext 把 NDRange 派发到具体后端。官方 HAT README 与 matmul 文章 列出的运行后端包括 ffi-opencl、ffi-cuda、java-seq（CPU 顺序，便于调试）等；构建产物形如 hat-backend-ffi-opencl-1.0.jar。PTX、SPIR-V 在 Babylon 路线文中作为 GPU 代码生成探索出现，独立 SPIR-V/HIP 运行后端在已核对 README/Config 中未列出——若听到 HIP 后端，应视为未验证扩展名。\n为什么：把「写什么」与「跑在哪」解耦，避免每个项目重写 JNI + 厂商 runtime 胶水。\n怎么做（仓库 code-reflection 分支可复现）：\ncd hat java @.bld java @.run java-seq life java @.run ffi-opencl life 底层 JVM 通常需要：--enable-preview --add-modules=jdk.incubator.code --enable-native-access=ALL-UNNAMED（见 README）。\nhat.java 的 bld 目标会编译 hat-core、hat-optkl、各 hat-backend-ffi-*，并触发 cmake 构建 native 封装；终端 OCR 中反复出现的 We will need jextract to create jextracted backends 指向 OpenCL/OpenGL 等需要头文件绑定的路径，与 kernel IR 算法无关，但会挡住「第一次 clone 就跑 demo」的预期。\n常见误区：以为换后端只改一个字符串即可而不重建 native 依赖；忽略预览/孵化模块开关导致 Op 类找不到；把 Config 里的 PTX 选项误解为独立「PTX 后端」——它表示 CUDA 路径可传递 PTX 而非 C99 源码（见 Config 注释）。\ncmake hat-backend-ffi-1.0 — 多模块 jar 与 hat-example 一并编译\njava @.bld — compiled 85 files to hat-core-1.0.jar 与 cmake hat-cmake-info-opencl\n三层边界：Kernel、Compute 与宿主 # 层 职责 @Reflect（现行实现） Kernel 每线程逻辑；通过 KernelContext 的 gix/giy 等取线程坐标 必须 Compute 在 NDRange 上 dispatchKernel 必须（ComputeContext 构造时 Op.ofMethod 为空会抛错） 宿主 分配 buffer、创建 Accelerator、I/O 通常不需要 演讲者观点称 Compute「通常不必标 @Reflect」——与当前 ComputeContext.java 及 Life/matmul 示例 不一致；凡走 accelerator.compute(...) 构建 compute 图的路径，Compute 入口在 code-reflection 分支上需要 @Reflect。文档强调对 compute 标 @Reflect 还便于检查可达 kernel 与数据流。\n最小骨架（概念合并 README 与 Life 示例）：\n@Reflect static void squareKernel(int i, @RW S32Array a) { a.array(i, a.array(i) * a.array(i)); } @Reflect static void squareCompute(ComputeContext cc, @RW S32Array a) { cc.dispatchKernel(NDRange.of1D(a.length()), (KernelContext kc, @RW S32Array buf) -\u0026gt; squareKernel((int) kc.gix(), buf)); } 参数上的 @RO / @RW / @WO（MappableIface）表达程序员意图；自动 buffer tagger 在内联后从 invoke 推断的 AccessType 会与之对照或补充，供 FFI 决定主机侧 staging。Kernel 内应只保留可翻译的算术、分支与 buffer 访问；Control 一类宿主语义对象在 Life 示例中作为只读上下文传入，不参与设备写回。\n常见误区：只给 kernel 标 @Reflect 却省略 compute；在 compute 里写重型 I/O 并期望被设备翻译；把「不必标 Reflect」的口语当成现行 API 合同。\n数组视图：把 array.load 降到 buffer 的 invoke # 设备后端天然理解「对某接口的 getter/setter 调用」，而不是 Java 二维数组的连续 array.load 链。HAT 用 array view 阶段（HATArrayViewPhase）在 SSA 上匹配 JavaOp.ArrayAccessOp.ArrayLoadOp / ArrayStoreOp，把索引 conv 为 long，再替换为对 hat.buffer.*（如 S32Array::array(long):int）的 invoke。\n为什么：程序员侧可写 cellGrid.cell(idx) 或 matmul 中的 array(i)，IR 侧统一为可分析的 invoke，与 Interface Mapper 一致。\n机制：对 2dArray[kc.gix][kc.giy]，未变换前 code model 会展开多步 pointer/array load（演讲者观点：维数越高链越长）。变换需识别「哪一次 load 产出最终标量」，沿 operand 收集下标（幻灯片 entry.transform + switch on ArrayLoadOp）。\nCode Model: Using Arrays — 2D 访问生成多行 pointer arithmetic，需压缩 code model\nTransformation: Example — case ArrayLoadOp，第二次 array load 返回 int 值\nCode Model: Using Arrays — array.load 经 conv 变为 invoke S32Array::array(long):int\n幻灯片中的变换伪代码与 JavaOp 密封层次一致：先 case ArrayLoadOp，沿 %12、%14 等 operand 回溯，判定「第二次 load 才返回 int 标量」这类模式，再用 core reflection 解析正确的 getter 签名，把后续使用点改接到新的 %r = invoke ... 结果上。Store 路径对称处理。若跳过 conv，长索引在设备侧可能与 layout 假设不一致。\n常见误区：以为接口名在所有 buffer 类型上统一叫 array()——Life 的 CellGrid 生成代码用 cell(long)，与 F32Array.array(long) 并存；以具体 Iface 映射为准；在 transform 里硬编码设备 API 字符串而非走 Iface 映射表。\nHAT dialect 与多维 IR 压缩 # 在 array view 之后，HAT 引入自定义 op（如 HATPtrOp）组成 HAT dialect，由 HATPhase 多阶段执行（含 memory、vector 等）。演讲者观点：经 array view 与 pointer 折叠后，设备端生成代码可与未使用 view 时语义等价——未在本文环境中运行 IR diff 验证。\n怎么做（变换入口形态，与 JEP FuncOp.transform 一致）：\nentry.transform(entry.funcName(), (bb, op) -\u0026gt; { switch (op) { case JavaOp.ArrayAccessOp.ArrayLoadOp alop -\u0026gt; { // 识别最终标量 load，收集下标，发射 pointer / invoke } default -\u0026gt; { /* copy or fold */ } } }); KernelCallGraph 在 inline 之后调用 HATTier.transform(HATTier.KernelPhases, ...)，把 array view、memory、vector 等阶段串成固定管线，而不是让应用代码随意拼 phase。对维护者而言，新增一种设备目标通常是加 backend 模块 与 lowering，而不是 fork 整条 Java 前端。\n常见误区：在 transform 中手写设备代码字符串；应在 IR 层完成语义保留的 lowering，再交后端；跳过 phase 顺序导致 pointer op 折叠在 array view 之前执行。\n内联与 buffer 标注：过程间追踪降为单遍 # 主机–设备 buffer 拷贝 往往贵于 kernel 本身。HAT 需要每个参数的 read-only / write-only / read-write（AccessType）以缩小传输。Kernel 若调用 helper(buf)，跨调用的形参–buffer 映射会很快变复杂。\n策略（KernelCallGraph 与演讲一致）：先 SSA.transform，再循环 Inliner.inline 直到无更多可内联 InvokeOp；对 内联后的单一 FuncOp 跑 BufferTagger；最后执行 HATTier.KernelPhases。\n内联约束：callee 含多个 return 时不能直接拼接——需在 callee 模型内合并为单一 return block，把 return 换成 branch（官方 Inliner Javadoc 同述）。\nBuffer Tagging Across kernel calls — inline method invocations，single pass 分析\nCode Reflection: Inliner — replace the return op with a branch to the return block\nBuffer tagger 规则（已核对源码逻辑）：\n有返回值的 getter invoke → 读（RO 或升级为 RW） void setter invoke → 写 同一 buffer 先读后写 → RW Buffer Tagging Example — getter on s32Array，tags read-only\nBuffer Tagging Example — setter on s32Array，Tag read-write\n内联器处理 return 的典型流程（与孵化模块 Inliner 一致）：遍历 callee body 的 op；若遇退出当前方法的 ReturnOp，先 compute 汇合多个 return 块，必要时新建带 block parameter 的 return block，再把原 return 替换为 branch；否则把 op append 到 caller 的 Block.Builder。这样 buffer tagger 看到的是单一、线性的 invoke 序列，而不是跨函数的虚调用图。\nBufferTagger.getAccessList 返回与 内联后入口 block 形参 顺序对应的 AccessType 列表，供 optkl/FFI 映射到 OpenCL/CUDA 参数修饰。getter 带返回值记读；void setter 记写；同一 IfaceValue 先读后写升为 RW；isReference 等路径避免把「仅传递引用」误判为读。\n常见误区：手工在每个参数写 @RW 却与 IR 实际访问矛盾；未内联 helper 就期望 tagger 跨调用边传播；不同 SSA 槽（如 %4 与 %5）代表不同 buffer 实例，不能合并。\n运行时：HAT=MINIMIZE_COPIES 与后端选择 # Config.MINIMIZE_COPIES（别名 MC）通过环境变量 HAT 或系统属性读取，注释标明主要作用于 FFI 路径以减少拷贝。演示中 Game of Life 用 java-seq 极慢，ffi-opencl 明显加速；再设 HAT=MINIMIZE_COPIES 时主讲称相对前一 OpenCL 运行约 2×——演讲者现场演示观点，无官方 benchmark 表。\nexport HAT=MINIMIZE_COPIES java @.run ffi-opencl life 注意：hat.java 帮助里曾出现 -DHAT=MINIMIZE_BUFFERS 字样，与 Config 不符；以 MINIMIZE_COPIES 为准。\njava @.run java-seq life — We will need jextract to create jextracted backends\n\u0026ndash;enable-preview \u0026ndash;add-modules=jdk.incubator.code \u0026ndash;enable-native-access=ALL-UNNAMED\n演示终端在 java @.run java-seq life 时可能打印 Java backend received computeContext（该字符串未在本次核对的源码快照中定位）。java-seq 的价值是在同一套 @Reflect kernel 上验证 IR 与逻辑，而非性能基线；OpenCL 再配合 MINIMIZE_COPIES 才检验「标注 → 少拷贝」。\n常见误区：在 java-seq 上期待拷贝优化；minimizeCopies() 面向 FFI；把演示倍速当作通用 SLA；使用帮助里的 MINIMIZE_BUFFERS 拼写。\nGame of Life：同一规则，两种表面语法 # Conway B3/S23 在 HAT 示例 life/Main.java 的 ComputeLife 中实现：lifePerIdx 统计八邻域后\n((count == 3) || ((count == 2) \u0026amp;\u0026amp; (cell == ALIVE))) ? ALIVE : DEAD 演示可先写 val(cellGrid, ...) 辅助访问，再改为更接近直觉的 cellGrid.cell(...) / array view；演讲者观点称重编译后行为一致，说明变换管线吸收语法差异。仓库当前以 cell(long) 与嵌入的 codeLifePerIdx OpenCL 片段对照为主，未在源码中并排保留两套等价路径供 diff。\npublic static void lifePerIdx(int idx, Control control, CellGrid cellGrid)\n@Reflect lifePerIdx — byte cell = cellGrid.array(idx: idx + from)\ncodeLifePerIdx — count == 3 与 Conway 存活规则字符串\nlife(KernelContext kc, ...) 在 NDRange 上对每个 gix 调用 lifePerIdx；八邻域读取在源码层可展开为大量 cellGrid.cell(...) 或 val(CLWrapCellGrid, ...) 字符串（与嵌入的 codeLifePerIdx 对照），编译后应收敛为同一套设备访问模式。演讲者观点称两种写法重编译后行为一致——若你要验证，应在本地对两种源码各跑一次 golden 网格比对，而不是依赖幻灯片口述。\nIDE 中可见 ComputeLife 嵌套类、@Reflect 与 hat-05-accelerator-compute.md 文档并列，说明示例同时服务「Accelerator 计算模型」教学与 IR 实验；生产集成应锁定依赖的 Babylon JDK 构建，而非系统默认 JDK。\n常见误区：把 val(...) 当成运行时函数留在设备路径上；在 kernel 内保留无法内联的 JDK 类库调用；未重建 hat-example-life-1.0.jar 就切换 array view 语法。\n收束：代码反射在 HAT 中的位置 # HAT 不是「把 Java 编译成 CUDA 字符串」的玩具，而是一条可核对的工程链：@Reflect 门槛 → SSA FuncOp → array view / HAT dialect → 内联 + buffer tagger → 可插拔 FFI 后端，底下是 Panama 的内存模型。对团队而言，价值在于用同一套 Java kernel 源码做 CPU 调试（java-seq）与 OpenCL/CUDA 加速，并用 MINIMIZE_COPIES 把 IR 上的 RO/RW 事实接到传输层——前提是接受孵化 API、预览特性与本地 native 构建成本。\n若你正在评估是否引入这条栈，可按以下顺序自检：（1）本地 Babylon JDK 能否加载 jdk.incubator.code；（2）java @.bld 是否生成所需 hat-backend-ffi-*；（3）java-seq 能否跑通目标示例；（4）再切 ffi-opencl 与 HAT=MINIMIZE_COPIES 做传输层对比。前三步失败时，优先查 jextract/cmake，而不是改 kernel 算法。\n未验证边界速览：≈2× 加速仅演示口述；HIP 后端、Java backend received computeContext 日志串未在已抓取源码定位；array view 与无 view 设备代码等价为演讲者观点；Compute 可不标 @Reflect 与现行 ComputeContext 冲突。生产采纳前应在目标 JDK 分支上对照 JEP 与 openjdk/babylon 标签自行跑通 hat 示例与测试（如 TestArrayView 一类）。\n参考与延伸阅读 # Project Babylon 项目页 JEP draft 8361105：Code reflection (Incubator) Code Models — 代码模型与 SSA 设计说明 JEP 454：Foreign Function \u0026amp; Memory API Optimizing GPU Programs from Java using Babylon and HAT（matmul） openjdk/babylon — code-reflection 分支 HAT 目录 HAT README — 构建、后端与完整示例 ComputeContext.java — compute 图与 @Reflect 硬性要求 KernelCallGraph.java — 内联与 buffer 标注顺序 Inliner.java — 多 return 合并为 branch BufferTagger.java — 基于 invoke 的 RO/RW/WO 推断 HATArrayViewPhase.java — array.load 到 invoke 的变换阶段 Config.java — MINIMIZE_COPIES 环境变量语义 life/Main.java — Game of Life 示例源码 JavaOp.ArrayAccessOp — 孵化模块中的数组访问 op ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-reflecting-on-hat-a-project-babylon-case-study/","section":"文章","summary":"用代码反射把 Java 内核送到 GPU：HAT 与 Project Babylon 的工程切面","title":"用代码反射把 Java 内核送到 GPU：HAT 与 Project Babylon 的工程切面","type":"posts"},{"content":" 用领域建模把 Java AI Agent 从「能跑」做到「可控」 # 企业把大模型接进客服、运维或交易辅助时，最先撞上的往往不是模型智商，而是编排与契约：同一条 while 工具循环里，读库、写库、对用户说话的工具同时暴露给模型，Prompt 里写再多「先确认再转账」，schema 仍在——模型仍可能一步走错。JetBrains 开源的 Koog 面向 JVM（Kotlin/Java 一等 API），文档将 Agent 分为 Basic、Functional、Graph-based、Planner 等策略；银行演示用领域 record + 子图限工具 + 图边条件把「识别问题 → 修复 → 验证 → 调整」钉成类型安全流水线，而不是单 Prompt 包办全程。\n若团队已在 Spring 生态内，可把 Koog 当作「Agent 运行时 + 编排 DSL」，通过 koog-spring-boot-starter 接入配置与 DI；需要与 Spring AI 生态互通时另有 koog-spring-ai-starter 模块，分工与 LangChain4j 的对比属 Q\u0026amp;A 层演讲者观点，本文以官方文档可核实 API 为主。\nKoog：在 JetBrains 产品内验证后开源（JAVAONE \u0026lsquo;26）\n默认工具循环：为什么 Prompt 护栏不够 # Basic agents 的默认行为等价于：向 LLM 发请求 → 若返回 tool call 则执行并再请求 → 直到出现 assistant 文本消息。幻灯片用伪代码把这一循环写得很直白：\nDefault Strategy Equivalent：while (true) 在 Assistant 与 Tool.Call 之间切换\n为什么：注册进 ToolRegistry 的工具会进入模型可见的 schema；「全开」带来高自由度，尾部风险（误调写操作、跳步）难以用概率表述消化。机制：循环本身确定，但每步选哪把工具由模型决定，属于非确定性分支。怎么做（反模式，仅作对照）：\nAIAgent.builder() .toolRegistry(ToolRegistry.of(communication, readTools, writeTools)) .build(); 常见误区：把业务阶段约束只写在 systemPrompt 里，却未收缩工具面或未拆子图——模型仍看见全部 @Tool。演讲者观点：企业场景里「最强模型也有约 0.1%–1% 失控」不可接受；该比例无官方基准，不宜当作 SLA。\n从可靠性工程角度，默认策略并非「不能用」，而是缺少可审计的阶段边界：日志里只有一串 tool call，难以回答「当时是否已允许写库」。迁到图策略后，每个节点的输入/输出类型、工具子集与边条件都可进入代码评审与单测夹具——这与传统状态机的测试方式更接近。\nFunctional agents 用 Kotlin/Java DSL 手写同类循环，可控性高于 Basic，但仍需自行保证阶段间类型传递；若业务已是「固定 DAG + 失败回路」，Graph 往往更省胶水代码。\n工具层：把副作用钉在 Java 方法上 # Koog 的 Agent 可理解为：Agent 本体 + 你的 JVM 应用（环境）+ 一个或多个模型；工具即带 schema 的 Java 方法。Annotation-based tools 要求类实现 ToolSet，方法标 @Tool 与 @LLMDescription。\nMore tools：AccountReadTools implements ToolSet，getLatestTransactions / 余额查询\n读侧与写侧分离是后续 limitedTools 的基础：\nAnd Even More tools：AccountWriteTools 含 dispute、取消、转账等写操作\npublic class AccountReadTools implements ToolSet { private final String userId; @Tool @LLMDescription(\u0026#34;Get account balance (in USD) for the current user\u0026#34;) public Integer getAccountBalance() { /* ... */ } } 常见误区：在 @Tool 里塞无关参数或返回不可序列化类型，导致 schema 生成失败；演示中的 CommunicationTools / AccountReadTools 为示例命名，非框架内置类型。另一个误区是把用户身份只写在 Prompt 里：演示在 AccountReadTools(String userId) 构造器中注入 userId，工具实现内强制按当前主体查库，比让模型自行拼接 accountId 更稳妥——这属于应用层授权，Koog 不替代 Spring Security，但工具边界应与 Principal 一致。\nBanking 示例 使用同类 MoneyTransferTools implements ToolSet 模式，可作为脱离幻灯片 OCR 噪声的参照实现。\n领域建模：用 record 代替冗长 Prompt # 为什么：多步子流程需要「填表才能前进」的交付物，而不是一段不可执行的愿望清单。Structured output 与节点上的 @LLMDescription 把字段语义交给 schema。机制：子图声明 withInput / withOutput 类型后，框架在工具循环中约束结构化结果；官方 Kotlin 示例多用 @Serializable + @LLMDescription，Java record 可行但不宜把演示里的 @JsonProperty 直接当作官方唯一范式。\nCustom Strategy: with Domain Modeling — Input Data Type / output Data Type / Subset Of Tools\nAccountIssueSummary record 与 @LLMDescription 字段说明\n@LLMDescription(\u0026#34;Full information about the issue with user\u0026#39;s bank account\u0026#34;) public record AccountIssueSummary( @LLMDescription(\u0026#34;Account number of the user in the database\u0026#34;) String accountNumber, @LLMDescription(\u0026#34;Current account balance in US dollars\u0026#34;) Integer currentBalance ) {} 演讲者观点：「Prompt 给你 hope，domain modeling 给你 contract。」常见误区：只定义 output 类型却不限制工具，模型仍可能在一步内调用写工具。\n幻灯片强调子图三要素：Input Data Type、output Data Type、Subset Of Tools——三者同时成立时，模型要在工具循环里「填齐」AccountIssueSummary 才能进入 fixProblem（输入类型从 String 变为 summary record）。这比在 Prompt 里列举 JSON 字段更可测：可对 record 做单元测试、对 schema 做快照比对，并在 CI 里回归。\n若字段说明过长，优先精炼 @LLMDescription 与枚举约束，而不是退回无类型 Map；Map 会重新引入解析歧义与 silent failure。\n子图：按阶段收缩工具面 # AIAgentSubgraph 为每个阶段声明输入/输出类型、.limitedTools(...) 子集、任务文案；验证步用 .withVerification(...)，返回类型为 CriticResult（OCR 中的 CritiqueResult 应以官方 CriticResult 为准）。\nIdentify Problem：AIAgentSubgraph.builder() + `LimitedTools(communicationTools, databaseReadTools)\nIdentify Problem / Fix Problem 子图链与 withVerification\nvar identifyProblem = AIAgentSubgraph.builder(\u0026#34;identify-problem\u0026#34;) .withInput(String.class) .withOutput(AccountIssueSummary.class) .limitedTools(List.of(communicationTools, databaseReadTools)) .withTask(input -\u0026gt; \u0026#34;Identify the problem with the user and formulate a problem description\u0026#34;) .build(); var verifySolution = AIAgentSubgraph.builder(\u0026#34;verify-solution\u0026#34;) .limitedTools(List.of(communicationTools, databaseReadTools)) .withVerification(solution -\u0026gt; \u0026#34;Now verify that the problem is actually solved: \u0026#34; + solution) .build(); 机制约束：未列入 limitedTools 的工具对模型不可见。演讲者观点：对话步可用偏对话的模型、工具密集步可用偏 tool-calling 的模型；文档子图示例使用 llmModel 配置，幻灯片 OCR 中的 .usingLLM(...) 须在本地 Javadoc 核对后再用。\n常见误区：把验证逻辑塞进普通 withTask 而不走 withVerification，失去 CriticResult 与图边的类型衔接。\n识别阶段通常只挂 communicationTools 与 databaseReadTools；修复阶段才加入 AccountWriteTools——这与最小权限原则一致。验证子图产出 CriticResult 后，成功路径可把原始输入映射到 nodeFinish（文档示例使用 CriticResult::getInput 一类映射），失败路径仅传递 getFeedback 给 adjust，避免把整段对话历史手工拷贝进 adjust 的 Prompt。\nCustom Strategy 流程：Identify Problem → Fix Problem → Verify Solution 子图编排\n图策略：类型安全的边与 verify→adjust 回路 # AIAgentGraphStrategy 用 AIAgentEdge 的 onCondition + transformed 表达分支：验证失败时把 critique 映射为 adjust 的 feedback，再连回 verify。\n图策略代码与 Identify → Fix → Verify → Adjust 回路示意\nAIAgentGraphStrategy.builder() 与 transformed(CriticResult :: getFeedback)\nvar graph = AIAgentGraphStrategy.builder(\u0026#34;banking-support\u0026#34;) .withInput(String.class) .withOutput(AccountIssueSolution.class); graph.edge(graph.getNodeStart(), identifyProblem); graph.edge(identifyProblem, fixProblem); graph.edge(fixProblem, verifySolution); graph.edge(AIAgentEdge.builder() .from(verifySolution) .to(adjust) .onCondition(v -\u0026gt; !v.isSuccessful()) .transformed(CriticResult::getFeedback) .build()); graph.edge(adjust, verifySolution); return graph.build(); 为什么：把银行业务固化为可编译检查的状态机，避免运行时字符串胶水。常见误区：边的 from/to 类型不相容——应在 builder 阶段让编译器拦住。\nKey features 提到 LLM switching and seamless history adaptation；子图共享 LLM 上下文（subgraphs overview）。「缩小工具集时自动修正历史 tool call」未在已查阅文档中找到等价表述，标为演讲者观点/待验证。\n跨节点业务数据可放入 AIAgentStorage，与 LLM transcript 的托管分开理解：前者是领域状态，后者由框架在子图间传递并在换模型时做 history adaptation（官方表述，非「re-explain」字面算法）。自定义 guardrail 可用 Custom nodes 的 AIAgentNode.builder，与图策略正交。\n条件边：graph.edge(adjust, verifySolution) 形成验证回路\nPlanner：路径搜索与手写图的取舍 # 当分支组合爆炸、手写边成本高时，可用 GOAP planner：声明 precondition、belief（状态副作用）与 goal，由 A* 在运行时选路径——规划器本身不用 LLM 选路（各 action 的 execute 内仍可调 LLM）。\nPlanner Strategies：AIAgentPlannerStrategy + identify-problem action\nJava 文档入口多为 Planners.goap(...)，与幻灯片 AIAgentPlannerStrategy.builder 语义接近但构造 API 名以 Javadoc 为准。Planner agents 明示可控性弱于手写图——适合探索型编排，不适合强合规固定流水线。\n为什么可能选 Planner：业务动作库稳定、前置条件可形式化，但具体顺序随用户输入变化。机制：GOAP 在离散 action 空间做 A*，不用 LLM 选边，降低「跳步」概率；代价是运行时路径不如图策略可预测，审计时要记录 planner 选中的 action 序列。常见误区：把 LLM 放进 precondition 字符串里却不固化 belief 更新，导致搜索空间与实际状态脱节。\nPlanner Strategies Example：precondition / belief / execute 与 identify-problem action\n嵌入 Spring：执行器、会话与记忆 # Spring Boot 集成 可注入 MultiLLMPromptExecutor 做 provider fallback；配置前缀为 ai.koog.\u0026lt;provider\u0026gt;.*（非演示草案中的 koog.models）。多轮对话安装 ChatMemory.Feature + ChatHistoryProvider 的 store/load，配合 agent.run(message, sessionId)。\nai.koog.openai: api-key: ${OPENAI_API_KEY} @PostMapping(\u0026#34;/support\u0026#34;) String support(Principal principal, @RequestBody String request) { return agent.run(request, principal.getName()); } 常见误区：把 ChatMemory 当成崩溃恢复——它通常在 run() 成功后 存消息；与下图的 Persistence 不同。\n安装示例（以官方为准）：\nAIAgent.builder(promptExecutor, model, toolRegistry, strategy) .install(ChatMemory.Feature, cfg -\u0026gt; cfg .historyProvider(jdbcHistoryProvider) .windowSize(50)) .build(); 依赖 ai.koog:agents-features-memory（≥ 0.7.0）。Postgres/JDBC 需自行实现 ChatHistoryProvider；若已用 Spring AI 的 ChatMemoryRepository，可走 spring-ai starter 适配。常见误区：windowSize 过小导致用户刚说过账号又被截断；过大则成本与延迟上升，需配合下文历史压缩。\n持久化：图节点 checkpoint 与 ChatMemory 分工 # Agent persistence 在图节点执行后 checkpoint：消息历史、最后成功节点、storage 等。Functional agents 文档写明 No state persistence，与「仅图/节点策略适合断点续跑」一致。\nKoog\u0026rsquo;s Persistence：install(Persistence.Feature, ...) 与 enableAutomaticPersistence\nvar agent = AIAgent.builder() .graphStrategy(graphStrategy) .install(Persistence.Feature, config -\u0026gt; { config.setStorage(storageProvider); config.setEnableAutomaticPersistence(true); }) .build(); 维度 ChatMemory Persistence 保存内容 对话消息 执行状态（含图节点路径） 典型场景 同一用户多次 /support 长链路、Pod 驱逐后续跑 保存时机 run 成功后 每节点后（可自动） 可观测性与历史压缩 # 安装 OpenTelemetry 特性 后，可对接 SpanExporter 或 Langfuse、W\u0026amp;B Weave、Datadog 等（文档已含 Datadog exporter，与演讲中「计划中」表述可能因时间线而异）。\nLangfuse：Tracing / Prompt Management 演示界面\nW\u0026amp;B Weave：Traces 与 playground 反馈\n长对话可在图中插入 AIAgentNode.llmCompressHistory(...)，策略如 Chunked(n) 或 FactRetrievalHistoryCompressionStrategy（非 OCR 中的 RetrieveFactsFromHistory）。\nKoog\u0026rsquo;s History Compression：AIAgentNode.llmCompressHistory()\nvar compressHistory = AIAgentNode.llmCompressHistory(\u0026#34;compress\u0026#34;) .withInput(AccountIssueSolution.class) .compressionStrategy(new FactRetrievalHistoryCompressionStrategy(/* concepts */)) .build(); 演讲者观点：事实检索式压缩在 JetBrains 内部基准约 6–8% 提升；无公开复现协议，不宜写进 SLA。\n可观测性侧，Langfuse 除 Trace 外还有 Prompt Management、Dataset 等（见演示截图）；生产环境建议把 traceId 与业务 sessionId 关联，便于从客服工单反查一次 Agent run。OpenTelemetry 导出配置可参考 Langfuse 文档中的 LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY 环境变量，而非硬编码密钥。\nLangfuse：Prompts / Playground / Scores 与 Tracing 并列\n压缩节点通常插在「修复」与「验证」之间：graph.edge(fixProblem, compressHistory); graph.edge(compressHistory, verifySolution);，在上下文触顶前用领域相关输入（如 AccountIssueSolution）抽事实，比整段原文截断更不易丢账号号等关键字段——仍须对压缩结果做抽检，因 LLM 摘要本质有损。\n策略选型（简表） # 策略 控制力 适用 Basic 低 原型、内部工具 Functional 中 代码即流程、可测试 DSL Graph 高 合规流水线、显式回路 GOAP Planner 中（路径动态） 分支多、愿牺牲固定拓扑 与 Spring AI、LangChain4j 的分工对比属 Q\u0026amp;A 演讲者观点，本文不展开竞品矩阵。\n落地检查清单（基于文档归纳）：（1）写操作是否只在后期子图 limitedTools 出现；（2）验证是否走 withVerification + CriticResult 边；（3）多轮 HTTP 是否使用 sessionId + ChatMemory；（4）长流程是否启用 Persistence 而非仅聊天记忆；（5）生产是否接通 OpenTelemetry 导出；（6）API 符号以 api.koog.ai 与依赖版本 Javadoc 为准，幻灯片 OCR 中的模型常量名会随版本变化。\nKoog 在 JetBrains 内部产品场景「battle tested」后再开源——具体产品线与规模属演讲者观点；技术选型仍应以你方合规要求下的图策略试验、故障注入与回放为准。若你正从 Python 编排迁回 JVM，优先复用现有事务边界与数据源连接，把 Koog 子图当作应用服务内的一层「有类型的用例步骤」，而不是旁路脚本——这样领域 record 与仓储层类型才能同源，减少 DTO 重复与漂移。\n参考与延伸阅读 # Koog 产品页（JetBrains） Key features — Java API 与能力清单 Basic agents — 默认工具循环 Annotation-based tools — ToolSet 与 @LLMDescription Custom subgraphs — limitedTools 与 Java 示例 Custom strategy graphs — AIAgentEdge 与条件边 GOAP agents — 规划器与 Java Planners.goap Spring Boot 集成 — MultiLLMPromptExecutor Chat memory — sessionId 与 windowSize Agent persistence — checkpoint 语义 OpenTelemetry support — SpanExporter 安装 Langfuse exporter 配置 History compression — 压缩策略 API Banking 示例 — 端到端领域建模 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-reliable-ai-agents-using-domain-modeling-with-koog-in-java/","section":"文章","summary":"用领域建模把 Java AI Agent 从「能跑」做到「可控」","title":"用领域建模把 Java AI Agent 从「能跑」做到「可控」","type":"posts"},{"content":" 有状态 Agent 与上下文编译：从 MemGPT 到 Letta 的工程分野 # 当团队把 LLM 应用从「单次 completion + 外挂向量库」推进到跨会话记忆、工具环、可回放调试时，争论往往不在「要不要 Agent」，而在三层张力：上下文谁编、记忆谁写、状态谁存。Letta（前身为 MemGPT）代表一条明确路线：把有限 context window 当作可编译的运行时视图，由框架做 Memory.compile() / recompile，由模型通过 tool 决定何时检索、何时改写 in-context 块。\n这与工作流引擎（LangGraph、Temporal 等）的争论并存：后者强调可恢复、确定性步骤；Letta 侧更强调同一套 tool-call 原语既能做开放环 agent，也能用 tool_rules 搭显式 workflow（演讲者观点；节目中未给出与 Temporal 的可靠性 benchmark）。本文按可核对文档/源码与嘉宾架构观点分开叙述——不给出单一「正确答案」。\nLetta Desktop Agents 列表：View and manage your agents below，含 weaviate-query-agent 等条目。\n创建 Agent：Starterkits 提供 Internet chatbot、Character roleplay、Start from scratch 等模板。\n问题空间：RAG、有状态服务与「黑箱上下文」 # 为什么要在 RAG 之外谈「Agent 框架」 # 常见生产 RAG 把检索结果自动拼进 prompt（Simple RAG 由应用决定何时查、查什么）。有状态 Agent 路径则把「是否检索、写什么记忆」推迟到模型显式 tool call（Agentic RAG：All the RAG logic is handled by the agent）。二者不是非此即彼：同一业务里，批处理索引仍可能是应用责任；对话环上的读/写时机才是分野。\n另一轴是状态持久化。无状态 API 每次带全量 history；Letta 文档将 agent 描述为 stateful agents——memory blocks、messages、tools 定义落在数据库，单步近似 读状态 → 编译上下文 → 推理 → 写回（嘉宾归纳为「agents as REST API」；实质为 REST/SDK 平台，表述宜与口号区分）。\n机制：两层「数据库」 # 角色 典型内容 谁触发读写 业务库（向量库/OLTP 等） 文档、交易、用户数据 Agent 经自定义 tool Letta 托管库 conversation、blocks、tool 定义 框架 + 内置 memory tools Context hierarchy 把 Memory Blocks（in-context）、Archival（out-of-context，on-demand tool）、Files、External RAG 分层——与「一切皆向量 top-k」的 naive 管线正交。\n怎么做（最小闭环） # Docker 起本地服务（官方指南）：docker run -p 8283:8283 letta/letta:latest。创建 agent 时挂载 human / persona 块（Memory blocks），通过 agents.messages.create 驱动一步推理；观察 ADE 中编译后的 token 占用而非仅看最终回复。\n常见误区 # 把 Letta 当成「又一个向量库封装」——archival 需 tool 拉取，blocks 才是始终可见的 in-context 面（文档对比表已写明）。 假设换模型会丢 persona——Models 文档 写明换 model 时 Agents keep their memory and tools（仍须自行回归 tool 兼容性）。 播客分屏：左侧 Weaviate podcast 标识，右侧嘉宾 Sarah Wooders（砖墙背景），属对话画面而非架构幻灯。\n虚拟上下文与 Core Memory：论文脉络 vs 产品术语 # 为什么 MemGPT 仍相关 # MemGPT 论文（arXiv 2310.08560）提出 virtual context management：在有限窗口内模拟更大上下文，并 intelligently manages different memory tiers（OS 式分层类比）。Letta 自述 Born from MemGPT（memgpt.ai / GitHub README）。\n边界：论文摘要未出现嘉宾口中的 “self-managed / self-editing memory” 原话；Memory blocks 文档 使用 self-editing 描述块行为——宜写「产品/文档术语」，不宜写成论文定理。「第一篇」自管记忆 agent 论文（演讲者观点）未做系统性 prior-work 检索。\n机制：human / persona 与工具写块 # 典型 chat 场景用 human、persona 两标签（Memory blocks）。模型通过 core_memory_append / core_memory_replace 改写（base.py）。块有 limit（文档示例 chars_limit=5000；源码默认 CORE_MEMORY_BLOCK_CHAR_LIMIT 更大——创建时以配置为准）。\n嘉宾观点：persona 不仅做人格，还可写入用户反馈，使后续每轮都带上可读「在线学习」，相对 fine-tune 更易审计。对立面来自生产 RAG 圈（如 Contextual AI 访谈 中把偏好写进权重的路线）——fine-tune 需要数据集、评测闭环与冲突解释（mechanistic interpretability 仅被点名，未展开）。\n路径 优势（归纳） 成本/风险 In-context blocks 人类可读、可手改、ADE 可见 占 token；模型可能写错 Fine-tune 权重 推理时零额外 token 数据与 eval 投入；与显式记忆冲突难 debug 本文不裁决产品选型；若合规要求可审计记忆，文档支持的 self-editing blocks 更贴近证据链。\nADE：AGENT SIMULATOR CONTEXT WINDOW 1954/8192 TOKENS，CORE MEMORY (2)，Bootup sequence complete。\nAgent 设置：TOOLS (6) 含 archival_memory_search；SYSTEMINSTRUCTIONS @ 5419 CHARS。\nLettacore tools (6)：core_memory_append 等与 MODEL letta/letta-free 同屏。\n怎么做 # # 概念示意：创建带双块的 agent（字段名以 SDK 为准） memory_blocks=[ {\u0026#34;label\u0026#34;: \u0026#34;human\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;limit\u0026#34;: 5000}, {\u0026#34;label\u0026#34;: \u0026#34;persona\u0026#34;, \u0026#34;value\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;limit\u0026#34;: 5000}, ] Bootup 后 Simulator 可见 token 从 ~1954 升至 ~2391（节目 OCR：ocr_pick_002 AGENT SIMULATOR CONTEXT WINDOW 2391/8192 TOKENS）——说明系统指令 + 块 + 消息共同吃掉预算，调试时要看编译结果而非单条 user message。\n常见误区 # 以为 core 块「自动同步全网事实」——块内容是 LLM 选择写入 的摘要，错误会固化（需 archival 纠错或人工改块）。 把 5000 字符当作全球统一上限——OCR 与文档示例一致，非源码默认上限的唯一真值。 上下文编译 vs 长窗口：预算工程，非「窗口越大越好」 # 为什么 200k 模型仍可能只用 ~30k # 行业常见做法：上下文变长后，把历史、检索、system 一并塞进窗口。嘉宾观点（未点名可复现论文）：痛点从「装不下」变为「装得下但难 debug、延迟/费用高、长窗推理质量未必优于受控短窗」。代码侧 LLM_MAX_CONTEXT_WINDOW[\u0026quot;DEFAULT\u0026quot;] = 30000（letta/constants.py）是缺省回退，不宜直接等同「官方唯一推荐 30k」；ADE 文档 举例将 200k 能力模型 artificially limit 到 16k。\n机制：编译管线里有什么 # ContextWindowOverview（源码 schema）统计项包括：core_memory、summary_memory、external_memory_summary（archival + recall 元数据）、system_prompt、functions_definitions、num_messages。Compaction 在过长时 summarizes older messages（默认 sliding_window 等）。嘉宾所称 「context compilation」 在文档中更多体现为 recompile、compiled system prompt、Memory.compile()——工程俚语与官方术语部分重叠（核验：部分验证）。\nSimulator：CONTEXT WINDOW 2391/8192，Bootup 后 core_memory_append 轨迹可见。\n怎么做 # # 文档示例方向：人为压低可用窗（字段以当前 SDK 为准） client.agents.update(agent_id, context_window_limit=30000) 用 Context window viewer 观察 Core memory blocks / message history / token usage 分项，再调 context_window_limit，而不是先换更大模型。\n常见误区 # 把「未用满 200k」当成浪费——受控预算是可观测性 + 成本 + 质量的折中（嘉宾观点 + 部分代码默认值）。 忽略 SYSTEMINSTRUCTIONS @ 5419 CHARS 与六类 Lettacore tools 定义占用的固定开销（节目画面 OCR）。 Agentic RAG 与 Archival：何时进上下文 # 为什么反对默认 top-k 灌 prompt # Archival memory：must be queried on-demand via tools，不能像 memory blocks 一样 pin 进窗口。RAG 对比表 写明 Simple RAG 由应用控制检索，Agentic RAG 由 Agent 控制——与 Weaviate 侧 Agentic RAG 叙事 同构，但 Letta 把「检索」实现为 archival_memory_search 等一等公民 tool。\n已验证边界：平台默认无「每条用户消息自动 top-k 插入」；开发者仍可在应用层预取后写入 blocks——属集成选择。\n机制：分页 + 更新 core 的「Map-Reduce」叙事 # 嘉宾观点：对超大数据集，可用 tool 分页扫外部库、把精炼结果写入 core memory，替代一次性 RAG top-k；主持人类比 LangChain Refine——无比 benchmark（类比）。\n与 MCP 路径对比（主持人经验）：Claude Desktop + Weaviate 经 MCP 写外部记忆；Letta 路径是 LLM 决定写 core / archival，框架编排持久化——非 Letta 官方 MCP 集成说明，仅架构对照。\n怎么做 # 仅当模型调用：\narchival_memory_search(query=\u0026#34;...\u0026#34;, top_k=5) 才把片段纳入后续轮次上下文；在此之前，向量库里的内容对 LLM 不可见。\n常见误区 # 建了 archival 却从不给 search tool——记忆成为黑洞。 把 in-context blocks 与 archival 混用标签——blocks 始终占 token，archival 按次计费检索。 约 13:50：双人对谈分屏，讨论记忆与工具执行时的对话帧（无 slide API 名）。\n统一原语：一切皆 tool call（含对用户说话） # 为什么要把 send_message 也做成 tool # 嘉宾观点：若聊天走 chat completion、工具走 function calling 两套 API，workflow / multi-agent 会分裂；Letta 将 send_message 列入 BASE_TOOLS（constants.py），用户可见回复经 assistant_message 发出（base.py）。\nMessage types 另有 reasoning_message / hidden_reasoning_message；MemGPT v2 系统提示 要求 inner monologue … before taking any action——部分验证「先推理再行动」；非所有 API 路径硬编码校验顺序。\n机制：tool rules 与 workflow # tool_rules（tool_rule.py）可约束子工具集合——ADE 中可配（UI 细节未完整核验）。同一原语下可搭「必须先后调用某工具」的 workflow agent（演讲者观点：不必单独的工作流运行时）。\n常见误区 # 以为「tool 步数」等于「对用户回复次数」——send_message 也可能多轮内部 tool 后才一次输出。 把 reasoning token 是否进入后续 context 照抄 OpenAI 文档——嘉宾转述会随版本变；Letta 侧用独立 message 类型承载（需读者自行核对当前 OpenAI reasoning 文档）。 Multi-Agent：共享块与消息 tool # 为什么不用「多 Agent 专用运行时」 # 嘉宾观点：multi-agent = 共享 memory blocks（update once, visible everywhere）+ inter-agent messaging tools。源码含 send_message_to_agent_and_wait_for_reply、send_message_to_agent_async、send_message_to_agents_matching_tags（multi_agent.py）——无单一名为 broadcast 的 tool（「广播」≈ tag 匹配发送，部分验证）。\n常见误区 # 共享块后不约定写权限——并发更新 persona 可能互相覆盖。 期待 ADE 内置 Chatbot Arena 式 pairwise 模型对比——嘉宾确认没有（演讲者观点）。 约 24 分钟：播客分屏，嘉宾闭眼倾听帧——用于节奏锚点，非技术规格图。\nTool 执行、沙箱与「Agent 就是普通应用」 # 服务端执行 vs 客户端 tool # Client tools：Server tools 在 Letta server 沙箱执行；Client tools 在客户端执行并经 approval 回传——相对 OpenAI Assistants「只返回 tool call、由客户端执行」的对比为访谈观点（本次未抓取 Assistants 当前文档）。\n沙箱选择（源码）：有 e2b_api_key 时走 E2B AsyncSandbox，否则 local dir sandbox——与「Cloud 默认 E2B、本地默认不 sandbox」的口述不完全一致（部分验证）：本地并非「完全不隔离」，而是 LOCAL 路径。\n状态存储：行记录 vs 整包 JSON # 嘉宾观点：不理解 Agent 领域流行「整应用状态一个 JSON blob」；Letta 用关系型持久化 conversation / blocks（Docker compose 含 letta_db pgvector）。竞品是否 JSON 未核验。\n常见误区 # 在 Cloud 跑不可信 Python tool 却未配置 E2B——可能落 LOCAL 路径，隔离级别需读部署配置。 把 letta/letta-free 当成自研权重——嘉宾称尚无自有模型、端点为 model router（访谈观点；产品名在 OpenAPI 中未完整核验）。 约 36 分钟：Weaviate podcast 分屏，讨论沙箱与部署时的对话画面。\n可观测性、评测与「品味」 # 开发期透明 vs Langfuse 类平台 # 行业做法：上下文黑箱 → 依赖 Langfuse 等还原 prompt。嘉宾观点：若框架像 ADE 一样展示编译后 context window，设计阶段对专用 observability 的绝对依赖下降；上线后仍要 trace/metrics（演讲者观点，无 A/B 数据）。\nNumeric eval vs vibe eval # 嘉宾观点：伴侣型、强个性化 agent 难以用 20 万题 benchmark 覆盖；「不是 eval，是 taste」；最佳评测常常是直接用 agent；大规模 numeric eval 易与真实行为脱节。Letta 侧机会：全状态落库便于回放「当时上下文」（evals 在规划，无承诺规模）。\n未核实：主持人提及 Berkeley「structuring context window + reasoning」新论文——无 arXiv 条目核对。\n对需要回归测试的团队，仍建议维护小规模黄金对话集（主持人提 50–100 条量级）：即使认同 vibe eval，也应固定「给定 agent 状态 + 用户输入 → 是否调用某 tool / 块内容是否含关键字」类断言；Letta 全状态落库使这类回放比纯 trace 平台更直接（eval 产品化在规划，非现成功能承诺）。\n与向量库协同：Weaviate 在画面中的位置 # 节目 ADE 列表出现 weaviate-query-agent（OCR：agent-5b01e7ea-06ee-42b3-9da0-e26f7cc0ad4e），说明典型集成是：Weaviate 存嵌入与对象，Letta agent 经自定义 tool 或 archival 后端查询；并非 Letta 替代向量库。主持人亦对比 MCP 把记忆写入 Weaviate 的路径——差异在谁发起写入（MCP 会话 vs Letta memory tools），不是「要不要向量检索」本身。\n若你要落地 # 先画上下文预算表：system + blocks + tools 定义 + 消息/compaction 各占多少 token；用 context_window_limit 人为压窗，再选模型，而不是反过来。 把 RAG 改成显式 tool 契约：archival/search 与 blocks 分工写进 agent 说明；集成测试覆盖「未调用 search 则不得引用库内事实」。 有状态部署按服务做：PostgreSQL 持久化 + Docker 或 Cloud；一步失败要能从 DB 回放 messages + 编译快照。 多 Agent 先共享块、后消息风暴：约定 persona/human 写权限；tag 广播前限制匹配范围。 沙箱与 tool 路径写进 runbook：核对 e2b_api_key、区分 client vs server tools，不可信代码勿假设「本地=无隔离」。 参考与延伸阅读 # MemGPT 论文（arXiv 2310.08560） Letta 文档首页 Context hierarchy（blocks / archival / files） Memory blocks 与 self-editing Archival memory 与 on-demand tools Compaction（旧消息摘要） Message types（tool_call / reasoning） RAG 模式对比 Simple vs Agentic Agentic RAG 教程 Stateful agents 与 DB 持久化 ADE Context window viewer Models 与 context_window_limit Docker 部署 Letta Server Client tools vs server sandbox Letta GitHub（letta-ai/letta） E2B 沙箱产品页 Weaviate：What is Agentic RAG ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-letta-ai-with-sarah-wooders-weaviate-podcast-117/","section":"文章","summary":"有状态 Agent 与上下文编译：从 MemGPT 到 Letta 的工程分野","title":"有状态 Agent 与上下文编译：从 MemGPT 到 Letta 的工程分野","type":"posts"},{"content":" 与 Agent 无关的 Java 质量护栏：用 AGENTS.md 与静态分析把标准写进仓库 # 团队一旦在 GitHub Copilot、Windsurf、Cursor 之间轮换 coding agent，最先碎掉的不是模型能力，而是编码标准落在哪份 Markdown 里。Copilot 认 .github/copilot-instructions.md，Windsurf 主推 .windsurf/rules/*.md 并同时支持 AGENTS.md，Claude Code 默认读的是 CLAUDE.md——文件名各一套，同步成本落在开发者身上。\n更稳妥的做法是把「给 agent 的 README」收敛到仓库内的 AGENTS.md 开放格式，再用 Maven verify 阶段的 Checkstyle、SpotBugs、ArchUnit 做可重复的验收轨。前者影响生成时的概率性合规；后者在构建失败时给出客观证据。下文按机制拆解，不依赖某次现场演示的违规条数作为行业基准（演讲者观点：双项目对比仅为说明性实验）。\nAgent 不一致：问题不在模型，而在标准没有单一事实来源 # 为什么 # Coding agent 输出非确定，同一 prompt 换版本或换厂商结果可能漂移；若规范只存在于聊天记忆或某 IDE 私有配置文件，PR 审查与 CI 都无法引用同一份约束。\n机制与约束 # 各产品在「持久化指令」上的路径不同，但方向一致：把跨会话规则从 prompt 里抽出来，放进仓库。演讲中将此概括为 agent churn 下的 instruction file 碎片化——痛点成立，但 Windsurf 官方路径是 .windsurf/rules/，而非口语里的根目录 rules.md；Codeium.md 在公开文档中未核实到同名约定，写稿应以各工具当前文档为准。\n怎么做 # 在版本库中选定 AGENTS.md 为跨 agent 主载体；对仍只认厂商文件的工具，用薄封装指向同一内容（见后文 Claude 互操作）。\n常见误区 # 以为「换更强的模型」就能替代团队标准文档。 把三套厂商 Markdown 手工复制粘贴当作长期方案——必然滞后于代码演进。 AGENTS.md：仓库绑定的开放约定，而非 IDE 附属品 # 为什么 # 标准应跟 Git 仓库走，而不是跟订阅或桌面客户端走；这样 Copilot agent、Windsurf Cascade、Cursor 等读取的是同一份 Markdown。\n机制与约束 # agents.md 自述为 A simple, open format for guiding coding agents，FAQ 明确：无强制 schema，普通 Markdown 即可。2025-12-09 Linux Foundation 新闻稿 宣布 Agentic AI Foundation (AAIF) 成立，创始捐赠项目包含 MCP 与 AGENTS.md（OpenAI 于 2025-08 发布、12 月捐赠）。精确表述宜用「纳入 AAIF」，而非「与 MCP 合并为同一项目」。\nGitHub Copilot 文档写明：多个 AGENTS.md 并存时，目录树中离编辑文件最近的一份优先；用户当前 chat 中的显式 prompt 仍可覆盖一切（agents.md FAQ）。\n怎么做 # 根目录放全局约定（构建命令、日志、横切架构）；controller/、service/、repository/ 等再各放一层局部说明，避免单次把整库规范塞进 context。\nrepo/ AGENTS.md src/main/java/.../controller/AGENTS.md src/main/java/.../service/AGENTS.md src/main/java/.../repository/AGENTS.md 常见误区 # 把 AGENTS.md 当成 Copilot 或某一家独有功能——多家已支持，Claude Code 例外见下节。 在根目录堆一份「万能圣经」——与「就近加载」机制背道而驰。 在 Spring Boot 分层项目里，可把「Controller 只调 Service」「@Service 类必须在 .service 包」写进各层 AGENTS.md，并在根文件用章节号索引（如 §2 架构、§3 包归属）。这样 agent 在编辑 BookService 时加载邻近说明，而不是整库规范。团队若维护 monorepo，agents.md 官方也建议 nested AGENTS.md——与 Copilot「最近文件优先」规则同构。\nContext 里 AGENTS.md 只占一层：别把它当成全部上下文 # 为什么 # Agent 实际「看见」的是多层信息的叠加：系统/厂商行为、自定义指令、会话历史、IDE 隐式上下文（打开文件、diff）、显式 @ 引用、终端与工具输出。演讲者观点：AGENTS.md 只是 custom instructions 层的一小块；若忽视其它层，仍会在中长会话里偏离标准。\n机制与约束 # 各产品对每层如何裁剪、计 token 实现各异，不存在跨厂商的「context engineering」统一规范——工程上应只注入与当前任务相关的标准（例如改 BookService 时优先加载 service/AGENTS.md）。\n怎么做 # 全局 AGENTS.md：\u0026lt; 2 屏能读完的 build/test、DoD、ArchUnit 索引。 层内文件：只写该包 API/分层规则。 在 DoD 中写清「改完必须跑 ./mvnw verify」，让工具输出进入反馈层。 常见误区 # 认为写了 AGENTS.md 就等于 100% 合规（演讲者经验估计约 80–90%，无官方统计）。 在 chat 里重复长篇规范，与文件内容双份占用 context。 编写 AGENTS.md：克制、可迭代，并把 DoD 写进 Definition of Done # 为什么 # 无 schema 不等于可以无限堆砌。agents.md 允许任意标题，但过长或充满 agent 已知的常识会挤占有效 context（演讲者观点：引用近月论文称自动生成/冗长指令会降低生成质量——论文题名未在材料中给出，未独立核实）。\n机制与约束 # 建议章节（均非强制）：项目概览、./mvnw verify 等构建与测试命令、代码风格指针（指向 Checkstyle 配置）、架构规则（与 ArchUnit 规则 人工对齐 章节号，工具不会解析 § 2）、安全要点。\n怎么做 # ## Definition of Done - 代码变更后执行 `./mvnw verify`。 - 修复全部 Checkstyle、SpotBugs、ArchUnit 失败后再结束任务。 - 分层规则见 service/AGENTS.md；Controller 不得直连 Repository（AGENTS.md §2）。 Maven 官方说明：verify 阶段适合运行集成测试及额外检查（如绑定在此阶段的 Checkstyle）。\n常见误区 # 用表格复述 Java 命名惯例——Checkstyle 已覆盖的不要再写一遍。 ArchUnit 与 Markdown 章节号「自动联动」——because(\u0026quot;… AGENTS.md §2\u0026quot;) 只是描述字符串，不会建立机器链接。 双轨护栏：指令左移 + 静态分析验收 # 为什么 # 文档标准若不能变成 BUILD FAILURE，仍会滑进 main；人工 review 慢且主观。幻灯片归纳：AGENTS.md 定义标准，静态工具执行标准。\n机制与约束 # 左轨（概率）：agent 读 AGENTS.md / 发现仓库里已有 pom.xml 插件后倾向按现有门禁生成（演讲者观点：「多数时候」即使用户未在 prompt 提 Checkstyle）。 右轨（确定）：maven-checkstyle-plugin 的 check goal 默认绑定 verify，failOnViolation 默认为 true（check Mojo 文档）。 与 SonarQube 对比：定性成立——SonarScanner 需配置 SONAR_HOST_URL 等与 Server 通信；本地 Maven 插件路径更适合 agent 快速反馈循环。Sonar 亦有 Community Build 可本地部署，不宜简单说「Sonar 一定要外置 SaaS」。\n怎么做 # pom.xml 片段（演示 OCR 为 3.3.1；插件站当前有更新版，坐标族一致即可）：\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-checkstyle-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.3.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;checkstyle-validate\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;verify\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt;\u0026lt;goal\u0026gt;check\u0026lt;/goal\u0026gt;\u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; 常见误区 # 只跑 mvn compile——绑在 verify 的检查会被跳过。 指望 Sonar 与 Checkstyle 二选一即可覆盖架构分层——ArchUnit 表达力不同，见下节。 Checkstyle：把风格与 import 规则钉在构建期 # 为什么 # 无规范时 agent 生成代码常出现 tab、AvoidStarImport、缺少空行等——这些在 review 里烦，在 CI 里更应直接失败。\n机制与约束 # Checkstyle 检查源码风格；常见模块包括 AvoidStarImport、FileTabCharacter、EmptyLineSeparator。\n怎么做 # 本地或 agent 执行：\n./mvnw verify 失败时日志中出现 maven-checkstyle-plugin:check (checkstyle-validate) 及具体违规行号，可将输出贴回 agent 迭代。\n常见误区 # 认为「能编译」等于「符合团队风格」。 对历史项目事后才接入 Checkstyle 却期待零改动——演示中无 AGENTS.md 的 demo-projects 在补插件后仍大量违规（现场构建输出，非通用基准）。 SpotBugs 与 ArchUnit：补足风格之外的缺陷与分层 # 为什么 # Checkstyle 不管 NPE 风险、资源泄漏或并发问题；包结构与「Controller 不得直连 Repository」等架构约束也不是风格检查能表达的。演示路线图中将三者并列：Checkstyle 管「看起来对不对」，SpotBugs 管「跑起来会不会炸」，ArchUnit 管「结构允不允许」——分工是 演讲者归纳，但各工具官方定位支持这一拆分。\n机制与约束 # SpotBugs：在 字节码上查找 bug patterns；Maven check goal 可使构建失败（SpotBugs Maven 文档）。与 Checkstyle 的 check goal 同名不同插件，绑定同一 verify 阶段时需用不同 execution id 区分。 ArchUnit：在 src/test 用 JUnit 表达架构规则，@AnalyzeClasses + @ArchTest 与 User Guide 中的 resideInAPackage、layeredArchitecture 等 API 一致。 怎么做 # SpotBugs 在 pom.xml 中通常与 spotbugs-maven-plugin 的 check goal 绑定 verify（细节见官方示例 POM）。本地验证：\n./mvnw verify -DskipTests=false ArchUnit 规则示例（与 OCR 中 ClassFileImporter、Service.class 语义一致）：\n@AnalyzeClasses(packages = \u0026#34;javaone.demo\u0026#34;, importOptions = ImportOption.DoNotIncludeTests.class) class ArchitectureTest { @ArchTest static final ArchRule servicesInServicePkg = classes() .that().areAnnotatedWith(Service.class) .should().resideInAPackage(\u0026#34;..service..\u0026#34;); @ArchTest static final ArchRule noControllerToRepo = noClasses() .that().resideInAPackage(\u0026#34;..controller..\u0026#34;) .should().accessClassesThat().resideInAPackage(\u0026#34;..repository..\u0026#34;) .because(\u0026#34;Controllers must not access repositories directly (AGENTS.md § 2)\u0026#34;); } OCR 可见 areAnnotatedWith(Service.class) 与 AGENTS.md § 交叉引用——需团队自行保持 Markdown 与测试同步。\n常见误区 # 在 AGENTS.md 写「禁止 Controller 访问 Repository」却没有 ArchUnit——agent 仍可能生成跨层调用。 把 ArchUnit 当成运行时 AOP——它是测试阶段断言，默认 lifecycle 中随 verify 里的 test 执行。 闭环：mvn verify 驱动 agent 自修复 # 为什么 # 标准写在 DoD 里却不执行命令，agent 仍可能提交红灯代码；IDE agent mode（Copilot agent、Windsurf Plan 等）若能跑终端，则 violations 可进入下一轮 prompt。\n机制与约束 # mvn verify 执行至 verify 阶段，聚合 Checkstyle、SpotBugs、Surefire（含 ArchUnit 测试）。退出码非 0 即硬失败——与 agent 是否「理解」无关。\n怎么做 # 在根 AGENTS.md 的 DoD 写明 ./mvnw verify。 让 agent 在任务末尾读取终端输出并修复，直至 green。 换 agent 时无需改标准文件——演示中 Windsurf Pre-Release 与 Copilot 均仍针对同一 BookService 代码库工作（演讲者观点：Claude 原生不读 AGENTS.md，需互操作）。 Claude Code 官方做法（优于非官方 hack）：在 CLAUDE.md 中 @AGENTS.md import，或 ln -s AGENTS.md CLAUDE.md（Windows 建议 import）。\n# CLAUDE.md @AGENTS.md 常见误区 # 假设 agent 一定会自动跑 Maven——取决于产品、权限与 DoD 是否足够明确（产品行为，非 Maven 规范）。 在 prompt 里写一遍 verify，却不在 AGENTS.md 固化——换会话即丢失。 落地清单：从碎片化指令到可验证仓库 # 步骤 动作 1 用根 AGENTS.md 替代多套厂商规则；Claude 用 @AGENTS.md 2 按包分层嵌套 AGENTS.md，遵循 nearest wins 3 pom.xml 绑定 Checkstyle / SpotBugs / ArchUnit 至 verify 4 DoD 要求 ./mvnw verify + 修复全部失败 5 ArchUnit because(...) 与 AGENTS.md 章节号人工对齐 6 CI 与本地使用同一 verify，避免「本地绿、流水线红」 三条收益（演讲者归纳，与幻灯片一致）：Agent Independence——换工具仍读同一标准；Build Accountability——未过门禁不可视为完成；Team Confidence——review 可聚焦业务逻辑。有护栏的 spring-boot-demo 在 agent 模式下会先读取各层 AGENTS.md 再生成，并出现 GlobalExceptionHandler 等横切结构；无 AGENTS.md 的 demo-projects 由 Junie 生成时则缺少统一异常处理与文档化注释——差异方向与机制一致，具体类名与违规数为演示现场结果，不宜外推为行业统计。\n具体 checkstyle.xml、SpotBugs include 过滤器与演示仓库 URL 未在公开材料中给出完整正文；落地时应从团队现有 Checkstyle 配置或 Google/Java 社区规则集 起步，再逐步收紧。\n参考与延伸阅读 # agents.md — 面向 coding agent 的开放 Markdown 格式 agentsmd/agents.md — 规范与示例仓库 Linux Foundation — Agentic AI Foundation 成立新闻稿（含 AGENTS.md、MCP） GitHub Copilot — 仓库自定义说明（copilot-instructions.md、AGENTS.md、路径级 instructions） Cursor — Rules 与嵌套 AGENTS.md Windsurf — AGENTS.md 与 Rules 引擎 Windsurf — Cascade Memories 与 .windsurf/rules Claude Code — Memory、CLAUDE.md 与 @AGENTS.md 互操作 Apache Maven — 生命周期与 verify 阶段 maven-checkstyle-plugin — check goal 与 failOnViolation Checkstyle — 检查模块索引 SpotBugs — 介绍（字节码 bug patterns） SpotBugs Maven Plugin — check goal ArchUnit — User Guide（@AnalyzeClasses、layeredArchitecture） SonarScanner CLI — 与 SonarQube Server 通信要求 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-javaone-2026-agent-agnostic-guardrails-universal-java-code-quality-with-agents-md-and/","section":"文章","summary":"与 Agent 无关的 Java 质量护栏：用 AGENTS.md 与静态分析把标准写进仓库","title":"与 Agent 无关的 Java 质量护栏：用 AGENTS.md 与静态分析把标准写进仓库","type":"posts"},{"content":" 语义查询引擎：当 LLM 算子进入查询优化器 # RAG 与 Agent 把「检索 + 生成」推成默认架构，但生产管道里仍大量存在 批式分析：对百万行图文音做 filter、join、map，再与关系算子混写。传统 TPC-H / TPC-DS 几乎不含长文本与多模态字段；Spider、BIRD 等 text-to-SQL 基准也缺少 search / 语义算子 维度。MIT 博士生 Matthew Russo 参与 SemBench 与 Palimpzest / Abacus 等工作，主张把 foundation model 上的 semantic filter / join / classify / map / rank 当作一等查询算子，由 logical / physical plan 与 代价–质量 优化器统一调度——这与「每个步骤手写 Python + 调模型」的脚本式 RAG 不是同一条路。\n下文按问题空间、算子本体、执行与优化、基准与落地边界组织；表达力可互写 与 优化器可重写 并不等价；访谈中的产品对比、工业 bear case、具体美元数字在无法核对处会单独标注。\n问题空间：从 ML UDF 到语义算子 # 为什么：关系型 WHERE 无法表达「图像路径指向红车」「合同条款是否含竞业限制」；历史上 BigQuery ML 等路线把预测函数嵌进 SQL，但泛化依赖特征工程。Foundation model 的 zero-shot 能力使 谓词与变换 可直接写在查询里（演讲者观点）。\n机制/约束：一旦管道含 LLM 或 vision 调用，延迟与美元成本 常压倒传统 join 与索引扫描——向量检索约毫秒级、单次 LLM 调用可达秒级是常见经验量级（演讲者观点；具体比例随模型与 batch 而变）。优化目标从「最小化 I/O」转向 cost–quality–latency 三维，且 quality 往往只能采样估计。\n怎么做（概念）：把用户意图编译为带语义算子的计划，而非在应用层逐行 apply()：\n# 概念示意：声明式语义 filter（非某系统完整 API） plan = ( scan(\u0026#34;papers\u0026#34;) .semantic_filter(\u0026#34;abstract mentions retrieval-augmented generation\u0026#34;) .semantic_map(\u0026#34;extract first author name\u0026#34;) ) # 优化器可选：先 filter 再 map、选 Flash vs GPT、RAG 预筛等 常见误区：把「语义查询引擎」等同于 text-to-SQL Agent。Weaviate Query Agent 侧重自然语言问答与 filter 生成；Transformation Agent 用 append_property / update_property 物化新列 回 collection——接近 semantic map/classify，但中间结果默认持久化。Palimpzest 类引擎强调 on-the-fly 计算、不必写回库（架构对比为演讲者观点）。另一条误区是把 向量库 当作完整语义 QP：ANN 解决相似度检索子问题，不自动提供跨表 semantic join 的物理计划选择，也不替代 filter pushdown 与 模型级实现规则。\n约 10 分钟处双人对谈：左侧 Weaviate podcast 角标与 FAU 文凭，右侧嘉宾居家背景；本集画面无架构幻灯片，机制需对照 SemBench / Abacus 论文。\n算子本体：五类 taxonomy 与 map 万能论 # 为什么：SemBench 将核心算子规范为 filter, join, classify, map, rank（论文写作 filters, joins, mappings, classification, ranking），以便横向比较 LOTUS、ThalamusDB、Palimpzest、BigQuery 等各自不同的 API 表面（已核验：arXiv:2511.01716 摘要与 README 表头）。\n机制/约束：存在「semantic map 万能论」：filter（二元谓词）、classify、rank 理论上都可写成 map（rank 在全表排序时更接近 aggregate）。Russo 的立场是：表达力等价 ≠ 优化等价——只有 filter 天然适合 Oracle + proxy 式近似查询处理（AQP）：proxy 与 Oracle 对同一行可判定「同意/不同意」，高/低置信区间可跳过昂贵 Oracle（演讲者观点；与 LOTUS cascade 中 learn_filter_cascade_thresholds 及 gold algorithm 术语方向一致，已部分核验）。semantic map（如生成摘要）难以用二元一致性做同样阈值带（演讲者观点）。\n怎么做（互写 vs 优化）：若仅需表达力，可用 map 模拟 filter：map(\u0026quot;does image show red car? yes/no\u0026quot;) 再关系过滤；若需 cascade，应保留独立 semantic_filter 算子类型，让优化器学习 proxy 阈值（LOTUS）或黑盒实现（Palimpzest）。semantic rank 在 SemBench 中单独成类，全表排序时更接近 aggregate + order，不宜强行并入 map（taxonomy 设计动机；已核验 README 分列 Rank）。\n常见误区：在 benchmark 设计里用开放式摘要作 ground truth——SemBench 倾向 刚性字段抽取（如论文作者名），开放生成评测仍属边界未闭合问题（演讲者观点）。\n画面角标 OCR：a\u0026amp;% Weaviate poc Jcas ‘SL vg NMMAMILE Moy（五类算子讨论时段；无技术幻灯片文本）。\n画面角标 OCR：@@\u0026amp; Weaviate q @ nodcast BF podcast（filter 与 Oracle/proxy 讨论时段）。\n执行模型：急切 DataFrame 与惰性 SQL 计划 # 为什么：算子顺序决定 调用次数（尤其 filter 与 map 的 reorder）及 能否 pushdown。\n机制/约束：\n路径 典型行为 优化空间（文献/代码） LOTUS DataFrame sem_* 逐算子 materialize（演讲者观点） LazyFrame + optimize() 支持 cascade / predicate pushdown（部分核验） Palimpzest / ThalamusDB / BigQuery 整查询提交 → logical reorder + implementation rules Abacus：transformation rules + implementation rules（如 PushDownFilter、EmbeddingJoinRule） 怎么做（计划示意）：扫描论文表 → 按主题 semantic filter → 两次 semantic map 时，应先 filter 再 map，避免对全文跑昂贵 map（Abacus 论文案例；已核验 存在 filter pushdown 规则）。\nThalamusDB 补充：ThalamusDB 自述 approximate query processing 引擎，与 LOTUS 的 cascade 是否同构 未在 README 中核验到 proxy/oracle 三元结构——嘉宾「可能类似」仍应标为 访谈观点。\n常见误区：认为有了向量库就自动获得 查询级 优化。ANN 解决的是检索子问题；语义 join 两侧若仍走 nested-loop LLM，复杂度仍可达 N×M（Palimpzest NestedLoopsJoin 对左右候选双重循环，已核验）。另：LOTUS 已有 LazyFrame 时，「DataFrame 完全无法 reorder」应弱化为 默认急切 API vs 显式 optimize() 的对比（部分核验）。\n约 20 分钟处：嘉宾手持水杯阐述 physical plan；背景仍为 Weaviate podcast 角标，无计划树幻灯片。\n画面角标 OCR：Weaviate podcast / Nlanite Bj, / a PC E Pry,（logical/physical plan 讨论时段）。\n语义 join：学术 bull case 与工业 bear case # 为什么：传统分析型 workload 里 join reorder 与物理实现选择曾是核心；语义 join 若同样主导美元账单，优化器值得投入（演讲者观点）。\n机制/约束：\nBull：含多模态 join 的查询里，实现选择可导致数量级费用差——嘉宾口述修订案例：约 12,000 张图、总费用 约 $2–$27、embedding join 显著便宜于逐张 vision LLM（访谈观点；公开 Abacus 论文与仓库 未核验到 12k 与 $2–$27 同数，MMQA 在 SemBench 规模为 1,000 图）。 Bear：真实 workload 难举大量刚需 multimodal join；更常见是两侧 semantic map 抽实体 + 关系 join。动物场景上 map 出 elephant 与 North African elephant 后字符串 join 失败（演讲者一手尝试；未给出 定量失败率）。 向量检索 vs 语义 join：约会匹配等「找相似 profile」更像 vector search，不必上升为声明式 join 算子（讨论性观点）。 EmbeddingJoin 先用样本 LLM 标定 min_matching_sim / max_non_matching_sim，相似度落在中间带才调用 LLM——与 filter 的「两阈值 + 中间带」同族（已部分核验）。\n常见误区：把所有「跨集合匹配」都建模为 semantic join。Many-to-many 文本对齐在工业界常被 map + equi-join 替代，且 map 抽取质量不稳定。\n画面角标 OCR：@@\u0026amp; Weaviate QF podcast / SN CN ATTN（语义 join 成本讨论时段；ATTN 不与字幕术语可靠对应）。\n约 40 分钟处对谈：嘉宾手势说明 join 场景与 embedding 路径；画面无费用曲线屏。\n优化器哲学：固定 Oracle vs 黑盒实现空间 # 为什么：用户不应手写每步的模型名、temperature、ensemble 与 RAG 的 K。\n机制/约束：\n流派 优化变量 代表机制 LOTUS 式（嘉宾概括） 在 固定 gold / Oracle 下用 proxy 降本 Cascade 阈值；论文强调相对 gold 的 accuracy guarantee（DOI 10.14778/3749646.3749685） Abacus / Palimpzest 式 实现为 黑盒，采样估 cost / quality / latency Pareto-Cascades、OptimizationStrategyType.PARETO、sample budget（论文示例约束 spend less than $1） Palimpzest 对 map/filter 的 implementation rules 包括：model selection、MixtureOfAgents*、CritiqueAndRefine*、RAGRule（上下文削减 + 检索）、k_budgets = [1,3,5,10,15,20,25] 等（已部分核验 rules.py）。语义 filter 可实现为单次 LLM、RAG → top-K → LLM，或由 agent 生成 Python / regex 一次编写再批量执行（演讲者观点，Palimpzest 设计哲学）。\n常见误区：把论文里的 $1 采样预算 理解成「每行 10¢」——公开 Policy 为 MaxQualityAtFixedCost(cost_budget) 等 计划级 约束（已核验）；per-row 10¢ 在已读文档中 未出现。\n画面角标 OCR：@\u0026amp; Weaviate 3@F podcast（Palimpzest 优化原语与采样预算讨论时段）。\n画面角标 OCR：\u0026gt; Weaviate 8@F podcast ye NIE ay（RAG/agent/Python 实现空间讨论时段）。\nSemBench：共同任务比排行榜名次更重要 # 为什么：没有共享 workload，「语义查询引擎」无法像 TPC 时代那样积累可复现的工程知识。\n机制/约束（对照 SemBench README，已核验）：\n场景（正式名） 查询数 模态 Movie 10 表 + 文 Animals（口语 wildlife） 10 表 + 图 + 音 E-Commerce 14 表 + 文 + 图 MMQA 11 表 + 文 + 图 Cars（嘉宾口语 medical） 10 表 + 文 + 图 + 音 合计 55 四模态均有 算子列仅有 Filter / Join / Map / Rank / Classify，尚无 semantic group-by / aggregate workload（已核验 表头无 GroupBy 列）。已评测系统：LOTUS、Palimpzest、ThalamusDB、BigQuery；README 指向 在线排行榜（本次环境 TLS 未能打开页面，榜单内容未独立核对）。\n评测哲学（演讲者观点）：早期 相对名次不如建立共同任务重要；prompt 敏感导致方差大。Ground truth 多来自 Kaggle 标签 + 合成列（如动物 location）；合成 filter 与 embedding 可能 相关（主持人以 Weaviate mod 10 合成 filter 经验类比）——benchmark 构造仍是开放问题。Abacus 论文在 CUAD 法律合同域报告相对次优系统 quality +6.7%–39.4%、10.8× cheaper、3.4× faster（已核验 摘要级数字；具体曲线需查正文表）。指标上 SemBench README 侧重 F1 / precision / recall / relative error，与生成任务的 pass@k 不是同一套口径——跨文献对比时需对齐 evaluator。\nsemantic GROUP BY：嵌入 → 聚类 → 簇摘要，嘉宾倾向归为 semantic GROUP BY（聚合本身也是语义），而非 join；与 Transformation Agent 物化属性形成对照（演讲者观点）。\n画面角标 OCR：@\u0026amp; Weaviate podcast 4 NUMERIC ayy（SemBench 规模与 workload 讨论时段；NUMERIC 未验证指 SQL 聚合）。\n画面角标 OCR：¢@\u0026amp; Weaviate 8@F podcast “= NILSMILE Mapp（semantic map 与 GROUP BY 讨论时段）。\n约 20 分钟处：主持人与嘉宾对拍；左侧 Weaviate podcast 标牌与波形装饰，无基准表格屏。\n混合查询与 deep research：尚未闭合的边界 # 为什么：真实系统常把 ClickHouse 式关系过滤与语义算子串在同一管道。\n机制/约束：当前多数 workload 下 只优化语义段 即可获得主要收益，因语义算子数量级更慢（演讲者观点）。反例：关系侧过滤到极少行后，语义调用次数很少时，关系引擎优化 与 Palimpzest 未专门优化关系段（嘉宾自述）会变得重要——Palimpzest 关系侧优化边界 未验证。\nDeep research（高层意图 → 系统自规划）与声明式 semantic QP 重叠程度待定（演讲者观点）。若未来 万级 filter 可并行 且 API rate limit 放松，瓶颈可能在关系与语义算子之间转移（嘉宾推测，未验证）。vLLM 自管权重：LOTUS 经 LiteLLM 可接 vLLM endpoint，但优化器是否利用自管权重降本——嘉宾称 Palimpzest 侧 非常基础、仍以 API 为主（访谈观点）。\n怎么做（混合管道）：关系引擎先做选择性极高的 WHERE / partition prune，再把 百行级 子集交给语义算子；在 Palimpzest 未优化关系段时，这一步往往要在外部完成，而非指望单一框架自动下推（演讲者观点 + Palimpzest 边界 未验证）。\n约 7 分 45 秒：双人对谈开场段；左侧 Weaviate podcast 角标，右侧嘉宾白墙背景。\n若你要落地 # 先固定算子语义与评测字段：开放摘要难评测；优先刚性抽取 + 可复现 F1 / relative error（SemBench README 方向），再谈复杂 map。 把 filter 与 map 分开建模：若需要 cascade / AQP，filter 类二值谓词与 map 的优化路径不同；勿因「map 能写出来」就合并算子类型。 用 SemBench 或自有 workload 量「计划级」美元：对照 sembench.org 与 Palimpzest Policy，在 join 上显式比较 NestedLoopsJoin vs EmbeddingJoin，勿假设向量库已优化 join。 Agent 物化 vs 查询内计算：需要多次复用的 enrich 列适合 Transformation Agent；一次性分析管道可评估 Palimpzest / LOTUS LazyFrame，避免中间结果写爆存储。 对访谈数字保持审计：12k 图、$2–$27、排行榜名次结论等，写作与采购决策前应回查论文修订与复现实验。 参考与延伸阅读 # SemBench 论文（arXiv:2511.01716） SemBench GitHub 与 workload 表 SemBench 在线排行榜（README 链接） Abacus 论文（arXiv:2505.14661） Abacus PVLDB 条目 Palimpzest GitHub Palimpzest 文档站 LOTUS GitHub LOTUS 语义算子优化（arXiv:2407.11418） LOTUS cascade 优化（VLDB 2025） ThalamusDB 项目站 Weaviate Query Agent 用法 Weaviate Transformation Agent 用法 RAG 综述（Lewis et al., 2020） TPC-H 规范入口 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-semantic-query-engines-with-matthew-russo-weaviate-podcast-131/","section":"文章","summary":"语义查询引擎：当 LLM 算子进入查询优化器","title":"语义查询引擎：当 LLM 算子进入查询优化器","type":"posts"},{"content":" 语音智能体时代的架构张力：SSM、低延迟 TTS 与「端到端」是否吃掉编排链 # 当 RAG、长上下文窗口与推理模型（reasoning models）同时进入生产栈，语音场景却暴露出一组更硬的约束：多模态信号能否在 ingestion 阶段完整保留、尾延迟（P99）是否压得住用户心理锚点、以及 架构创新是否必须让位于数据飞轮。Cartesia 联合创始人 Karan Goel（State Space Models / Mamba 研究背景）在公开讨论中给出的路线，与「百万 token 已商品化」的行业叙事并不重合。\n下文按 可文献核对的事实、厂商文档自述 与 演讲者观点 分层书写；凡本集未出现的 MOS、WER、行业占比数字，一律不补写。目标读者是已部署过向量库或语音栈的工程师——需要能直接对照自家 SLO 与架构选项，而非节目摘要。\n问题空间：Compound AI 与语音栈的硬指标 # 常见生产形态仍是 ASR → LLM → TTS 的级联（compound orchestration），配合向量检索或长上下文缓存。语音智能体（voice agent）在此基础上增加 电话/实时会话 维度：用户容忍的不仅是平均延迟，更是 P99 与 首包音频（time-to-first-audio）（演讲者观点；厂商文档侧有 WebSocket 低延迟示例）。\n与此同时，「Agent」在语音产品里往往指 可接电话、可定制人格的语音实体，并不等价于带 tool loop 的自主 agent 定义（演讲者观点）。高敏感通话保留人工、其余由 AI 承接的 bifurcation，是嘉宾对客服赛道的判断，未见行业统计支撑。\n分屏访谈开场：主持侧背板可见 Weaviate podcast 标识，无技术幻灯片。\n画面 OCR 残留字样含「Weaviate podcast Ailaatle」——品牌条，不能据此推断架构图内容。\n长上下文「堆窗口」与 SSM「压状态」：两条未收敛的路径 # 为什么仍谈二次复杂度 # 标准 Transformer 自注意力对序列长度的 per-layer 复杂度为 O(n²·d)（Vaswani et al., 2017）；FlashAttention 摘要将时间与内存复杂度表述为对序列长度 quadratic，属于可核验结论。行业侧则用 Gemini 百万 token 上下文、Command R+ 128k 等证明 工程堆窗口 + exact attention 优化 已可商品化——这与「架构性突破才能做机器记忆」的立场形成张力。\n机制与约束 # 路径 核心机制 约束 Exact attention 工程化 FlashAttention 1–3 等 IO-aware 实现；Context caching 复用前缀 token 复杂度仍为二次；窗口再大也有上限（演讲者观点） SSM / Mamba 状态压缩 选择性状态空间；Mamba-2 SSD 声称线性 scaling 长期推理、在线适应仍需「多次突破」（演讲者观点，无法核验路线图） 嘉宾将 context caching 类比为 prompt 的 KV cache（Hugging Face KV cache 文档 可核对机制）；Mamba 论文摘要给出相对 Transformer 的吞吐倍数，但 任务与硬件相关，不宜外推为全行业定律。嘉宾称 SSM 可在 NVIDIA GPU 上高效实现（Mamba README 要求 Linux + NVIDIA GPU）；关于 AWS Trainium/Inferentia 原生 Mamba 的表述 未找到一手文档页，宜视为待补证据的市场判断。公司仍训练 Transformer 对照组以回答「哪种架构更对」（演讲者观点）。\n怎么做（核对复杂度，而非站队） # # 从 arXiv 源码核对 Self-Attention 复杂度表（P01） curl -sL \u0026#34;https://arxiv.org/src/1706.03762v7\u0026#34; -o /tmp/attn.tar.gz tar -xzf /tmp/attn.tar.gz -C /tmp/attn \u0026amp;\u0026amp; grep \u0026#39;O(n^2\u0026#39; /tmp/attn/why_self_attention.tex 常见误区 # 把 窗口变大 等同于 范式改变——FlashAttention 解决的是 exact attention 的 wall-clock，不改变渐近阶（文献 + 演讲者观点）。 把嘉宾「近似 attention 通常不如 exact」 过度简化——论文批评的是部分近似方法在 wall-clock 上不占优，并非所有任务上近似都更差（核验报告边界）。 约 6 分钟处仍为双人 talking-head；背板 Weaviate podcast，未见采样率或模型结构幻灯片。\nOCR 片段「Weaviate podcast antir aN Metogy」——讨论 RETRO / RAG 时段的画面，无论文图表。\n从 SSM 研究公司到先做 TTS：Sonic 与生产约束 # 为什么音频成了「最简单」的 testbed # Cartesia 源自 Stanford Chris Ré 组脉络；公司博客 Announcing Sonic（datePublished 2024-05-31）写明基于 state space models (SSM) 发布 Sonic，并自建 state space model inference stack。嘉宾口述首发约在 2024 年 6–7 月，与博客 相差约一个月（可能为 API 公测或记忆偏差，未核验）。\n反直觉之处在于：公司因 SSM 与「近乎无限上下文」愿景成立，却先商业化 TTS——嘉宾解释为多模态长记忆的 音频 testbed，且 TTS 是音频子问题中最「简单」的一块（演讲者观点）。\n机制：四目标拉扯 # 文档与博客侧面支持的多目标包括（非独立 benchmark）：\n表现力（naturalness、emotion 等，见 Sonic 3.5 文档） 极低延迟（博客自称 model latency 135ms——厂商自述） 可控性（volume / speed / emotion；自然语言精确描述音频仍不成熟，演讲者观点） 发音可靠性（地址、电话号等；文档有 拼写/拆读指南，未核验嘉宾列举的全部实体类型） 本集 未给出 MOS、WER、RTF 分位数；博客中的 NISQA 分数 ≠ 标准 MOS。\n怎么做（最小集成） # # 伪代码：流式 TTS 关注首包与分位延迟，而非仅 mean # 需在自家网络下实测 TTFA / P99（指标定义见厂商示例） import cartesia # 以 docs.cartesia.ai 当前 SDK 为准 client = cartesia.Client(api_key=\u0026#34;...\u0026#34;) for chunk in client.tts.bytes(model_id=\u0026#34;sonic-3\u0026#34;, transcript=\u0026#34;...\u0026#34;, stream=True): play(chunk) # 记录 time_to_first_audio, p99_turn_latency 常见误区 # 把 135ms model latency 直接当作 语音智能体 P99（嘉宾强调 P99，但未给毫秒数；指标定义可能不同）。 认为 TTS「只是读稿」——生产投诉往往集中在 罕见词、号码、专有名词（演讲者观点）。 进入 Sonic / voice agents 话题时的画面；OCR 仅「@\u0026amp; Weaviate」，无产品 UI。\n嘉宾 Cap 带像素化标识的分屏镜头；背板无 API 名称，需以官方文档为准。\n级联栈 vs 端到端多模态：信号在何处断裂 # 为什么级联仍占主流 # 级联 能工作且相当 impress（演讲者原话方向）——Cartesia 同时提供 STT 与 TTS 也说明厂商覆盖组件化栈。主持熟悉 LangChain、DSPy、STORM 等 分解式推理链；嘉宾并不否定 search \u0026amp; planning（「ML 只有 learning + search」），但认为 tone、emotion、prosody 在 ASR 文本化后不可逆丢失，共情回应受限（演讲者观点；本集无消融实验）。\n机制对比 # 端到端愿景是 单一 API 背后 ingest + respond（多模态），语音交互「像人一样」（演讲者观点）。未决问题：端到端是否会 取代 编排链，还是 吸收 显式工具调用 / DB 搜索——嘉宾未给出产品路线图细节。\n怎么做 # 若坚持级联：在 ASR 侧保留 置信度、说话人、韵律特征 等元数据传入 LLM（工程补偿，不能等价于 原始波形）。 若评估端到端：用 同一剧本 对比「情感指令是否被遵守」，勿只用文本 BLEU/WER。 常见误区 # 因级联「好用」就认定 永远不会被替代——嘉宾押注的是 模态损失 + 延迟锚点（用过极快系统后 4–5× 主观落差，演讲者观点，无量化研究引用）。 把 Vapi + Cartesia 集成（Vapi 文档 可核对）当作 端到端已成熟 的证据——仅为编排商集成 TTS。 讨论级联丢失 prosody 时段；OCR「Weaviate podc ast」无技术字。\n主持书架可见 A Thousand Brains 书脊；仍为访谈画面，非架构图。\nRAG、Pull/Push 与「记忆极大主义」的自相矛盾 # 为什么长上下文杀不死 RAG # 嘉宾自称 memory maximalist：理想是 一次 warmup 灌满上下文、不必 RAG；同时承认 just-in-time 信息仍必要（演讲者观点）——与「RAG 被长上下文杀死」的流行叙事相反。\n主持侧引用 RETRO（检索 + 生成，参数量约为 GPT-3 的 1/25 而性能可比）与 Fusion-in-Decoder（检索 passage 注入解码器）。需注意：主持口述「8B ≈ 175B」无法在 FiD 摘要中找到；RETRO 摘要表述为与 GPT-3 / Jurassic-1 可比 且 25× fewer parameters，不宜写成逐字 8B/175B 对标（核验边界）。\nPull vs Push # 模式 行为 嘉宾倾向 Push 系统预塞 context / 检索结果 当前主流 RAG Pull 模型按需索取信息 更理想的交互（演讲者观点） 主持希望 MCTS 式推理能 call the whole database；嘉宾对 embedding 注入中间层 无明确观点（自称非 RAG 专家）。这与 Weaviate 代表的 向量检索基础设施 形成互补而非替代关系——两套叙事可并存。\n怎么做 # 长文档：组合 Gemini context caching（或 vLLM prefix caching）+ 增量检索，勿假设「窗口够大就不用索引」。 评测：除 needle-in-haystack，增加 时效性事实 与 权限边界 case（本集未讨论基准）。 常见误区 # 把嘉宾的 maximalism 当成 产品承诺——其后立刻承认不现实。 忽视 Pull 仍需要 检索接口与权限模型，只是调用方从编排器变成模型策略。 可控 TTS / compound systems 前段；OCR「Weaviate j : podcaster」无架构信息。\n约 20 分钟分屏：主持 FAU 文凭与 Weaviate podcast 背板，讨论推理与搜索时段无幻灯片。\n推理时钟、Test-Time Compute 与「投诉优于基准」 # 机制 # 更快 inference clock → 同等 wall-clock 内更多 reasoning reps / 搜索步（演讲者观点）。Let\u0026rsquo;s Verify Step by Step 支持对 intermediate reasoning step 做 process supervision，与「分步推理」方向一致，不等于 OpenAI o1 产品机制（o1 一手页本次未抓取，宜标为访谈推断）。\no1 类 讨论常落在：显式生成 token 再喂回 vs 隐式 latent 搜索——嘉宾对后者 不清楚（演讲者观点）。\n数据飞轮与 many-shot # 嘉宾强调 生产投诉 优于抽象 eval（演讲者观点）。架构创新公司仍投数据；SSM 甚至 反过来决定 该收集何种数据（演讲者观点）。\nMany-Shot In-Context Learning 显示数百示例可持续提升；嘉宾事先不知 many-shot 术语，但认同与 长序列、loop 内改进 相关——弱相关于嘉宾口中的 in-loop meta-learning（权重更新），勿混为一谈。\n常见误区 # 用公开 leaderboard 替代 认真用户的抱怨——适合早期信号，不能替代合规与偏见审计。 把 many-shot 当成 在线学习已实现——论文是 in-context，非持久权重更新。 数据 / 架构突破讨论附近；OCR 噪声高，无可用技术字。\nmany-shot 话题帧；OCR「Weaviate podca」仅为品牌残留。\n约 50 分钟边缘应用（教育、玩具、端侧）时段：仍为访谈，无法从画面验证 Cartesia 产品 UI。\n长上下文与行业观察对话分屏；无 token 窗口数字叠加。\n若你要落地 # 先定义延迟指标：在同一地区/网络下测 TTFA、P99 轮次延迟，区分厂商 model latency 与智能体端到端；勿直接引用 135ms 充当 SLO。 级联栈加元数据：ASR 输出附 confidence / 说话人 / 韵律摘要，缓解但 不能替代 端到端多模态实验（需自建情感剧本集）。 长上下文 + RAG 并联：静态知识用 prefix cache；动态事实用检索；为 Pull 预留工具 schema，即使当前模型不会主动调用。 评测分层：TTS 用 罕见实体发音表 + 用户投诉聚类；推理用 step-wise 校验；勿混 MOS 与 NISQA。 架构叙事保持假设清单：SSM 生产路径（Cartesia 博客）与 FlashAttention 长窗口 可共存——用业务指标选栈，而非论文立场选 religion。 参考与延伸阅读 # Attention Is All You Need — Vaswani et al. FlashAttention — Dao et al. FlashAttention GitHub Mamba: Linear-Time Sequence Modeling — Gu \u0026amp; Dao Mamba-2 / State Space Duality — Dao \u0026amp; Gu Announcing Sonic — Cartesia 博客 Cartesia 文档总览 Sonic 3.5 模型说明 Gemini 长上下文 Gemini Context caching RETRO — Borgeaud et al. STORM — multi-perspective writing agent Many-Shot In-Context Learning Let\u0026rsquo;s Verify Step by Step — process supervision vLLM Automatic Prefix Caching 设计 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-cartesia-ai-with-karan-goel-weaviate-podcast-113/","section":"文章","summary":"语音智能体时代的架构张力：SSM、低延迟 TTS 与「端到端」是否吃掉编排链","title":"语音智能体时代的架构张力：SSM、低延迟 TTS 与「端到端」是否吃掉编排链","type":"posts"},{"content":" 在 exabyte 级非结构化内容上做企业 AI：权限、检索分层与 Agent 边界 # 企业把文档、合同、标书、媒体丢进 Box 一类内容云之后，真正难的不是「能不能调 GPT」，而是三件事同时成立：数据在变、谁能看见什么在变、索引与推理的账单在变。Box CTO Ben Kus 与 Weaviate 联合创始人 Bob van Luijt、Connor Shorten 在一期技术播客里，从存储与多租户权限一直谈到 RAG、embedding 经济学和 Agent——下文按工程主题重组，不按时序复述；凡未在 SEC 10-K、OpenAPI 或论文中核验的，均标为演讲者观点。\n问题空间：为什么「大窗口 + 通用模型」仍不够 # 行业常见论调是：context window 变大以后，RAG「几乎不需要了」。Ben Kus 的反驳（演讲者观点）针对的是企业私有库，而非公开网页问答：内容持续上传与修订；同一问题在不同用户下可见集合不同；把「全库」塞进 prompt 在成本与合规上都不现实。Bob 补充的视角更偏实现：窗口里的 token 序列在模型侧仍对应向量表示；用 Hugging Face Transformers 等栈对照 token→embedding 管线，有助于理解「窗口变大」并未消除检索前置的必要性——这是可复现的学习路径，不是 Box 产品规格。\n与此同时，Box FY2026 10-K 披露：截至 2026 年 1 月 31 日，超过 10 万家付费组织（paying organizations）；生成式 AI / Box AI、BYOM（bring your own model）为明确方向。Ben 在本场提到的 exabyte 级存储、数千亿至万亿对象、12 万+ 客户、数千万用户——在可访问的 10-K 中未出现 exabyte 或万亿级对象表述；迁移工具 Box Shuttle 仅描述 petabyte-scale 迁移能力。写作与选型时，宜把 SEC 数字与 CTO 口述规模分开标注。\n约 10 分钟处：三分屏访谈，主持方背景可见「Weaviate podcast」标识，Ben Kus 手势说明平台规模与架构约束。\n三层基础设施：存储、权限图、AI 不变量 # 为什么 # 非结构化内容平台的第一性约束不是 embedding 维度，而是 L1 吞吐与可靠性（百万级/秒交互量级、对象存储）、L2 协作权限（跨公司共享）、L3 AI（多模型、权限一致、快速接入新模型且不泄漏）。三者任一薄弱，上层 RAG 都会变成「能 demo、不能上线」。\n机制与约束 # L2 的关键在跨租户：用户 A@客户甲分享给用户 B@客户乙时，不能简单用「按企业硬隔离 shard」解决可见性（演讲者观点）。10-K 描述 internal/external collaboration、Smart Access、共享链接与分类策略；OpenAPI 中 Hubs 按 requesting user 列出资源——与「全局多租户可见性图」的表述方向一致，但未以该术语出现在官方文档。\nL3 的不变量：AI 回答不得越权。检索与生成只能使用调用者有权访问的内容；与 Azure OpenAI、Vertex AI、Claude、Bedrock 等集成时亦须保持（演讲者观点；与 Box API「不绕过 content permissions」原则一致，见 Box Developer Security）。安全文化上，Ben 强调权限与合规侧不能用「move fast and break things」——与创始人推动 AI 愿景形成张力，属组织层面取舍，非可单条引用的技术规格。\n怎么做（最小示例） # 在应用层，把「用户身份 + 内容 ACL」作为检索的硬过滤器，再调向量或关键词：\n# 伪代码：检索前绑定调用者可见 file_id 集合 visible_ids = box.acl.resolve_visible_files(user_id, hub_id=None) candidates = hybrid_search(query, filter={\u0026#34;file_id\u0026#34;: {\u0026#34;$in\u0026#34;: visible_ids}}) 常见误区 # 把「租户 ID = 分片键」当成权限模型，忽略 B2B 外链协作。 在向量库侧只做 app 层过滤，却不同步 Box 权限变更事件，导致陈旧可见性。 开场前后：三分屏画面，左下「Weaviate podcast」角标；OCR 可见片段 a / 3 =x / } Weaviate（无架构幻灯，仅为访谈画面）。\nRAG 与大上下文：对立命题各自成立的条件 # 为什么 # 企业 RAG 的收益被归纳为（演讲者观点）：最新数据 + 权限过滤后的上下文 + 生成——而不是单纯扩大 token 窗口。这与行业对 RAG 的常规定义（检索增强生成）相容，但 Ben 的论证重点是 ACL + 动态库，属场景判断。\n机制与约束 # Bob 将技术栈演进概括为：向量库 → RAG（embedding 做候选）→ Agent（生成结果可回写）（演讲者/产品方观点）。Ben 回忆 ChatGPT / GPT-3.5 API 达到「可生产」前后（约 2022 末–2023 初，宜与公开 API 时间线交叉核对）是企业认真投入内容的拐点；并主观认为早年惊艳的 GPT-3.5 Turbo 相对 GPT-4.1、Gemini 2.5、Claude 3.7 等在企业用例上已差距巨大——说明模型迭代本身是平台风险，而非一次性选型。\n怎么做 # 权限感知的 RAG 最小闭环：\nquery → (metadata/keyword/粗向量) recall@K → ACL filter → rerank / optional \u0026#34;deep\u0026#34; read → LLM w/ citations 常见误区 # 把「128k/1M 窗口」当成可替代增量索引的方案，忽略日更文档与撤销授权。 不做引用粒度（文件 vs 段落），导致答案无法审计到具体 chunk。 讨论 RAG 是否被大窗口取代时：访谈三分屏；OCR 含 tabs tH Wilts 等噪声及画面元素，无可读幻灯标题。\n约 17 分钟：Bob 双手比划说明向量/RAG/Agent 演进，主持方背景为 Weaviate podcast 标识。\n「找文件」与「找答案」：双粒度 embedding # 为什么 # 企业搜索长期混淆两件事：定位哪份材料 vs 定位材料里的哪段。AI 时代必须拆开，否则 recall 不足或 embedding 数量爆炸（演讲者观点）。\n机制与约束 # 文件级：哪份 PPT、哪份计划书——适合导航式查询。 段落/页级：财报中的收入或风险段——适合问答式查询。 Connor（Weaviate）将 ColBERT（Khattab \u0026amp; Zaharia, SIGIR 2020）作为长 PDF 的参考路线：多向量 + late interaction，相对固定 500-token 粗分块更细——论文机制已核验；Ben 未声称 Box 已上线 ColBERT。Ben 对「一页五段是否只要五个向量」的回答是：视查询类型而定，非固定五（演讲者观点）。\nColBERT 核心（文档侧）：查询与文档独立编码，通过 late interaction 做细粒度相似度，文档表示可预计算——适合长文档，但存储侧往往是多向量/文档，与下文成本节直接相关。\n怎么做 # 对同一 file_id 维护两级索引（概念示意）：\n{ \u0026#34;file_id\u0026#34;: \u0026#34;f_123\u0026#34;, \u0026#34;file_vector\u0026#34;: \u0026#34;[...]\u0026#34;, \u0026#34;chunks\u0026#34;: [{\u0026#34;page\u0026#34;: 3, \u0026#34;span\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;vector\u0026#34;: \u0026#34;[...]\u0026#34;}] } 查询路径：先 file_vector 或元数据缩小候选，再对子集做段落级向量或 ColBERT 式重排。\n常见误区 # 全库段落级预计算 embedding——Ben 称在 Box 规模下成本可达「数年客户付费总和」量级（演讲者观点，无公开账单）。 忽视「短段落 + 高维 float32」时，向量体积可超过原文（见下一节）。 embedding 粒度讨论时段：画面含 Weaviate 角标；OCR 无完整 ColBERT 字样，不宜臆造幻灯术语。\nEmbedding 经济学与分层检索 # 为什么 # Ben 的早期账单经验（演讲者观点）：存储与快速检索 embedding 的开销可超过计算本身；在 exabyte 内容旁再挂 exabyte 级向量不现实。Bob 给出可复现估算：1500 维 × float32 ≈ 6000 bytes（约 5.86 KiB）：\ndims = 1500 bytes = 1500 × 4 # float32 # → 6000 bytes 短文本 chunk 若逐段全量索引，存储侧出现 10×–100× 于原文 并不罕见（演讲者观点；精确倍数取决于 chunk 大小、量化与是否多向量）。\nWeaviate 侧，Bob 提到从「全内存」走向 disk-backed flat index、vector cache、超阈值转 HNSW 的 dynamic index（见 Weaviate Vector Indexing）。口语中的 「warm storage」在官方文档中无同名产品；正文宜写 flat index / disk-backed，避免写成已核验的产品名。\n机制与约束 # Ben 描述的高层检索管线（演讲者观点；「Deep Search」未在 Box OpenAPI v2025.0 中命中，不得写成已公开 API）：\n缩小 recall：元数据、关键词、预计算 embedding（含文件级）等。 对小子集做更贵分析：称为 Deep Search 的 AI 重排/深读（命名未核验）。 必要时再算段落级 embedding；部分场景查询时计算优于全量预计算。 行业常见的 two-stage retrieval（cheap recall → expensive rerank）与上述叙事弱关联，但不能等同 Box 实现。\n怎么做 # 分层检索伪流程：\ncandidates = cheap_recall(query, top_k=500) # file-level + BM25 scored = deep_rerank(query, candidates[:50]) # 演讲者所称 Deep Search；实现各异 context = embed_on_demand(scored[:10]) # 按需段落向量 常见误区 # 大客户要求「先把 30PB 全索引」——双方都应质疑；与「先 recall 再深算」相反（演讲者观点）。 只优化 embedding 计算 GPU，却忽视向量存储与索引账单。 分层检索与 Deep Search 口述段：Ben 双手比划；OCR 含 We hs Oy Tas 等噪声及 Weaviate 角标。\nembedding 存储讨论：三分屏访谈；OCR 片段 Weaviate : == fC = A（无账单或维度幻灯）。\nAgent、Workflow 与「复制粘贴税」 # 为什么 # Workflow / RPA 适合步骤固定、单元能力弱的流程（演讲者观点）。Agent 在 Ben 的工作定义里（刻意不教条）：目标驱动、有一定自主性、能代表用户执行多步——复杂度可分查询、变换、更重度的 transformational 等层级。Weaviate 产品侧的 query / transformation / personalization 三类 agent 是另一套产品分类，只能类比，不能划等号。\n企业价值案例：RFP。RAG 把「10 人×小时」压到约 1 小时，但人工在 Hub 与问卷间 copy-paste 仍重；完整 Agent 链（检索→变换→Box Doc Gen 所述 2025 年 2 月 GA）才接近「团队替我干活」（演讲者观点；RFP 指标与端点未在 OpenAPI 核验）。\nHubs：OpenAPI 存在 GET /hubs，按 requesting user 列出 Hub，可作为跨文件 RAG 容器的技术锚点。Box × Weaviate agents 集成由 Bob 称已发布（访谈观点）；宜以 Weaviate Agents 文档 与双方新闻稿为准补链。\n机制与约束 # Box 平台叙事：理解内容（AI）+ 生成内容（doc gen）；Agent 串检索、变换、写回文档。与纯聊天机器人的差别在副作用：写文件、填表、跨 Hub 编排——失败模式从「答错」扩展到「写错、泄露、越权写」。\n怎么做 # RFP 类工作流（概念层）：\nHub(历史标书) → retrieve Q\u0026amp;A pairs → draft answers → human review gate → doc_gen(新文档) 常见误区 # 把单次 RAG 问答当成 Agent，忽略状态、工具调用与写回。 Agent 上线后不保留人工审批门，在受监管行业不可接受。 Agent / RFP 讨论：三分屏；OCR 含 \u0026quot;POUR eR 噪声与 Weaviate 角标。\n尾声附近：主持与嘉宾三分屏；OCR 含 weirs “iy ire 及 Weaviate。\n约 40 分钟后段：Ben Kus 主画面，讨论 Agent 与文档生成；背景为抽象红橙色图案，无技术幻灯。\n云与模型：文档可核对部分 vs 口述 # 主题 可核对（示例） 访谈口径（未完全核验） 托管 10-K：substantial majority 外包至 GCP，备选 alternative providers 「100% hyperscaler 多云」 AI 合作 Azure OpenAI、Vertex AI、Claude、Amazon Titan（Bedrock）等 与权限不变量绑定（推断） 客户规模 \u0026gt;100,000 paying organizations（FY2025/2026） 12 万+、数千万用户 存储规模 Shuttle petabyte-scale 迁移 exabyte 平台总存储 若你的架构决策依赖托管拓扑或精确规模，以 10-K 为准；播客提供的是超大规模内容平台上的工程权衡样本。\n技术演进讨论：OCR 含 \u0026lt;\u0026gt; Awe 与 Weaviate；画面为访谈三分屏，非时间线图幻灯。\n若你要落地 # 先建 ACL 感知的 recall，再谈段落 embedding：文件级 + 元数据 + 关键词缩小候选；段落向量或 ColBERT 式多向量放在第二阶段，并算清 dims × sizeof(float) 与索引开销。 把「RAG 已死」改写成场景命题：动态库 + 权限边界下，检索仍是硬需求；大窗口用于压缩多段证据，不替代可见性求解。 权限与审计一等公民：检索过滤、生成引用、写回操作同一 user_id；定期对账 ACL 变更。 产品命名与官方 API 对齐：勿把访谈中的 Deep Search、warm storage 直接写进对外设计文档；Weaviate 侧用 flat index / vector cache 等文档术语。 Agent 从消除「复制粘贴税」的场景切入：Hub + 检索 + 人工门 + doc gen，比「全自动投标」更可交付。 参考与延伸阅读 # Box FY2026 Form 10-K（截至 2026-01-31） — 付费组织数、GCP 托管、Box AI / Doc Gen、模型合作 Box FY2025 Form 10-K — 同比口径对照 Box OpenAPI v2025.0 — Hubs 等 REST 面 Box Developer 文档入口 — 安全与集成指南 Box Developer — Security 原则 — API 不绕过内容权限 ColBERT 论文（arXiv 2004.12832） — 多向量 + late interaction Retrieval-Augmented Generation 综述（Lewis et al., 2020） — RAG 基线概念 Weaviate — Vector Indexing 概念 — flat / HNSW / dynamic index Weaviate — vector-index 配置参考 — vectorCacheMaxObjects 等 Weaviate Agents 介绍 — Agent 产品能力边界 Hugging Face Transformers 文档 — token 与表示层学习路径 Google Cloud — Vertex AI 文档 — 10-K 提到的合作方向之一 Azure OpenAI 服务文档 — 企业模型托管选项 Anthropic Claude 文档 — 第三方模型集成参照 Amazon Bedrock 用户指南 — Titan 等模型接入 本文重组自 Box × Weaviate 技术对谈中的工程观点，并与 SEC 10-K、OpenAPI、ColBERT 论文及 Weaviate 官方文档交叉核对；冲突处以文档为准。播客画面为三分屏访谈，插图不含可引用的架构幻灯文本。\n","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-box-ai-with-ben-kus-and-bob-van-luijt-weaviate-podcast-120/","section":"文章","summary":"在 exabyte 级非结构化内容上做企业 AI：权限、检索分层与 Agent 边界","title":"在 exabyte 级非结构化内容上做企业 AI：权限、检索分层与 Agent 边界","type":"posts"},{"content":" 真实仓库上的软件工程智能体：SWE-Bench 与评测脚手架之争 # 当 RAG 与 Agent 的讨论从「能不能写 LeetCode」滑向「能不能修生产 bug」，评测对象也随之变化：不再是单函数通过率，而是 GitHub issue → patch → 项目测试是否转绿。Princeton 与 Stanford 一作团队提出的 SWE-bench 把这一链条固化成可复现基准；配套的 SWE-agent 则把争论焦点推到 Agent-Computer Interface（ACI）——模型与代码库之间该留多少「智能」给 LM，多少交给符号脚手架。\n下文按工程主题整理机制、证据边界与仍在并行的路线；不把播客口述当作唯一事实来源，凡官方文档/论文可核对的与「演讲者观点」分开标注。\n开场画面：主持人举手致意，背景书架可见 Florida Atlantic University 文凭与 A THOUSAND BRAINS 书脊\n评测对象：从合成题到 PR 驱动的 resolved # 为什么 # HumanEval 类基准测的是孤立函数上的单测通过；真实工程却是跨文件定位、环境依赖、回归风险并存。若 Agent 产品只在这类指标上优化，上线后仍可能在「修一个 issue 弄挂十个测试」上翻车。\n机制与约束 # SWE-bench 论文摘要 写明：从 12 个流行 Python 仓库 收集 2,294 条真实 GitHub issues 与 pull requests；模型任务为 editing the codebase to address the issue；验收依赖 unit test verification，以 PR 合并后行为为参考。官网 original 页 进一步描述 pipeline：爬取 PR/Issue → 构建 Docker image → 对提交 patch 跑 Fail-to-Pass 等测试，判定 resolved。\n主指标是 resolution rate（提交中成功 resolved 的比例），不是检索场景的 MRR；SWE-agent 在 SWE-bench 上报告 pass@1 = 12.5%（摘要页可核对）。不同子集（Full / Lite / Verified / Multimodal）分母与难度不同，不宜不标明 split 就横比百分比。\n怎么做（minimal example） # # 概念流程：本地需 Docker；具体 CLI 以 swebench 文档为准 # 1) 取实例：repo + base_commit + problem_statement # 2) 模型产出 unified diff / patch # 3) harness 在容器内 apply patch → 跑 FAIL_TO_PASS 测试 # 4) 全绿 → resolved Hugging Face 数据集卡 提供 Oracle 与 BM25 retrieval 等设置，对应 SWE-bench_bm25_* 派生集——初版论文即含检索基线，而非「纯端到端猜文件」一种玩法。\n常见误区 # 把 leaderboard 上的 pass@1 式 resolved 率当成「已能替代中级工程师」——绝对分仍低，且子集 curated 程度不同。 忽略 FAIL_TO_PASS / PASS_TO_PASS 语义：resolved 是测试契约下的修复，不等于产品验收或 UI 主观正确。 名称含 SWE，便默认覆盖 Java、移动端、运维脚本；演讲者观点：当前系列只覆盖一部分仓库与工程活动，Multimodal 才系统扩展到 JavaScript / 视觉 UI（617 instances、17 个 JS 库，论文摘要可核对）。 约 4 分钟处嘉宾画面：OCR 可见 HANI Mi、Hi WA 等噪声片段；画面为 talking-head，无技术幻灯片\nAgent 循环 vs Pipeline：控制权与泛化税 # 为什么 # 「Agent」在工程里常指 LM 每步决定是否调工具、是否结束；另一条路线把流程写死为 localization → search → edit，并用 AST、向量库、类/函数清单替模型完成部分导航。两条路争的是同一预算：美元/实例、是否需要 Docker、换语言是否重写脚手架。\n机制与约束 # 维度 Agent（如 SWE-agent） Pipeline / compound 式（Agentless、AlphaCodium 类等） 控制流 模型驱动工具调用与停止 预设阶段，符号步骤更多 代码库表示 偏简单接口，让 LM 自导航 预处理为 NL 摘要、向量索引、符号表 执行环境 通常需 Docker、可跑测试 不少方案弱化或省略在线执行 泛化 演讲者观点：更通用，但更贵、运维更重 演讲者观点：Python SWE-bench 上常很强，换 JS/React 易失效 SWE-bench Multimodal 摘要 给出一次对照：SWE-agent 约 12% resolved，次佳约 6%（特定实验设定）。演讲者观点（John Yang）：在同一套 bash/CLI 交互下，agent 在 Multimodal 域仍相对最好，但绝对分仍低于 Python 版 SWE-bench——「接口通用」≠「任务已解决」。\n无单一最优解：存在 LM 能力 vs scaffold 责任 的显式 trade-off；Multimodal 论文中强调，为 Python OOP 定制的 parser 在 React hooks / 函数式 代码上可能系统性失效——符号脚手架越重，换域时越可能要拆掉重写（演讲者观点，与论文「跨语言更难」方向一致）。\n怎么做（minimal example） # # Agent 路线（概念） while not done: obs = env.run(last_tool_output) # 测试日志、文件片段、搜索摘要 action = lm.choose_tool(obs) # edit | search | run_tests | finish env.execute(action) # Pipeline 路线（概念） files = bm25_or_ast_localize(issue_text) patch = lm.edit_only(files) # 常无在线测试环 常见误区 # 用 Python 子榜高分推断「pipeline 全面优于 agent」——可能只是在 BM25 + 固定文件集 设定下过拟合。 把「无需 Docker」当作永久优势——官方评测仍以容器内跑测试为主线；pipeline 的「不执行」与 可复现 leaderboard 不是同一回事。 把 compound AI system（固定图：检索→重排→编辑）与 ReAct 式 agent 对立成意识形态；二者是可组合的工程选择。 约 8:42 嘉宾近景：OCR 锚定片段含 SS == SE OBR；画面无架构图，讨论点依赖口述\n约 9 分钟处：嘉宾白 T 恤近景，对应 ACI 与「语言无关接口」讨论时段\n检索、长上下文与「整库输入」 # 为什么 # Issue 文本往往不足以指向正确文件；要么 检索 缩小上下文，要么用 长上下文 吞下更多文件。2023 年前后长窗口刚实用时，缺少「真正吃掉整库」的 coding benchmark，团队曾尝试 把整个 codebase 交给 LM 修 bug（演讲者观点，Carlos E. Jimenez）。\n机制与约束 # SWE-bench 初版在论文与 HF README 中纳入 BM25 retrieval baseline——演讲者观点（John Yang）：当时选的是「最快能跑通」的方案，更细的检索/符号结构问题留到 SWE-agent 再系统回答。\n并行探索仍在 leaderboard 上共存：file-by-file 长上下文、向量库 schema、agent 自带 grep/搜索（ACI 专用搜索命令）。三者对 上下文预算、歧义空结果、假阳性文件 的权衡不同；未决，无公开结论证明哪一种在「全 split、全模型」下支配。\n怎么做（minimal example） # # BM25 检索 + 编辑（概念，与 HF 派生集一致） candidates = bm25_index.query(issue.title + issue.body, top_k=20) context = \u0026#34;\\n\u0026#34;.join(read_file(f) for f in candidates) patch = llm.generate_patch(context, issue) 常见误区 # 认为「128K 上下文 = 不需要检索」——成本、注意力稀释与 无关文件噪声 仍可能拖垮 pass@1。 只优化 embedding 模型，却不在 FAIL_TO_PASS 上验证 patch——检索 MRR 与 resolved 脱钩。 演讲者观点（主持人提出、嘉宾未否定但无对照实验）：增强 docstring / 仓库元数据有时比改 agent 更有效——值得做 ablation，不能当已证事实。 约 10 分钟处：嘉宾侧脸近景，浅灰墙面背景；对应数据动机与 BM25 基线讨论时段\nACI：工具输出形状决定 agent 上限 # 为什么 # 同一 LM，换交互界面可能大幅改变 resolved。SWE-agent 论文核心主张是定制 Agent-Computer Interface：让模型顺畅地读文件、搜代码、跑测试，而不是把原始 shell 输出无节制灌回上下文。\n机制与约束 # SWE-agent ACI 文档（可核对）要点包括：\n专用搜索命令：只列出有匹配的文件路径，避免刷屏。 文件查看器：每轮约 100 行；文档写明展示更多 match 上下文反而迷惑模型。 与裸 grep/bash 相比，强调摘要式搜索结果。 演讲者观点（John Yang）：曾尝试分页展示 grep 命中，翻到第 37 条仍占满上下文却未增加信号——分页浏览比一次性摘要更差（设计叙事；未见播客给出量化 A/B 数字，机制以 ACI 文档与论文为准）。\n怎么做（minimal example） # # 反模式：把 500 行 grep 原样塞进 prompt # 较优：工具返回「路径 + 行号摘要 + 可选局部片段」 search(\u0026#34;TypeError.*NoneType\u0026#34;) → src/foo.py:42-48 (snippet) tests/test_foo.py:10 (snippet) 常见误区 # 堆更多工具（GDB、浏览器、K8s）而不改 观测压缩——CTF agent 与 SWE-agent 初始工具集不同，演讲者观点 来自 John 的 InterCode / CTF 经验。 假设「语言无关」等于「零归纳偏置」——bash 接口在 Windows/C# 仓库上仍可能吃亏。 约 16:42 画面：OCR 含 EZ SS PEEEZTEAAAA 等噪声；嘉宾口述长上下文实验与接口设计\n评测基础设施：Docker、Verified 与数字的可信度 # 为什么 # Leaderboard 上的 resolved 率，隐含「patch 在什么环境里跑测试」。环境漂移、依赖缺失、 flaky test 会把 模型能力 与 harness 缺陷 混为一谈。\n机制与约束 # SWE-bench GitHub README 时间线（可核对）：\n2024-06-27：与 OpenAI Preparedness 合作，迁移到 fully containerized evaluation harness using Docker。 2024-08-13：发布 SWE-bench Verified——500 题，工程师审核描述、测试与可解性（verified 页）。 评测指南 将 Docker 作为现行评测要件。\n演讲者观点（John Yang）：称 2025 年 1 月 才意识到初版「现有 CI 即可复现」是错的，必须与 OpenAI 做 Verified 时强化容器化——这与官方 2024 年中已宣布容器化 在时间上冲突；更稳妥的表述是：团队对早期非容器实践的反思，或针对 Multimodal/JS 环境的再加固，不能写成「Docker 评测到 2025 年 1 月才引入」。\n演讲者观点：Verified 的 Docker 沙箱工程与 500 例人工筛选 同等重要，但前者不易在 Twitter 上传播。沙箱极限：浏览器渲染、headless Chrome、部分能力在沙箱中不可用——leaderboard 数字依赖底层设定（与 Multimodal 论文 增加 Node.js、Chrome、Xvfb 一致）。\n无法核实：John 口头「单实例 Docker 镜像最大约 10 GB」——已查 Multimodal 论文/HF/官网未见单镜像体积表；官网建议评测机 ~120 GB 磁盘（全库镜像规模），勿将口述当作上限定理。\n怎么做（minimal example） # # 提交评测前自检（概念） docker info # 按 SWE-bench 文档构建/拉取对应 repo@commit 镜像 # 使用 sb-cli / Modal 等云评测时核对 split 名称与 harness 版本 常见误区 # 只报 Verified 500 题分数，却用 Full 集训练的检索索引——数据泄漏风险需查项目政策，播客未展开。 在本地无沙箱环境「目测 patch 合理」代替跑测试——与 resolved 定义不符。 忽视 PyPI 导向 采集（collect README）与 Node/前端 仓库生态的差异——Multimodal 是为补这一缺口。 约 28 分钟处：John Yang 灰色拉链上衣、卧室背景；口述 Docker 与 Verified 工程贡献时段\n约 31:23：OCR 含 EEA 5 E—ddtg AAA；同段讨论 leaderboard 与 agent/pipeline 成本对比\n多模态与「看见界面」：Xvfb、Computer Use 与域迁移 # 为什么 # 大量前端 bug 的验收标准是 像素/布局/交互，Issue 文本却不包含截图。Multimodal 把 visual、user-facing JavaScript 纳入同一 resolved 契约。\n机制与约束 # Multimodal 论文 描述：在 SWE-bench Docker 上增加 Node.js、Chrome；用 Xvfb 模拟显示、xwd 截图；SWE-agent M 提供浏览器/看图能力。\n演讲者观点（Carlos E. Jimenez）：与 Anthropic Computer Use「同类」——未见 SWE-bench 官方文档建立产品级等价；Xvfb + 浏览器基础设施有论文支持，类比本身未证实。\n演讲者观点：容器化 + 不允许 agent 常驻本机 将成为常态——安全与可复现优先于「本地全自动」；官方推 Docker/云评测（Modal、sb-cli），但并非字面「禁止本地开发」。\n怎么做（minimal example） # # 概念环：容器内 headless 显示 → 截图 → 模型 → patch → 视觉/单元测试 Xvfb :99 → 打开 PR 复现步骤 → screenshot → VLM → edit JS/TS → npm test 常见误区 # 把 Multimodal 的 12% vs 6% 读成「多模态已接近人类」——绝对分仍低，且 HF split 合计 612（102 dev + 510 test）与论文 617、官网 517「含视觉元素」等口径可能不同，写作须标明来源。 在 Python pipeline 上微调 prompt，便期望零改动迁移到 React hooks——与嘉宾「拆掉 Python 专用 scaffold」警告一致。 约 50:19：OCR 含 lh Ii、Hh KK 等片段；口述 headless X11 与 Computer Use 类比时段\nsequenceDiagram participant I as Issue @ 截图线索 participant A as SWE-Agent M participant X as Xvfb @ Chrome participant H as Harness I --\u0026gt; A A --\u0026gt; X: 复现 UI X --\u0026gt; A: 像素观测 A --\u0026gt; H: patch H --\u0026gt; H: FAIL_TO_PASS + 视觉测试 人在环路：自主修 bug 还是协作副驾驶 # 为什么 # 生产环境问责在人；「全自动 merge」与「daemon 只总结 stack trace」是不同产品承诺。\n机制与约束 # 演讲者观点（John Yang，ReAct 脉络）：有人要指挥 agent 逐步执行，有人要 agent 给建议、人动手写；近未来答案因场景而异。「最终一切 agent 化」在同一段对话里与「协作形态高度场景化」并存——张力未消解。\n监控/诊断类 daemon：未必自动改代码（演讲者观点），避免无人负责的自动 patch。\n怎么做（minimal example） # # 分级落地 L0: 只读 — 总结 trace、猜根因、列可能文件 L1: 建议 patch — 人 review 后应用 L2: 自动 PR — 必需测试绿 + 人 approve + 回滚策略 常见误区 # 用 SWE-bench resolved 推销「无人值守 on-call」——基准不含 on-call 轮转、事故沟通、跨团队协调。 忽视 成本：演讲者观点 SWE-agent 修一例约 2+ 美元，pipeline 约 0.4–0.5 美元或更低（当时 leaderboard 经验）——官方无固定报价；官网 leaderboard JSON 可见单例 约 $0.19–$3+ 浮动，随模型与步数变化（无法核实为常数）。 约 23:34 分屏：嘉宾讨论 leaderboard 与工程取舍；画面仍为访谈头像\n约 55:01 收尾段：OCR 含 ie US、cS a 等噪声；嘉宾侧脸，总结未来方向与评测局限\n若你要落地 # 先钉评测契约：选定 split（Full / Verified / Multimodal）、harness 版本与 Docker 能力；用 评测指南 理解 resolved，勿用检索指标偷换。 在 Agent 与 Pipeline 间做显式 trade-off 表：是否要在线跑测试、单次美元上限、目标语言是否只有 Python；换 JS/前端时预案 拆掉 Python 专用 parser（Multimodal 教训）。 投资 ACI 而非堆模型：搜索/读文件的输出形状优先于再加一个 405B；对照 ACI 文档 做摘要式工具返回。 把环境当一等公民：跟官方 2024-06 容器化时间线对齐；若听到「2025 才上 Docker」类口述，以 README 为准做内部复盘，而非对外宣传。 产品分层人在环路：从只读诊断到自动 PR 分级上线；用 Verified 子集做回归，再考虑是否追求 Full 集分数。 参考与延伸阅读 # SWE-bench 论文（arXiv:2310.06770） — 任务定义、2,294 实例与测试验收 SWE-bench OpenReview（ICLR 2024） — 会议与审稿信息 SWE-bench 官网与 Leaderboard — 各子榜与提交入口 SWE-bench 原版说明（original.html） — 数据采集与 Fail-to-Pass SWE-bench Verified 说明 — 500 题人工审核子集 SWE-bench GitHub 仓库 README — Docker 迁移与 Verified 时间线 SWE-bench 评测指南 — Docker 要求与 resolution 指标 Hugging Face：princeton-nlp/SWE-bench — Oracle / BM25 等设置 SWE-agent 论文（arXiv:2405.15793） — ACI 与 pass@1 12.5% SWE-agent ACI 设计文档 — 搜索与文件查看器约束 SWE-bench Multimodal 论文 — JS、视觉测试与 Xvfb Hugging Face：SWE-bench_Multimodal — 公开 split 与字段 OpenAI：Introducing SWE-bench Verified — 与 OpenAI 合作背景（访问性以实际 HTTP 为准） Berkeley BAIR：Compound AI Systems — pipeline 式多组件系统框架 ReAct 论文（arXiv:2210.03629） — 推理与行动交织的 agent 脉络 Anthropic：Computer Use 文档 — 与 Multimodal 口述类比的独立阅读 ","date":"2026年5月18日","externalUrl":null,"permalink":"/zh-cn/posts/2026-05-18-weaviate-podcast-swe-bench-with-john-yang-and-carlos-e-jimenez-weaviate-podcast-107/","section":"文章","summary":"真实仓库上的软件工程智能体：SWE-Bench 与评测脚手架之争","title":"真实仓库上的软件工程智能体：SWE-Bench 与评测脚手架之争","type":"posts"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/categories/development/","section":"Categories","summary":"","title":"Development","type":"categories"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/kotlin/","section":"Tags","summary":"","title":"Kotlin","type":"tags"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/mvc/","section":"Tags","summary":"","title":"Mvc","type":"tags"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/servlet/","section":"Tags","summary":"","title":"Servlet","type":"tags"},{"content":" Spring Boot 4 技术栈纵览：Starter 粒度、MVC 版本协商与安全演进 # 摘要：Spring Boot 4 与 Spring Framework 7 组合下，依赖可按能力拆成更细的 Starter，Web 出站客户端可与服务端 MVC 依赖分离；同一代码库可在 Spring MVC 中启用内置 API 版本解析，并与 Spring Data JDBC、RestClient / 声明式 @HttpExchange 客户端协同。Spring Security 7 侧重可叠加的 Customizer\u0026lt;HttpSecurity\u0026gt;、一次性令牌登录、WebAuthn 与注解式多因子模型；配合 JVM AOT 与 Spring Data JDBC 的编译期仓储片段，可将启动路径向「训练—缓存—回放」方向推进，但参数需与 JDK、模块开关严格对齐。下文按依赖层与安全边界展开，示例坐标以当前 Initializr 与参考手册为准；配图中所见的 Boot 4.1.0 (SNAPSHOT) 与稳定线 4.0.x 并存时，以所选 BOM 为准。\n图中 Initializr 文案可见依赖说明：Spring Boot integration for RestClient and RestTemplate to make HTTP requests，以及 PostgreSQL Driver 段落的 A.JDBC and R2DBC driver that allows Java programs to connect to a PostgreSQL database。\n图中勾选列表含 GraalVM Native Support 说明文字 Support for compiling Spring applications to native executables using the GraalVM native-image，以及 Spring Modulith 条目 Support for building modular monolithic applications。\n1. Spring Boot 4：Starter 重组与出站 HTTP 依赖分离 # (1) 原理与动机\nBoot 4 将「按技术拆 Starter」作为常态：Classpath 上仅引入所需技术的 starter，可减少无关自动配置类的装载面。典型诉求是：构建基于 Servlet 的 Web 应用与「只做出站 HTTP 调用」可选用不同 starter，避免在无服务端场景拖入嵌入式 Web 容器相关配置。\n(2) 实现抓手\nSpring Initializr 依赖元数据 将依赖 ID 映射到具体 Maven 坐标；Initializr 界面仍可能出现 spring-boot-starter-web 等历史文案与 Boot 4 命名演进并存的现象，工程应以生成器输出的 spring-boot-starter-webmvc、spring-boot-starter-restclient（依赖 ID 常为 spring-restclient）等为准。 Spring Boot 4.0 迁移指南 — Starters 说明 starter 集合更一致、多数技术有专用 starter 及配套 test starter。 Modulith：org.springframework.modulith:spring-modulith-starter-core 由 BOM 管理，可在同一单体中约束模块边界。 (3) 怎么用\n\u0026lt;!-- 片段示意：仅出站客户端（坐标以 Initializr 生成为准） --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-restclient\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; spring-boot-starter-restclient 与 RestClient.Builder bean 的装配细节见 Boot 参考手册「调用 REST 服务」一章。\n2. Spring Data JDBC 与只读 HTTP API（/dogs） # (1) 原理与动机\nSpring Data JDBC 以聚合根为中心映射表行，适合中小型域模型快速暴露 CRUD。Java record 在存在与列名匹配的非零参构造器时，可按 JDBC 映射文档 的构造器绑定规则实例化实体。\n(2) 实现抓手\n仓储接口继承 ListCrudRepository。 控制器侧使用 Spring MVC 常规注解（@GetMapping 等）暴露 JSON。 (3) 怎么用\ninterface DogRepository extends ListCrudRepository\u0026lt;Dog, Integer\u0026gt; {} @RestController class DogsController { private final DogRepository dogs; DogsController(DogRepository dogs) { this.dogs = dogs; } @GetMapping(\u0026#34;/dogs\u0026#34;) Iterable\u0026lt;Dog\u0026gt; all() { return dogs.findAll(); } } curl -s http://localhost:8080/dogs 浏览器地址栏与 JSON 片段中出现 localhost:8080/dogs 与 [{\u0026quot;id\u0026quot;:45,\u0026quot;name\u0026quot;:\u0026quot;Prancer\u0026quot;,\u0026quot;owner\u0026quot;:\u0026quot;josh\u0026quot;,\u0026quot;description\u0026quot;:\u0026quot;A demonic, neurotic, man hating, animal hating。\n3. Spring MVC：内置 API 版本协商 # (1) 原理与动机\n在同一部署内并行维护多版本处理器时，需要在请求解析阶段确定版本、拒绝非法版本并可对接弃用提示。Spring Framework 7 提供统一的 ApiVersionConfigurer 编程入口；Boot 则用 spring.mvc.apiversion.* 将常见策略映射为应用属性。\n(2) 实现抓手\n机制（文档用语） Boot 属性键（附录中出现） 请求头 spring.mvc.apiversion.use.header 查询参数 spring.mvc.apiversion.use.query-parameter 完整键名见 Boot Application Properties；策略总览见 MVC API 版本解析。\n(3) 怎么用\nspring.mvc.apiversion.default=1.1 # 自定义头名仅为演示约定；生产环境请与网关及客户端统一命名 spring.mvc.apiversion.use.header=X-Dogs-Version application.properties 编辑区可见 spring.mvc.apiversion.default=1.1 与关于 spring.mvc.apiversion.use.header 的 IDE 提示行 (Use the HTTP header th。\ncurl -s http://localhost:8080/dogs curl -s -H \u0026#34;X-Dogs-Version: 1.0\u0026#34; http://localhost:8080/dogs 默认版本与显式头在两套请求中的路由差异，可通过 @RequestMapping 等映射注解上的版本约束观察；属性值类型以 Boot 属性附录为准。\n4. RestClient 与声明式 @HttpExchange 客户端 # (1) 原理与动机\n出站调用既可命令式构造 RestClient，也可用带 @HttpExchange / @GetExchange 的接口由 HttpServiceProxyFactory 生成代理，以便用类型契约约束远程调用。Framework 7 起 RestTemplate 已标记弃用，新项目优先 RestClient。\n(2) 实现抓手\nBoot：RestClient.Builder bean 与定制器。 注解：GetExchange Javadoc。 应用入口常用 @Import 注册 HttpServiceProxyRegistry 相关配置（具体类的包名以当前 Framework 版本为准）。 (3) 怎么用\n客户端接口（示例：第三方 HTTP API，URI 请替换为真实端点）：\ninterface CatFactsClient { @GetExchange(\u0026#34;https://example.catfacts/api\u0026#34;) CatFacts facts(); } 服务端聚合（避免「只写请求不写处理侧」）：\n@RestController class CatsController { private final CatFactsClient client; CatsController(CatFactsClient client) { this.client = client; } @GetMapping(\u0026#34;/cats\u0026#34;) CatFacts cats() { return client.facts(); } } 源码编辑区可见 @Import 参数片段 (CatFactsClient.class) 以及 import org.springframework.web.service.annotation 等行。\n同项目文件中可见 import org.springframework.web.service.registry 与注解 (CatFactsClient.class) 靠近 SpringApplication.run。\n5. BeanRegistrar 程序化注册 Bean 与 JSpecify @NullMarked # (1) 原理与动机\n静态 @Bean 方法不足以表达「按环境或循环批量注册」时，BeanRegistrar 在 register(BeanRegistry, Environment) 中增量声明 Bean，典型搭配 @Import(MyRegistrar.class)。与此同时，org.springframework.beans.factory 包级 @NullMarked 将默认空安全语义收紧为「非空除非 @Nullable」，与 JSpecify @NullMarked 定义 一致。\n(2) 实现抓手\nBeanRegistrar、BeanRegistry.registerBean(...)。 JSpecify 入门站点以 jspecify.dev 为准（注解语义以源码/JavaDoc 为准）。 (3) 怎么用\n@Configuration @Import(DynamicBeans.class) class AppConfig {} class DynamicBeans implements BeanRegistrar { @Override public void register(BeanRegistry registry, Environment env) { for (int i = 0; i \u0026lt; 4; i++) { registry.registerBean(MyRunner.class); } } } record MyRunner() {} 调试窗口上方源码可见 registry.registerBean(MyRunner.class) 以及日志行 Tomcat started on port 8080 (http) with context path '/'。\n结构视图列出 BeanRegistrar、BeanRegistry，下部源码窗口含 public @interface NullMarked 与句子 For a comprehensive introduction to JSpecify, please see jspecify.org。\n6. Spring Security 7：JDBC 用户存储与运行期边界 # (1) 原理与动机\nServlet 栈下，JdbcUserDetailsManager 在 UserDetailsManager 语义上扩展 JdbcDaoImpl，便于以数据库行驱动认证主体。引入 Security 并不自动等价于某种固定用户模型；需显式提供 UserDetailsService / UserDetailsManager 与 SecurityFilterChain。\n(2) 实现抓手\nJdbcUserDetailsManager 文档示例。 HttpSecurity Java 配置 中的 Customizer.withDefaults()。 (3) 怎么用\n@Bean SecurityFilterChain chain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(a -\u0026gt; a.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .build(); } curl -s -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; -u user:pass http://localhost:8080/actuator/health 控制台日志含 Global AuthenticationManager configured with UserDetailsService bean with name jdbcUserDetailsManager 与 Tomcat started on port 8080 (http)。\n7. 可叠加的 Customizer\u0026lt;HttpSecurity\u0026gt;：一次性令牌登录与 WebAuthn # (1) 原理与动机\nSpring Security 7 允许以 Customizer\u0026lt;HttpSecurity\u0026gt; Bean 增量拼装 DSL，而不必整体替换默认过滤器链形态。一次性令牌登录（OTT）将魔法链接式登录纳入 oneTimeTokenLogin，默认交互路径包含 /login/ott。WebAuthn / Passkeys 则在 http.webAuthn 上配置 RP 名称、RP ID 与 allowedOrigins，以匹配浏览器 ceremony 的来源约束。\n(2) 实现抓手\nOTT：DSL、DefaultOneTimeTokenSubmitPageGeneratingFilter 相关描述见同一手册页。 Passkeys：依赖可选 spring-security-webauthn（Initializr 映射见 依赖 JSON）。 (3) 怎么用\n@Bean Customizer\u0026lt;HttpSecurity\u0026gt; httpSecurityCustomizer() { return http -\u0026gt; http .webAuthn(w -\u0026gt; w.rpName(\u0026#34;spring\u0026#34;).rpId(\u0026#34;localhost\u0026#34;) .allowedOrigins(\u0026#34;http://localhost:8080\u0026#34;)) .oneTimeTokenLogin(ott -\u0026gt; ott.tokenGenerationSuccessHandler((req, res, token) -\u0026gt; { res.setContentType(\u0026#34;text/plain;charset=UTF-8\u0026#34;); res.getWriter().println(\u0026#34;please go to http://localhost:8080/login/ott?token=\u0026#34; + token.getTokenValue()); })); } 源码窗口可见 return HttpSecurity http — http.oneTimeTokenLogin( OneTimeTokenLoginConfigurer\u0026lt; 与控制台输出 please go to http: //localhost:8080/login/ott?token=aa26d038-edf0-420f。\n同一配置区域可见链式片段 http.webAuthn(、Customizer\u0026lt;HttpSecurity\u0026gt; httpSecurityCustomizer()，以及控制台重复的 please go to http: //localhost:8080/login/ott?token=aa26d038-edf0-420f。\n8. 多因子认证：@EnableMultiFactorAuthentication # (1) 原理与动机\n注解模型将「密码因子 + 额外因子（如 OTT）」映射为框架识别的 FactorGrantedAuthority，缺因子时可导向已配置的 OTT 登录页。文档明确密码认证默认附带 FactorGrantedAuthority.PASSWORD_AUTHORITY；配图 OCR 中出现的噪声片段 ONETIME TOKEN_AUTHORITY 与官方常量命名并不一致，以手册与 FactorGrantedAuthority 为准。\n(2) 实现抓手\n@EnableMultiFactorAuthentication 章节。 典型示例：@EnableMultiFactorAuthentication(authorities = { FactorGrantedAuthority.OTT_AUTHORITY })。 (3) 怎么用\n@EnableMultiFactorAuthentication(authorities = { FactorGrantedAuthority.OTT_AUTHORITY }) @Configuration class MfaConfiguration {} 浏览器端为多步交互；因子与 UserDetails 中 authority 的对应关系需在自有用户模型中落地。\nIDE 中并列打开 class CatsController、class SecurityConfiguration，以及类型提示中的 Customizer \u0026lt;HttpSecurity\u0026gt; 与 HttpSecurity 继承层次片段。\n9. JVM AOT、Spring Data JDBC 仓储片段与 Leyden 语境 # (1) 原理与动机\nBoot 的 JVM AOT 处理 旨在用预先计算的初始化与缓存缩短启动路径；Spring Data JDBC 在 AOT 模式下可生成仓储实现片段（官方文档描述命名模板为 \u0026lt;Repository FQCN\u0026gt;Impl__Aot，并强调属内部优化）。OpenJDK Project Leyden 汇总多份 AOT 相关 JEP，与「训练运行—生成缓存—回放」同属一类优化范畴。示例运行日志中曾出现 AOT 缓存与模块属性不一致报错：需在 dump 与 runtime 阶段对齐 jdk.module.addmods 等 JVM 参数。\n(2) 实现抓手\nSpring Data JDBC — AOT Repositories（Asciidoc 源码）：spring.aot.enabled、spring.aot.repositories.enabled、spring.aot.jdbc.repositories.enabled，以及「提供 JdbcDialect 以避免方言探测引发过早数据库访问」的建议。具体方言类型名应以所选数据库模块文档为准；若口头示例提到特定类名而公开手册未列出同名片段，应视为未在公开章节逐字核实。 生成类截图中的 DogRepositoryImpl__AotRepository 与文档模板在字面后缀上可能因版本演进不完全一致，以当前构建产物为准。 (3) 怎么用\npublic interface DogRepository extends ListCrudRepository\u0026lt;Dog, Integer\u0026gt; { Collection\u0026lt;Dog\u0026gt; findByName(String name); } 构建与 JVM 参数须对照 Boot AOT 手册与 JDK 发行说明逐步校准。\n生成源码中出现 public class DogRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport 与方法 public Collection\u0026lt;Dog\u0026gt; findByName(String name)。\n终端输出含 Reading AOTConfiguration app.aot.config and writing AOTCache app.aot 以及 [error][aot] Mismatched values for property jdk.module.addmods。\n10. 工程落地：脚手架解压与依赖树核对 # (1) 原理与动机\n示例工程从 Zip 解压后经 Maven Wrapper 构建；IDE 依赖树用于核对 Spring Framework 补丁版本、Jackson 主版本与 Modulith、Security 组件坐标，避免「本地可运行、CI 缺坐标」类问题。\n(2) 实现抓手\n通用：unzip、./mvnw -q -DskipTests package。 BOM：spring-boot-dependencies POM 中的 jackson-bom.version 等属性可与依赖树交叉验证。 (3) 怎么用\nunzip -q adoptions-springio.zip \u0026amp;\u0026amp; cd adoptions-springio ./mvnw -q -DskipTests package 终端解压日志可见 creating: adoptions-springio 与 inflating: adoptions-springio/pom.xml [binary]。\nMaven 依赖树列表中含 org.springframework:spring-core:7.0.6 与多行 org.springframework.modulith:spring-modulith-s。\n参考与延伸阅读 # Spring Initializr 依赖 ID 与 Maven 坐标索引 Spring Boot 4.0 迁移指南（Starter 重组） Spring Boot 4.0 — 调用 REST 服务（RestClient 集成） Spring Framework — REST 客户端总览（RestClient、声明式 HTTP） Spring Framework — MVC API Version 编程配置 Spring Framework — @RequestMapping 与 API 版本映射 Spring Boot 4.0 — Application Properties（含 spring.mvc.apiversion.*） Spring Data JDBC — 实体映射（Asciidoc 源码） ListCrudRepository JavaDoc Spring Framework — BeanRegistrar 接口源码（v7.0.6） JSpecify — @NullMarked 注解源码（v1.0.0） Spring Security — JDBC 认证与用户管理器 Spring Security — One-Time Token Login Spring Security — Passkeys / WebAuthn Spring Security — Multi-Factor Authentication Spring Boot 4.0 — JVM 上的 Ahead-of-Time 处理 Spring Data JDBC — AOT Repositories 章节（Asciidoc 源码） OpenJDK Project Leyden（AOT 相关 JEP 索引） Maven Central — spring-boot-dependencies 4.0.6 BOM（Jackson 等版本属性） ","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/posts/bootiful-spring-boot-4-by-josh-long-spring-io-2026/","section":"文章","summary":"Spring Boot 4 与 Spring Framework 7 组合下，依赖可按能力拆成更细的 Starter，Web 出站客户端可与服务端 MVC 依赖分离；同一代码库可在 Spring MVC 中启用内置 API 版本解析，并与 Spring Data JDBC、\u003ccode\u003eRestClient\u003c/code\u003e / 声明式 \u003ccode\u003e@HttpExchange\u003c/code\u003e 客户端协同。Spring Security 7 侧重可叠加的 \u003ccode\u003eCustomizer\u0026lt;HttpSecurity\u0026gt;\u003c/code\u003e、一次性令牌登录、WebAuthn 与注解式多因子模型；","title":"Spring Boot 4 技术栈纵览：Starter 粒度、MVC 版本协商与安全演进","type":"posts"},{"content":" Spring 工程上的 AI 编码代理：实时链路、可验证闭环与上下文治理 # 摘要：面向已在 JVM/Web 栈上交付服务的工程师，本文从一类典型 Spring Boot + Kotlin 实时互动应用出发，梳理「数据库信号 → 响应式 SSE → 浏览器」的数据路径，并把人机协作拆成可核对的三层：编译与测试闭合、可版本化的项目记忆（CLAUDE.md / 规则 / Skills）、工具调用路径上的 Hooks 与 MCP。后半部分讨论无规格迭代导致的测试与状态机缺口、结构化澄清（Interview）如何把导航与安全决策写进规格，以及长对话中跨切面步骤被静默丢弃的现象与分段执行思路。文中涉及的具体演示项目名为公开幻灯材料中出现的示例工程；其与第三方仓库的逐行对应关系不在此文证实范围内。\n1. 案例语境：投影仪实时互动与常用栈组合 # (1) 原理与动机\n此类场景通常包含大屏展示、移动端参与与限时交互：管理员发起一轮活动，参与者扫码加入，在计时器驱动下消耗「反应预算」 viewing 投影内容，最后在若干类别下揭晓排名。实现上常见诉求是 低延迟信号分发（多客户端订阅同一事件源）与 服务器渲染或渐进增强（例如 HTMX）以降低前端复杂度。\n(2) 实现抓手\n公开材料中反复出现的工程锚点包括：Spring Boot、Kotlin、jOOQ、HTMX。Kotlin 入口习惯使用 runApplication\u0026lt;Application\u0026gt;(*args)；jOOQ 在 Boot 下通过自动配置的 DSLContext 与数据源方言属性衔接 SQL 生成层（参见 Spring Boot 参考手册 — Using jOOQ）；浏览器侧若采用 HTMX 的 SSE 扩展，可通过 EventSource 语义连接推送端（参见 htmx SSE 扩展说明）。\n(3) 怎么用\n@SpringBootApplication class DemoApplication fun main(args: Array\u0026lt;String\u0026gt;) { runApplication\u0026lt;DemoApplication\u0026gt;(*args) } Kotlin 与 Spring Boot 的配套约束（插件、runApplication 等）以官方章节为准（Spring Boot — Kotlin 支持）。\n图注：幻灯片标题区可见英文描述「A photo reaction game on a beamer. The manager starts a round, quests join via QR code.」，并配有德语界面字样（如 Ergebnisse），用于说明演示产品的交互形态而非某一固定开源仓库版本。\n2. 约定、编译反馈与测试：代理协作的「闭合回路」 # (1) 原理与动机\n分层清晰（Controller / Service / Repository）与广泛采用的注解模型，使自动化助手更容易在陌生仓库中定位扩展点；更重要的是，一次 Gradle 编译或测试运行即可暴露类型与集成错误，从而把「猜测」变成可机器核验的信号，这与下文 Headless CLI、Hooks 与 CI 叙事相容。\n(2) 实现抓手\nKotlin Gradle 插件暴露标准任务名 compileKotlin / compileTestKotlin（参见 Kotlin Gradle 编译选项）。Web 层可组合 @SpringBootTest（Spring Boot 测试）、MockMvc（MockMvc 概述）与 Testcontainers（Spring Boot 与 Testcontainers；项目主页见 Testcontainers for Java）。\n(3) 怎么用\n./gradlew compileKotlin compileTestKotlin test 3. PostgreSQL NOTIFY、WebFlux 与浏览器 SSE # (1) 原理与动机\n数据库 NOTIFY 将在给定 channel 上向已执行 LISTEN 的会话投递载荷，适合作为轻量级内部事件总线（PostgreSQL NOTIFY）。Spring WebFlux 可将 Flux\u0026lt;ServerSentEvent\u0026gt; 暴露为 text/event-stream，由浏览器 EventSource 消费（ServerSentEvent JavaDoc；MediaType.TEXT_EVENT_STREAM 常量；MDN — EventSource）。幻灯材料中的个案把监听线程、Reactor Sink、轮询间隔等画在一起；这些接线细节属于演示架构示意，并非 Postgres 或 Spring 的唯一规范写法。\n(2) 实现抓手\n要点链条（与幻灯英文一致）：pg_notify → 通知服务 → Reactor Sink → Flux\u0026lt;ServerSentEvent\u0026gt; → HTTP SSE；浏览器侧标注 EventSource / HTMX。实现者可替换 JDBC 轮询策略或使用 R2DBC 等，但 produces = TEXT_EVENT_STREAM_VALUE 与响应式返回类型仍是 WebFlux SSE 的核心 API 面。\n(3) 怎么用\n@GetMapping(path = [\u0026#34;/api/sse/slideshow\u0026#34;], produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) fun slideshow(): Flux\u0026lt;ServerSentEvent\u0026lt;String\u0026gt;\u0026gt; = slideshowEvents() const es = new EventSource(\u0026#34;/api/sse/slideshow\u0026#34;); es.onmessage = (ev) =\u0026gt; console.log(ev.data); 图注：终端摘要写明 The project uses PostgreSQL LISTEN/NOTIFY as the event transport, with Spring WebFlux Flux streams delivering events to browsers.，并列出步骤「Guest uploads photo \u0026gt; pg_notify \u0026gt; SlideshowNotificationService \u0026gt; Reactor Sink \u0026gt; SSE stream」。\n图注：同一概述页可见「Guest uploads photo » pg_notify » SlideshowNotificationService » Reactor Sink \u0026gt; SSE stream」与架构行「Spring Controller (Flux)」。\n4. 子代理、摘要回流与 Headless 快照检索 # (1) 原理与动机\n重型代码探索会消耗大量 token；把研读任务放到 独立子代理上下文，仅让结构化摘要回到主会话，可降低主窗口的早期信息被挤出的风险。文档侧对子代理、模型路由与钩子交互有专章说明（自定义子代理）。与此同时，打印模式（-p） 与 --model 等 CLI 标志适合脚本化「只读问答」（Claude Code CLI 参考）。\n(2) 实现抓手\n概念分工：主会话保留决策与编辑；子任务负责广读文件与检索；下游消费仅为摘要或结构化列表。与数据库相关的「服务端」此处指 schema 契约：通过迁移或 information_schema 列出表、列与唯一约束，便于低成本对齐领域模型。\n(3) 怎么用\nclaude -p --model haiku \u0026#34;列出 ranking game 相关迁移中的表、唯一约束与状态枚举列\u0026#34; 5. 数据库建模示例：排行会话、参与者与反应 # (1) 原理与动机\n实时多人玩法需要在会话维度维护状态机（如 LOBBY / ACTIVE / FINISHED / REVEAL）、参与者集合以及每人对每条内容的反应；唯一约束用来禁止重复加入或重复投票，是把业务规则下沉到存储层的直接手段。\n(2) 实现抓手\n幻灯列举表 ranking_game_session、ranking_game_participant、ranking_game_reaction，并写出 Unique constraint: one participant per guest per session 及 one reaction per participant per photo per session 一类语义（英文原文以屏幕为准）。具体列名与 CHECK 约束应以实际迁移为准。\n(3) 怎么用\nCREATE UNIQUE INDEX ux_participant_per_guest ON ranking_game_participant (session_id, event_guest_id); CREATE UNIQUE INDEX ux_one_reaction_per_submission ON ranking_game_reaction (participant_id, submission_id); 图注：幻灯片以大写标题陈述「The ranking game feature uses three main database tables:」，并列出「ranking_game_session」「ranking_game_participant」「ranking_game_reaction」及「Unique constraint: one participant per guest per session」等英文要点。\n6. 「氛围编码」基线：缺失测试与终端状态被静默跳过 # (1) 原理与动机\n在缺少规格与分层记忆结构时，仅凭自然语言 prompt 快速堆代码，容易出现 未写自动化测试、手工才能发现的缺陷，以及 状态机分支不完整——例如枚举中存在 REVEAL 阶段却无任何迁移路径到达该状态。此类问题根因常被归结为 需求在长上下文中漂移，而非单一语法错误。\n(2) 实现抓手\n对照可靠栈：JUnit / Spring Boot Test 用于回归；状态迁移可用单元测试表驱动穷举 when(event)；关键终端状态应在测试中显式断言可达。\n(3) 怎么用\nenum class Phase { LOBBY, ACTIVE, FINISHED, REVEAL } fun next(p: Phase, timerEnded: Boolean): Phase = when (p) { Phase.ACTIVE -\u0026gt; if (timerEnded) Phase.FINISHED else p Phase.FINISHED -\u0026gt; Phase.REVEAL else -\u0026gt; p } 图注：幻灯片标题为「THE VIBE CODING BASELINE」，正文包含「Iteration 1: no config, no skills, no plan」「No tests written.」「The whole REVEAL state skipped.」等英文句子。\n7. 会话内 checkpoint 与 /rewind：试错而不丢对话语义 # (1) 原理与动机\n代理按检查点记录文件编辑；当一次重构偏离预期时，使用者可以中断并在 Rewind / checkpoint 语义下回滚工作区改动，同时 保留对话历史，以便修正指令后继续。官方文档将主题概括为跟踪、回滚与汇总编辑（Checkpointing）。\n(2) 实现抓手\n产品侧提供会话状态管理与检查点选择交互；工程侧被修改的焦点文件在演示材料中为 RankingGameService.kt。Escape 连按等具体操作应以当前客户端文档为准。\n(3) 怎么用\n人机流程可描述为：中断当前工具执行 → 调用 rewind → 选中目标检查点 → 确认恢复 → 用更精确的增量指令继续编辑同一服务类。\n图注：界面列出多次「Update (../rankinggame/RankingGameService.kt) Added 19 lines」类 diff 摘要，并出现提示「Interrupted - What should Claude do instead?」。\n8. CLAUDE.md、路径规则与 Skills 的分工 # (1) 原理与动机\nCLAUDE.md 在每次会话开始加载，适合放置「永远成立」的编译与验收纪律；更细的规则可放入 .claude/rules，仅在读取匹配路径下的文件时注入上下文，从而节省 token（项目记忆与 CLAUDE.md；路径限定规则见 path-specific rules——front matter 使用 paths 列表承载 glob，而非单独的 globs 键名）。Skills 的正文则在 被调用时才加载，用于在动作发生时刷新流程说明（Skills）。\n(2) 实现抓手\nHooks 文档中的 InstructionsLoaded 事件标明：CLAUDE.md 或 .claude/rules/*.md 进入上下文时会触发（Hooks 参考）。 /context 命令用于查看上下文构成的实时拆分（Context window）。\n(3) 怎么用\n# CLAUDE.md（示意） - Compile after .kt changes: `./gradlew compileKotlin compileTestKotlin` - Verify before done: run tests or demonstrate correctness --- paths: - \u0026#34;**/src/test/**\u0026#34; --- Prefer Spring Boot Test + Testcontainers for integration scenarios... 图注：幻灯片段标题为「INSIDE MY CLAUDE.MD」，可见条目「Compile after .kt changes:」「./gradlew compileKotlin compileTestKotlin」以及「Verify before done: never mark a task complete without proving it works」。\n9. Interview 工作流：先 Explore，再用结构化提问固化规格 # (1) 原理与动机\n在编码前把 入口路径、边界状态、安全假设 写成可评审文档，能显著降低后续返工。演示材料展示了一条轨迹：先自动执行「Explore codebase architecture」，再通过多选问答澄清 管理员从何处进入 Ranking Game 等产品决策；Skill 文本要求 规格必须以 User Stories 结尾且导航类故事靠前。工具名 AskUserQuestion 出现在 Hooks 与子代理文档中（Hooks — AskUserQuestion；Create custom subagents）。「Interview Skill」固定模板与行数是否为厂商内置产品，公开文档未以同名条目证实；读者应把它理解为 可复制的 Markdown 技能脚手架。\n(2) 实现抓手\nMandatory topics 覆盖：Entry points、User journey、Edge cases、State transitions、Terminal states \u0026amp; dead ends 等；输出约束：Write the spec to a file、Spec MUST end with User Stories。\n(3) 怎么用\n澄清所得最终会映射到路由与页面结构，例如为 /admin 或活动详情页增加独立分区。\n@Controller class RankingGameAdminController { @GetMapping(\u0026#34;/admin/events/{id}/ranking-game\u0026#34;) fun rankingGamePage(@PathVariable id: UUID): String = \u0026#34;admin/ranking-game\u0026#34; } 图注：界面可见句子「Let me explore the codebase first to understand the existing architecture before interviewing you.」以及「Explore(Explore codebase architecture) Done (53 tool uses」开头的一行状态摘要。\n图注：问答面板标题包含「Where does the manager find the Ranking Game?..」，选项行可见「Manager dashboard (/admin)」「Add a ‘Start Ranking Game\u0026rsquo; button」等英文短语。\n图注：文档草稿中出现「Interview me relentlessly ## Mandatory interview topics about every aspect\u0026hellip;」与「Use AskUserQuestion tool to」「Write the spec to a file.」等连续英文原文。\n10. 反应预算与防篡改：HTTP 演示下的服务端强制 # (1) 原理与动机\n若预算仅存于浏览器 Cookie 或前端状态，客户端可篡改计数；服务端必须以可认证的访客主体（如 event_guest）与数据库累计值为权威。幻灯中的选项枚举了 Server-side budget enforcement、纯客户端与 cookie 混合等路径（英文标签以屏幕为准）。\n(2) 实现抓手\nSpring MVC 常用 @PostMapping、@PathVariable、@RequestBody、ResponseEntity（参见 Spring MVC 注解驱动控制器）。演示约定自定义头名 X-Demo-Guest 仅用于下文草图，真实系统应使用会话或 JWT 等成熟机制。\n(3) 怎么用\nPOST /api/events/550e8400-e29b-41d4-a716-446655440000/reactions HTTP/1.1 Content-Type: application/json X-Demo-Guest: guest-token-demo {\u0026#34;submissionId\u0026#34;: 99, \u0026#34;type\u0026#34;: \u0026#34;HEART\u0026#34;} @RestController @RequestMapping(\u0026#34;/api/events/{eventId}/reactions\u0026#34;) class ReactionController( private val reactions: ReactionService, ) { @PostMapping fun react( @PathVariable eventId: UUID, @RequestBody body: ReactionBody, @RequestHeader(\u0026#34;X-Demo-Guest\u0026#34;) guestToken: String, ): ResponseEntity\u0026lt;Void\u0026gt; { reactions.react(eventId, guestToken, body.submissionId, body.type) return ResponseEntity.accepted().build() } } data class ReactionBody(val submissionId: Long, val type: String) 图注：选项列表可见「Security: should we prevent reaction manipulation?..」「1. Server-side budget enforcement」「DB tracks reactions per guest\u0026hellip;」等英文描述。\n图注：同一「Storage model」对话分支可见「Clarifying: reactions stored in the DB, but budget tracked client-side via a cookie?..」以及编号行「1. DB reactions, cookie budget」「Each reaction is a DB row. Budget is client-side.」等英文句子。\n11. 领域校验：沿用既有异常层次与双语消息 # (1) 原理与动机\n新增业务校验（如计时器时长合法区间）时，复制仓库内既有 PhotoQuestException 子类风格，可避免一人一类错误模型；幻灯片段展示 InvalidTimerDurationException 同时携带德语用户文案与英语系统文案。OCR 将「30」误识为「3@」；正确区间应以项目源码或产品需求为准，此处按常规理解为 1–30 分钟。\n(2) 实现抓手\nKotlin 可用 require 抛出非法参数（require 标准库）；与 Spring 的 HTTP 映射结合时，异常宜通过 @ControllerAdvice 映射为 400/422（参见 Spring MVC 异常处理）。\n(3) 怎么用\nclass InvalidTimerDurationException : PhotoQuestException( userDe = \u0026#34;Die Timer-Dauer muss zwischen 1 und 30 Minuten liegen.\u0026#34;, systemEn = \u0026#34;Timer duration must be between 1 and 30 minutes\u0026#34;, ) fun createSession(timerMinutes: Long) { if (timerMinutes !in 1..30) throw InvalidTimerDurationException() } 图注：diff 摘要包含「class InvalidTimerDurationException : PhotoQuestException(」以及「Timer duration must be between 1 and 3@ minutes」一行英文（字符「@」为 OCR 噪声，应以源码为准）。\n12. Hooks、静态分析与 Jackson 3 迁移叙事 # (1) 原理与动机\n规则文档可能被模型忽略；Hooks 在 PreToolUse / PostToolUse / SessionEnd 等事件上插入脚本，用于阻断危险命令或在写盘后运行 formatter / linter（Hooks reference）。演示提及用 Detekt 在自定义规则中拦截 Jackson 2 的 ObjectMapper 引用，引导至 Jackson 3 的 JsonMapper；Spring Boot 4 迁移指南写明 com.fasterxml.jackson → tools.jackson 的包迁移方向（Spring Boot 4.0 Migration Guide — Jackson）。Detekt 规则 DSL 细节未在此文附官方页核实。\n(2) 实现抓手\nHook 配置以产品 JSON schema 为准；匹配器可过滤 Bash、Edit|Write 等工具名。\n(3) 怎么用\n{ \u0026#34;hooks\u0026#34;: { \u0026#34;PreToolUse\u0026#34;: [{ \u0026#34;matcher\u0026#34;: \u0026#34;Bash\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;./hooks/git-guard.sh\u0026#34; }], \u0026#34;PostToolUse\u0026#34;: [{ \u0026#34;matcher\u0026#34;: \u0026#34;Edit|Write\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;./hooks/detekt-on-write.sh\u0026#34; }] } } 13. MCP：把 IDE 能力暴露为代理可调用的接口 # (1) 原理与动机\nModel Context Protocol 将外部系统（IDE、缺陷跟踪、可观测性后端）以标准工具面暴露给客户端；没有 MCP 时，代理往往依赖 shell 与原始文件 IO。Spring AI 提供多种 MCP Server Boot Starters（Spring AI — MCP 概述）。协议规范文档托管于 modelcontextprotocol.io，仓库自述见 modelcontextprotocol/modelcontextprotocol README。\n(2) 实现抓手\n幻灯对比「Without MCP / With IntelliJ MCP」列出 reformat_file、execute_run_configuration、自动 import 等能力，并写到「The IDE becomes an API that the agent calls.」；JetBrains MCP Steroid 项目在 README 中声明暴露更广的 IntelliJ Platform API（MCP Steroid README）。各工具的确切标识符以实现仓库为准。\n(3) 怎么用\nCLI 侧通过 claude mcp 配置服务端（参见 CLI reference）；IDE 侧安装对应 MCP 插件并授予代理最小权限集。\n图注：表格对比「Without MCP」「With IntelliJ MCP」，并包含句子「The IDE becomes an API that the agent calls.」以及「github.com/JetBrains/mcp-steroid」。\n14. 长计划漂移与分段 Headless 执行（Ralph 思路） # (1) 原理与动机\n单一超长会话里，模型可能 有计划但未逐步执行：幻灯将 /frontend-design、agent-browser、/simplify 等步骤标记为「Phase 早期使用后丢弃」或「Never used」，标题写作「Plans work — until the context window doesn\u0026rsquo;t.」，结论句为「The agent isn\u0026rsquo;t a deterministic machine」。这与「把每个阶段放到独立 claude -p 会话、由 shell 循环顺序消耗计划文件」的思路相容；「Ralph」一词是否为社区昵称需对照各自工具文档，厂商页面未强制使用该名称。\n(2) 实现抓手\n分段执行依赖：机器可读的计划工件、阶段边界、冷启动会话；Skills 文档列出捆绑命令 /simplify（Skills），其实际是否在某一 runs 中被调用属于过程观测。\n(3) 怎么用\nwhile read -r phase; do claude -p -- \u0026#34;$(cat docs/plan.md)\u0026#34; --phase \u0026#34;$phase\u0026#34; done \u0026lt; docs/phases.txt 上述 --phase 参数名为示意；真实标志以 CLI reference 为准。\n图注：幻灯主标题为「LEVEL 4: THE DRIFT PROBLEM」，副标题「Plans work — until the context window doesn\u0026rsquo;t.」，底部总结句「The agent isn\u0026rsquo;t a deterministic machine」，并标注「Dropped after Phase 1」「Never used」等英文状态。\n参考与延伸阅读 # Spring Boot 官方文档 — Kotlin 语言支持 Spring Boot 官方文档 — 使用 jOOQ 与 DSLContext htmx 扩展 — Server-Sent Events（sse-connect 等） PostgreSQL 手册 — NOTIFY Spring Framework JavaDoc — ServerSentEvent MDN — EventSource API Kotlin 文档 — Gradle 中的 compileKotlin 任务 Spring Boot 参考 — SpringBootTest Spring Framework 参考 — MockMvc Testcontainers for Java 项目主页 Claude Code 文档 — 子代理 Claude Code 文档 — CLI 参考（含 -p、\u0026ndash;model） Claude Code 文档 — Checkpointing（回滚编辑） Claude Code 文档 — 项目记忆与 CLAUDE.md Claude Code 文档 — 路径限定 rules（paths glob） Claude Code 文档 — Skills Claude Code 文档 — Hooks（含 AskUserQuestion） Claude Code 文档 — 上下文窗口与 /context Anthropic 文档 — Prompt caching 机制说明 Kotlin 标准库 — require Spring Framework 参考 — 控制器 advice 异常处理 Spring Boot 4.0 迁移指南 — Jackson 3 / tools.jackson modelcontextprotocol 仓库 README（协议入口） Spring AI 参考 — Model Context Protocol 概述与 Boot Starters JetBrains — MCP Steroid 项目 README ","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/posts/claude-code-for-spring-developers-by-thomas-schilling-spring-io-2026/","section":"文章","summary":"面向已在 JVM/Web 栈上交付服务的工程师，本文从一类典型 Spring Boot + Kotlin 实时互动应用出发，梳理「数据库信号 → 响应式 SSE → 浏览器」的数据路径，并把人机协作拆成可核对的三层：\u003cstrong\u003e编译与测试闭合\u003c/strong\u003e、\u003cstrong\u003e可版本化的项目记忆（CLAUDE.md / 规则 / Skills）\u003c/strong\u003e、\u003cstrong\u003e工具调用路径上的 Hooks 与 MCP\u003c/strong\u003e。后半部分讨论无规格迭代导致的测试与状态机缺口、结构化澄清（Interview）如何把导航与安全决策写进规格，以及长对话中跨切面步骤被静默丢弃的现象与分段执行思路。","title":"Spring 工程上的 AI 编码代理：实时链路、可验证闭环与上下文治理","type":"posts"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/starter/","section":"Tags","summary":"","title":"Starter","type":"tags"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/war/","section":"Tags","summary":"","title":"War","type":"tags"},{"content":"","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/tags/webassembly/","section":"Tags","summary":"","title":"Webassembly","type":"tags"},{"content":" WebAssembly 作为 JVM 生态的嵌入层：模型、运行时与工程抓手 # 摘要： WebAssembly（Wasm）在规范层面不仅是紧凑字节码，更是一套把 guest 模块钉在宿主边界上的执行模型。把 Wasm 放进 JVM 时，团队需要在「外挂原生运行时」与「纯字节码托管」之间做分发、观测与故障隔离上的取舍；Chicory、QuickJS4J、OPA Wasm、protobuf4j、Lumis4j 等栈展示了同一思路的不同切面——用 Wasm 把既有 C/Rust/JS 资产封进可版本化的工件，再由 Java API 暴露给 Spring Gateway、构建插件或 CLI。下文按依赖关系从规范语义写到基准与样本仓库，并对口述体积类数字与幻灯源码路径保留可复核性说明。\n1. 规范视角：模块、宿主函数与 import/export 契约 # (1) 原理与动机： Wasm 把计算封装成带类型的模块；宿主通过 import 提供能力（含由宿主实现的 host function），模块用 export 暴露可被调用的入口。这样同一二进制可在浏览器、独立运行时或嵌入 JVM 的引擎中复用，差异主要集中在「宿主实现了哪些 import」。W3C《WebAssembly Core Specification》将 Wasm 描述为可嵌入宿主环境的低级代码格式，并单独定义宿主函数等概念（WebAssembly Core Specification — 总述与语义模型）。\n(2) 实现抓手： 评审集成时应对齐规范中的 instantiation、import、export、invoke 等用语；不要在架构图上把 Wasm 画成与操作系统进程一一对应的孤立运行时，而应标明「embedder 提供的 syscall / WASI / 自定义 shim」边界。\n(3) 用法示意： 最小 WAT 骨架仅表达「guest 调用宿主日志再导出 run」，真实栈还需补全内存与表格：\n(module (import \u0026#34;env\u0026#34; \u0026#34;host_log\u0026#34; (func (param i32))) (func (export \u0026#34;run\u0026#34;) (result i32) i32.const 0 call 0)) 幻灯可见表述节选：facilitating interactions between such programs and their host environment，与规范强调的宿主交互一致；旁列 WAT 片段示意 (param $var$1 i32 等形式。\n2. JVM 承载 Wasm：原生 FFI 路径与纯 Java 运行时 # (1) 原理与动机： 通过 JNI 或相似 FFI 嵌入 V8、Wasmtime 等原生引擎可保留峰值性能，但重新引入多平台 .so / .dylib 分发、符号兼容与供应链审计成本；guest 侧内存错误也更常见地表现为进程级故障而非可控的 Java 异常。相对地，纯 JVM 字节码后端把执行拉回同一观察平面（栈轨迹、Profiler、容器镜像），但可能牺牲部分原生优化。\n(2) 实现抓手： Dylibso Chicory 文档与 README 将「外挂需 native + FFI」与「纯 JVM」对照叙述（Chicory README — 分发与运行时定位）。JDK 22 起稳定的 Foreign Function \u0026amp; Memory API（JEP 454）为替代手写 JNI 提供了官方路径；后续 Redline 一类方案则依赖 Panama / jffi 绑定生成的机器码。\n(3) 用法： 选型检查表可包含：原生工件是否纳入 SBOM；guest fault 是否可为 Error/信号级别；Tracer 与 Micrometer 度量是否跨 FFI 丢失上下文。\n幻灯要点 OCR 可见：Chicory Compiler、Java Bytecode、Chicory + JVM，以及结论句 The JVM translates and executes wasm for us。\n3. QuickJS4J：QuickJS → Wasm → Chicory 字节码 → 小体积 JAR # (1) 原理与动机： 在 JVM 上嵌入 JS 历史上依赖 Rhino/Nashorn 或体量更大的 GraalJS 路径；QuickJS4J 文档描述链路为把 QuickJS 编成 Wasm，再用 Chicory Compiler 转成可在任意 JVM 上执行的 Java 字节码，并以独立 JAR 分发，同时声明 Native-image friendly（QuickJS4J Readme — How it works）。\n(2) 实现抓手： Maven 坐标 io.roastedroot:quickjs4j；运行时 API 以 Runner、Engine 为主，宿主函数通过 @HostFunction / @Builtins 绑定（同 README Quick Start）。\n(3) 用法： 官方 Quick Start 推荐：\nimport io.roastedroot.quickjs4j.core.Runner; try (var runner = Runner.builder().build()) { runner.compileAndExec(\u0026#34;console.log(\\\u0026#34;Hello QuickJs4J!\\\u0026#34;);\u0026#34;); System.out.println(runner.stdout()); } 幻灯代码区域 OCR 可见：import io.roastedroot.quickjs4j.core.Engine; 与 QuickJs4J / JAR 标题字样。\n4. OPA：策略编成 Wasm，并在 Spring Cloud Gateway 内评估 # (1) 原理与动机： 经典拓扑是业务服务 HTTP 调用独立 OPA；将 Rego opa build -t wasm 的产物随应用分发，可在网关或微服务进程内完成评估，省去往返延迟与额外拓扑（OPA 文档 — WebAssembly）。Wasm 路径下内置 http.send 等能力未必可用，需由宿主实现（文档同一页说明）。\n(2) 实现抓手： Styra opa-java-wasm 提供进程内 API，示例含 OpaPolicy.builder().withPolicy(policyWasm).build()（opa-java-wasm README — Usage）。网关侧过滤器通常继承 Spring Cloud Gateway 的 AbstractGatewayFilterFactory（具体包名与泛型签名以当前发行版为准），参见官方过滤器工厂手册（Spring Cloud Gateway — GatewayFilter Factories）。\n(3) 用法： 下列 X-User 头仅为演示约定，用于把主体传给策略层；读取侧应与 Rego 输入 schema 对齐。\n客户端探测（输出 HTTP 状态码）：\ncurl -s -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; \u0026#34;http://localhost:8081/opa\u0026#34; -H \u0026#34;X-User: Alice\u0026#34; curl -s -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; \u0026#34;http://localhost:8081/opa\u0026#34; -H \u0026#34;X-User: Bob\u0026#34; 接收端过滤器骨架（示意，省略依赖注入与错误映射；生产环境应避免在响应式链路中长期阻塞）：\n@Component public class OPAFilter extends AbstractGatewayFilterFactory\u0026lt;Object\u0026gt; { private final OPAService opaService; public OPAFilter(OPAService opaService) { this.opaService = opaService; } @Override public GatewayFilter apply(Object config) { return (exchange, chain) -\u0026gt; { String user = exchange.getRequest().getHeaders().getFirst(\u0026#34;X-User\u0026#34;); if (!opaService.authz(user)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.setComplete(); } return chain.filter(exchange); }; } } 幻灯 OCR 可见：OPA Spring cloud gateway filter Open Policy Agent 与 public class OPAFilter extends AbstractGatewayFilterFactory\u0026lt;Object。\n终端演示 OCR 可见：spring-cloud-gateway-wasm-demos git:(main) 与 localhost:8081/opa -H\u0026quot;X-User:Alice\u0026quot;（HTTP 状态码在截帧中被 OCR 噪声干扰，复现应以本地终端为准）。\n5. protobuf4j：把 protoc 工具链 Wasm 化以收敛构建依赖 # (1) 原理与动机： 将 protoc 一类多平台 CLI 作为传递依赖拉入 Java 构建时，镜像与缓存体积可膨胀；把插件编译为 Wasm 再走 Chicory 字节码路径，可把原生二进制矩阵替换为单一 Wasm 工件思路（动机来自社区项目自述与会议线路，具体 MB 级对比应以当前仓库 README 与实测为准）。\n(2) 实现抓手： protobuf4j README 描述「compile protobuf to Wasm → Chicory → pure Java bytecode」，列出 protobuf4j-v3 / protobuf4j-v4 与生成插件映射（protobuf4j README）。\n(3) 用法： 常规触发仍为 Maven/Gradle 编译；pom.xml 中插件坐标与 goal 以仓库示例为准，此处不虚构 groupId。\n6. Chicory：构建期 Wasm→.class 与运行时零原生依赖叙事 # (1) 原理与动机： Chicory 的 build-time compiler 将 Wasm 指令译为 JVM 字节码并产出 .class，定位上可作为解释器的 drop-in replacement 并通过同一套规范测试（Chicory — Build time Compilation（文档站））。\n(2) 实现抓手： Maven 插件 com.dylibso.chicory:chicory-compiler-maven-plugin，goal 为 compile；配置项含 \u0026lt;wasmFile\u0026gt;、\u0026lt;name\u0026gt;（生成类名）；文档另述 interpreterFallback 与超大函数回退行为（同页 Interpreter Fall Back 小节）。\n(3) 用法：\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.dylibso.chicory\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;chicory-compiler-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt;\u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt;\u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;wasmFile\u0026gt;src/main/resources/guest.wasm\u0026lt;/wasmFile\u0026gt; \u0026lt;name\u0026gt;com.example.wasm.Guest\u0026lt;/name\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; 7. 能力矩阵：提案、WASI 与线程等特性的对齐成本 # (1) 原理与动机： guest 若启用 threads、multi-memory、WASI Preview1 或 Wasm GC 等能力，必须与运行时支持表一致，否则局部 POC 通过但在目标 JDK/Android 上失败。\n(2) 实现抓手： 幻灯列举「Full spec test suite」「Shared memory + atomics」「Multi-Memory」「WASI Preview 1」等条目；线上清单以 Chicory README Roadmap 与 Wiki 对照页为准（Chicory README — Roadmap · Wiki — WebAssembly feature support）。\n(3) 用法： 交付前用目标 Chicory 版本跑 guests 的提案组合；WASI 映射到宿主文件系统/时钟/网络时核对 syscall 覆盖。\n幻灯标题 OCR：Chicory — Implemented WebAssembly Proposals，并可见指引句 See the Chicory README and WebAssembly/feature page for details。\n8. HotSpot 巨型方法与字节码后端的天花板 # (1) 原理与动机： Chicory 文档说明 Wasm 函数过大时会退回解释执行并给出含 WASM function size exceeds the Java method size limits 语义的报错指引（Build time Compilation — Interpreter Fall Back）。与此同时，HotSpot 若判定 Java 方法体超过阈值且启用相关选项，可能跳过 JIT 而长期解释执行，放大「巨型翻译单元」的成本。\n(2) 实现抓手： OpenJDK 源码可见 DontCompileHugeMethods 与 HugeMethodLimit 声明（路径随版本演进，应在所用 JDK 的 HotSpot 源码树检索）（OpenJDK — compiler_globals.hpp（主线快照））。诚实限定： 幻灯截取 jdk11u 树下 globals.hpp 片段；HugeMethodLimit 在源码宏类别上是否为发行版可写 -XX: 开关，需对具体 JDK 运行 java -XX:+PrintFlagsFinal 验证，不宜默认假设生产可调。\n(3) 用法： 遇到 Chicory 编译告警或吞吐异常时，可并行查看 -XX:+PrintCompilation 类诊断输出与 Chicory fallback 配置。\n幻灯 OCR 可见：The problem 与源码行片段 develop(intx, HugeMethodLimit, 8000, 及 \u0026quot;Don't compile methods larger than this if \u0026quot;。\n9. Chicory Redline：Cranelift 机器码路径、mmap 与 Panama/jffi # (1) 原理与动机： Redline 在自述中与「Wasmtime + JNI」对照：构建期把 Cranelift（Wasmtime 同款代码生成后端）本身编成 Wasm 交由 Chicory 执行以产出机器码资源；运行期通过 mmap 映射可执行页，并在 JDK≥25 走 Panama FFM、JDK≥11 走 jffi；不支持平台 fallback 回纯 Chicory 字节码（chicory-redline README — How It Works · Dylibso 博文 — Chicory Redline）。\n(2) 实现抓手： Maven 坐标含 io.roastedroot:redline 与 redline-compiler-maven-plugin；README 示例出现 MyModule.builder().build()、instance.export(\u0026quot;my_function\u0026quot;).apply()、instance.isNative() 等 API 形态（以前述 README 为准）。\n(3) 用法： 本地验证 isNative() 是否命中原生后端；CI 矩阵覆盖「无原生制品的平台」以检验 fallback。\n10. 基准：wasm-score shootout 与多运行时对照 # (1) 原理与动机： 公开 wasm-score 仓库的 shootout 改编自 The Computer Language Benchmarks Game，用于横向对比不同 Wasm 宿主实现（wasm-score — benchmarks/shootout README）。幻灯表格中的毫秒/秒级数字强烈依赖硬件、JDK 与 Chicory/Redline 版本；读者应以固定 harness 复现而非引用单次截屏。\n(2) 实现抓手： 表中出现的 chicory-redline、wasmtime、wazero 等标签对应不同宿主或编译后端；条目名如 shootout-ed25519、shootout-heapsort 即基准名称。\n(3) 用法： 克隆 Bytecode Alliance 仓库后按其 README 运行基准目标；对比时记录 JDK java -version、CPU 型号与 turbo 状态。\n性能表 OCR 可见条目 shootout-ed25519 及 chicory-redline、wasmtime、wazero 列。\n性能表 OCR 可见：shootout-heapsort、shootout-memmove、chicory-redline 15.93 ms、wasmtime 18.16 ms 等字样。\n11. Lumis4j 与 TamboUI：终端语法高亮的Wasm化动机 # (1) 原理与动机： Lumis4j README 写明基于 Tree-sitter，并通过 Chicory 以 Wasm 在纯 Java 中执行高亮逻辑（lumis4j README）。TamboUI README 将自身定位为 Java 侧现代终端 UI 库，并类比 Rust ratatui、Go bubbletea（tamboui README）；与高亮栈并列时，可理解为「终端 UI 需要接近编辑器级着色，而 JVM 侧缺乏一等公民方案」的工程叙事。\n(2) 实现抓手： Lumis4j 提供 Lumis.builder()、withLang、withTheme、Formatter.TERMINAL（ANSI）等组合；官方示例亦含 HTML 内联格式化器枚举。\n(3) 用法：\ntry (var htmlLumis = Lumis.builder() .withLang(Lang.JAVA) .withTheme(theme) .withFormatter(Formatter.TERMINAL) .build()) { // htmlLumis.highlight(...) 具体调用以 README 为准 } 幻灯 OCR 可见：Lumis4j、lumis4j git: (9e4c43a) 与代码行 try (var htmlLumis = Lumis.builder()。\n幻灯 OCR 可见：Code highlight、README .md 与句子 AJava library for building modern terminal user interfaces.（OCR 将 “A Java” 合并为 “AJava”，引文保持截帧字形）。\n12. 可运行样本：spring-cloud-gateway-wasm-demos # (1) 原理与动机： 单一幻灯不足以覆盖 Gateway 路由、过滤器 Bean 与 Wasm 工件装载的组装关系；公共样本仓库把多种 Wasm 风格的过滤器演示收敛在一处，默认 mvn spring-boot:run、端口 8081（spring-cloud-gateway-wasm-demos README）。\n(2) 实现抓手： 仓库继承 wasm-gateway-filters 并说明可切换到 Chicory；具体模块划分以 pom.xml 为准。\n(3) 用法： 克隆后按 README 选择模块启动；将前文 curl 示例中的主机与路径对齐到样本路由。\n参考与延伸阅读 # WebAssembly Core Specification — 语义、宿主与模块模型 Chicory README — 纯 JVM 与原生嵌入对照 Chicory — 构建期编译与解释器回退（文档站） Chicory Wiki — WebAssembly 特性对照 JEP 454 — Foreign Function \u0026amp; Memory API（JDK 22） OpenJDK HotSpot — compiler_globals.hpp 中与 JIT 阈值相关的声明（主线快照） QuickJS4J Readme — 链路说明与 Quick Start OPA 官方文档 — WebAssembly 构建与限制 Styra opa-java-wasm — 进程内评估示例 Spring Cloud Gateway — GatewayFilter 工厂参考手册 protobuf4j README — Wasm 化 protoc 插件说明 chicory-redline README — Cranelift、Panama、jffi 与 fallback Dylibso — Chicory Redline 介绍博文（源码树内 Markdown） wasm-score — shootout 基准说明 lumis4j README — Tree-sitter + Chicory 高亮栈 tamboui README — 终端 UI 定位 spring-cloud-gateway-wasm-demos — Gateway + Wasm 演示集合 ","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/posts/webassembly-and-the-future-of-the-jvm-ecosystem-by-andrea-peruffo-spring-io-20/","section":"文章","summary":"WebAssembly 作为 JVM 生态的嵌入层：模型、运行时与工程抓手","title":"WebAssembly 作为 JVM 生态的嵌入层：模型、运行时与工程抓手","type":"posts"},{"content":" 遗留 Servlet 应用渐进接入 Spring Boot：构建、自动配置与 WAR 双模式 # 摘要：大规模迁移 Spring Boot 前，应先有可重复的集成验证与可控的依赖基线；随后按「Starter → 自动配置排障 → 外置容器内的 Spring 上下文 → 过渡期 Holder → Bean 化 → Servlet 注解化 → 可执行 WAR」分层推进。下文按依赖与运行时层次组织，并对照官方参考手册区分「演示型」引导路径与手册主推路径；个别行为（例如仅启动非 Web 上下文时的进程生命周期）若官方未逐句界定，则保留工程层面的不确定性说明。\n1. 集成测试护栏：构建生命周期与嵌入式 Servlet 容器 # (1) 原理与动机：渐进重构时，单元测试难以覆盖类加载、部署描述符与真实 HTTP 路径；将 WAR 交给 Maven 插件驱动的嵌入式 Servlet 容器启停，并把探测请求绑定到 verify 等阶段，可在 CI 中与 mvn clean verify 同类流程组合，形成「改一点、验一轮」的闭环。\n(2) 实现抓手：Codehaus Cargo 的 Maven 3 Plugin 文档给出可与 Maven 生命周期组合的用法及 containerId 示例；Tomcat 10.x 对应标识见 Tomcat 10.x 容器说明（tomcat10x）。Maven 各阶段职责见 Maven 生命周期介绍。\n(3) 怎么用：客户端仅做存活与路由探测（以下为约定路径示例；实际 context-path 须与部署一致）。\ncurl -sS -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; \u0026#34;http://localhost:8080/petclinic/api/owners\u0026#34; 服务端侧即插件启停的容器内已部署 WAR：上述请求由运行在容器中的同一个应用接收（例如映射到既有 Servlet），无需额外桩服务。\n2. 目标形态与版本阶梯：Initializr、Java、Jakarta 与 BOM # (1) 原理与动机：用 Spring Initializr 生成「目标依赖集合」，可避免手工拼 starter 造成的漂移；同时 Java / Jakarta EE 代际决定了可选的 Spring Boot 主版本——Boot 3 GA 起官方叙事明确落在 Jakarta EE 10（EE 9 baseline）与 Java 17 基线之上。\n(2) 实现抓手：Spring Initializr（start.spring.io） 生成 POM 或 Gradle；系统要求见 Spring Boot 系统要求；代际背景可参考 Spring Boot 3.0 Goes GA — Java 17 与 Jakarta EE。依赖版本聚合可使用 BOM：构建系统 — spring-boot-dependencies。多 BOM import 时的精确优先规则若在复杂场景中存在歧义，应以 dependency:tree 实测为准（Maven import 语义见 Importing Dependencies）。\n(3) 怎么用：在自有 dependencyManagement 中导入 Boot BOM 后，对 WEB-INF/lib（或等价产物）在导入前后做目录级 diff，可快速发现 Jackson 等同族模块版本撕裂。\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot.version}\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 3. 首个 Boot 依赖：spring-boot-starter 与日志栈 # (1) 原理与动机：spring-boot-starter 引入核心引导能力并默认落到 Logback（见 日志特性 — 默认 Logback）；与遗留日志实现并存时易出现绑定冲突，需要在 POM 层显式排除或切换。\n(2) 实现抓手：切换到 Log4j 2 的典型做法是排除 spring-boot-starter-logging 并引入 spring-boot-starter-log4j2（How-to — Configure Log4j）；starter 清单仍见 Starters 表。\n(3) 怎么用：\n图中幻灯片标题为「Add first spring dependency」，可见 \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; 与「Recheck logging library」提示。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-log4j2\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 4. 应用入口：@SpringBootApplication、扫描边界与 Web 环境判定 # (1) 原理与动机：主类应置于根包之上以便组件扫描（组织代码）。若 classpath 上既无 Spring MVC 又无 WebFlux，SpringApplication 会走非 Servlet 的 AnnotationConfigApplicationContext（Web Environment 判定）。此时进程是否立即退出、退出码是否为 0，还受非守护线程等因素影响；文档未明确将「必为 0」与该行绑定，线上应以观测为准。\n(2) 实现抓手：@SpringBootApplication 聚合 @Configuration、@EnableAutoConfiguration 与 @ComponentScan；入口为 SpringApplication.run。\n(3) 怎么用：\n@SpringBootApplication public class PetclinicApplication { public static void main(String[] args) { SpringApplication.run(PetclinicApplication.class, args); } } 5. 自动配置与数据源：条件报告、debug 与 exclude # (1) 原理与动机：classpath 上出现 HikariCP 与 JDBC 相关线索时，DataSourceAutoConfiguration 可能在缺少 spring.datasource.* 配置时仍尝试创建 dataSource，从而在启动期失败。提高可见性的首选开关包括 --debug 与条件报告（自动配置）；亦可暂时 exclude 相关自动配置以恢复渐进节奏。\n(2) 实现抓手：排除方式包括 @SpringBootApplication(exclude = …) 或 spring.autoconfigure.exclude；具体自动配置类的全限定名随 Boot 大版本可能调整，需以当前依赖中的类名为准。\n(3) 怎么用：排除目标类的全限定名随 Boot 主版本可能调整，应与当前 classpath 中实际的自动配置类对齐（必要时在 IDE 中从条件报告跳转核对）。\n幻灯片标题「But it can fail as well (depends on the classpath)」与日志「Error creating bean with name ‘dataSource\u0026rsquo;」片段同时可见。\n调试输出可见「Inspect active auto configurations」以及「DataSourceConfiguration.Hikari matched」段落。\n@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class PetclinicApplication { /* ... */ } # 等价于提高自动配置可见性的一种属性路径（具体 logger 名以团队约定为准） logging.level.org.springframework.boot.autoconfigure=DEBUG 6. 外置 Servlet 容器中的 Spring 上下文：Listener 方案与手册主推路径 # (1) 原理与动机：部署在外置 Tomcat 时，main 方法不会作为容器入口执行；需要在 Web 生命周期中显式启动 SpringApplication。一种做法是在 ServletContextListener 的 contextInitialized 中调用 SpringApplication.run，并用 @WebListener 或 web.xml 注册。Spring Boot 参考文档主推 SpringBootServletInitializer 与 configure 方法完成同类目标（传统部署）。工程上应在维护成本与团队熟悉度之间选型，并将选定路径写进架构决策。\n(2) 实现抓手：Listener 方案涉及 SpringApplication、ConfigurableApplicationContext 以及 contextDestroyed 中的 close()；WAR 打包仍需 spring-boot-starter-tomcat 为 provided 等约束（见第 10 节与传统部署章节）。\n(3) 怎么用：\n幻灯片标题「Introduce ServletContextListener to start context」与「public class SpringContextListener implements ServletContextListener」代码骨架可见。\n@WebListener public class SpringContextListener implements ServletContextListener { private ConfigurableApplicationContext applicationContext; @Override public void contextInitialized(ServletContextEvent sce) { applicationContext = SpringApplication.run(PetclinicApplication.class, new String[]{}); } @Override public void contextDestroyed(ServletContextEvent sce) { if (applicationContext != null) { applicationContext.close(); } } } 手册路径骨架（推荐对照官方完整步骤）：\npublic class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(PetclinicApplication.class); } } 运行视图可见「Run our web application in Servlet Gontainer」标题及日志行「Spring Boot (v4.0.5)」「Started application in 0.468 seconds」。\n7. 过渡期的静态访问：ApplicationContextHolder 与 ApplicationContextInitializer # (1) 原理与动机：遗留代码若以 getInstance() 访问对象，与 Spring 构造器注入并存时会遇到初始化次序问题。静态 ApplicationContextHolder 被普遍视为反模式，但在迁移窗口可缩小改动面；关键在于在任意 Bean 可能触碰 Holder 之前写入上下文。ApplicationContextInitializer 在 Bean 定义加载前介入（事件语义见 SpringApplication — ApplicationContextInitializedEvent），配合 SpringFactoriesLoader — META-INF/spring.factories 注册清单键 org.springframework.context.ApplicationContextInitializer，可把 Holder 初始化推到安全时点。\n(2) 实现抓手：实现类、META-INF/spring.factories 中的键值行续写格式，以及 Holder 内 Objects.requireNonNull 防呆。\n(3) 怎么用：\n幻灯片可见「Introduce ApplicationGontextlnitializer to init holder」及 ApplicationContextInitializer\u0026lt;ConfigurableApplicationContext\u0026gt; 实现片段与 spring.factories 注册示意。\n幻灯片标题「Introduce Context holder」与「public final class ApplicationContextHolder」及英文提示「This is a major antipattern」同在。\npublic final class ApplicationContextHolder { private static ApplicationContext ctx; public static void setApplicationContext(ApplicationContext applicationContext) { ApplicationContextHolder.ctx = Objects.requireNonNull(applicationContext); } public static \u0026lt;T\u0026gt; T getBean(Class\u0026lt;T\u0026gt; type) { return ctx.getBean(type); } private ApplicationContextHolder() {} } public class ApplicationContextHolderInitializer implements ApplicationContextInitializer\u0026lt;ConfigurableApplicationContext\u0026gt; { @Override public void initialize(ConfigurableApplicationContext applicationContext) { ApplicationContextHolder.setApplicationContext(applicationContext); } } # META-INF/spring.factories org.springframework.context.ApplicationContextInitializer=\\ com.example.petclinic.spring.ApplicationContextHolderInitializer 8. 从单例工厂到 @Bean / @Service：首批领域组件 # (1) 原理与动机：将仍手工读取配置的模块（例如数据源配置类）迁入 @Configuration + @Bean，可把「如何构造」委托给容器；遗留 getInstance() 暂时转发到 ApplicationContextHolder.getBean(...)，以便分文件、分模块替换调用方。\n(2) 实现抓手：@Bean 方法、@Configuration 类、@Service 构造型、@Import 聚合配置，以及构造器注入取代字段中的 new。\n(3) 怎么用：\n幻灯片标题「Define first bean」及 @Bean public DatabaseConfig databaseConfig()、DatabaseConfigConfiguration 等示意可见。\n幻灯片可见「Define more beans」「annotate as @Service」与「public class OwnerRepository」。\n@Configuration public class DatabaseConfigConfiguration { @Bean public DatabaseConfig databaseConfig() { return new DatabaseConfig(\u0026#34;jdbc:postgresql://localhost/petclinic\u0026#34;, \u0026#34;user\u0026#34;, \u0026#34;secret\u0026#34;); } } public class DatabaseConfig { public static DatabaseConfig getInstance() { return ApplicationContextHolder.getBean(DatabaseConfig.class); } /* ... */ } @Service public class OwnerRepository { private final DatabaseConfig databaseConfig; public OwnerRepository(DatabaseConfig databaseConfig) { this.databaseConfig = databaseConfig; } } 循环依赖若在静态图中被掩盖，迁入 Spring 后可能暴露；@Lazy 与懒初始化语义需结合依赖方向谨慎使用，不宜当作架构修复。\n9. Servlet 映射现代化：@WebServlet 与 @ServletComponentScan # (1) 原理与动机：用 Jakarta Servlet 注解取代纯 web.xml 映射可减少部署描述符漂移；在嵌入式容器路径下，Boot 提供扫描注册能力（@ServletComponentScan），更低版本则可用 ServletRegistrationBean 等显式注册同一批组件。\n(2) 实现抓手：jakarta.servlet.annotation.WebServlet、主类上的 @ServletComponentScan、ServletRegistrationBean / FilterRegistrationBean（见 Servlet Web Applications）。\n(3) 怎么用：\n幻灯片标题「Migrate servlets」及「Before Spring Boot 4: use ServletRegistrationBean」「@ServietComponentScan (since boot 4.0)」等字样可见（OCR 将注解名识别为近似拼写，语义指 @ServletComponentScan）。\n@WebServlet(urlPatterns = {\u0026#34;/api/owners\u0026#34;, \u0026#34;/api/owners/*\u0026#34;}) public class OwnerServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(HttpServletResponse.SC_OK); resp.getWriter().write(\u0026#34;ok\u0026#34;); } } @SpringBootApplication @ServletComponentScan public class PetclinicApplication { public static void main(String[] args) { SpringApplication.run(PetclinicApplication.class, args); } } 演示约定：自定义请求头仅在跨切面观测时需要；上述 doGet 若需读取头字段，使用 req.getHeader(\u0026quot;X-Demo-Trace\u0026quot;) 等与客户端约定一致的名字即可。\n10. 可执行 WAR：repackage、provided 与 WEB-INF/lib-provided # (1) 原理与动机：Spring Boot Maven 插件的 repackage 目标生成可在命令行 java -jar 启动的布局，同时仍可作为标准 WAR 部署；把嵌入式 Tomcat 标记为 provided 可使相关 JAR 落入 WEB-INF/lib-provided，降低与外置容器自带实现的类重复加载风险（spring-boot-maven-plugin — packaging、传统部署 — lib-provided）。\n(2) 实现抓手：\u0026lt;goal\u0026gt;repackage\u0026lt;/goal\u0026gt;、spring-boot-starter-tomcat + \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt;、WEB-INF/lib-provided 关键词。\n(3) 怎么用：\n幻灯片标题「Explain uber war」及 \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt;、「spring-boot-starter-toncat」「provided」等片段可见（OCR 对 artifact 名存在字符误差，语义指 spring-boot-starter-tomcat）。\n\u0026lt;packaging\u0026gt;war\u0026lt;/packaging\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-tomcat\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt;\u0026lt;goal\u0026gt;repackage\u0026lt;/goal\u0026gt;\u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 11. 双引导路径与上下文路径：重复上下文风险与 server.servlet.context-path # (1) 原理与动机：若 main 启动路径仍会触发与 WAR 部署相同的 ServletContextListener（或其它第二条引导链），理论上可能出现嵌套的 Spring 上下文直至资源耗尽——这类组合属于具体部署方式的实例风险，官方手册未必逐场景描述，应以日志中重复的 Banner、Bean 重复注册或资源告警为信号排查。另：java -jar 与外置容器的默认 context-path 往往不同，需与既有集成测试 URL 对齐。\n(2) 实现抓手：server.servlet.context-path（配置元数据描述为应用的 context path）；应在「仅容器」「仅 main」「Uber WAR 两种启动」三条路径上分别验证。\n(3) 怎么用：\nserver.servlet.context-path=/petclinic java -jar target/petclinic.war curl -sS -o /dev/null -w \u0026#34;%{http_code}\\n\u0026#34; \u0026#34;http://localhost:8080/petclinic/api/owners\u0026#34; 架构对照：分层迁移与运行时路径 # 下列示意图压缩多步演示为可沟通的静态视图（标签刻意使用引号包裹含 @、/ 的文本以降低渲染器歧义）。\nflowchart LR subgraph war_paths[\u0026#34;同一 WAR 两条入口\u0026#34;] A[\u0026#34;\\\u0026#34;SpringBootServletInitializer.configure\\\u0026#34;\u0026#34;] B[\u0026#34;\\\u0026#34;ServletContextListener + SpringApplication.run\\\u0026#34;\u0026#34;] end subgraph packaging[\u0026#34;打包语义\u0026#34;] R[\u0026#34;\\\u0026#34;spring-boot-maven-plugin repackage\\\u0026#34;\u0026#34;] P[\u0026#34;\\\u0026#34;provided\\\u0026#34; Tomcat -\u0026gt; WEB-INF/lib-provided\u0026#34;] end war_paths --\u0026gt; R R --\u0026gt; P 参考与延伸阅读 # Codehaus Cargo — Maven 3 Plugin 用法与生命周期组合 Codehaus Cargo — Tomcat 10.x 容器标识 tomcat10x Maven — 生命周期各阶段职责 Spring Initializr（start.spring.io） Spring Boot — 系统要求（Java 版本范围） Spring Boot 3.0 GA 博文 — Java 17 与 Jakarta EE 方向 Spring Boot — 构建系统、spring-boot-dependencies BOM 与 Starters 表 Maven — Importing Dependencies（import scope 语义） Spring Boot — 日志参考（默认 Logback） Spring Boot — How-to：切换到 Log4j2（含 exclusion 示例） Spring Boot — 组织代码与 @SpringBootApplication 扫描边界 Spring Boot — SpringApplication 与 Web 环境判定 Spring Boot — 自动配置、--debug、排除项与 DataSourceAutoConfiguration 示例 Spring Boot — 传统部署（WAR、provided、可执行布局、SpringBootServletInitializer） Spring Boot — SpringBootServletInitializer JavaDoc Jakarta Servlet — ServletContextListener API Jakarta Servlet — @WebListener JavaDoc Spring Framework — SpringFactoriesLoader（META-INF/spring.factories） Spring Framework — ApplicationContextInitializer JavaDoc Spring Framework — @Lazy 与懒初始化语义 Spring Boot — Servlet Web（@ServletComponentScan 与 RegistrationBean） Spring Boot Maven Plugin — repackage 与 WAR 布局说明 ","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/posts/how-to-migrate-the-legacy-project-to-spring-boot-by-sergei-chernov-spring-io-2/","section":"文章","summary":"大规模迁移 Spring Boot 前，应先有可重复的集成验证与可控的依赖基线；随后按「Starter → 自动配置排障 → 外置容器内的 Spring 上下文 → 过渡期 Holder → Bean 化 → Servlet 注解化 → 可执行 WAR」分层推进。下文按依赖与运行时层次组织，并对照官方参考手册区分「演示型」引导路径与手册主推路径；个别行为（例如仅启动非 Web 上下文时的进程生命周期）若官方未逐句界定，则保留工程层面的不确定性说明。","title":"遗留 Servlet 应用渐进接入 Spring Boot：构建、自动配置与 WAR 双模式","type":"posts"},{"content":" 用 Kotlin 表达力加固 Spring Boot 测试：断言、夹具与响应式边界 # 摘要：Spring Boot 与 Kotlin 在 JVM 上互操作成熟，团队常先在 src/test 引入 Kotlin，把扩展函数、默认参数、类型安全 DSL 与 Kotest 等断言风格用在集成测试与 MockMvc 场景中，以降低样板代码并收紧失败信息。与此同时，Java Builder、静态工具重载与 Project Reactor 的 StepVerifier 仍有各自的认知成本；文中按依赖层次归纳常见动机、可对齐的公开 API，以及需注意的语义边界（例如 JVM 泛型擦除、响应式校验是否真正订阅完成）。文中关于「测试代码与生产代码行数比例」等经验数值并无单一厂商规范可复核，若用于治理指标应绑定可追溯的组织内度量或外部研究。\n1. 测试模块先行引入 Kotlin 与生产代码互操作 # (1) 原理与动机：在既有 Java 单体或模块化工程中，将 Kotlin 限于测试源码集可在不改变对外 API 的前提下验证构建链、依赖与 IDE 体验；控制器与服务实现仍可保持 Java，由 Kotlin 测试通过 Spring 容器或 MockMvc 消费契约。\n(2) 实现抓手：Gradle/Maven 为测试配置 Kotlin 插件与 kotlin-test / JUnit 5；Spring Boot 在 Kotlin 支持说明中归纳与 Java 库的互操作性，并指向 Spring Framework 的 Kotlin 语言支持。主代码中的 @RestController、TalkService 等 Java 类型可被 Kotlin 测试直接 @Autowired 或构造注入。\n(3) 怎么用：下面展示 Kotlin 测试依赖 Java 控制器返回的 DTO，不涉及业务细节，仅说明互操作轴线。\n@SpringBootTest class TalkHttpContractTest @Autowired constructor( private val talkService: TalkService // Java 接口 ) { @Test fun `delegates to Java service`() { val created = talkService.createTalk(CreateTalkRequest(/* … */)) assertNotNull(created.id) } } IDE 项目树列出「Integrationrest」「EOL Exploration ipynd」「© 10 TagServicerest」等 OCR 可见条目；源码区含「public TalkDto createTalk(QValid @RequestBody CreateTalkRequest request)」。\n2. 集成测试骨架：@SpringBootTest 与可读断言 DSL # (1) 原理与动机：同类场景下，JUnit 对 assertEquals 的重载列表在 IDE 中噪声较大，失败信息也未必突出「集合语义」。Kotest 等库提供 shouldHaveSize、shouldBe 等入口，使断言片段更接近自然语言描述。\n(2) 实现抓手：测试类仍使用 Spring 的 @SpringBootTest、@Transactional（若需回滚）；断言侧引入 io.kotest.matchers.collections.shouldHaveSize 等。需要多条失败一次看清时，可选用 Kotest 的 assertSoftly（注意官方列出与 assertSoftly 不兼容的断言类别）；若偏好编译期增强 assert 的诊断输出，可使用 Kotlin Power Assert 编译器插件（kotlin(\u0026quot;plugin.power-assert\u0026quot;) 等配置以当前 Kotlin 发行说明为准）。AssertJ 的软断言则是并行选项，见 Soft assertions。\n(3) 怎么用：\n@Test fun `tags size and name`() { val created = tagService.createTags(CreateTagsRequest(names = listOf(\u0026#34;Kotlin\u0026#34;))) created shouldHaveSize 1 created.first().name shouldBe \u0026#34;kotlin\u0026#34; } 并排对照中，Kotlin 侧出现 @SpringBootTest 注解与「class £10_TagServicesuperchargedTest @Autowired constructor(」形式的构造注入测试类声明。\nIDE 补全列表中出现「shouldHaveSize(size: Int) for I in io.kotest.matchers.coLtection」等与 Kotest 集合匹配器相关的候选条目。\n3. 作用域函数收紧局部断言：apply 与单元素焦点 # (1) 原理与动机：对单个 DTO 的多字段断言若层层解开临时变量，噪声上升。apply 在接收者上打开块作用域并返回自身，适合「拿到集合首个元素后就地断言」。\n(2) 实现抓手：语义参见 Kotlin 手册 Scope functions — apply；常与 Kotest 匹配器组合，而非引入额外测试框架。\n(3) 怎么用：\ncreatedTags.single().apply { name shouldBe \u0026#34;kotlin\u0026#34; id.shouldNotBeNull() } 4. 测试数据：copy、Builder.from 与编译期约束 # (1) 原理与动机：庞大 Java Builder 往往难以在类型层面区分必填关联实体，Talk 缺少 Speaker 仍可能推迟到运行期才暴露。Kotlin 的 data class copy、命名参数与可空类型可把部分约束前移；项目自定义的 CreateSpeakerRequestBuilder.from(primary) 用于「自基准对象继承字段再局部改写」，具体 API 需对照各自仓库实现（文档未统一命名）。\n(2) 实现抓手：从 Java 调用带默认参数的 Kotlin 工厂时，可用 @JvmOverloads 暴露多组重载；非空类型约束参见 Null safety。\n(3) 怎么用：\nval primary = createSpeakerRequest(company = \u0026#34;Tst AG\u0026#34;) val co = primary.copy(name = \u0026#34;Sec Undo\u0026#34;, email = \u0026#34;sec.undo@example.com\u0026#34;) Java 测试方法中出现「var prinarySpeakerRequest = CreateSpeakerRequest Builder」以及「-abreateSpeakerRequest().withCompany(\u0026ldquo;Tst AG\u0026rdquo;) .buitd();」等 Builder 调用片段。\nKotlin 侧可见「open fun “should create speaker and talk with object mother’ () {」以及基于 primarySpeakerRequest.copy( 与「-from(primarySpeakerRequest) //\u0026lt;- copy all fields from prinarySpeake」的并列演示。\n5. MockMvc：用 Kotlin 扩展折叠重复请求头与 JSON 胶水 # (1) 原理与动机：控制器测试中，Authorization、X-Correlation-Id、Content-Type 与 ObjectMapper 序列化反复出现。Spring 的 MockMvc 与 MockHttpServletRequestBuilder#header 提供统一入口；Kotlin 扩展函数与默认参数可在不修改框架类型的前提下形成团队内部的「测试 DSL」。\n(2) 实现抓手：mockMvc.perform(RequestBuilder) 契约见 MockMvc Javadoc。下列演示约定：X-Correlation-Id 与 Authorization 头名称仅用于示例，生产环境应遵循组织网关与观测标准。\n(3) 怎么用（客户端 MockMvc + 接收端读取演示头）：\nfun MockHttpServletRequestBuilder.correlationIdHeader(id: String) = header(\u0026#34;X-Correlation-Id\u0026#34;, id) fun MockHttpServletRequestBuilder.authorizationHeader(token: String) = header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer $token\u0026#34;) fun \u0026lt;T\u0026gt; MockHttpServletRequestBuilder.jsonBody(mapper: ObjectMapper, body: T) = contentType(MediaType.APPLICATION_JSON).content(mapper.writeValueAsString(body)) @RestController @RequestMapping(\u0026#34;/api/tags\u0026#34;) class TagController { @PostMapping ResponseEntity\u0026lt;Void\u0026gt; create(@RequestHeader(name = \u0026#34;X-Correlation-Id\u0026#34;, required = false) String corrId, @RequestBody CreateTagsRequest body) { // 演示：读取关联 ID；真实系统可接入 MDC / 追踪上下文 return ResponseEntity.status(HttpStatus.CREATED).build(); } } @Test fun `post tags with headers`() { mockMvc.perform( post(\u0026#34;/api/tags\u0026#34;) .correlationIdHeader(\u0026#34;1284567890\u0026#34;) .authorizationHeader(\u0026#34;token\u0026#34;) .jsonBody(objectMapper, CreateTagsRequest(names = listOf(\u0026#34;java\u0026#34;, \u0026#34;kotlin\u0026#34;))) ).andExpect(status().isCreated) } Java 侧 MockMvc 调用中出现「header( name: “X-Correlation-Id\u0026quot;, values: \u0026ldquo;1284567890\u0026rdquo;)」与「sheader( name: “Authorization, values: “Bearer token\u0026quot;)」；右栏可见对 MockHttpServletRequestBuilder 的 default-token 默认参数扩展封装。\n6. Java 静态辅助方法爆炸 vs Kotlin 默认参数 # (1) 原理与动机：为每一种请求头组合新增 Java 重载，易出现参数顺序相近、语义重叠的静态方法族；Kotlin 默认参数与 扩展函数可把交叉关注点收敛为单一入口。\n(2) 实现抓手：对比侧沿用 Spring 同一套 MockHttpServletRequestBuilder；差异仅在调用形态。「必然产生参数位置冲突」属于经验命题，文档未给出形式化证明。\n(3) 怎么用：将上一节的 correlationIdHeader / authorizationHeader 组合为带默认值的 withStdHeaders(token = \u0026quot;…\u0026quot;, correlationId = \u0026quot;…\u0026quot;) 即可覆盖多数用例，无需为缺省头再写重载。\nJava 工具类「public class E06 MockHveTestUtiLs」中可见「performindGetResponseliithHeaders」及多段仅参数顺序不同的 performAnd…WithHeaders 重载草案。\n7. reified 与响应体反序列化样板 # (1) 原理与动机：JVM 泛型擦除后，List\u0026lt;T\u0026gt; 的运行时 Type 常需通过匿名子类携带；Spring 提供 ParameterizedTypeReference，Jackson 侧常见 TypeReference（具体包名与构造方式需对照所用 jackson-databind 版本 Javadoc）。Kotlin inline + reified 可把这一模式封装为单次调用的扩展。\n(2) 实现抓手：MvcResult#getResponse 返回 MockHttpServletResponse，正文可用 getContentAsString 取出。\n(3) 怎么用（示意：Jackson TypeReference 名称以实现为准）：\ninline fun \u0026lt;reified T\u0026gt; MvcResult.readBody(mapper: ObjectMapper): T = mapper.readValue(response.contentAsString, object : TypeReference\u0026lt;T\u0026gt;() {}) @Test fun `parse tag list`() { val tags: List\u0026lt;TagDto\u0026gt; = mockMvc.perform(get(\u0026#34;/api/tags\u0026#34;)) .andExpect(status().isOk).andReturn() .readBody(objectMapper) tags shouldHaveSize 2 } 8. 类型安全 DSL 描述嵌套领域图 # (1) 原理与动机：多实体、多 Talk / Speaker / Tag 关系的持久化夹具若仅靠零散 Builder 与临时变量，场景增长后难以扫读。Kotlin 带接收者的函数字面量 配合 @DslMarker 可限制非法嵌套，使「局部作用域内声明实体」成为语法约束。\n(2) 实现抓手：内部可变 builder + build() 返回不可变快照；对外暴露 testGraph { talks { talk { … } } } 一类入口。\n(3) 怎么用：\n@Test fun `nested talks dsl`() = testGraph { talks { talk { title = \u0026#34;Kotlin DSL Power\u0026#34; primarySpeaker { name = \u0026#34;Ada\u0026#34;; email = \u0026#34;ada@example.com\u0026#34; } } } }.let { persistGraph(it) } DSL 代码块中出现「abstractIext = \u0026ldquo;Scope fixtures without temporary variables\u0026rdquo;」以及 talks { talk { title = \u0026quot;KotLin OSL Pore” 等嵌套结构。\n9. Project Reactor：StepVerifier 与完成信号 # (1) 原理与动机：Mono.zip 与 Tuple2 组合并行调用时，断言链若层次过深，失败定位困难。StepVerifier 要求显式触发验证；verifyComplete 文档说明其期望 完成信号 作为终结事件——若测试从未调用 verify / verifyComplete 等终止步骤，则不构成完整订阅校验（是否表现为「假绿」仍取决于具体写法与运行器，此处按官方语义强调风险）。\n(2) 实现抓手：Reactor Test 模块；Mono.zip 返回 Mono\u0026lt;Tuple2\u0026lt;…\u0026gt;\u0026gt;。\n(3) 怎么用：\n@Test void recordsParallel() { StepVerifier.create(service.recordBoth(1L, req1, req2)) .assertNext(t -\u0026gt; assertThat(t.getT1().views()).isEqualTo(1)) .verifyComplete(); } Java 侧可见「Stepverifien.create(」、Mono.zip 组合以及收尾处的「verifyconplete()」字样（OCR 识别误差不影响所指 API）。\n10. Kotlin 协程测试与 Reactor 挂起互操作 # (1) 原理与动机：将 Mono 视为挂起边界可以把主断言路径写回顺序风格。runTest 提供协程测试调度；Mono.awaitSingle 在非阻塞线程上等待单元素结果。\n(2) 实现抓手：awaitAll 针对 Deferred 集合；若业务 API 返回 Mono 列表，应使用 async { mono.awaitSingle() } 再 awaitAll，或改用 Reactor 算子（如 Flux 合并后一次性断言），直接把 List\u0026lt;Mono\u0026lt;T\u0026gt;\u0026gt;.awaitAll() 当作 kotlinx 的 Deferred 扩展会与签名不符——落地前需对照返回类型。\n(3) 怎么用：\n@Test fun `record engagements sequentially suspended`() = runTest { val talk = talkService.createTalk(createTalkRequest(speaker)) val recorded = coroutineScope { engagements.map { req -\u0026gt; async { engagementService.recordEngagement(talk.id, req).awaitSingle() } }.awaitAll() } val current = engagementService.getCurrentEngagement(talk.id).awaitSingle() recorded shouldHaveSize 2 current.views shouldBe 3 } 并排片段中出现 Java StepVerifier.create 路径与 Kotlin「open fun “should record engagements and read counts” () =」及「val recordedengagenents = engagements.nap」一侧的对照。\n11. Kotlin Notebook 与运行期探测（能力边界） # (1) 原理与动机：交互式 Notebook 适合短路径验证 HTTP 与 JSON；IntelliJ Kotlin Notebook 说明描述单元执行模型。Kotlin Notebook 概述提到可调用 API 与处理 JSON。是否在单元格中直接持有运行中 Spring 应用的 ApplicationContext Bean，取决于所选内核与工程集成方式，官方 Kotlin Notebook 概述未承诺自动注入远端 Spring 上下文——若需该能力，应查阅所用 Jupyter / Kotlin Jupyter 插件与示例工程的配置。\n(2) 实现抓手：HTTP 侧可使用 Spring 6 引入的同步 RestClient（RestClient.create(baseUrl)、retrieve() 等）。\n(3) 怎么用（仅客户端探测示意）：\nval client = RestClient.create(\u0026#34;http://localhost:8080\u0026#34;) val tags = client.get().uri(\u0026#34;/api/tags\u0026#34;).retrieve().body\u0026lt;List\u0026lt;TagDto\u0026gt;\u0026gt;() tags.size 参考与延伸阅读 # Spring Boot — Kotlin 支持与互操作入口 Spring Framework — Kotlin 语言支持 Spring Framework — MockMvc 测试指南 MockHttpServletRequestBuilder JavaDoc（含 header） Spring Framework — RestClient 参考章节 Kotest — Core matchers（shouldHaveSize 等） Kotest — Soft Assertions（assertSoftly） AssertJ — Soft assertions Kotlin — Power-assert compiler plugin Kotlin — Scope functions（apply） Kotlin — Data classes — copy() Kotlin — @JvmOverloads 与 Java 互操作 Kotlin — Inline functions — Reified type parameters ParameterizedTypeReference JavaDoc Kotlin — Type-safe builders 与 @DslMarker Project Reactor — Mono API（含 zip） Reactor Test — StepVerifier 源码（verifyComplete 注释） Kotlin — runTest API Mono.awaitSingle（kotlinx-coroutines-reactor） awaitAll（Deferred 集合） Kotlin — Kotlin Notebook 概述 IntelliJ IDEA — Kotlin Notebook ","date":"2026年5月6日","externalUrl":null,"permalink":"/zh-cn/posts/supercharge-spring-boot-tests-with-kotlin-dsl-power-by-urs-peter-spring-io-202/","section":"文章","summary":"Spring Boot 与 Kotlin 在 JVM 上互操作成熟，团队常先在 \u003ccode\u003esrc/test\u003c/code\u003e 引入 Kotlin，把扩展函数、默认参数、类型安全 DSL 与 Kotest 等断言风格用在集成测试与 \u003ccode\u003eMockMvc\u003c/code\u003e 场景中，以降低样板代码并收紧失败信息。与此同时，Java Builder、静态工具重载与 Project Reactor 的 \u003ccode\u003eStepVerifier\u003c/code\u003e 仍有各自的认知成本；文中按依赖层次归纳常见动机、可对齐的公开 API，以及需注意的语义边界（例如 JVM 泛型擦除、响应式校验是否真正订阅完成）。","title":"用 Kotlin 表达力加固 Spring Boot 测试：断言、夹具与响应式边界","type":"posts"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/configuration/","section":"Tags","summary":"","title":"Configuration","type":"tags"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/categories/diagnostic/","section":"Categories","summary":"","title":"Diagnostic","type":"categories"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/diagnostic/","section":"Tags","summary":"","title":"Diagnostic","type":"tags"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/categories/error-handling/","section":"Categories","summary":"","title":"Error Handling","type":"categories"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/event-collection/","section":"Tags","summary":"","title":"Event Collection","type":"tags"},{"content":"","date":"1 December 2025","externalUrl":null,"permalink":"/categories/jdk-tough-way/","section":"Categories","summary":"","title":"JDK Tough Way","type":"categories"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/jfr/","section":"Tags","summary":"","title":"JFR","type":"tags"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/categories/jvm/","section":"Categories","summary":"","title":"JVM","type":"categories"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/categories/performance/","section":"Categories","summary":"","title":"Performance","type":"categories"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/performance-monitoring/","section":"Tags","summary":"","title":"Performance Monitoring","type":"tags"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/tags/usage/","section":"Tags","summary":"","title":"Usage","type":"tags"},{"content":"","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/categories/%E5%85%A8%E7%BD%91%E6%9C%80%E7%A1%AC%E6%A0%B8-jdk-%E8%A7%A3%E6%9E%90/","section":"Categories","summary":"","title":"全网最硬核 JDK 解析","type":"categories"},{"content":" 1. 概述 # 1.1. JFR 事件采集概述 # JFR（Java Flight Recorder） 是 Java 平台提供的一个低开销的性能监控和诊断工具。它最初是 Oracle JDK 的商业特性，从 JDK 11 开始作为开源特性集成到 OpenJDK 中，对应的 JEP 和 JBS 分别是：\nJEP 328: Flight Recorder JBS JDK-8193393: JEP 328: Flight Recorder JFR 通过事件采集机制，记录 JVM 运行时的各种信息，包括：\nGC 事件：GC 类型、持续时间、回收的内存大小等 线程事件：线程创建、销毁、状态变化等 方法执行事件：方法调用、执行时间、采样信息等 对象分配事件：对象分配位置、大小、类型等 系统事件：CPU 使用率、内存使用率、网络 I/O 等 自定义事件：应用程序可以定义和记录自定义事件 JFR 的核心优势在于其低开销特性。通过精心设计的事件采集机制，JFR 可以在生产环境中持续运行，对应用程序性能的影响通常小于 1%（吹牛逼，实际则需要根据你的实际需要进行配置后，可以在很低的消耗下仍能持续采集定位问题）\n传统的性能监控工具通常采用采样或插桩的方式，但这些方式往往带来较大的性能开销。JFR 是一种可以持续开启的低开销 JVM 监控机制，特别适合事后分析性能瓶颈和极限问题。在生产环境中，JFR 的核心价值体现在以下几个方面：\n问题发生时的应急处理：当 JVM 进程出现问题时，首要任务是尽快恢复业务（重启实例、下线问题实例、扩容等），而不是立即进行问题分析。此时需要的是能够持续记录、事后分析的工具。 问题现场的时间窗口：问题出现时，往往已经错过了问题发生的关键时间点。即使通过下线实例来保护现场，使用实时分析工具（如 JVisualVM、Arthas）可能已经无法获取到问题发生时的现场数据。 JVM 无响应场景：某些严重问题（如 OOM、死锁导致线程阻塞等）可能导致 JVM 无法响应外部诊断请求（JMX、jcmd），此时实时分析工具完全失效。JFR 由于是持续记录机制，即使 JVM 出现问题，也能保留问题发生前的历史数据。具体案例可参考：6. 如何通过 JFR 快速定位 Java 堆 OOM 的实战与底层原理 以下是几类常见工具与 JFR 的对比：\n1.1.1. JVisualVM # JVisualVM 是 JDK 自带的图形化监控工具，通过 JMX 连接获取 JVM 运行时数据。\n优势：图形化界面，易于使用；支持堆转储、线程转储分析 劣势： 基于 JMX 连接 + Agent 插桩采样，性能开销大（10~20%） 在生产环境 JVM 压力大时，采集可能中断 无法持续记录历史数据，不适合事后分析 与 JFR 对比：JFR 开销更低（\u0026lt;5%），支持持续记录历史数据，更适合生产环境持续运行和事后分析 1.1.2. Arthas # Arthas 是阿里巴巴开源的 Java 诊断工具，通过字节码增强实现动态监控。\n优势：无需重启应用；支持动态修改代码；提供丰富的诊断命令 劣势： 很多功能基于字节码增强，性能开销较大（10~20%） 不适合长期开启，存在安全风险 需要 JVM 能够响应外部请求，不适合事后问题分析 与 JFR 对比：JFR 开销更低，更适合生产环境持续运行；Arthas 更适合临时诊断和问题排查 1.1.3. OpenTelemetry / Skywalking # OpenTelemetry 是分布式追踪和 APM（Application Performance Monitoring）标准，Skywalking 是基于 OpenTelemetry 实现的分布式追踪工具，通过 Java Agent 插桩实现。\n优势：支持分布式追踪；提供完整的调用链分析；支持多语言 劣势： 基于字节码插桩，性能开销较大（5-15%） 需要额外的存储和计算资源，全量上报成本很高（远高于日志成本） 专注于应用层调用链分析，不适合 JVM 内部事件监控 与 JFR 对比：JFR 专注于 JVM 内部事件，开销更低；APM 工具专注于应用层调用链，两者可以互补使用 1.1.4. 实际使用建议 # 在实际生产环境中，建议采用分层监控策略，结合多种工具的优势：\n1. OpenTelemetry + APM 工具（如 Skywalking）：\nMetrics 采集：使用 Prometheus + Grafana 进行指标监控和告警 Traces 采集：使用 Jaeger 或 Skywalking 进行调用链追踪 由于全量上报成本很高，建议采用采样策略（如 5% 采样率） 主要用于告警和初步定位问题（定位到出问题的微服务实例和大致位置） 如果 Traces 数据不足以定位根因，再查看对应实例的 JFR 数据 2. JFR 持续采集： 每个微服务实例都开启 JFR，利用 JFR 自带的机制在本地临时文件目录保存最近 3 天的 JFR 文件 当出现问题时，在 JVM 实例下线前，将对应实例临时文件目录的所有 JFR 文件上传到集中式存储（如 S3、OSS）用于事后分析 JFR 数据用于深入分析 JDK + JVM 内部事件，定位性能瓶颈和根因问题： 比如所有的锁问题，包括分布式锁，都可以通过 JFR 的锁相关事件（Monitor 相关事件，Thread Sleep，Thread Park 等等） JFR 还可以帮助分析 GC 行为、线程状态、热点方法执行情况等 1.2. JFR 相关配置概览 # flowchart LR A[JFR 配置体系] --\u003e B[全局配置] A --\u003e C[记录级别配置] B --\u003e B1[-XX:+FlightRecorderJDK 11 引入作用: 启用/禁用 JFR 功能] B --\u003e B2[全局选项通过 -XX:FlightRecorderOptions= 指定或 jcmd JFR.configure 修改或通过 JMX 修改] B2 --\u003e B21[repositoryJDK 11 引入默认: 临时目录作用: Repository 路径] B2 --\u003e B22[globalbuffersizeJDK 11 引入默认: 512k作用: 全局缓冲区大小] B2 --\u003e B23[numglobalbuffersJDK 11 引入默认: 20作用: 全局缓冲区数量] B2 --\u003e B24[memorysizeJDK 11 引入默认: 10m作用: JFR 占用总内存限制] B2 --\u003e B25[threadbuffersizeJDK 11 引入默认: 8k 或操作系统 pageSize作用: 线程缓冲区大小] B2 --\u003e B26[maxchunksizeJDK 11 引入默认: 12m作用: 最大 Chunk 大小] B2 --\u003e B27[samplethreadsJDK 11 引入JDK 19 废弃默认: true作用: 是否启用线程采样] B2 --\u003e B28[stackdepthJDK 11 引入默认: 64作用: 堆栈跟踪深度] B2 --\u003e B29[retransformJDK 11 引入默认: true作用: 是否允许 JVMTI retransform] B2 --\u003e B30[old-object-queue-sizeJDK 11 引入默认: 256作用: OldObjectSample 队列大小] B2 --\u003e B31[dumppathJDK 17 引入默认: 当前工作目录作用: 紧急转储路径] B2 --\u003e B32[preserve-repositoryJDK 21 引入默认: false作用: JVM 退出时是否保留 Repository] B2 --\u003e B33[sampleprotectionJDK 11 引入仅 DEBUG 模式默认: false/true作用: 采样线程时堆栈遍历的保护措施] C --\u003e C1[单个记录配置通过 -XX:StartFlightRecording= 指定或 jcmd JFR.start 指定或 JDK API 指定或 JMX 指定] C --\u003e C2[记录级别但影响全局的配置每个记录可设置，但实际影响全局] C1 --\u003e C11[settingsJDK 11 引入默认: default.jfc作用: 事件配置文件] C1 --\u003e C12[nameJDK 11 引入默认: null作用: 记录名称] C1 --\u003e C13[delayJDK 11 引入默认: 0s作用: 延迟启动时间] C1 --\u003e C14[durationJDK 11 引入默认: 0s作用: 自动停止时间] C1 --\u003e C15[filenameJDK 11 引入默认: null作用: dump 时的文件名] C1 --\u003e C16[maxageJDK 11 引入默认: 0s作用: 最大保留时间] C1 --\u003e C17[maxsizeJDK 11 引入默认: 0作用: 最大总大小] C1 --\u003e C18[dumponexitJDK 11 引入默认: false作用: JVM 退出时是否 dump] C1 --\u003e C19[path-to-gc-rootsJDK 11 引入默认: false作用: 是否记录 GC 根] C1 --\u003e C20[report-on-exitJDK 25 引入默认: null作用: JVM 退出时生成报告视图] C2 --\u003e C21[diskJDK 11 引入默认: true作用: 是否保存到磁盘影响: 全局共享 chunk 文件] C2 --\u003e C22[flush-intervalJDK 17 引入默认: 1s作用: 定时 flush 间隔影响: 使用所有记录的最小值] C11 --\u003e C23[jfc 文件default.jfc, profile.jfc] C23 --\u003e C24[enabled: JDK 11 引入所有事件类型控制事件是否被记录] C23 --\u003e C25[threshold: JDK 11 引入仅对持续时间事件有效指定持续时间阈值] C23 --\u003e C26[period: JDK 11 引入仅对周期性事件有效指定周期性事件采集间隔] C23 --\u003e C27[stackTrace: JDK 11 引入所有事件类型控制是否采集堆栈跟踪] C23 --\u003e C28[throttle: JDK 16 引入所有事件类型控制事件最大采集速率] C23 --\u003e C29[filter: JDK 25 引入仅对特定事件有效指定方法过滤器] C23 --\u003e C30[cutoff: JDK 11 引入仅对 jdk.OldObjectSample 有效限制查找 GC root 路径的最大时间] C23 --\u003e C31[level: JDK 22 引入仅对 jdk.DeprecatedInvocation 有效控制记录哪些废弃方法调用] %% 根节点和主要分类 style A fill:#2c3e50,stroke:#34495e,stroke-width:3px,color:#fff style B fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff style C fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff %% 全局配置 - 启用标志（特殊） style B1 fill:#27ae60,stroke:#229954,stroke-width:2px,color:#fff %% 全局配置 - 容器 style B2 fill:#5dade2,stroke:#3498db,stroke-width:2px,color:#fff %% 全局配置 - 缓冲区相关（蓝色系） style B22 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px style B23 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px style B24 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px style B25 fill:#aed6f1,stroke:#5dade2,stroke-width:1.5px %% 全局配置 - 存储相关（绿色系） style B21 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style B26 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px %% 全局配置 - 采样相关（紫色系） style B27 fill:#d2b4de,stroke:#bb8fce,stroke-width:1.5px style B33 fill:#d2b4de,stroke:#bb8fce,stroke-width:1.5px %% 全局配置 - 其他基础配置（浅蓝色系） style B28 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px style B29 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px style B30 fill:#d6eaf8,stroke:#85c1e9,stroke-width:1.5px %% 全局配置 - 新版本特性（粉色系，JDK 17+） style B31 fill:#f8c9d4,stroke:#ec7063,stroke-width:1.5px style B32 fill:#f8c9d4,stroke:#ec7063,stroke-width:1.5px %% 记录级别配置 - 容器 style C1 fill:#f39c12,stroke:#e67e22,stroke-width:2px,color:#fff style C2 fill:#e74c3c,stroke:#c0392b,stroke-width:2px,color:#fff %% 记录级别配置 - 单个记录配置（橙色系） style C11 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C12 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C13 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C14 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C15 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C16 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C17 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C18 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px style C19 fill:#fad7a0,stroke:#f39c12,stroke-width:1.5px %% 记录级别配置 - 新版本特性（红色系，JDK 22+） style C20 fill:#f1948a,stroke:#e74c3c,stroke-width:1.5px %% 记录级别配置 - 影响全局的配置（深橙色系） style C21 fill:#f5b041,stroke:#e67e22,stroke-width:1.5px style C22 fill:#f5b041,stroke:#e67e22,stroke-width:1.5px %% jfc 文件配置（绿色系） style C23 fill:#58d68d,stroke:#27ae60,stroke-width:2px,color:#fff style C24 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C25 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C26 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C27 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C28 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C29 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C30 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px style C31 fill:#a9dfbf,stroke:#52be80,stroke-width:1.5px 1.3. JFR 相关配置详情与演进 # 全局配置：控制 JFR 功能的启用和全局选项 是否启用 JFR，不启用则连模块都不会加载：-XX:+FlightRecorder（JDK 11 引入，默认值：false，这个配置已过期，并且虽然默认值是 false，但是如果没有设置，JVM 还是将其设置为 true，即默认启用 JFR 功能，只有在显式设置为 false 时才会禁用 JFR 功能） 全局选项：通过 -XX:FlightRecorderOptions= 指定，或者通过 jcmd \u0026lt;pid\u0026gt; JFR.configure 命令修改，或者通过 JMX 修改 repository：JDK 11 引入，默认值：null（实际使用 java.io.tmpdir 系统属性指定的目录，即 Java 临时文件目录），作用：Flight recorder disk repository location（Repository 路径）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定 JFR 存储 chunk 文件的磁盘目录。当启用磁盘录制时，JFR 会在此目录创建子目录（格式：\u0026lt;timestamp\u0026gt;_\u0026lt;pid\u0026gt;）存储 chunk 文件。影响磁盘 I/O 性能和存储空间管理 运行时动态修改：可以在运行时通过 jcmd \u0026lt;pid\u0026gt; JFR.configure repositorypath=\u0026lt;path\u0026gt; 或 JMX 修改。如果 JFR 已初始化，修改时会切换到新的 repository 路径，旧的 repository 数据会保留在原位置，不会被迁移。如果旧的 repository 目录已经在 cleanupDirectories 中（即之前通过 newChunk() 创建过），JFR 会继续追踪它，并在 JVM 关闭时自动删除（如果 preserve-repository=false）。如果有正在运行的磁盘记录，当前 chunk 会被标记为 final 并轮转，新的 chunk 会写入新的 repository 路径 相关 JBS： JDK-8243452：修复了在有超过 200 个记录时，无法在存储库中创建 chunk 的问题（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8243452 globalbuffersize：JDK 11 引入，默认值：512k，作用：Global buffer size（全局缓冲区大小）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：单个全局缓冲区的大小（范围：64K-2G）。与 numglobalbuffers 共同决定总内存占用（memorysize = globalbuffersize * numglobalbuffers）。缓冲区越大，单次可保留的事件数据越多，但内存占用也越大。通常建议通过 memorysize 统一调整 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层） 相关 JBS： JDK-8213015：修复了 JFR.configure 和 -XX:FlightRecorderOptions 之间的设置不一致问题，统一了 globalbuffersize 等选项的值格式和默认值（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8213015 JDK-8203457：修复了在全局缓冲区满时，没有向记录器线程发送通知以刷新到磁盘的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203457 JDK-8242088：将互斥列表替换为并发替代方案，提高了缓冲区管理的并发性能（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8242088 numglobalbuffers：JDK 11 引入，默认值：20，作用：Number of global buffers（全局缓冲区数量）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：全局缓冲区的数量（最小值：2）。缓冲区数量越多，并发写入能力越强，但内存占用也越大。与 globalbuffersize 共同决定总内存占用。通常建议通过 memorysize 统一调整 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层）。 相关 JBS： JDK-8213015：修复了 JFR.configure 和 -XX:FlightRecorderOptions 之间的设置不一致问题，统一了 globalbuffercount 等选项的值格式和默认值（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8213015 JDK-8203457：修复了在全局缓冲区满时，没有向记录器线程发送通知以刷新到磁盘的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203457 JDK-8242088：将互斥列表替换为并发替代方案，提高了缓冲区管理的并发性能（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8242088 memorysize：JDK 11 引入，默认值：10m，作用：Size of memory to be used by Flight Recorder（Flight Recorder 使用的内存大小）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：JFR 全局缓冲区的总内存大小（最小值：1M），计算公式：memorysize = globalbuffersize * numglobalbuffers。注意：memorysize 仅包含全局缓冲区的内存，不包含线程本地缓冲区的内存。线程本地缓冲区通过 C 堆独立分配，不受 memorysize 限制。JFR 实际占用的总内存 = memorysize + 线程本地缓冲区内存（threadbuffersize * 活跃线程数，实际可能更少，因为缓冲区是动态分配的）。如果只设置此选项，JFR 会自动计算 globalbuffersize 和 numglobalbuffers 的最优组合 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层） 相关 JBS： JDK-8213015：修复了 JFR.configure 和 -XX:FlightRecorderOptions 之间的设置不一致问题，统一了 memorysize 等选项的值格式和默认值（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8213015 JDK-8203457：修复了在全局缓冲区满时，没有向记录器线程发送通知以刷新到磁盘的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203457 JDK-8242088：将互斥列表替换为并发替代方案，提高了缓冲区管理的并发性能（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8242088 threadbuffersize：JDK 11 引入，默认值：8k（或操作系统页大小，取较大值），作用：Thread buffer size（线程缓冲区大小）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：每个线程的本地缓冲区大小（范围：4K-2G，必须 ≤ globalbuffersize）。重要：threadbuffersize 不受 memorysize 影响，线程本地缓冲区通过 C 堆（JfrCHeapObj）独立分配，是堆外内存。线程缓冲区用于暂存事件数据，满后刷新到全局缓冲区。缓冲区越大，线程本地可缓存的事件越多，减少全局缓冲区竞争，但每个线程的内存占用也越大。注意：线程本地缓冲区池的 free_list 最多缓存 8 个缓冲区（thread_local_cache_count = 8，写死在 JDK 源码中不可修改），这是缓存限制而非线程数限制。当活跃线程数超过 8 个时，多出的线程会直接分配新缓冲区；当缓冲区释放时，如果 free_list 已满（8 个），则直接释放内存而非缓存。JFR 实际占用的总内存 = memorysize + 线程本地缓冲区内存（threadbuffersize * 活跃线程数，实际可能更少，因为缓冲区是动态分配的） 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层） 相关 JBS： JDK-8213015：修复了 JFR.configure 和 -XX:FlightRecorderOptions 之间的设置不一致问题，统一了 thread_buffer_size 等选项的值格式和默认值（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8213015 maxchunksize：JDK 11 引入，默认值：12m，作用：Maximum size of a single repository disk chunk（单个 repository 磁盘块的最大大小）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：单个 chunk 文件的最大大小（最小值：1M）。当 chunk 达到此大小时会触发轮转（rotation），创建新的 chunk 文件。值越大，chunk 文件越少，但单文件越大；值越小，chunk 文件越多，但便于分段分析。影响磁盘写入频率和文件管理 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层） 相关 JBS： JDK-8213015：修复了 JFR.configure 和 -XX:FlightRecorderOptions 之间的设置不一致问题，统一了 maxchunksize 等选项的值格式和默认值（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8213015 JDK-8243452：修复了在有超过 200 个记录时，无法在存储库中创建 chunk 的问题（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8243452 JDK-8240783：修复了 TestClose 测试无法完成 chunk 的问题（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8240783 JDK-8236487：修复了 JFR Recorder Thread 崩溃的问题，崩溃发生在 chunk writer 无效时（Fix Version: 14）- https://bugs.openjdk.org/browse/JDK-8236487 samplethreads：JDK 11 引入，JDK 19 废弃（但仍可用），默认值：true，作用：Thread sampling enable / disable (only sampling when event enabled and sampling enabled)（线程采样启用/禁用）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 废弃：JDK-8259774: Deprecate -XX:FlightRecorderOptions:samplethreads (Fix Version: 19) - https://bugs.openjdk.org/browse/JDK-8259774 影响说明：控制是否启用线程采样。当为 false 时，即使启用了需要采样的事件（如 ExecutionSample），也不会进行线程采样。JDK 19 后建议使用 -XX:StartFlightRecording:method-profiling 替代 相关 JBS： JDK-8203635：修复了 JFR 采样器线程不记录堆栈信息的问题，这对于 NMT（Native Memory Tracking）很重要（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203635 JDK-8215727：恢复了 JFR 线程采样器循环的旧行为，确保采样器能够收集到足够的样本（Fix Version: 13）- https://bugs.openjdk.org/browse/JDK-8215727 JDK-8240819：为 JfrThreadSampler 线程分配一个名称，以便于调试（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8240819 JDK-8288663：修复了禁用 JfrThreadSampler 时只提交部分禁用状态的问题（Fix Version: 20）- https://bugs.openjdk.org/browse/JDK-8288663 JDK-8367953：修复了 JFR 采样器线程不出现在线程转储中的问题（Fix Version: 26）- https://bugs.openjdk.org/browse/JDK-8367953 stackdepth：JDK 11 引入，默认值：64，作用：Stack depth for stacktraces (minimum 1, maximum 2048)（堆栈跟踪的堆栈深度）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：堆栈跟踪的最大深度（范围：1-2048）。深度越大，堆栈信息越完整，但占用内存和 CPU 开销也越大（堆栈遍历成本）。深度过小可能丢失关键调用信息。需要平衡信息完整性和性能开销 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。如果 JFR 已初始化，通过 jcmd JFR.configure 修改此选项会被忽略（参数不会被传递到 Java 层）。JDK 17 修复了此契约的强制执行（JDK-8278419） 相关 JBS： JDK-8278419：修复了 jcmd JFR.configure 命令中某些选项的契约未强制执行的问题，stackdepth 在 JFR 初始化后无法更改（Fix Version: 17）- https://bugs.openjdk.org/browse/JDK-8278419 JDK-8249713：修复了 java.base 事件的堆栈跟踪不完整的问题，堆栈跟踪的 skip level 设置过大（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8249713 retransform：JDK 11 引入，默认值：true，作用：If event classes should be instrumented using JVMTI (by default true)（是否使用 JVMTI 对事件类进行插桩）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明： retransform=true（默认）：使用 JVMTI RetransformClasses 功能，可在类加载后动态对事件类进行插桩，支持动态启用事件和方法追踪（Method Tracing）功能 retransform=false：使用\u0026quot;急切插桩\u0026quot;（eager instrumentation），仅在类初始加载时插桩；已加载但未插桩的事件类无法再插桩，方法追踪的新过滤器会被忽略，可能影响动态启用事件的能力 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。此选项不在 jcmd JFR.configure 命令的支持列表中 old-object-queue-size：JDK 11 引入，默认值：256，作用：Maximum number of old objects to track（要跟踪的旧对象的最大数量）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：OldObjectSample 事件跟踪的旧对象最大数量。值越大，可跟踪的潜在内存泄漏对象越多，但内存占用也越大（每个对象需要存储引用链信息）。设置为 0 可禁用 OldObjectSample 事件，节省内存 运行时动态修改：不可以在运行时修改。只能在 JFR 初始化前通过 -XX:FlightRecorderOptions 设置。此选项不在 jcmd JFR.configure 命令的支持列表中 相关 JBS： JDK-8212232：修复了 OldObjectSample 事件的 cutoff 配置元数据错误，元数据显示为 BooleanFlag 类型但应该是 Timespan 类型（Fix Version: 12）- https://bugs.openjdk.org/browse/JDK-8212232 JDK-8214542：修复了在深度堆上 Old Object Sample 事件在调试构建中运行缓慢的问题（Fix Version: 13）- https://bugs.openjdk.org/browse/JDK-8214542 JDK-8313394：修复了 OldObjectSample 事件中 Array Elements 字段描述不正确的问题（Fix Version: 21）- https://bugs.openjdk.org/browse/JDK-8313394 JDK-8330215：优化了 OldObjectSamples 的工作集，减少内存占用（Fix Version: 22）- https://bugs.openjdk.org/browse/JDK-8330215 dumppath：JDK 17 引入，默认值：null（实际使用当前工作目录，即 JVM 启动时的当前目录），作用：Path to emergency dump（紧急转储路径）： 引入：JDK-8271949: dumppath in -XX:FlightRecorderOptions does not affect (Fix Version: 17) - https://bugs.openjdk.org/browse/JDK-8271949 影响说明：指定紧急转储文件的保存目录。当 JVM 发生异常关闭（crash、OOM、栈溢出等）时，JFR 会将 repository 中的所有数据合并写入紧急转储文件（文件名格式：hs_err_pid\u0026lt;pid\u0026gt;.jfr、hs_oom_pid\u0026lt;pid\u0026gt;.jfr 或 hs_soe_pid\u0026lt;pid\u0026gt;.jfr）。如果指定目录无法写入，会自动回退到当前工作目录。这对于在异常情况下保存 JFR 数据用于问题诊断非常重要 紧急转储详解： 作用：在 JVM 崩溃或异常关闭时，自动将 repository 中的所有 JFR chunk 文件按时间顺序合并写入一个紧急转储文件，确保在异常情况下也能保存 JFR 数据用于问题诊断 触发条件： VM 错误时：通过 JfrEmergencyDump::on_vm_error() 触发（在 JfrRepository::on_vm_error() 中调用），当 JVM 遇到严重错误（如崩溃、断言失败等）时触发 VM 关闭时：通过 JfrEmergencyDump::on_vm_shutdown() 触发（在 Jfr::on_vm_shutdown() 中调用，在 java.cpp 的 before_exit() 中调用），当 JVM 正常关闭时也会触发 注意：如果 WatcherThread 崩溃，不会生成紧急转储（因为 WatcherThread 是安全网，用于在紧急转储死锁时超时退出） 转储内容： Repository 数据：将 repository 目录中的所有 .jfr chunk 文件按时间顺序（ISO8601 时间戳）合并写入紧急转储文件 事件数据：在 on_vm_shutdown() 时，还会触发以下事件并写入转储文件： EventDumpReason：记录转储原因（reason 字段为 \u0026ldquo;Out of Memory\u0026rdquo; 或 \u0026ldquo;Crash\u0026rdquo;） EventShutdown：如果 emit_event_shutdown=true，记录 VM 关闭事件 Old Object Samples：如果 emit_old_object_samples=true，发出旧对象样本（用于 OOM 分析） 文件命名规则： 一般错误：hs_err_pid\u0026lt;pid\u0026gt;.jfr（默认情况，包括崩溃、断言失败等） 内存不足（OOM）：hs_oom_pid\u0026lt;pid\u0026gt;.jfr（当 JfrJavaSupport::cause() 返回 OUT_OF_MEMORY 时） 栈溢出（SOE）：hs_soe_pid\u0026lt;pid\u0026gt;.jfr（当 JfrJavaSupport::cause() 返回 STACK_OVERFLOW 时） 转储路径： 如果设置了 dumppath，会在该目录下创建紧急转储文件 如果未设置 dumppath 或设置失败，会回退到当前工作目录 如果指定目录无法写入，会记录警告并尝试在当前目录创建 实现细节： 使用 1MB 的缓冲区块进行文件复制，避免内存不足 在转储过程中会释放所有持有的锁（Threads_lock、Module_lock、Heap_lock 等），避免死锁 使用重入保护机制（guard_reentrancy()），确保同一时间只有一个线程执行紧急转储 如果紧急转储过程中发生死锁，WatcherThread 会作为安全网在超时后强制退出 JVM 运行时动态修改：可以在运行时通过 jcmd \u0026lt;pid\u0026gt; JFR.configure dumppath=\u0026lt;path\u0026gt; 或 JMX 修改 相关 JBS： JDK-8206254：修复了在安全点（safepoint）期间无法完成 JFR 紧急转储的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8206254 JDK-8233706：修复了紧急转储在错误报告之前执行的问题，现在紧急转储在错误报告（如 NMT 报告、CI Replay）之后执行（Fix Version: 15）- https://bugs.openjdk.org/browse/JDK-8233706 JDK-8235390：修复了 JfrEmergencyDump::on_vm_shutdown 崩溃的问题，在 VM 关闭过程中安全地完成转储（Fix Version: 14）- https://bugs.openjdk.org/browse/JDK-8235390 JDK-8249878：修复了紧急转储中的二次崩溃问题，改进了对非 JavaThread（如 VMThread）的处理（Fix Version: 16）- https://bugs.openjdk.org/browse/JDK-8249878 JDK-8282947：修复了在某些条件下，关闭时转储（dump on shutdown）导致活锁的问题（Fix Version: 20）- https://bugs.openjdk.org/browse/JDK-8282947 preserve-repository：JDK 21 引入，默认值：false，作用：Preserve disk repository after JVM exit（JVM 退出后保留磁盘 repository）： 引入：JDK-8303229: JFR: Preserve disk repository after exit (Fix Version: 21) - https://bugs.openjdk.org/browse/JDK-8303229 影响说明：控制 JVM 退出时是否保留 repository 目录中的 chunk 文件。为 true 时，JVM 正常退出后 repository 文件不会被清理，便于后续分析；为 false 时，JVM 退出时会自动清理 repository，避免磁盘空间占用 运行时动态修改：可以在运行时通过 jcmd \u0026lt;pid\u0026gt; JFR.configure preserve-repository=\u0026lt;true|false\u0026gt; 或 JMX 修改 sampleprotection：JDK 11 引入（仅 DEBUG 模式），默认值：false（DEBUG 模式）/true（非 DEBUG 模式），作用：Safeguard for stackwalking while sampling threads (false by default)（采样线程时堆栈遍历的保护措施）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：仅在 DEBUG 构建中可用，用于在采样线程时保护堆栈遍历操作。DEBUG 模式下默认为 false，非 DEBUG 模式下默认为 true。主要用于开发和调试场景，生产环境通常不可用 记录级别配置：控制单个记录的事件配置和存储选项，一个 JVM 进程可以有多个记录同时运行，每个记录配置可以独立设置，互不影响 单个记录配置：通过 -XX:StartFlightRecording= 指定，或者通过 jcmd \u0026lt;pid\u0026gt; JFR.start 命令修改，或者通过 JDK API 指定，或者通过 JMX 指定 settings：JDK 11 引入，默认值：default.jfc，作用：Settings file(s) that identifies which events to record（事件配置文件）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定 JFR 事件配置文件（.jfc 文件），用于确定要记录哪些事件及其配置。可以指定多个配置文件，使用 settings 参数重复指定。如果文件不在 JAVA_HOME/lib/jfr 目录中，需要包含完整路径。可以使用 \u0026ldquo;none\u0026rdquo; 来启动不包含预定义配置的记录。默认使用 default.jfc，提供低开销的预定义事件集，适合生产环境持续使用。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改。修改会立即生效，如果记录正在运行（RUNNING 状态），会调用 recorder.updateSettings(true) 更新事件配置 相关 JBS： JDK-8216064：修复了 -XX:StartFlightRecording:settings= 选项不能正常工作的问题，改进了错误处理并支持 \u0026ldquo;none\u0026rdquo; 值（Fix Version: 13）- https://bugs.openjdk.org/browse/JDK-8216064 单个事件的配置： enabled - JDK 11 引入 作用：控制事件是否被记录 适用事件：所有事件类型 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 threshold - JDK 11 引入 作用：指定持续时间阈值，低于此阈值的事件不记录 适用事件：仅对有持续时间（Duration）的事件有效 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 示例：jdk.ThreadSleep#threshold=20 ms 表示只记录睡眠时间超过 20ms 的线程睡眠事件 period - JDK 11 引入 作用：指定周期性事件的采集间隔 默认值：\u0026quot;everyChunk\u0026quot;（每次 chunk 轮转时采集） 适用事件：仅对周期性事件（Periodic Events）有效 配置值： Chunk 相关值： \u0026quot;everyChunk\u0026quot;（默认值）：在每次 chunk 轮转时采集事件，包括开始和结束。事件数量取决于 chunk 轮转次数。如果记录期间没有轮转，至少采集一次。适用于统计类事件（如 jdk.ClassLoadingStatistics、jdk.ThreadAllocationStatistics） \u0026quot;beginChunk\u0026quot;：只在 chunk 开始轮转时采集。每个新 chunk 开始时采集一次 \u0026quot;endChunk\u0026quot;：只在 chunk 结束轮转时采集。每个 chunk 结束时采集一次 时间间隔值（固定频率）： 格式：\u0026lt;数字\u0026gt; \u0026lt;单位\u0026gt; 支持的单位：ns（纳秒）、us（微秒）、ms（毫秒）、s（秒）、m（分钟）、h（小时）、d（天） 示例：\u0026quot;20 ms\u0026quot;（每 20 毫秒）、\u0026quot;1 s\u0026quot;（每秒）、\u0026quot;5 m\u0026quot;（每 5 分钟）、\u0026quot;1 h\u0026quot;（每小时） 最小间隔：实际最小间隔为 1 毫秒，即使设置更小的值也会被调整为 1 毫秒 如果设置为 \u0026quot;0 ns\u0026quot; 或 Long.MAX_VALUE，会被视为禁用周期性采集 值的合并规则（多个记录同时运行时）： 如果指定了时间间隔值，优先使用最小的时间间隔 如果只指定了 chunk 相关值，按以下规则： 同时指定 beginChunk 和 endChunk → 等同于 everyChunk 只指定 beginChunk → 返回 beginChunk 只指定 endChunk → 返回 endChunk 默认返回 everyChunk 示例： jdk.CPULoad#period=1 s 表示每秒采集一次 CPU 负载事件 jdk.ClassLoadingStatistics#period=everyChunk 表示每次 chunk 轮转时采集类加载统计信息 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 注意事项： period 只对周期性事件有效：只有标注了 @Period 的事件才支持此配置 Chunk 轮转频率：everyChunk、beginChunk、endChunk 的采集频率取决于 chunk 轮转频率，而 chunk 轮转由 maxchunksize 等全局配置控制 性能影响：时间间隔越小，采集频率越高，性能开销越大 stackTrace - JDK 11 引入 作用：控制是否在事件提交时采集堆栈跟踪 适用事件：所有事件类型（但是某些事件采集堆栈没有啥意义，比如定时事件） 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 throttle - JDK 16 引入 作用：控制事件的最大采集速率（每秒/每分钟等） 适用事件：所有事件类型 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 示例：jdk.ObjectAllocationSample#throttle=100/s 表示每秒最多采集 100 个对象分配采样事件 filter - JDK 25 引入 引入：JDK-8352738: Implement JEP 520: JFR Method Timing and Tracing (Fix Version: 25) - https://bugs.openjdk.org/browse/JDK-8352738 作用：指定方法过滤器，用于过滤要记录的方法 适用事件：仅对特定事件有效，主要是： jdk.MethodTrace（方法追踪事件） jdk.MethodTiming（方法计时事件） 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 语法：支持类名、方法名、注解等过滤规则 cutoff - JDK 11 引入 作用：限制查找 GC root 路径的最大时间，用于控制 jdk.OldObjectSample 事件中查找对象引用链的时间开销 默认值：\u0026quot;infinity\u0026quot;（无时间限制） 适用事件：仅对 jdk.OldObjectSample 事件有效 配置值： 时间间隔格式：\u0026quot;1 h\u0026quot;、\u0026quot;0 ns\u0026quot;、\u0026quot;infinity\u0026quot; 等 \u0026quot;0 ns\u0026quot;：不查找 GC root 路径，只记录对象本身（无引用链信息），性能开销最小 \u0026quot;infinity\u0026quot;：无时间限制，完整查找 GC root 路径 其他时间值：限制查找路径的最大时间，超过时间限制可能无法获取完整的引用链 与 path-to-gc-roots 的关系： 当 path-to-gc-roots=true 时，cutoff 自动设置为 \u0026quot;infinity\u0026quot; 当 path-to-gc-roots=false 时，cutoff 自动设置为 \u0026quot;0 ns\u0026quot; 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 示例：jdk.OldObjectSample#cutoff=1 h 表示查找 GC root 路径的最大时间为 1 小时 注意：cutoff 不直接过滤对象年龄，而是限制查找引用链的时间。对象年龄通过事件的 objectAge 字段记录 level - JDK 22 引入 引入：JDK-8211238: @Deprecated JFR event (Fix Version: 22) - https://bugs.openjdk.org/browse/JDK-8211238 作用：控制 jdk.DeprecatedInvocation 事件记录哪些废弃方法调用 默认值：\u0026quot;forRemoval\u0026quot; 适用事件：仅对 jdk.DeprecatedInvocation 事件有效 配置值： \u0026quot;forRemoval\u0026quot;（level 0，默认值）：只记录标记为 @Deprecated(forRemoval=true) 的方法调用，开销较小 \u0026quot;all\u0026quot;（level 1）：记录所有 @Deprecated 方法调用（包括 forRemoval=false），信息更全但开销更大 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 或 JMX 修改，修改会立即生效 示例：jdk.DeprecatedInvocation#level=all 表示记录所有废弃方法调用 注意：GC Phase 相关事件（如 jdk.GCPhasePauseLevel1、jdk.GCPhasePauseLevel2 等）的 level 是通过不同的事件名称区分的，不是通过 level 配置项控制 name：JDK 11 引入，默认值：null（系统生成），作用：Name that can be used to identify recording（记录名称）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定记录的标识名称，用于在多个记录中区分不同的记录。如果不指定名称，系统会自动生成一个名称。记录名称不能为纯数字，以避免与记录 ID 混淆。名称用于通过 jcmd 命令管理记录（如 JFR.dump、JFR.stop 等）。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setName() 或 JMX 修改，只要记录不是 CLOSED 状态即可修改 delay：JDK 11 引入，默认值：0s，作用：Length of time to wait before starting to record（延迟启动时间）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定在 JVM 启动后等待多长时间再开始记录。时间格式：整数后跟 \u0026rsquo;s\u0026rsquo;（秒）、\u0026rsquo;m\u0026rsquo;（分钟）、\u0026lsquo;h\u0026rsquo;（小时）或 \u0026rsquo;d\u0026rsquo;（天）。最小值为 1 秒。延迟启动可以用于跳过应用启动阶段，只记录应用运行稳定后的数据，减少记录文件大小和启动开销。每个记录可以独立设置，互不影响 运行时动态修改：不可以在记录运行时修改。只能在记录创建时（NEW 状态）通过 Recording.scheduleStart() 设置，一旦记录进入 DELAYED 或 RUNNING 状态后无法修改 duration：JDK 11 引入，默认值：0s（无限制），作用：Length of time to record（自动停止时间）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定记录的持续时间，时间格式：整数后跟 \u0026rsquo;s\u0026rsquo;（秒）、\u0026rsquo;m\u0026rsquo;（分钟）、\u0026lsquo;h\u0026rsquo;（小时）或 \u0026rsquo;d\u0026rsquo;（天）。0s 表示无限制，记录会一直运行直到手动停止。最小值为 1 秒。当达到指定时间时，记录会自动停止。如果同时指定了 filename，记录停止后会自动转储到文件。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setDuration() 或 JMX 修改，只要记录不是 STOPPED 或 CLOSED 状态即可修改。修改后会重新计算停止时间并更新定时器 filename：JDK 11 引入，默认值：null（系统生成），作用：Name of the file to which the flight recording data is written when the recording is stopped（dump 时的文件名）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定记录停止时转储的文件路径和名称。如果未指定，系统会根据 PID 和当前日期自动生成文件名。如果指定的是目录，会在该目录中生成文件名。文件名中可以使用 %p（PID）和 %t（时间戳，格式：yyyy_MM_dd_HH_mm_ss）占位符。如果指定了 filename，默认 dumponexit=true。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setDestination() 或 JMX 修改，只要记录不是 STOPPED 或 CLOSED 状态即可修改 相关 JBS： JDK-8323425：修复了自动生成的文件名在时间限制记录中不工作的问题（Fix Version: 23）- https://bugs.openjdk.org/browse/JDK-8323425 maxage：JDK 11 引入，默认值：0s（无限制），作用：Maximum time to keep the recorded data on disk（最大保留时间）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定磁盘上记录数据的最大保留时间，时间格式：整数后跟 \u0026rsquo;s\u0026rsquo;（秒）、\u0026rsquo;m\u0026rsquo;（分钟）、\u0026lsquo;h\u0026rsquo;（小时）或 \u0026rsquo;d\u0026rsquo;（天）。0s 表示无限制。此参数仅在 disk=true 时有效。超过指定时间的旧 chunk 文件会被自动删除，用于控制磁盘空间占用。与 maxsize 配合使用可以同时限制时间和大小。注意：虽然不同记录的 chunk 文件存储在同一个 repository 目录中，但每个记录通过自己的 chunks 列表跟踪属于它的 chunk，每个记录独立应用 maxage 限制，只删除自己不再需要的 chunk（通过引用计数机制管理） 运行时动态修改：可以在记录运行时通过 Recording.setMaxAge() 或 JMX 修改，只要记录不是 CLOSED 状态即可修改。修改后会立即触发 trimToAge() 清理超过保留时间的旧 chunk 相关 JBS： JDK-8203929：为 JFR.dump 命令添加了 maxage 参数，允许限制转储的数据量（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203929 JDK-8294242：修复了 jfr print 命令处理无限持续时间的问题，当 maxAge 为无限时显示更友好（Fix Version: 20）- https://bugs.openjdk.org/browse/JDK-8294242 maxsize：JDK 11 引入，默认值：0（无限制），作用：Maximum size of the data to keep on disk（最大总大小）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：指定磁盘上记录数据的最大总大小，支持 \u0026lsquo;k\u0026rsquo;/\u0026lsquo;K\u0026rsquo;（KB）、\u0026rsquo;m\u0026rsquo;/\u0026lsquo;M\u0026rsquo;（MB）或 \u0026lsquo;g\u0026rsquo;/\u0026lsquo;G\u0026rsquo;（GB）后缀。0 表示无限制。此参数仅在 disk=true 时有效。值不能小于全局配置中的 maxchunksize。当达到指定大小时，最旧的 chunk 文件会被自动删除，用于控制磁盘空间占用。与 maxage 配合使用可以同时限制大小和时间。注意：虽然不同记录的 chunk 文件存储在同一个 repository 目录中，但每个记录通过自己的 chunks 列表跟踪属于它的 chunk，每个记录独立应用 maxsize 限制，只删除自己不再需要的 chunk（通过引用计数机制管理） 运行时动态修改：可以在记录运行时通过 Recording.setMaxSize() 或 JMX 修改，只要记录不是 CLOSED 状态即可修改。修改后会立即触发 trimToSize() 清理超过大小限制的旧 chunk 相关 JBS： JDK-8203929：为 JFR.dump 命令添加了 maxsize 参数，允许限制转储的数据量（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8203929 dumponexit：JDK 11 引入，默认值：false，作用：Flag for writing the recording to disk when the JVM shuts down（JVM 退出时是否 dump）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：控制 JVM 退出时是否将记录转储到文件。为 true 时，JVM 正常退出或异常退出（如 OOM）时会自动转储记录。如果未指定 filename，会在进程启动目录生成系统文件名（格式：id-\u0026lt;recording-id\u0026gt;-\u0026lt;timestamp\u0026gt;.jfr）。如果指定了 filename，默认 dumponexit=true。这对于在异常情况下保存 JFR 数据用于问题诊断非常重要。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setDumpOnExit() 或 JMX 修改，没有状态限制，可以在任何时候修改 相关 JBS： JDK-8198337：修复了使用 -XX:StartFlightRecording=dumponexit=true,disk=false 启动内存记录时，退出时转储的文件大小为 0 的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8198337 JDK-8282947：修复了在某些条件下，关闭时转储（dump on shutdown）导致活锁的问题（Fix Version: 20）- https://bugs.openjdk.org/browse/JDK-8282947 path-to-gc-roots：JDK 11 引入，默认值：false，作用：Flag for saving the path to garbage collection (GC) roots at the end of a recording（是否记录 GC 根）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：控制是否在记录结束时保存到 GC 根的路径信息。为 true 时，会在记录结束时收集 OldObjectSample 事件中对象的 GC 根路径信息，这对于查找内存泄漏非常有用，但收集过程耗时较长。如果 settings 参数设置为 \u0026lsquo;profile\u0026rsquo;，收集的信息还包括潜在泄漏对象的分配堆栈跟踪。建议仅在怀疑存在内存泄漏时启用。每个记录可以独立设置，互不影响 运行时动态修改：可以在记录运行时通过 Recording.setSettings() 修改 jdk.OldObjectSample#path-to-gc-roots 设置或 JMX 修改，修改会立即生效。注意：修改 path-to-gc-roots 会自动同步修改 jdk.OldObjectSample#cutoff 设置（true → infinity，false → 0 ns） report-on-exit：JDK 25 引入，默认值：null，作用：Generate report view(s) when the JVM shuts down（JVM 退出时生成报告视图）： 引入：JDK-8351266: JFR: -XX:StartFlightRecording:report-on-exit (Fix Version: 25) - https://bugs.openjdk.org/browse/JDK-8351266 影响说明：指定在 JVM 退出时生成的报告视图标识符。可以重复指定多个视图，例如 report-on-exit=jvm-information,report-on-exit=system-properties。报告视图用于在退出时自动生成特定格式的分析报告，便于快速查看关键信息。支持的视图包括 jvm-information、system-properties 等。每个记录可以独立设置，互不影响 运行时动态修改：不可以在记录运行时修改。只能在记录创建时通过 -XX:StartFlightRecording 或 jcmd JFR.start 设置 相关 JBS： JDK-8359248：改进了 -XX:StartFlightRecording:report-on-exit 选项的帮助文本，说明该值可以重复（Fix Version: 26）- https://bugs.openjdk.org/browse/JDK-8359248 记录级别但影响全局的配置：每个记录可以独立设置，但实际影响是全局的，多个记录共享底层资源 disk：JDK 11 引入，默认值：true，作用：Flag for also writing the data to disk while recording（是否保存到磁盘）： 引入：JDK-8199712: Flight Recorder (Fix Version: 11) - https://bugs.openjdk.org/browse/JDK-8199712 影响说明：控制记录是否写入磁盘。为 true 时，数据会持续写入磁盘 repository，支持 maxage 和 maxsize 限制；为 false 时，数据仅存储在内存中，适合短期记录或内存受限场景。内存记录在 JVM 退出或手动转储时才会写入文件。重要：虽然每个记录可以独立设置 disk 参数，但实际影响是全局的。如果任何一个运行中的记录设置为 disk=true，系统就会创建全局的 chunk 文件并写入磁盘 repository。所有运行中的记录共享同一个物理 chunk 文件（PlatformRecorder.currentChunk），当一个 chunk 完成时，会分配给所有运行中的记录（通过 finishChunk() 方法）。这意味着即使某个记录设置为 disk=false，如果其他记录设置为 disk=true，该记录也会从共享的 chunk 中获取数据。影响磁盘 I/O 性能和存储空间管理 运行时动态修改：不可以在记录运行时修改。只能在记录创建时（NEW 或 DELAYED 状态）通过 Recording.setToDisk() 设置，一旦记录进入 RUNNING 状态后无法修改 相关 JBS： JDK-8198337：修复了使用 -XX:StartFlightRecording=dumponexit=true,disk=false 启动内存记录时，退出时转储的文件大小为 0 的问题（Fix Version: 11）- https://bugs.openjdk.org/browse/JDK-8198337 JDK-8304844：修复了 ActiveRecording 事件缺少 disk 参数的问题（Fix Version: 22）- https://bugs.openjdk.org/browse/JDK-8304844 flush-interval：JDK 17 引入，默认值：1s，作用：Minimum time before flushing buffers（定时 flush 间隔）： 引入：JDK-8265036: JFR: Add flush-interval option (Fix Version: 17) - https://bugs.openjdk.org/browse/JDK-8265036 影响说明：指定将缓冲区数据刷新到磁盘的最小时间间隔，时间格式：整数后跟 \u0026rsquo;s\u0026rsquo;（秒）、\u0026rsquo;m\u0026rsquo;（分钟）、\u0026lsquo;h\u0026rsquo;（小时）或 \u0026rsquo;d\u0026rsquo;（天）。0 表示仅在记录结束时刷新。此参数仅在 disk=true 时有效。较小的间隔可以减少数据丢失风险，但会增加磁盘 I/O 开销；较大的间隔可以减少 I/O 开销，但可能在异常退出时丢失更多数据。需要平衡数据安全性和性能开销。重要：虽然每个记录可以独立设置 flush-interval 参数，但实际影响是全局的。系统使用所有运行中记录的最小 flush-interval 值作为全局刷新间隔（通过 Math.min() 计算）。这是因为所有记录共享同一个物理 chunk 文件，只能有一个全局的刷新间隔。FlushTask 是全局单例，所有记录共享同一个刷新任务。当启动或停止记录时，系统会重新计算所有运行中记录的最小 flush-interval，并更新全局刷新间隔 运行时动态修改：可以在记录运行时通过 Recording.setFlushInterval() 或 JMX 修改，只要记录不是 CLOSED 状态即可修改。修改后系统会重新计算所有运行中记录的最小 flush-interval，并更新全局刷新间隔 1.4. 事件类型分类与配置适用性 # 1.4.1. 按采集方式分类 # 同步事件（Synchronous Events）\n特点：在业务线程中直接提交，如 jdk.ThreadStart、jdk.ThreadEnd 适用配置：enabled、stackTrace、threshold（如果有持续时间）、throttle、filter（如果是方法相关事件） 异步事件（Asynchronous Events）\n特点：在后台线程中采集，如采样事件 适用配置：enabled、stackTrace、throttle 周期性事件（Periodic Events）\n特点：按固定间隔采集，如 jdk.CPULoad、jdk.ClassLoadingStatistics 适用配置：enabled、period、stackTrace（但是没有意义，因为周期触发堆栈我们不关心）、throttle（也没有意义，周期就能控制频率了） 特殊说明：period 配置只对这类事件有效 请求式事件（Requestable Events）\n特点：按需触发，如 chunk 开始/结束事件 适用配置：enabled、stackTrace 1.4.2. 按事件特性分类 # 持续时间事件（Duration Events）\n特点：有明确的开始和结束时间，如 jdk.ThreadSleep、jdk.GCPhase 适用配置：enabled、threshold、stackTrace、throttle 特殊说明：threshold 配置只对这类事件有效 瞬时事件（Instant Events）\n特点：没有持续时间，如 jdk.ThreadStart、jdk.ClassLoad，另外上面的周期性事件和请求式事件也属于这里的瞬时事件 适用配置：enabled、stackTrace、throttle 特殊说明：不支持 threshold 配置 采样事件（Sampling Events）\n特点：通过采样机制采集，如 jdk.ExecutionSample、jdk.ObjectAllocationSample 适用配置：enabled、stackTrace、throttle 特殊说明：通常使用 throttle 控制采样频率 1.4.3. 按照实现层面分类 # JDK Java 层面事件（JDK Java-level Events）\n特点：事件类定义在 JDK 的 Java 代码中，继承自 AbstractJDKEvent 或 jdk.jfr.Event 实现位置： 事件类定义：src/jdk.jfr/share/classes/jdk/jfr/events/ 包中 事件触发：在 jdk.jfr.internal.JDKEvents 类中实现，或通过 Java 代码直接调用 Event.commit() 典型事件： jdk.ActiveRecording：记录 JFR 记录配置信息 jdk.ActiveSetting：记录事件配置设置 jdk.ThreadStart、jdk.ThreadEnd：线程生命周期事件 jdk.ClassLoad、jdk.ClassUnload：类加载/卸载事件 jdk.FileRead、jdk.FileWrite：文件 I/O 事件（通过 JVMTI 插桩） jdk.SocketRead、jdk.SocketWrite：网络 I/O 事件（通过 JVMTI 插桩） 实现机制： 在 Java 代码的关键位置直接调用 Event.commit() 提交事件 对于 I/O 相关事件，通过 JVMTI 在类加载时或运行时进行字节码插桩，在插桩代码中调用事件提交 事件类通过 JVMTI RetransformClasses 或 Eager Instrumentation 机制进行插桩 优势：实现简单，易于维护和扩展；可以访问 Java 层的丰富信息 适用场景：JDK 库中的事件、应用程序自定义事件 JVM C++ 层面事件（JVM C++-level Events）\n特点：事件定义在 JVM 的 C++ 代码中，在 JVM 内部的关键点直接调用 JFR API 实现位置： 事件元数据定义：src/hotspot/share/jfr/metadata/metadata.xml 事件实现：JVM 的 C++ 代码中（src/hotspot/share/jfr/ 目录下） 子分类： 周期性事件（Periodic Events）： 实现位置：src/hotspot/share/jfr/periodic/jfrPeriodic.cpp 实现方式：通过 TRACE_REQUEST_FUNC 宏定义，在周期性任务中触发 典型事件： jdk.JVMInformation：JVM 信息 jdk.CPULoad：CPU 负载 jdk.ClassLoadingStatistics：类加载统计 jdk.ThreadAllocationStatistics：线程分配统计 jdk.GCConfiguration、jdk.GCHeapConfiguration：GC 配置信息 JVM 内部钩子事件（JVM Internal Hook Events）： 实现方式：在 JVM 的关键执行路径中直接调用 JFR API 典型事件： jdk.GCPhase：GC 阶段事件（在 GC 代码中触发） jdk.GCHeapSummary：GC 堆摘要（在 GC 代码中触发） jdk.ObjectAllocationInNewTLAB、jdk.ObjectAllocationOutsideTLAB：对象分配事件（在对象分配路径中触发） jdk.MonitorWait、jdk.MonitorWaited：锁等待事件（在同步代码中触发） jdk.ThreadSleep、jdk.ThreadPark：线程睡眠/挂起事件（在线程代码中触发） 优势：可以访问 JVM 内部状态，性能开销低（直接调用，无需 JNI 开销） 适用场景：JVM 内部事件、GC 事件、线程事件、对象分配事件等 基于 JVMTI 插桩的事件（JVMTI Instrumentation-based Events）\n特点：通过 JVMTI 在类加载时或运行时进行字节码插桩，在插桩代码中触发事件 实现位置： 插桩逻辑：src/hotspot/share/jfr/instrumentation/jfrEventClassTransformer.cpp JVMTI Agent：src/hotspot/share/jfr/instrumentation/jfrJvmtiAgent.cpp Java 层回调：src/jdk.jfr/share/classes/jdk/jfr/internal/JVMUpcalls.java 实现机制： Eager Instrumentation：在类初始加载时进行插桩（当 retransform=false 时） Retransform Instrumentation：在类加载后通过 JVMTI RetransformClasses 进行插桩（当 retransform=true 时，默认） 插桩内容：在 Event.commit() 方法中插入调用 JFR 缓冲区的代码 典型事件： jdk.FileRead、jdk.FileWrite：文件 I/O 事件（插桩 java.io.FileInputStream、FileOutputStream 等） jdk.SocketRead、jdk.SocketWrite：网络 I/O 事件（插桩 java.net.Socket、ServerSocket 等） jdk.MethodTrace、jdk.MethodTiming：方法追踪/计时事件（JDK 25 引入，通过方法过滤器插桩） 优势：可以监控 JDK 库和应用程序代码的执行，无需修改源代码 适用场景：I/O 事件、方法追踪事件、需要监控应用程序代码的事件 基于采样机制的事件（Sampling-based Events）\n特点：通过独立的采样线程定期采样目标线程的状态，生成事件 实现位置： 采样器实现：src/hotspot/share/jfr/recorder/checkpoint/types/jfrThreadSampler.cpp 采样线程：JfrThreadSampler 线程 实现机制： 采样线程定期唤醒（默认间隔由 samplethreads 配置控制） 遍历所有 Java 线程，获取线程状态和堆栈跟踪 根据事件配置决定是否生成事件 典型事件： jdk.ExecutionSample：方法执行采样事件（采样线程的堆栈跟踪） jdk.NativeMethodSample：本地方法采样事件（采样本地方法调用） 优势：开销低，不需要在业务代码中插桩，对性能影响小 适用场景：需要周期性采样线程状态的事件，如性能分析事件 混合实现事件（Hybrid Implementation Events）\n特点：结合多种实现机制的事件 典型事件： jdk.ObjectAllocationSample： 分配路径触发：在对象分配路径中通过 C++ 代码触发（类似 jdk.ObjectAllocationInNewTLAB） 采样机制：通过采样机制控制采集频率（通过 throttle 配置） jdk.OldObjectSample： GC 触发：在 GC 过程中通过 C++ 代码识别旧对象 引用链查找：在 Java 层查找 GC root 路径（通过 Java 代码实现） 优势：结合不同实现机制的优势，实现复杂的事件采集逻辑 2. JFR 快速开始 # 首先需要准备好如下工具或者环境：\nJava 25 JDK Mission Control（JMC） 8.3+ Jmeter 代码库：https://github.com/spring-projects/spring-petclinic.git maven 或者 gradle git clone 下来代码库后，我们使用 maven 构建：\ncd spring-petclinic mvn clean package 然后，我们使用下面的 JVM 参数启动 spring-petclinic 应用：\n-XX:StartFlightRecording=disk=true -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 这里的配置含义是：\n-XX:StartFlightRecording=disk=true：表示通过 JVM 参数 StartFlightRecording 启动一个 JFR 记录，并且将数据写入磁盘（即前面提到的 repository 目录）。后面我们会看到，可以使用多个 StartFlightRecording 参数启动多个记录。 -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true：表示配置 JFR 的全局选项，这里配置了 chunk 大小为 128MB，repository 目录为当前目录，堆栈深度为 256，preserve-repository=true 表示保留 repository 目录中的 chunk 文件（默认情况下，JFR 会在 JVM 退出时删除 repository 目录中的 chunk 文件）。 启动后，我们可以看到启动目录（我们指定为了当前目录）下有了下面结构的 repository 目录：\n├── 2025_11_19_17_41_00_3833 │ ├── 2025_11_19_17_41_00.jfr 其中文件夹名称为 \u0026lt;yyyy_MM_dd_HH_mm_ss_pid\u0026gt; 格式，表示 JVM 启动时间和进程 ID，里面的 *.jfr 文件就是 JFR 记录的 chunk 文件，文件名为 \u0026lt;yyyy_MM_dd_HH_mm_ss\u0026gt;.jfr 格式，表示 chunk 生成的时间。\n使用 jfr 命令可以查看文件的内容：\n% jfr summary ./2025_11_19_17_41_00_3833/2025_11_19_17_41_00.jfr Version: 2.1 Chunks: 1 Start: 2025-11-17 09:41:00 (UTC) Duration: 165 s Event Type Count Size (bytes) ============================================================= jdk.NativeMethodSample 6992 79112 jdk.GCPhaseParallel 6127 150540 jdk.ModuleExport 1615 19253 jdk.SystemProcess 1032 119761 jdk.NativeLibrary 877 76574 ...... 如果你的进程在运行中，这个命令可能会提示 jfr 文件损坏（在你查看的是 repository 中的最后一个文件的时候），这个因为元数据还没有 flush 进去，不用担心，默认 1s 执行一次 flush，你多试几次就能看到正确的结果了。\n查看某个事件的详情，可以使用 jfr print 命令：\n% jfr print --events jdk.CPULoad ./2025_11_19_17_41_00_3833/2025_11_19_17_41_00.jfr jdk.CPULoad { startTime = 17:41:02.893 (2025-11-19) jvmUser = 7.24% jvmSystem = 0.93% machineTotal = 20.19% } jdk.CPULoad { startTime = 17:41:03.898 (2025-11-19) jvmUser = 0.81% jvmSystem = 0.25% machineTotal = 14.56% } 3. 通过 JVM 参数使用 JFR # 通过 JVM 参数使用 JFR 是最常用的方式，可以在 JVM 启动时直接配置和启动 JFR 记录。JFR 提供了两个主要的 JVM 参数：\n``-XX:+FlightRecorder`：JDK 11 引入，默认值：false，这个配置已过期，并且虽然默认值是 false，但是如果没有设置，JVM 还是将其设置为 true，即默认启用 JFR 功能，只有在显式设置为 false 时才会禁用 JFR 功能 -XX:FlightRecorderOptions：配置 JFR 的全局选项 -XX:StartFlightRecording：启动一个或多个 JFR 记录 3.1. -XX:FlightRecorderOptions（全局配置） # -XX:FlightRecorderOptions 用于配置 JFR 的全局选项，这些选项影响所有 JFR 记录的行为。\n语法：\n-XX:FlightRecorderOptions=\u0026lt;option1\u0026gt;=\u0026lt;value1\u0026gt;,\u0026lt;option2\u0026gt;=\u0026lt;value2\u0026gt;,... 支持的选项（详见第 1.3 节对于配置的详细解析）：\nrepository=\u0026lt;path\u0026gt;：Repository 路径（默认：临时目录） globalbuffersize=\u0026lt;size\u0026gt;：全局缓冲区大小（默认：512k） numglobalbuffers=\u0026lt;count\u0026gt;：全局缓冲区数量（默认：20） memorysize=\u0026lt;size\u0026gt;：JFR 占用总内存限制（默认：10m） threadbuffersize=\u0026lt;size\u0026gt;：线程缓冲区大小（默认：8k） maxchunksize=\u0026lt;size\u0026gt;：最大 Chunk 大小（默认：12m） stackdepth=\u0026lt;depth\u0026gt;：堆栈跟踪深度（默认：64） retransform=\u0026lt;true|false\u0026gt;：是否允许 JVMTI retransform（默认：true） old-object-queue-size=\u0026lt;size\u0026gt;：OldObjectSample 队列大小（默认：256） dumppath=\u0026lt;path\u0026gt;：紧急转储路径（JDK 17+，默认：当前工作目录） preserve-repository=\u0026lt;true|false\u0026gt;：JVM 退出时是否保留 Repository（JDK 21+，默认：false） sampleprotection=\u0026lt;true|false\u0026gt;：采样线程时堆栈遍历的保护措施（仅 DEBUG 模式） 示例：\n# 配置全局选项 -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true # 配置内存相关选项 -XX:FlightRecorderOptions=memorysize=50m,globalbuffersize=1m,numglobalbuffers=50 # 配置缓冲区大小 -XX:FlightRecorderOptions=threadbuffersize=16k,globalbuffersize=1m 注意事项：\n格式要求：选项之间使用逗号（,）分隔，选项名和值之间使用等号（=）分隔 大小单位：内存大小支持 k/K（KB）、m/M（MB）、g/G（GB） 路径格式：路径可以是绝对路径或相对路径（相对于当前工作目录） 运行时修改：部分选项（如 repository、maxchunksize）可以通过 jcmd JFR.configure 命令在运行时修改 3.2. -XX:StartFlightRecording（启动记录） # -XX:StartFlightRecording 用于在 JVM 启动时启动一个或多个 JFR 记录。可以多次指定此参数来启动多个独立的记录。\n语法：\n-XX:StartFlightRecording[=\u0026lt;parameter1\u0026gt;=\u0026lt;value1\u0026gt;,\u0026lt;parameter2\u0026gt;=\u0026lt;value2\u0026gt;,...] 参数格式：\n参数之间使用逗号（,）分隔 参数名和值之间使用等号（=）分隔 如果未指定任何参数，默认使用 dumponexit=false（即不自动转储） 支持的参数：\n3.2.1. 基本参数 # name=\u0026lt;name\u0026gt;（可选）：\n作用：指定记录的标识名称 默认值：系统自动生成（格式：Recording \u0026lt;id\u0026gt;） 限制：名称不能为纯数字，以避免与记录 ID 混淆 示例：name=MyRecording、name=Production-Monitoring settings=\u0026lt;jfc-files\u0026gt;（可选）：\n作用：指定 JFC 配置文件（详见第 5 章） 默认值：default.jfc 格式： 单个文件：settings=default、settings=profile、settings=/path/to/custom.jfc 多个文件（合并）：settings=default,settings=profile（用逗号分隔，可重复指定） 不使用配置：settings=none 示例： settings=default settings=profile settings=default,settings=/path/to/custom.jfc settings=none disk=\u0026lt;true|false\u0026gt;（可选）：\n作用：控制记录是否写入磁盘 默认值：true 说明： true：数据持续写入磁盘 repository，支持 maxage 和 maxsize 限制 false：数据仅存储在内存中，适合短期记录 示例：disk=true、disk=false 3.2.2. 时间相关参数 # delay=\u0026lt;time\u0026gt;（可选）：\n作用：延迟启动时间 默认值：0s（立即启动） 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） 最小值：1s 示例：delay=5m（延迟 5 分钟）、delay=1h（延迟 1 小时） duration=\u0026lt;time\u0026gt;（可选）：\n作用：自动停止时间 默认值：0s（无限制） 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） 最小值：1s 说明：当达到指定时间时，记录会自动停止；如果同时指定了 filename，记录停止后会自动转储 示例：duration=1h（记录 1 小时）、duration=30m（记录 30 分钟） maxage=\u0026lt;time\u0026gt;（可选）：\n作用：最大保留时间（仅在 disk=true 时有效） 默认值：0s（无限制） 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） 说明：超过指定时间的旧 chunk 文件会被自动删除 示例：maxage=2d（保留 2 天）、maxage=24h（保留 24 小时） flush-interval=\u0026lt;time\u0026gt;（可选，JDK 17+）：\n作用：定时 flush 间隔（仅在 disk=true 时有效） 默认值：1s 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） 说明：0 表示仅在记录结束时刷新 示例：flush-interval=5s、flush-interval=0（仅在结束时刷新） 3.2.3. 文件相关参数 # filename=\u0026lt;path\u0026gt;（可选）：\n作用：指定记录停止时转储的文件路径 默认值：系统自动生成（格式：hotspot-pid-\u0026lt;pid\u0026gt;-id-\u0026lt;id\u0026gt;-\u0026lt;timestamp\u0026gt;.jfr） 格式： 文件路径：filename=/path/to/recording.jfr 目录路径：filename=/path/to/directory（会在该目录中生成文件名） 占位符：filename=recording-%p-%t.jfr（%p=PID，%t=时间戳 yyyy_MM_dd_HH_mm_ss） 说明：如果指定了 filename，默认 dumponexit=true 示例： filename=recording.jfr filename=./recordings/ filename=recording-%p-%t.jfr dumponexit=\u0026lt;true|false\u0026gt;（可选）：\n作用：JVM 退出时是否 dump 默认值：false（如果指定了 filename，则为 true） 说明：为 true 时，JVM 正常退出或异常退出时会自动转储记录 示例：dumponexit=true 3.2.4. 其他参数 # maxsize=\u0026lt;size\u0026gt;（可选）：\n作用：最大总大小（仅在 disk=true 时有效） 默认值：0（无限制，但如果 duration、maxage、maxsize 都未指定，默认使用 250MB） 格式：整数后跟单位（k/K=KB、m/M=MB、g/G=GB） 限制：值不能小于全局配置中的 maxchunksize 示例：maxsize=500M、maxsize=1G path-to-gc-roots=\u0026lt;true|false\u0026gt;（可选）：\n作用：是否记录 GC 根 默认值：false 说明：为 true 时，会在记录结束时收集 OldObjectSample 事件中对象的 GC 根路径信息，用于查找内存泄漏 示例：path-to-gc-roots=true report-on-exit=\u0026lt;view-names\u0026gt;（可选，JDK 25+）：\n作用：JVM 退出时生成报告视图 默认值：无（不生成报告） 格式：可以重复指定多个视图，用逗号分隔 说明：仅在 disk=true 时有效 示例：report-on-exit=jvm-information,report-on-exit=system-properties 3.2.5. 事件配置参数 # 除了上述参数，还可以直接在 -XX:StartFlightRecording 中指定事件配置：\nJFC 选项（\u0026lt;option\u0026gt;=\u0026lt;value\u0026gt;）：\n格式：\u0026lt;jfc-option\u0026gt;=\u0026lt;value\u0026gt; 说明：修改 JFC 文件中的选项（如 gc=high、method-profiling=high） 示例：gc=high、method-profiling=high 事件设置（\u0026lt;event-name\u0026gt;#\u0026lt;setting-name\u0026gt;=\u0026lt;value\u0026gt;）：\n格式：\u0026lt;event-name\u0026gt;#\u0026lt;setting-name\u0026gt;=\u0026lt;value\u0026gt; 说明：直接修改事件的配置项 添加新事件：使用 + 前缀，如 +jdk.CustomEvent#enabled=true 示例： jdk.ThreadSleep#threshold=50ms jdk.CPULoad#period=2s +jdk.CustomEvent#enabled=true 时间间隔格式：\n时间值可以省略空格，如 20ms 等同于 20 ms 支持的单位：ns（纳秒）、us（微秒）、ms（毫秒）、s（秒）、m（分钟）、h（小时）、d（天） 3.3. 使用示例 # 3.3.1. 基本使用 # # 最简单的使用方式（使用默认配置） -XX:StartFlightRecording # 使用预定义配置 -XX:StartFlightRecording=settings=default # 使用自定义配置并指定文件名 -XX:StartFlightRecording=settings=profile,filename=recording.jfr # 指定记录名称和文件名 -XX:StartFlightRecording=name=MyRecording,filename=myrecording.jfr 3.3.2. 时间限制记录 # # 记录 1 小时后自动停止并转储 -XX:StartFlightRecording=duration=1h,filename=recording.jfr # 延迟 5 分钟后开始记录，持续 30 分钟 -XX:StartFlightRecording=delay=5m,duration=30m,filename=recording.jfr # 记录 1 小时，但保留最近 2 天的数据 -XX:StartFlightRecording=duration=1h,maxage=2d,maxsize=5GB 3.3.3. 内存记录（不写入磁盘） # # 内存记录，仅在退出时转储 -XX:StartFlightRecording=disk=false,dumponexit=true,filename=recording.jfr # 内存记录，手动转储（通过 jcmd） -XX:StartFlightRecording=disk=false 3.3.4. 事件配置覆盖 # # 使用 default.jfc，但提高锁等待阈值 -XX:StartFlightRecording=settings=default,jdk.ThreadSleep#threshold=50ms # 使用 profile.jfc，但禁用方法采样 -XX:StartFlightRecording=settings=profile,jdk.ExecutionSample#enabled=false # 使用 JFC 选项（可以看 jfc 的 Control 部分，其中的 Selection 可以通过下面的方式修改值），后面第 5 章会详细介绍有哪些选项 -XX:StartFlightRecording=settings=default,gc=high,method-profiling=high # 配置自定义事件 -XX:StartFlightRecording=settings=none,+jdk.CustomEvent#enabled=true,+jdk.CustomEvent#stackTrace=true 3.3.5. 多个记录（JDK 11+） # 重要：可以多次指定 -XX:StartFlightRecording 来启动多个独立的记录，每个记录有独立的配置和生命周期。\n# 启动两个记录：一个用于长期监控，一个用于短期分析 -XX:StartFlightRecording=name=LongTerm,maxage=2d,maxsize=5GB -XX:StartFlightRecording=name=ShortTerm,maxage=10m,dumponexit=true,filename=shortterm.jfr # 启动三个记录，使用不同的配置 -XX:StartFlightRecording=name=Recording1,filename=recording1.jfr -XX:StartFlightRecording=name=Recording2,filename=recording2.jfr,settings=profile -XX:StartFlightRecording=name=Recording3,disk=false # 混合使用冒号和等号（都支持） -XX:StartFlightRecording:name=myrecording1,filename=myrecording1.jfr -XX:StartFlightRecording=name=myrecording2,filename=myrecording2.jfr 多个记录的使用场景：长期监控 + 短期分析：\n一个记录用于长期监控（maxage=2d, maxsize=5GB），保留历史数据 另一个记录用于短期分析（maxage=10m, dumponexit=true），在退出时转储 3.3.6. 完整示例 # # 生产环境配置示例 java \\ -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true \\ -XX:StartFlightRecording=name=Production,settings=default,maxage=3d,maxsize=10GB \\ -XX:StartFlightRecording=name=Debug,settings=profile,duration=1h,filename=debug.jfr \\ -jar MyApp.jar # 性能分析配置示例 java \\ -XX:FlightRecorderOptions=stackdepth=128 \\ -XX:StartFlightRecording=settings=profile,gc=high,method-profiling=high,duration=30m,filename=profile.jfr \\ -jar MyApp.jar # 内存泄漏分析配置示例 java \\ -XX:StartFlightRecording=settings=default,path-to-gc-roots=true,duration=1h,filename=leak-analysis.jfr \\ -jar MyApp.jar 3.4. 查看帮助信息 # 可以通过以下方式查看 -XX:StartFlightRecording 的详细帮助信息：\n# 查看帮助信息（会打印所有可用参数和示例） java -XX:StartFlightRecording:help 4. 通过 jcmd 命令使用 JFR # jcmd 是 JDK 提供的诊断命令工具，可以在运行时通过命令行管理 JVM 进程。JFR 提供了多个 jcmd 子命令来管理 JFR 记录和配置。\n基本语法：\njcmd \u0026lt;pid\u0026gt; \u0026lt;command\u0026gt; [options] 其中：\n\u0026lt;pid\u0026gt;：目标 JVM 进程的进程 ID \u0026lt;command\u0026gt;：JFR 命令（如 JFR.start、JFR.stop 等） [options]：命令参数（使用空格分隔，格式为 key=value） 查看所有可用的 JFR 命令：\n# 查看所有 jcmd 命令 jcmd \u0026lt;pid\u0026gt; help # 查看 JFR 相关命令的帮助 jcmd \u0026lt;pid\u0026gt; help JFR.start jcmd \u0026lt;pid\u0026gt; help JFR.stop jcmd \u0026lt;pid\u0026gt; help JFR.dump jcmd \u0026lt;pid\u0026gt; help JFR.check jcmd \u0026lt;pid\u0026gt; help JFR.configure jcmd \u0026lt;pid\u0026gt; help JFR.view 4.1. JFR.start（启动记录） # JFR.start 命令用于在运行时启动一个新的 JFR 记录。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.start [options] 重要：与 -XX:StartFlightRecording 不同，jcmd JFR.start 的参数之间使用空格分隔，而不是逗号。\n支持的参数（与 -XX:StartFlightRecording 相同，区别只有分割符不一样，详见第 3.2 节）：\nname=\u0026lt;name\u0026gt;：记录名称 settings=\u0026lt;jfc-files\u0026gt;：JFC 配置文件（多个文件用逗号分隔，可重复指定） disk=\u0026lt;true|false\u0026gt;：是否写入磁盘（默认：true） delay=\u0026lt;time\u0026gt;：延迟启动时间 duration=\u0026lt;time\u0026gt;：自动停止时间 filename=\u0026lt;path\u0026gt;：转储文件名 maxage=\u0026lt;time\u0026gt;：最大保留时间（仅在 disk=true 时有效） maxsize=\u0026lt;size\u0026gt;：最大总大小（仅在 disk=true 时有效） flush-interval=\u0026lt;time\u0026gt;：定时 flush 间隔（JDK 17+，仅在 disk=true 时有效） dumponexit=\u0026lt;true|false\u0026gt;：JVM 退出时是否 dump path-to-gc-roots=\u0026lt;true|false\u0026gt;：是否记录 GC 根 report-on-exit=\u0026lt;view-names\u0026gt;：JVM 退出时生成报告视图（JDK 25+，仅在 disk=true 时有效） JFC 选项：\u0026lt;option\u0026gt;=\u0026lt;value\u0026gt;（如 gc=high、method-profiling=high） 事件设置：\u0026lt;event-name\u0026gt;#\u0026lt;setting-name\u0026gt;=\u0026lt;value\u0026gt;（如 jdk.ThreadSleep#threshold=50ms） 使用示例：\n# 使用默认配置启动记录 jcmd \u0026lt;pid\u0026gt; JFR.start # 使用预定义配置启动记录 jcmd \u0026lt;pid\u0026gt; JFR.start settings=default # 指定记录名称和文件名 jcmd \u0026lt;pid\u0026gt; JFR.start name=MyRecording filename=recording.jfr # 启动时间限制记录 jcmd \u0026lt;pid\u0026gt; JFR.start duration=1h filename=recording.jfr # 启动内存记录（不写入磁盘） jcmd \u0026lt;pid\u0026gt; JFR.start disk=false # 使用 profile.jfc 并覆盖事件配置 jcmd \u0026lt;pid\u0026gt; JFR.start settings=profile jdk.ThreadSleep#threshold=50ms # 使用 JFC 选项 jcmd \u0026lt;pid\u0026gt; JFR.start settings=default gc=high method-profiling=high # 延迟启动并指定持续时间 jcmd \u0026lt;pid\u0026gt; JFR.start delay=5m duration=30m filename=recording.jfr # 启动记录并配置保留策略 jcmd \u0026lt;pid\u0026gt; JFR.start maxage=2d maxsize=5GB # 启动记录并启用 GC 根路径收集 jcmd \u0026lt;pid\u0026gt; JFR.start path-to-gc-roots=true duration=1h filename=leak-analysis.jfr 注意事项：\n参数分隔符：参数之间使用空格分隔（不同于 JVM 参数使用逗号） 参数格式：参数名和值之间使用等号（=）分隔 多次启动：可以多次执行 JFR.start 启动多个独立的记录 记录 ID：启动成功后会显示记录 ID，用于后续管理 4.2. JFR.stop（停止记录） # JFR.stop 命令用于停止一个正在运行的 JFR 记录。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.stop name=\u0026lt;name\u0026gt; [filename=\u0026lt;path\u0026gt;] 参数：\nname=\u0026lt;name\u0026gt;（必需）：记录的标识名称或 ID filename=\u0026lt;path\u0026gt;（可选）：停止时转储的文件路径 如果指定了 filename，会覆盖启动时设置的转储路径，记录停止后转储到新指定的文件 如果未指定 filename： 如果启动时指定了 filename（文件路径或目录路径），记录停止后会自动转储到启动时指定的位置 如果启动时没有指定 filename，记录数据会被丢弃 注意：filename 必须指定完整的文件路径，不支持目录路径（与 JFR.start 和 JFR.dump 不同） 支持占位符：%p（PID）、%t（时间戳 yyyy_MM_dd_HH_mm_ss） 使用示例：\n# 停止记录（如果启动时没有指定转储路径则不转储，数据丢弃） jcmd \u0026lt;pid\u0026gt; JFR.stop name=1 # 停止记录（如果启动时指定了 filename，会自动转储到启动时指定的位置） jcmd \u0026lt;pid\u0026gt; JFR.stop name=MyRecording # 停止记录并转储到新文件（覆盖启动时设置的转储路径） jcmd \u0026lt;pid\u0026gt; JFR.stop name=MyRecording filename=recording.jfr # 使用占位符生成文件名 jcmd \u0026lt;pid\u0026gt; JFR.stop name=1 filename=recording-%p-%t.jfr 注意事项：\n记录标识：可以使用记录名称或记录 ID（数字）来标识记录 数据保存行为： 如果 JFR.stop 时指定了 filename，会覆盖启动时设置的转储路径，转储到新指定的文件 如果 JFR.stop 时未指定 filename，但启动时指定了 filename，会自动转储到启动时指定的位置（文件路径或目录路径） 如果启动时和停止时都没有指定 filename，记录数据会被丢弃，无法恢复 文件路径限制：filename 参数必须指定完整的文件路径，不能是目录。如果需要指定目录，应使用 JFR.dump 命令或 JFR.start 命令的 filename 参数（它们支持目录路径） 4.3. JFR.dump（转储记录） # JFR.dump 命令用于将正在运行的记录转储到文件，记录会继续运行。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.dump [options] 参数：\nname=\u0026lt;name\u0026gt;（可选）：记录的标识名称或 ID 如果指定，只转储指定的记录 如果未指定，转储所有正在运行的记录 filename=\u0026lt;path\u0026gt;（可选）：转储文件名 如果未指定，系统自动生成文件名 可以指定文件路径或目录路径（如果指定目录，会在该目录中自动生成文件名） 支持占位符：%p（PID）、%t（时间戳） maxage=\u0026lt;time\u0026gt;（可选）：转储的时间范围（从当前时间往前推） 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） 示例：maxage=1h（转储最近 1 小时的数据） 注意：不能与 begin 或 end 同时使用 maxsize=\u0026lt;size\u0026gt;（可选）：转储的大小限制 格式：整数后跟单位（k/K=KB、m/M=MB、g/G=GB） 示例：maxsize=500M begin=\u0026lt;time\u0026gt;（可选）：开始时间 格式支持： ISO 8601 格式：2020-03-17T09:00:00、2020-03-17T09:00:00Z 本地时间：13:20:15、2020-03-17T09:00:00 相对时间：-12h（12 小时前）、-15m（15 分钟前）、-30s（30 秒前） 注意：不能与 maxage 同时使用 end=\u0026lt;time\u0026gt;（可选）：结束时间 格式与 begin 相同 注意：end 必须晚于 begin path-to-gc-roots=\u0026lt;true|false\u0026gt;（可选）：是否收集 GC 根路径 默认值：false 仅在需要内存泄漏分析时启用 使用示例：\n# 转储所有记录（使用自动生成的文件名） jcmd \u0026lt;pid\u0026gt; JFR.dump # 转储指定记录到文件 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 filename=recording.jfr # 转储最近 1 小时的数据 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 maxage=1h filename=recording.jfr # 转储最近 1 小时且最多 500MB 的数据 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 maxage=1h maxsize=500M filename=recording.jfr # 转储指定时间范围的数据 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 begin=13:15 end=21:30:00 filename=recording.jfr # 转储指定日期时间范围的数据 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 begin=2021-09-15T09:00:00 end=2021-09-15T10:00:00 filename=recording.jfr # 转储相对时间范围的数据 jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 begin=-1h end=-5m filename=recording.jfr # 转储并收集 GC 根路径（用于内存泄漏分析） jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 filename=leaks.jfr path-to-gc-roots=true # 转储到目录（自动生成文件名） jcmd \u0026lt;pid\u0026gt; JFR.dump name=1 filename=./recordings/ 时间格式说明：\nISO 8601 格式：\n2020-03-17T09:00:00（本地时间） 2020-03-17T09:00:00Z（UTC 时间） 本地时间格式：\n13:20:15（今天的时间） 2020-03-17T09:00:00（指定日期时间） 相对时间格式：\n-12h：12 小时前 -15m：15 分钟前 -30s：30 秒前 -1d：1 天前 注意事项：\n记录继续运行：JFR.dump 不会停止记录，记录会继续运行 时间范围：maxage 与 begin/end 不能同时使用 数据过滤：转储的数据会根据 maxage、maxsize、begin、end 进行过滤 GC 根路径：收集 GC 根路径会导致应用短暂暂停，仅在需要时启用 4.4. JFR.check（检查记录） # JFR.check 命令用于查看正在运行的 JFR 记录信息。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.check [name=\u0026lt;name\u0026gt;] [verbose=\u0026lt;true|false\u0026gt;] 参数：\nname=\u0026lt;name\u0026gt;（可选）：记录的标识名称或 ID 如果指定，只显示指定记录的信息 如果未指定，显示所有正在运行的记录信息 verbose=\u0026lt;true|false\u0026gt;（可选）：是否显示详细的事件配置 默认值：false 为 true 时，会显示每个事件的详细配置（enabled、threshold、period 等） 使用示例：\n# 查看所有记录的基本信息 jcmd \u0026lt;pid\u0026gt; JFR.check # 查看所有记录的详细信息（包括事件配置） jcmd \u0026lt;pid\u0026gt; JFR.check verbose=true # 查看指定记录的基本信息 jcmd \u0026lt;pid\u0026gt; JFR.check name=1 # 查看指定记录的详细信息 jcmd \u0026lt;pid\u0026gt; JFR.check name=MyRecording verbose=true 输出示例：\n# 基本输出 $ jcmd \u0026lt;pid\u0026gt; JFR.check Recording 1: name=MyRecording duration=1h maxsize=500M maxage=2d (running) # 详细输出（verbose=true） $ jcmd \u0026lt;pid\u0026gt; JFR.check name=1 verbose=true Recording 1: name=MyRecording duration=1h maxsize=500M maxage=2d (running) Thread Sleep (jdk.ThreadSleep) [enabled=true, threshold=20 ms, stackTrace=true] CPU Load (jdk.CPULoad) [enabled=true, period=1 s] Java Monitor Enter (jdk.JavaMonitorEnter) [enabled=true, threshold=20 ms, stackTrace=true] 注意事项：\n记录状态：输出会显示记录的状态（running、stopped 等） 记录信息：包括记录 ID、名称、持续时间、最大大小、最大保留时间等 事件配置：verbose=true 时会显示所有已配置事件的详细设置 4.5. JFR.configure（配置全局选项） # JFR.configure 命令用于在运行时配置 JFR 的全局选项。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.configure [options] 支持的选项（与 -XX:FlightRecorderOptions 相同，详见第 1.3 节）：\nrepositorypath=\u0026lt;path\u0026gt;：Repository 路径 dumppath=\u0026lt;path\u0026gt;：紧急转储路径（JDK 17+） stackdepth=\u0026lt;depth\u0026gt;：堆栈跟踪深度（仅在 JFR 初始化前可修改） globalbuffercount=\u0026lt;count\u0026gt;：全局缓冲区数量（仅在 JFR 初始化前可修改，遗留选项） globalbuffersize=\u0026lt;size\u0026gt;：全局缓冲区大小（仅在 JFR 初始化前可修改，遗留选项） thread_buffer_size=\u0026lt;size\u0026gt;：线程缓冲区大小（仅在 JFR 初始化前可修改） memorysize=\u0026lt;size\u0026gt;：JFR 占用总内存限制（仅在 JFR 初始化前可修改） maxchunksize=\u0026lt;size\u0026gt;：最大 Chunk 大小（仅在 JFR 初始化前可修改） preserve-repository=\u0026lt;true|false\u0026gt;：JVM 退出时是否保留 Repository（JDK 21+） 使用示例：\n# 查看当前配置 jcmd \u0026lt;pid\u0026gt; JFR.configure # 修改 Repository 路径 jcmd \u0026lt;pid\u0026gt; JFR.configure repositorypath=/tmp/jfr # 注意：旧的 repository 数据会保留在原位置，不会被迁移 # 如果旧的 repository 目录已经在 cleanupDirectories 中，JFR 会继续追踪它，并在 JVM 关闭时自动删除（如果 preserve-repository=false） # 如果有正在运行的磁盘记录，当前 chunk 会被标记为 final 并轮转，新的 chunk 会写入新的 repository 路径 # 修改紧急转储路径 jcmd \u0026lt;pid\u0026gt; JFR.configure dumppath=/tmp/emergency # 修改堆栈深度（仅在 JFR 初始化前有效） jcmd \u0026lt;pid\u0026gt; JFR.configure stackdepth=128 # 修改内存大小（仅在 JFR 初始化前有效） jcmd \u0026lt;pid\u0026gt; JFR.configure memorysize=50M # 修改最大 Chunk 大小（仅在 JFR 初始化前有效） jcmd \u0026lt;pid\u0026gt; JFR.configure maxchunksize=128M # 启用保留 Repository jcmd \u0026lt;pid\u0026gt; JFR.configure preserve-repository=true # 同时修改多个选项 jcmd \u0026lt;pid\u0026gt; JFR.configure repositorypath=/tmp/jfr dumppath=/tmp/emergency preserve-repository=true 注意事项：\n初始化限制：部分选项（如 stackdepth、memorysize、maxchunksize）只能在 JFR 初始化前修改，如果 JFR 已经初始化，这些选项无法修改 运行时修改：repositorypath、dumppath、preserve-repository 可以在运行时修改 遗留选项：globalbuffercount 和 globalbuffersize 是遗留选项，建议使用 memorysize 统一调整 查看配置：不指定任何参数时，会显示当前配置 Repository 路径修改的影响： 当修改 repositorypath 时，旧的 repository 目录和其中的 chunk 文件会保留在原位置，不会被迁移。如果旧的 repository 目录已经在 cleanupDirectories 中（即之前通过 newChunk() 创建过），JFR 会继续追踪它，并在 JVM 关闭时自动删除（如果 preserve-repository=false） 如果有正在运行的磁盘记录，当前 chunk 会被标记为 final 并轮转，新的 chunk 会写入新的 repository 路径 新的 repository 目录会在新路径下创建（格式：\u0026lt;base-path\u0026gt;/\u0026lt;timestamp\u0026gt;_\u0026lt;pid\u0026gt; 或 \u0026lt;base-path\u0026gt;/\u0026lt;timestamp\u0026gt;_\u0026lt;pid\u0026gt;_\u0026lt;index\u0026gt;） 旧的 repository 目录的清理行为： JFR 内部使用 cleanupDirectories（Set\u0026lt;Path\u0026gt;）来跟踪需要在 JVM 关闭时清理的 repository 目录 只有当通过 Repository.newChunk() 创建新的 repository 目录时，该目录才会被添加到 cleanupDirectories 如果旧的 repository 目录在成为\u0026quot;旧的\u0026quot;之前，已经通过 newChunk() 被添加到 cleanupDirectories，那么它仍然在 cleanupDirectories 中，会在 JVM 关闭时被清理（如果 preserve-repository=false） 如果旧的 repository 目录从来没有被添加到 cleanupDirectories（例如：在 JFR 初始化之前就设置了 repository 路径，但从来没有创建过 chunk），那么它不会在 JVM 关闭时被自动清理 如果修改路径后有正在运行的磁盘记录，migrate() 会调用 rotateDisk() → newChunk()，此时新的 repository 目录会被创建并添加到 cleanupDirectories 在 JVM 关闭时，如果 preserve-repository=false，cleanupDirectories 中的所有目录都会被清理（Repository.clear() 方法，第173-180行） 注意：cleanupDirectories 是 JFR 内部自动管理的机制，用户无法直接添加目录到该集合 4.6. JFR.view（查看记录） # JFR.view 命令用于以预定义的视图格式显示 JFR 记录数据，无需转储文件。\n语法：\njcmd \u0026lt;pid\u0026gt; JFR.view \u0026lt;view\u0026gt; [options] 参数：\n\u0026lt;view\u0026gt;（必需）：视图名称或事件类型名称 预定义视图：gc、hot-methods、allocation-by-class、contention-by-site 等 注意：视图在 view.ini 中定义时使用完整名称（如 [jvm.gc]、[application.hot-methods]），但在 JFR.view 命令中只需要使用视图名称部分，不需要加前缀。例如： 使用 jcmd \u0026lt;pid\u0026gt; JFR.view gc 而不是 jcmd \u0026lt;pid\u0026gt; JFR.view jvm.gc 使用 jcmd \u0026lt;pid\u0026gt; JFR.view hot-methods 而不是 jcmd \u0026lt;pid\u0026gt; JFR.view application.hot-methods 视图名称匹配是大小写不敏感的（equalsIgnoreCase） 事件类型：jdk.GarbageCollection、jdk.ThreadStart 等 可以直接使用事件类型名称查看该事件的所有数据 特殊值： types：列出所有可用的事件类型 all-views：显示所有预定义视图 all-events：显示所有事件 maxage=\u0026lt;time\u0026gt;（可选）：查看的时间范围（默认：10m） 格式：整数后跟单位（s=秒、m=分钟、h=小时、d=天） maxsize=\u0026lt;size\u0026gt;（可选）：查看的大小限制（默认：32MB） 格式：整数后跟单位（m/M=MB、g/G=GB） width=\u0026lt;integer\u0026gt;（可选）：视图宽度（字符数，默认：100） truncate=\u0026lt;beginning|end\u0026gt;（可选）：截断模式（默认：end） beginning：从开头截断 end：从结尾截断 cell-height=\u0026lt;integer\u0026gt;（可选）：表格单元格的最大行数 verbose=\u0026lt;true|false\u0026gt;（可选）：是否显示视图的查询信息（默认：false） 使用示例：\n# 查看 GC 视图 jcmd \u0026lt;pid\u0026gt; JFR.view gc # 查看热点方法视图 jcmd \u0026lt;pid\u0026gt; JFR.view hot-methods # 查看分配视图（按类） jcmd \u0026lt;pid\u0026gt; JFR.view allocation-by-class # 查看锁竞争视图（按位置） jcmd \u0026lt;pid\u0026gt; JFR.view contention-by-site # 查看指定事件类型 jcmd \u0026lt;pid\u0026gt; JFR.view jdk.GarbageCollection # 查看所有可用视图 jcmd \u0026lt;pid\u0026gt; JFR.view all-views # 查看所有事件类型 jcmd \u0026lt;pid\u0026gt; JFR.view types # 自定义视图参数 jcmd \u0026lt;pid\u0026gt; JFR.view width=160 hot-methods # 查看最近 1 小时的数据 jcmd \u0026lt;pid\u0026gt; JFR.view maxage=1h gc # 显示视图的查询信息 jcmd \u0026lt;pid\u0026gt; JFR.view verbose=true allocation-by-class # 设置截断模式 jcmd \u0026lt;pid\u0026gt; JFR.view truncate=beginning SystemProcess 预定义视图详解：\nJFR 提供了丰富的预定义视图，这些视图定义在 view.ini 配置文件中，使用自定义的查询语言来聚合和展示事件数据。视图分为三类：JVM 视图（jvm.*）、应用视图（application.*）和环境视图（environment.*）。\nview.ini 文件位置：\n源码位置：src/jdk.jfr/share/classes/jdk/jfr/internal/query/view.ini 编译后位置：view.ini 文件会被打包到 jdk.jfr 模块的 JAR 文件中（通常在 lib/jfr.jar 或 jmods/jdk.jfr.jmod 中），作为资源文件存在 运行时访问：通过 ViewFile.class.getResourceAsStream(\u0026quot;/jdk/jfr/internal/query/view.ini\u0026quot;) 在运行时读取 文件格式：INI 格式配置文件，包含视图定义和查询语句 用户访问：用户可以通过解压 jdk.jfr 模块的 JAR 文件来查看 view.ini 文件内容，但不建议直接修改 视图结构：\n每个视图定义包含以下部分：\n视图名称：格式为 \u0026lt;category\u0026gt;.\u0026lt;name\u0026gt;，例如 jvm.gc、application.hot-methods 标签（label）：视图的显示名称 查询类型： table：表格视图，用于显示多行数据，支持排序、分组、聚合 form：表单视图，用于显示单行汇总数据，通常使用 LAST() 函数获取最新值 查询语言语法：\n视图使用类似 SQL 的查询语言，支持以下语法：\nCOLUMN：定义列标题 FORMAT：定义列格式（none、normalized、cell-height、truncate-beginning/end、missing:whitespace 等） SELECT：选择字段和聚合函数 FROM：指定事件类型（支持多个事件类型的并集） WHERE：过滤条件 GROUP BY：分组聚合 ORDER BY：排序（ASC/DESC） LIMIT：限制结果数量 支持的聚合函数：\nCOUNT(*)：计数 SUM(field)：求和 AVG(field)：平均值 MIN(field)：最小值 MAX(field)：最大值 MEDIAN(field)：中位数 P90/P95/P99/P999(field)：百分位数 FIRST(field)：第一个值 LAST(field)：最后一个值 LAST_BATCH(field)：最后一批值（相同结束时间戳） DIFF(field)：差值（最后一个值减去第一个值） STDEV(field)：标准差 LIST(field)：所有值的逗号分隔列表 SET(field)：所有唯一值的逗号分隔列表 UNIQUE(field)：唯一值数量 堆栈跟踪访问：\n查询语言支持访问堆栈跟踪的特定帧：\nstackTrace.topFrame：堆栈顶部帧（最外层方法） stackTrace.topApplicationFrame：堆栈顶部应用帧（排除 JDK 内部方法） stackTrace.topNotInitFrame：堆栈顶部非初始化帧（排除 \u0026lt;init\u0026gt; 方法） 常用预定义视图分类：\nJVM 视图（jvm.*）：\ngc：垃圾回收统计 事件：GarbageCollection、GCHeapSummary 设计：表格视图，显示每次 GC 的开始时间、GC ID、GC 名称、GC 前后堆使用情况、最长暂停时间 聚合：按 gcId 分组，关联 GC 前后的堆摘要数据 gc-pauses：GC 暂停统计 事件：GCPhasePause 设计：表单视图，显示总暂停时间、暂停次数、最小/中位数/平均/P90/P95/P99/P99.9/最大暂停时间 聚合：使用 SUM、COUNT、MIN、MEDIAN、AVG、P90/P95/P99/P999、MAX 函数 gc-configuration：GC 配置信息 事件：GCConfiguration 设计：表单视图，显示年轻代/老年代收集器、GC 线程数、显式 GC 设置等 聚合：使用 LAST() 获取最新配置值 gc-parallel-phases：并行 GC 阶段 事件：GCPhaseParallel 设计：表格视图，按阶段名称分组，显示平均、P95、最长、计数、总时间 gc-concurrent-phases：并发 GC 阶段 事件：GCPhaseConcurrent、GCPhaseConcurrentLevel1 设计：表格视图，按阶段名称分组，显示平均、P95、最长、计数、总时间 gc-pause-phases：GC 暂停阶段 事件：GCPhasePause、GCPhasePauseLevel1-4 设计：表格视图，按阶段名称分组，显示事件类型、平均、P95、最长、计数、总时间 gc-references：GC 引用统计 事件：GCReferenceStatistics 设计：表格视图，按 GC ID 分组，显示软引用、弱引用、虚引用、终结引用的数量 gc-allocation-trigger：GC 分配触发 事件：AllocationRequiringGC 设计：表格视图，按应用方法分组，显示触发次数和总请求大小 堆栈：使用 stackTrace.topApplicationFrame 获取应用层方法 gc-cpu-time：GC CPU 时间 事件：GCCPUTime 设计：表单视图，显示 GC 用户时间、系统时间、挂钟时间、总时间、GC 次数 heap-configuration：堆配置 事件：GCHeapConfiguration 设计：表单视图，显示初始大小、最小大小、最大大小、压缩 OOPs 设置 compiler-configuration：编译器配置 事件：CompilerConfiguration 设计：表单视图，显示编译器线程数、动态线程数、分层编译设置 compiler-statistics：编译器统计 事件：CompilerStatistics 设计：表单视图，显示编译次数、峰值时间、总时间、回退次数、OSR/标准编译统计等 compiler-phases：编译器阶段 事件：CompilerPhase 设计：表格视图，按编译级别和阶段分组，显示平均、P95、最长、计数、总时间 longest-compilations：最长编译 事件：Compilation 设计：表格视图，显示编译时间最长的 25 个方法 safepoints：安全点 事件：SafepointBegin、SafepointEnd、SafepointStateSynchronization 设计：表格视图，按安全点 ID 分组，显示开始时间、持续时间、状态同步时间、JNI 关键线程数、总线程数 vm-operations：VM 操作 事件：jdk.ExecuteVMOperation 设计：表格视图，按操作类型分组，显示平均持续时间、最长持续时间、次数、总持续时间 deoptimizations-by-reason：按原因的反优化 事件：Deoptimization 设计：表格视图，按反优化原因分组，显示次数 deoptimizations-by-site：按位置的反优化 事件：Deoptimization 设计：表格视图，按方法、行号、BCI 分组，显示次数 class-modifications：类修改 事件：RetransformClasses、RedefineClasses 设计：表格视图，按重定义 ID 分组，显示持续时间、请求者（应用方法）、操作类型、类数量 堆栈：使用 stackTrace.topApplicationFrame 获取请求者 blocked-by-system-gc：被 System.gc() 阻塞 事件：SystemGC 设计：表格视图，过滤非并发调用，显示开始时间、持续时间、堆栈跟踪 native-memory-committed：已提交的本地内存 事件：NativeMemoryUsage 设计：表格视图，按内存类型分组，显示首次观察值、平均值、最后观察值、最大值 native-memory-reserved：保留的本地内存 事件：NativeMemoryUsage 设计：表格视图，按内存类型分组，显示首次观察值、平均值、最后观察值、最大值 tlabs：线程本地分配缓冲区 事件：ObjectAllocationInNewTLAB、ObjectAllocationOutsideTLAB 设计：表单视图，显示 TLAB 内外的分配统计（计数、最小/平均/最大大小、总分配） 应用视图（application.*）：\nhot-methods：热点方法 事件：ExecutionSample 设计：表格视图，按堆栈顶部帧分组，显示方法、样本数、百分比，限制前 25 个 聚合：使用 COUNT(*) 计数，normalized 格式显示百分比 堆栈：使用 stackTrace.topFrame 获取最外层方法 cpu-time-hot-methods：CPU 时间采样热点方法 事件：CPUTimeSample 设计：表格视图，按堆栈顶部帧分组，显示方法、样本数、百分比，限制前 25 个 聚合：使用 COUNT(*) 计数，normalized 格式显示百分比 cpu-time-statistics：CPU 时间采样统计 事件：CPUTimeSample、CPUTimeSamplesLost 设计：表单视图，显示成功样本、失败样本、有偏样本、总样本、丢失样本数 过滤：使用 WHERE 条件过滤不同状态的样本 allocation-by-class：按类分配统计 事件：ObjectAllocationSample 设计：表格视图，按对象类分组，显示对象类型、分配压力，限制前 25 个 聚合：使用 SUM(weight) 计算分配压力，normalized 格式显示 allocation-by-thread：按线程分配统计 事件：ObjectAllocationSample 设计：表格视图，按事件线程分组，显示线程、分配压力，限制前 25 个 聚合：使用 SUM(weight) 计算分配压力 allocation-by-site：按位置分配统计 事件：ObjectAllocationSample 设计：表格视图，按堆栈顶部帧分组，显示方法、分配压力，限制前 25 个 堆栈：使用 stackTrace.topFrame 获取分配位置 contention-by-thread：按线程锁竞争统计 事件：JavaMonitorEnter 设计：表格视图，按事件线程分组，显示线程、次数、平均、P90、最大持续时间 contention-by-class：按锁类竞争统计 事件：JavaMonitorEnter 设计：表格视图，按监视器类分组，显示锁类、次数、平均、P90、最大持续时间 contention-by-site：按位置锁竞争统计 事件：JavaMonitorEnter 设计：表格视图，按堆栈跟踪分组，显示堆栈跟踪、次数、平均、最大持续时间 contention-by-address：按监视器地址竞争统计 事件：JavaMonitorEnter 设计：表格视图，按监视器类分组，显示地址、类、线程数、最大持续时间 聚合：使用 FIRST() 获取第一个类名，UNIQUE() 获取唯一线程数 memory-leaks-by-class：按类内存泄漏候选 事件：OldObjectSample 设计：表格视图，按对象类型分组，显示分配时间、对象类、对象年龄、堆使用情况 聚合：使用 LAST_BATCH() 获取最后一批值 注意：使用 memory-leaks 视图时会自动触发 OldObjectSample 事件收集 memory-leaks-by-site：按位置内存泄漏候选 事件：OldObjectSample 设计：表格视图，按应用方法分组，显示分配时间、应用方法、对象年龄、堆使用情况 堆栈：使用 stackTrace.topApplicationFrame 获取应用层分配位置 exception-by-type：按类型异常统计 事件：JavaErrorThrow、JavaExceptionThrow 设计：表格视图，按抛出类分组，显示类、次数 exception-by-message：按消息异常统计 事件：JavaErrorThrow、JavaExceptionThrow 设计：表格视图，按消息分组，显示消息、次数 exception-by-site：按位置异常统计 事件：JavaErrorThrow、JavaExceptionThrow 设计：表格视图，按堆栈顶部非初始化帧分组，显示方法、次数 堆栈：使用 stackTrace.topNotInitFrame 排除 \u0026lt;init\u0026gt; 方法 exception-count：异常统计 事件：ExceptionStatistics 设计：表单视图，显示抛出的异常总数（使用 DIFF(throwables) 计算差值） thread-allocation：线程分配统计 事件：ThreadAllocationStatistics 设计：表格视图，按线程分组，显示线程、已分配、百分比 聚合：使用 LAST() 获取最新值，normalized 格式显示百分比 thread-cpu-load：线程 CPU 负载 事件：ThreadCPULoad 设计：表格视图，按事件线程分组，显示线程、系统 CPU、用户 CPU thread-start：平台线程启动 事件：ThreadStart、ThreadEnd 设计：表格视图，按事件线程分组，显示开始时间、堆栈跟踪、线程、持续时间 聚合：使用 DIFF(startTime) 计算线程生命周期 thread-count：Java 线程统计 事件：JavaThreadStatistics 设计：表格视图，显示所有字段（SELECT *） pinned-threads：固定虚拟线程 事件：VirtualThreadPinned 设计：表格视图，按应用方法分组，显示方法、固定次数、最长固定时间、总固定时间 堆栈：使用 stackTrace.topApplicationFrame 获取应用层方法 file-reads-by-path：按路径文件读取统计 事件：FileRead 设计：表格视图，按路径分组，显示路径、读取次数、总读取字节数 格式：cell-height:5 支持多行路径显示 file-writes-by-path：按路径文件写入统计 事件：FileWrite 设计：表格视图，按路径分组，显示路径、写入次数、总写入字节数 socket-reads-by-host：按主机 Socket 读取统计 事件：SocketRead 设计：表格视图，按主机分组，显示主机、读取次数、总读取字节数 socket-writes-by-host：按主机 Socket 写入统计 事件：SocketWrite 设计：表格视图，按主机分组，显示主机、写入次数、总写入字节数 latencies-by-type：按类型延迟统计 事件：JavaMonitorWait、JavaMonitorEnter、ThreadPark、ThreadSleep、SocketRead、SocketWrite、FileWrite、FileRead 设计：表格视图，按事件类型分组，显示事件类型、次数、平均、P99、最长、总持续时间 多事件：使用多个事件类型的并集 object-statistics：对象统计（占用超过 1%） 事件：ObjectCountAfterGC、ObjectCount 设计：表格视图，按对象类分组，显示类、计数、堆空间、增长量 聚合：使用 LAST_BATCH() 获取最后一批值，DIFF() 计算增长量 class-loaders：类加载器 事件：ClassLoaderStatistics 设计：表格视图，按类加载器分组，显示类加载器、隐藏类数、类数 格式：missing:null-bootstrap 将引导类加载器显示为 null longest-class-loading：最长类加载 事件：ClassLoad 设计：表格视图，显示加载时间最长的 25 个类 finalizers：终结器 事件：FinalizerStatistics 设计：表格视图，按可终结类分组，显示类、对象数、总运行次数 聚合：使用 LAST_BATCH() 获取最后一批值 deprecated-methods-for-removal：标记为移除的废弃方法 事件：DeprecatedInvocation 设计：表格视图，按方法分组，显示废弃方法、调用来源类集合 过滤：WHERE forRemoval = 'true' 聚合：使用 SET() 获取唯一调用来源类集合 格式：truncate-beginning 和 cell-height:10000 支持长内容显示 method-timing：方法计时 事件：jdk.MethodTiming 设计：表格视图，按方法分组，显示方法、调用次数、最小/平均/最大时间 聚合：使用 LAST_BATCH() 获取最后一批值 格式：ms-precision:6 显示微秒精度 method-calls：方法调用 事件：jdk.MethodTrace 设计：表格视图，按被跟踪方法和调用者分组，显示被跟踪方法、调用者、调用次数 堆栈：使用 stackTrace.topFrame.method 获取调用者方法 monitor-inflation：监视器膨胀 事件：jdk.JavaMonitorInflate 设计：表格视图，按堆栈跟踪和监视器类分组，显示堆栈跟踪、监视器类、次数、总持续时间 modules：模块 事件：ModuleRequire 设计：表格视图，按源模块名称分组，显示模块名称 native-methods：本地方法 事件：NativeMethodSample 设计：表格视图，按堆栈顶部帧分组，显示方法、样本数 环境视图（environment.*）：\nrecording：记录信息 事件：所有事件（FROM *） 设计：表单视图，显示事件计数、第一个/最后一个记录事件、记录长度、转储原因 聚合：使用 COUNT()、FIRST()、LAST()、DIFF() 函数，从 jdk.Shutdown 获取转储原因 active-recordings：活动记录 事件：ActiveRecording 设计：表格视图，按记录 ID 分组，显示开始时间、持续时间、名称、目标、最大年龄、最大大小 格式：cell-height:5 支持多行目标路径显示 active-settings：活动设置 事件：ActiveSetting 设计：表格视图，按事件类型 ID 分组，显示事件类型、启用状态、阈值、堆栈跟踪、周期、截止时间、节流 多源：使用多个 ActiveSetting 别名（E、T、S、P、C、U）分别获取不同设置 过滤：使用 WHERE 条件过滤不同设置名称 cpu-load：CPU 负载统计 事件：CPULoad 设计：表单视图，显示 JVM 用户/系统 CPU 和机器总 CPU 的最小/平均/最大值 cpu-load-samples：CPU 负载样本 事件：CPULoad 设计：表格视图，显示每个样本的开始时间、JVM 用户/系统 CPU、机器总 CPU cpu-information：CPU 信息 事件：CPUInformation 设计：表单视图，显示 CPU、插槽数、核心数、硬件线程数、描述 cpu-tsc：CPU 时间戳计数器 事件：CPUTimeStampCounter 设计：表单视图，显示快速时间自动启用、快速时间启用、快速时间频率、OS 频率 system-information：系统信息 事件：CPUInformation、PhysicalMemory、OSInformation、VirtualizationInformation 设计：表单视图，显示物理内存大小、OS 版本、虚拟化、CPU 类型、核心数、硬件线程数、插槽数、CPU 描述 多事件：使用多个事件类型的并集 system-properties：系统属性 事件：InitialSystemProperty 设计：表格视图，按键分组，显示键、值 格式：cell-height:25 支持长属性值显示 system-processes：系统进程 事件：SystemProcess 设计：表格视图，按 PID 分组，显示首次/最后观察时间、PID、命令行 格式：truncate-beginning 截断长命令行 environment-variables：环境变量 事件：InitialEnvironmentVariable 设计：表格视图，按键分组，显示键、值 格式：cell-height:20 支持长环境变量值显示 network-utilization：网络利用率 事件：NetworkUtilization 设计：表格视图，按网络接口分组，显示接口、平均/最大读取速率、平均/最大写入速率 native-libraries：本地库 事件：NativeLibrary 设计：表格视图，按名称分组，显示名称、基地址、顶部地址 native-library-failures：本地库加载/卸载失败 事件：NativeLibraryLoad、NativeLibraryUnload 设计：表格视图，过滤失败操作，显示操作类型、库名称、错误消息 过滤：WHERE success = 'false' jvm-flags：JVM 标志 事件：IntFlag、UnsignedIntFlag、BooleanFlag、LongFlag、UnsignedLongFlag、DoubleFlag、StringFlag 及其变更事件 设计：表格视图，按名称分组，显示名称、最后值 多事件：使用多个标志事件类型的并集 jvm-information：JVM 信息 事件：JVMInformation 设计：表单视图，显示 PID、VM 启动时间、名称、版本、VM 参数、程序参数 jdk-agents：JDK 代理 事件：JavaAgent、NativeAgent 设计：表格视图，显示初始化时间、初始化持续时间、名称、选项 格式：truncate-beginning 和 cell-height:10 支持长选项显示 container-configuration：容器配置 事件：ContainerConfiguration 设计：表单视图，显示容器类型、CPU 切片周期、CPU 配额、CPU 份额、有效 CPU 数、内存软限制、内存限制、交换内存限制、主机总内存 container-cpu-usage：容器 CPU 使用 事件：ContainerCPUUsage 设计：表单视图，显示 CPU 时间、用户时间、系统时间 container-memory-usage：容器内存使用 事件：ContainerMemoryUsage 设计：表单视图，显示内存失败次数、内存使用、交换内存使用 container-io-usage：容器 I/O 使用 事件：ContainerIOUsage 设计：表单视图，显示服务请求数、数据传输量 container-cpu-throttling：容器 CPU 节流 事件：ContainerCPUThrottling 设计：表单视图，显示 CPU 已用切片、CPU 节流切片、CPU 节流时间 events-by-count：按计数的事件类型 事件：所有事件（FROM *） 设计：表格视图，按事件类型标签分组，显示事件类型、计数，按计数降序排序 events-by-name：按名称的事件类型 事件：所有事件（FROM *） 设计：表格视图，按事件类型标签分组，显示事件类型、计数，按名称升序排序 视图设计特点：\n分类组织：视图按功能分类（JVM、应用、环境），便于查找和使用 查询优化：使用 LIMIT 限制结果数量，避免输出过多数据 格式化选项：使用 FORMAT 控制显示格式，提高可读性 多事件支持：支持从多个事件类型查询数据（使用并集） 堆栈跟踪访问：提供多种堆栈帧访问方式，适应不同分析场景 聚合函数丰富：支持统计、百分位、集合等多种聚合函数 实时查询：直接从 repository 读取数据，无需转储文件 注意事项：\n实时查看：JFR.view 直接从 repository 读取数据，无需转储文件 内存泄漏视图：使用 memory-leaks 视图时，会自动触发 OldObjectSample 事件收集（调用 OldObjectSample.emit(0) 并等待刷新） 时间范围：默认查看最近 10 分钟的数据，可以通过 maxage 调整 大小限制：默认最多查看 32MB 的数据，可以通过 maxsize 调整 查询执行：视图查询通过 QueryExecutor 执行，使用 EventStream 读取事件数据 性能考虑：复杂查询（如 all-views）可能消耗较多时间和内存 事件缺失：如果记录中缺少视图所需的事件，会显示错误信息但不会中断执行 视图名称格式：视图在 view.ini 中定义时使用完整名称（如 [jvm.gc]、[application.hot-methods]），但在 JFR.view 命令中只需要使用视图名称部分（如 gc、hot-methods），不需要加类别前缀（jvm.、application.、environment.）。视图名称匹配是大小写不敏感的。 自定义查询与查询语言：\nJFR.view 命令只能使用预定义的视图，不支持直接执行 SQL 查询。\n查询语言的使用场景：\n预定义视图（JFR.view）：使用 view.ini 中预定义的视图，通过视图名称调用 视图定义（view.ini）：在配置文件中定义视图，供 JFR.view 使用 自定义查询（jfr query）：针对已转储的 JFR 文件，使用 jfr query 命令直接编写查询语句（注意：jfr query 命令仅在 debug builds 中可用，面向 OpenJDK 开发者，可能被移除或语法改变） 关于自定义查询的说明：\njcmd JFR.query 命令不存在：虽然源代码中存在 DCmdQuery.java 实现，但在 jfrDcmds.cpp 中该命令的注册被注释掉了（// JFR.query Uncomment when developing new queries for the JFR.view command），因此在实际的 JDK 构建中无法使用 jcmd JFR.query 命令。 jfr query 命令：如果需要执行自定义查询，可以使用 jfr query 命令（针对已转储的 JFR 文件），该命令支持与 view.ini 中相同的查询语言语法。但需要注意： jfr query 命令仅在 debug builds 中可用 该工具面向 OpenJDK 开发者，可能被移除或语法改变 使用方式：jfr query \u0026quot;\u0026lt;query\u0026gt;\u0026quot; \u0026lt;file.jfr\u0026gt; 可以使用 verbose=true 选项查看预定义视图的查询语句，然后复制到 jfr query 中使用或修改：\n# 查看预定义视图的查询语句 jcmd \u0026lt;pid\u0026gt; JFR.view verbose=true gc # 使用自定义查询（jfr query）- 针对已转储的 JFR 文件（仅在 debug builds 中可用） jfr query \u0026#34;SELECT * FROM GarbageCollection\u0026#34; recording.jfr jfr query \u0026#34;SELECT stackTrace.topFrame AS T, SUM(weight) FROM ObjectAllocationSample GROUP BY T\u0026#34; recording.jfr 4.7. 命令使用流程示例 # 以下是一个完整的使用流程示例：\n# 1. 查看当前运行的记录 jcmd \u0026lt;pid\u0026gt; JFR.check # 2. 启动一个新记录 jcmd \u0026lt;pid\u0026gt; JFR.start name=PerformanceAnalysis settings=profile duration=30m filename=analysis.jfr # 3. 查看记录状态 jcmd \u0026lt;pid\u0026gt; JFR.check name=PerformanceAnalysis # 4. 在记录运行期间转储数据（记录继续运行） jcmd \u0026lt;pid\u0026gt; JFR.dump name=PerformanceAnalysis maxage=10m filename=snapshot.jfr # 5. 查看实时视图 jcmd \u0026lt;pid\u0026gt; JFR.view hot-methods jcmd \u0026lt;pid\u0026gt; JFR.view gc # 6. 停止记录并转储 jcmd \u0026lt;pid\u0026gt; JFR.stop name=PerformanceAnalysis filename=final.jfr # 7. 再次查看所有记录 jcmd \u0026lt;pid\u0026gt; JFR.check 5. jfc 配置文件格式与配置和使用方式 # JFC（Java Flight Recorder Configuration）文件是 XML 格式的配置文件，用于定义 JFR 记录中要采集的事件及其配置参数。JFC 文件是 JFR 的核心配置机制，通过它可以精确控制哪些事件被记录、事件的采集频率、阈值等。\n5.1. JFC 文件格式 # JFC 文件采用 XML 格式，必须符合 jfc.xsd Schema 定义。文件的基本结构如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;配置名称\u0026#34; description=\u0026#34;配置描述\u0026#34; provider=\u0026#34;提供者\u0026#34;\u0026gt; \u0026lt;event name=\u0026#34;事件名称\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;配置项名称\u0026#34;\u0026gt;配置值\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;配置项名称\u0026#34; control=\u0026#34;控制标识符\u0026#34;\u0026gt;配置值\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 更多事件配置 --\u0026gt; \u0026lt;/configuration\u0026gt; 根元素 \u0026lt;configuration\u0026gt; 属性：\nversion（必需）：JFC 文件格式版本，当前版本为 \u0026quot;2.0\u0026quot;。JFR 只能读取 2.x 版本的 JFC 文件 label（必需）：配置文件的显示名称，用于在工具中标识此配置 description（可选）：配置文件的描述信息 provider（可选）：配置文件的提供者，如 \u0026quot;Oracle\u0026quot;、\u0026quot;OpenJDK\u0026quot; 等 \u0026lt;event\u0026gt; 元素：\nname（必需）：事件名称，如 \u0026quot;jdk.ThreadStart\u0026quot;、\u0026quot;jdk.GCPhase\u0026quot; 等 label（可选）：事件的显示标签 description（可选）：事件的描述信息 \u0026lt;setting\u0026gt; 元素：\nname（必需）：配置项名称，如 \u0026quot;enabled\u0026quot;、\u0026quot;threshold\u0026quot;、\u0026quot;period\u0026quot;、\u0026quot;stackTrace\u0026quot;、\u0026quot;throttle\u0026quot; 等 control（可选）：控制标识符，用于关联 \u0026lt;control\u0026gt; 元素中定义的控件。当控件值改变时，关联的 setting 值会自动更新 元素内容：配置值，如 \u0026quot;true\u0026quot;、\u0026quot;20 ms\u0026quot;、\u0026quot;100/s\u0026quot; 等 \u0026lt;control\u0026gt; 元素（可选，JVM 不读取）：\n位置：位于 \u0026lt;configuration\u0026gt; 根元素下，与 \u0026lt;event\u0026gt; 元素同级 作用：定义 UI 控件和条件逻辑，用于 JDK Mission Control 和 jfr configure 工具 包含的子元素： \u0026lt;text\u0026gt;：文本输入控件 \u0026lt;selection\u0026gt;：选择控件（下拉菜单） \u0026lt;flag\u0026gt;：布尔标志控件 \u0026lt;condition\u0026gt;：条件逻辑，根据其他控件的值动态设置配置值 示例：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;Custom\u0026#34; description=\u0026#34;Custom JFR configuration\u0026#34; provider=\u0026#34;User\u0026#34;\u0026gt; \u0026lt;!-- 启用线程睡眠事件，只记录超过 20ms 的睡眠，并采集堆栈跟踪 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ThreadSleep\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 启用 CPU 负载事件，每秒采集一次 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.CPULoad\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;period\u0026#34;\u0026gt;1 s\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 禁用类加载统计事件 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ClassLoadingStatistics\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 5.2. Control 元素详解 # \u0026lt;control\u0026gt; 元素是 JFC 文件中的一个特殊部分，用于定义 UI 控件和条件逻辑。重要：JVM 在运行时不读取 \u0026lt;control\u0026gt; 元素的内容，它仅用于 JDK Mission Control（JMC）和 jfr configure 工具，提供友好的配置界面和自动化配置逻辑。\n5.2.1. Control 的作用机制 # UI 控件定义：为 JMC 和 jfr configure 工具提供配置界面元素 配置关联：通过 \u0026lt;setting\u0026gt; 元素的 control 属性，将事件配置关联到控件 自动更新：当控件值改变时，所有关联的 setting 值会自动更新 条件逻辑：通过 \u0026lt;condition\u0026gt; 元素实现基于其他控件值的动态配置 5.2.2. Control 元素类型 # 1. \u0026lt;text\u0026gt; 元素（文本输入控件）\n用于定义文本输入控件，支持不同类型的内容：\n\u0026lt;text name=\u0026#34;控件名称\u0026#34; label=\u0026#34;显示标签\u0026#34; contentType=\u0026#34;内容类型\u0026#34; minimum=\u0026#34;最小值\u0026#34; maximum=\u0026#34;最大值\u0026#34;\u0026gt;默认值\u0026lt;/text\u0026gt; 属性：\nname（必需）：控件标识符，用于在 \u0026lt;setting\u0026gt; 的 control 属性中引用 label（必需）：控件的显示标签 description（可选）：控件描述 contentType（可选）：内容类型，如 \u0026quot;timespan\u0026quot;（时间间隔）、\u0026quot;method-filter\u0026quot;（方法过滤器）、\u0026quot;text\u0026quot;（普通文本）等 minimum（可选）：最小值（适用于可排序的内容类型） maximum（可选）：最大值（适用于可排序的内容类型） 示例：\n\u0026lt;!-- 定义锁等待阈值控件 --\u0026gt; \u0026lt;text name=\u0026#34;locking-threshold\u0026#34; label=\u0026#34;Locking Threshold\u0026#34; contentType=\u0026#34;timespan\u0026#34; minimum=\u0026#34;0 s\u0026#34;\u0026gt;20 ms\u0026lt;/text\u0026gt; \u0026lt;!-- 定义方法过滤器控件 --\u0026gt; \u0026lt;text name=\u0026#34;method-trace\u0026#34; label=\u0026#34;Method Trace Filter\u0026#34; contentType=\u0026#34;method-filter\u0026#34; description=\u0026#34;A filter can be an annotation (@jakarta.ws.rs.GET), a full qualified class name (com.example.Foo), a fully qualified method reference (java.lang.HashMap::resize) or a class initializer (::\u0026amp;lt;clinit\u0026amp;gt;). Use \u0026amp;lt;init\u0026amp;gt; for constructors. Separate multiple filters with semicolon.\u0026#34;\u0026gt;\u0026lt;/text\u0026gt; 使用方式：\n\u0026lt;!-- 在事件配置中关联控件 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ThreadSleep\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34; control=\u0026#34;locking-threshold\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.JavaMonitorEnter\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34; control=\u0026#34;locking-threshold\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; 当用户通过 JMC 或 jfr configure 修改 locking-threshold 控件的值时，所有关联的 setting 值会自动更新。\n2. \u0026lt;selection\u0026gt; 元素（选择控件）\n用于定义下拉选择控件，提供多个预定义选项：\n\u0026lt;selection name=\u0026#34;控件名称\u0026#34; label=\u0026#34;显示标签\u0026#34; default=\u0026#34;默认选项名称\u0026#34;\u0026gt; \u0026lt;option label=\u0026#34;选项显示标签\u0026#34; name=\u0026#34;选项名称\u0026#34;\u0026gt;选项值\u0026lt;/option\u0026gt; \u0026lt;!-- 更多选项 --\u0026gt; \u0026lt;/selection\u0026gt; 属性：\nname（必需）：控件标识符 label（必需）：控件的显示标签 description（可选）：控件描述 default（必需）：默认选项的名称（必须匹配某个 \u0026lt;option\u0026gt; 的 name 属性） \u0026lt;option\u0026gt; 元素：\nname（必需）：选项标识符，用于在 default 属性中引用 label（必需）：选项的显示标签 description（可选）：选项描述 元素内容：选项的实际值 示例：\n\u0026lt;!-- 定义方法分析级别选择控件 --\u0026gt; \u0026lt;selection name=\u0026#34;method-profiling\u0026#34; default=\u0026#34;normal\u0026#34; label=\u0026#34;Method Profiling\u0026#34;\u0026gt; \u0026lt;option label=\u0026#34;Off\u0026#34; name=\u0026#34;off\u0026#34;\u0026gt;off\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Normal\u0026#34; name=\u0026#34;normal\u0026#34;\u0026gt;normal\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;High\u0026#34; name=\u0026#34;high\u0026#34;\u0026gt;high\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Maximum (High Overhead)\u0026#34; name=\u0026#34;max\u0026#34;\u0026gt;max\u0026lt;/option\u0026gt; \u0026lt;/selection\u0026gt; \u0026lt;!-- 定义 GC 事件级别选择控件 --\u0026gt; \u0026lt;selection name=\u0026#34;gc\u0026#34; default=\u0026#34;normal\u0026#34; label=\u0026#34;GC\u0026#34;\u0026gt; \u0026lt;option label=\u0026#34;Off\u0026#34; name=\u0026#34;off\u0026#34;\u0026gt;off\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Normal\u0026#34; name=\u0026#34;normal\u0026#34;\u0026gt;normal\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Detailed\u0026#34; name=\u0026#34;detailed\u0026#34;\u0026gt;detailed\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;High, incl. TLABs/PLABs (may cause many events)\u0026#34; name=\u0026#34;high\u0026#34;\u0026gt;high\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;All, incl. Heap Statistics (may cause long GCs)\u0026#34; name=\u0026#34;all\u0026#34;\u0026gt;all\u0026lt;/option\u0026gt; \u0026lt;/selection\u0026gt; 3. \u0026lt;flag\u0026gt; 元素（布尔标志控件）\n用于定义布尔类型的开关控件：\n\u0026lt;flag name=\u0026#34;控件名称\u0026#34; label=\u0026#34;显示标签\u0026#34;\u0026gt;true|false\u0026lt;/flag\u0026gt; 属性：\nname（必需）：控件标识符 label（必需）：控件的显示标签 description（可选）：控件描述 元素内容：默认值（\u0026quot;true\u0026quot; 或 \u0026quot;false\u0026quot;） 示例：\n\u0026lt;!-- 定义类加载事件开关 --\u0026gt; \u0026lt;flag name=\u0026#34;class-loading\u0026#34; label=\u0026#34;Class Loading\u0026#34;\u0026gt;false\u0026lt;/flag\u0026gt; 使用方式：\n\u0026lt;!-- 在事件配置中关联控件 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ClassLoad\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;class-loading\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.ClassUnload\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;class-loading\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; 4. \u0026lt;condition\u0026gt; 元素（条件逻辑）\n用于根据其他控件的值动态设置配置值，实现条件逻辑：\n\u0026lt;condition name=\u0026#34;控件名称\u0026#34; true=\u0026#34;条件为真时的值\u0026#34; false=\u0026#34;条件为假时的值\u0026#34;\u0026gt; \u0026lt;!-- 条件表达式：\u0026lt;test\u0026gt;、\u0026lt;and\u0026gt;、\u0026lt;or\u0026gt;、\u0026lt;not\u0026gt; --\u0026gt; \u0026lt;/condition\u0026gt; 属性：\nname（必需）：控件标识符，用于在 \u0026lt;setting\u0026gt; 的 control 属性中引用 true（可选）：当条件表达式为真时返回的值 false（可选）：当条件表达式为假时返回的值 条件表达式元素：\n\u0026lt;test\u0026gt;：测试条件，检查某个控件的值 name：要测试的控件名称 operator：操作符，目前只支持 \u0026quot;equal\u0026quot; value：要比较的值 \u0026lt;and\u0026gt;：逻辑与，所有子条件都为真时返回真 \u0026lt;or\u0026gt;：逻辑或，任一子条件为真时返回真 \u0026lt;not\u0026gt;：逻辑非，反转子条件的结果 示例：\n\u0026lt;!-- 根据 method-profiling 选择控件的值，动态设置方法采样间隔 --\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;999 d\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;off\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;20 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;normal\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;10 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;high\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;1 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;max\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;!-- 根据 GC 选择控件的值，动态启用/禁用 GC 事件 --\u0026gt; \u0026lt;condition name=\u0026#34;gc-enabled-normal\u0026#34; true=\u0026#34;true\u0026#34; false=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;or\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;normal\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;detailed\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;high\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;all\u0026#34;/\u0026gt; \u0026lt;/or\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;!-- 根据 thread-dump 选择控件的值，动态启用/禁用线程转储 --\u0026gt; \u0026lt;condition name=\u0026#34;thread-dump-enabled\u0026#34; true=\u0026#34;false\u0026#34; false=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;thread-dump\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;999 d\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; 使用方式：\n\u0026lt;!-- 在事件配置中关联条件控件 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ExecutionSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;method-sampling-enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;period\u0026#34; control=\u0026#34;method-sampling-java-interval\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.GCPhase\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;gc-enabled-normal\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; 5.2.3. Control 的完整示例 # 以下是一个完整的 control 示例，展示了各种控件类型的组合使用：\n\u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;Custom\u0026#34; description=\u0026#34;Custom JFR configuration\u0026#34;\u0026gt; \u0026lt;!-- 事件配置 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ThreadSleep\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34; control=\u0026#34;locking-threshold\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.ExecutionSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;method-sampling-enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;period\u0026#34; control=\u0026#34;method-sampling-java-interval\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.GCPhase\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;gc-enabled-normal\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- Control 部分 --\u0026gt; \u0026lt;control\u0026gt; \u0026lt;!-- 文本控件：锁等待阈值 --\u0026gt; \u0026lt;text name=\u0026#34;locking-threshold\u0026#34; label=\u0026#34;Locking Threshold\u0026#34; contentType=\u0026#34;timespan\u0026#34; minimum=\u0026#34;0 s\u0026#34;\u0026gt;20 ms\u0026lt;/text\u0026gt; \u0026lt;!-- 选择控件：方法分析级别 --\u0026gt; \u0026lt;selection name=\u0026#34;method-profiling\u0026#34; default=\u0026#34;normal\u0026#34; label=\u0026#34;Method Profiling\u0026#34;\u0026gt; \u0026lt;option label=\u0026#34;Off\u0026#34; name=\u0026#34;off\u0026#34;\u0026gt;off\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Normal\u0026#34; name=\u0026#34;normal\u0026#34;\u0026gt;normal\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;High\u0026#34; name=\u0026#34;high\u0026#34;\u0026gt;high\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Maximum (High Overhead)\u0026#34; name=\u0026#34;max\u0026#34;\u0026gt;max\u0026lt;/option\u0026gt; \u0026lt;/selection\u0026gt; \u0026lt;!-- 条件控件：根据方法分析级别设置采样间隔 --\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;999 d\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;off\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;20 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;normal\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;10 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;high\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-java-interval\u0026#34; true=\u0026#34;1 ms\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;max\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;!-- 条件控件：根据方法分析级别启用/禁用采样 --\u0026gt; \u0026lt;condition name=\u0026#34;method-sampling-enabled\u0026#34; true=\u0026#34;false\u0026#34; false=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;test name=\u0026#34;method-profiling\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;off\u0026#34;/\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;!-- 选择控件：GC 事件级别 --\u0026gt; \u0026lt;selection name=\u0026#34;gc\u0026#34; default=\u0026#34;normal\u0026#34; label=\u0026#34;GC\u0026#34;\u0026gt; \u0026lt;option label=\u0026#34;Off\u0026#34; name=\u0026#34;off\u0026#34;\u0026gt;off\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Normal\u0026#34; name=\u0026#34;normal\u0026#34;\u0026gt;normal\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;Detailed\u0026#34; name=\u0026#34;detailed\u0026#34;\u0026gt;detailed\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;High\u0026#34; name=\u0026#34;high\u0026#34;\u0026gt;high\u0026lt;/option\u0026gt; \u0026lt;option label=\u0026#34;All\u0026#34; name=\u0026#34;all\u0026#34;\u0026gt;all\u0026lt;/option\u0026gt; \u0026lt;/selection\u0026gt; \u0026lt;!-- 条件控件：根据 GC 级别启用/禁用 GC 事件 --\u0026gt; \u0026lt;condition name=\u0026#34;gc-enabled-normal\u0026#34; true=\u0026#34;true\u0026#34; false=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;or\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;normal\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;detailed\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;high\u0026#34;/\u0026gt; \u0026lt;test name=\u0026#34;gc\u0026#34; operator=\u0026#34;equal\u0026#34; value=\u0026#34;all\u0026#34;/\u0026gt; \u0026lt;/or\u0026gt; \u0026lt;/condition\u0026gt; \u0026lt;/control\u0026gt; \u0026lt;/configuration\u0026gt; 5.2.4. Control 的工作原理 # 根据源代码分析（JFCModel.java），control 的工作流程如下：\n解析阶段（addControls()）：\n解析所有 \u0026lt;control\u0026gt; 元素及其子元素（\u0026lt;text\u0026gt;、\u0026lt;selection\u0026gt;、\u0026lt;flag\u0026gt;、\u0026lt;condition\u0026gt;） 将控件按名称索引到 controls Map 中 关联阶段（wireSettings()）：\n遍历所有事件的 setting 如果 setting 有 control 属性，查找对应的控件 建立监听关系：当控件值改变时，自动更新关联的 setting 值 条件计算阶段（wireConditions()）：\n解析所有 \u0026lt;condition\u0026gt; 元素的表达式 建立依赖关系：当被测试的控件值改变时，重新计算条件值 条件值计算后，自动更新关联的 setting 值 运行时：\nJVM 不读取 control 部分，只读取最终的 setting 值 Control 仅用于配置工具（JMC、jfr configure）提供友好的配置界面 5.2.5. Control 的优势 # 简化配置：通过高级控件（如\u0026quot;Method Profiling\u0026quot;级别）自动设置多个相关配置 一致性保证：相关事件的配置通过同一个控件统一管理，确保配置一致 用户友好：在 JMC 中提供图形化配置界面，无需手动编辑 XML 条件逻辑：通过 \u0026lt;condition\u0026gt; 实现复杂的配置依赖关系 5.2.6. 注意事项 # JVM 不读取：Control 元素对 JVM 运行时没有影响，JVM 只读取最终的 setting 值 唯一性要求：多个 JFC 文件合并时，control 名称不能重复（会抛出异常） 引用完整性：如果 setting 的 control 属性引用了不存在的控件，会记录警告但不会报错 手动编辑：如果手动编辑 JFC 文件，需要确保 control 名称和 setting 的 control 属性匹配 5.4. 预定义的 JFC 配置文件 # JDK 提供了两个预定义的 JFC 配置文件，位于 JAVA_HOME/lib/jfr/ 目录：\ndefault.jfc（默认配置）：\n标签：\u0026quot;Continuous\u0026quot; 描述：\u0026quot;Low overhead configuration safe for continuous use in production environments, typically less than 1 % overhead.\u0026quot; 特点：低开销配置，适合生产环境持续使用，开销通常小于 1% 适用场景：生产环境持续监控 profile.jfc（性能分析配置）：\n标签：\u0026quot;Profiling\u0026quot; 描述：\u0026quot;Low overhead configuration for profiling, typically around 2 % overhead.\u0026quot; 特点：性能分析配置，开销约为 2%，包含更多事件和堆栈跟踪 适用场景：性能分析和问题诊断 查看预定义配置：\n# 查看 default.jfc 的内容 cat $JAVA_HOME/lib/jfr/default.jfc # 查看 profile.jfc 的内容 cat $JAVA_HOME/lib/jfr/profile.jfc # 列出 JAVA_HOME/lib/jfr/ 目录中的所有 .jfc 文件 ls $JAVA_HOME/lib/jfr/*.jfc # 查看 jfr configure 命令的帮助信息（会显示可用的输入文件选项） jfr help configure 注意：jfr configure 命令没有 --list 选项。要列出所有可用的预定义配置，可以通过以下方式：\n直接查看目录：JAVA_HOME/lib/jfr/ 目录中通常包含 default.jfc 和 profile.jfc 使用 JDK API：通过 Configuration.getConfigurations() 方法获取配置列表（需要编写 Java 代码） 查看帮助信息：jfr help configure 会显示可用的输入文件选项 5.5. 创建和编辑 JFC 配置文件 # 5.5.1. 使用 jfr configure 命令创建 # jfr configure 命令提供了交互式和命令行两种方式创建 JFC 文件：\n交互式创建：\n交互式模式需要显式指定 --interactive 参数，会启动配置向导，逐步询问配置选项。向导会遍历 JFC 文件中所有 \u0026lt;control\u0026gt; 元素定义的输入项（\u0026lt;selection\u0026gt;、\u0026lt;text\u0026gt;、\u0026lt;flag\u0026gt;），逐个询问用户输入。\n# 基于 default.jfc 创建自定义配置（交互式） jfr configure --interactive --input default.jfc --output custom.jfc # 基于 profile.jfc 创建自定义配置（交互式） jfr configure --interactive --input profile.jfc --output custom.jfc # 仅使用 --interactive，不指定 --input（默认使用 default.jfc） jfr configure --interactive # 示例交互： ============== .jfc Configuration Wizard ============ This wizard will generate a JFR configuration file by asking 12 questions. Press ENTER to use the default value, or type Q to abort the wizard. Garbage Collector: Normal (default) 1. Off 2. Normal 3. Detailed 4. High, incl. TLABs/PLABs (may cause many events) 5. All, incl. Heap Statistics (may cause long GCs) 交互式模式的工作方式：\n启动向导：显示欢迎信息，告知将询问多少个问题 遍历输入项：对于每个 \u0026lt;control\u0026gt; 中的输入项（\u0026lt;selection\u0026gt;、\u0026lt;text\u0026gt;、\u0026lt;flag\u0026gt;），依次询问： \u0026lt;selection\u0026gt;：显示选项列表（1, 2, 3\u0026hellip;），用户输入数字选择，或按 ENTER 使用默认值 \u0026lt;text\u0026gt;：显示标签和默认值，用户输入文本，或按 ENTER 使用默认值 \u0026lt;flag\u0026gt;：显示 Y/N 选择，用户输入 Y 或 N，或按 ENTER 使用默认值 输入验证：对于时间跨度（timespan）和方法过滤器（method-filter）类型，会进行格式验证 退出方式：输入 Q 可随时退出向导 保存文件：最后询问输出文件名（默认 custom.jfc） 命令行创建：\n# 基于 default.jfc 创建，并修改特定事件的配置 jfr configure \\ --input default.jfc \\ --output custom.jfc \\ jdk.ThreadSleep#threshold=50ms \\ jdk.CPULoad#period=2s # 使用 + 前缀添加新的事件配置（如果事件不在基础配置中） jfr configure \\ --input default.jfc \\ --output custom.jfc \\ +jdk.CustomEvent#enabled=true # 注意：不能合并包含相同 control 名称的 JFC 文件 # 例如 default.jfc 和 profile.jfc 都定义了 \u0026#39;gc\u0026#39; control，不能直接合并 # 如果尝试合并会报错：Control with \u0026#39;gc\u0026#39; is declared in multiple files # 正确做法：只使用一个基础 JFC 文件，然后通过命令行参数覆盖配置 jfr configure \\ --input default.jfc \\ --output custom.jfc \\ gc=high \\ method-profiling=high # 或者使用 profile.jfc 作为基础 jfr configure \\ --input profile.jfc \\ --output custom.jfc \\ gc=high 配置选项格式：\n选项格式：\u0026lt;选项名称\u0026gt;=\u0026lt;值\u0026gt;（如 gc=high、method-profiling=high） 事件设置格式：\u0026lt;事件名称\u0026gt;#\u0026lt;配置项名称\u0026gt;=\u0026lt;配置值\u0026gt;（如 jdk.ThreadSleep#threshold=50ms、jdk.CPULoad#period=2s） 添加新事件：使用 + 前缀，如 +jdk.CustomEvent#enabled=true 时间跨度格式：支持 20ms、1s、5m 等格式，空格可省略 注意事项：\n交互式模式的前提：JFC 文件中必须包含 \u0026lt;control\u0026gt; 元素定义的输入项，否则交互式模式不会询问任何问题 默认 JFC 文件：如果不指定 --input，默认使用 default.jfc 空配置：可以使用 --input none 从空配置开始创建 显示修改：使用 --verbose 参数可以显示所有被修改的设置 多个 JFC 文件合并的限制： jfr configure 命令的限制：不能合并包含相同 control 名称的 JFC 文件。例如，default.jfc 和 profile.jfc 都定义了 gc、method-profiling 等 control，尝试使用 jfr configure --input default.jfc,profile.jfc 会报错：Control with 'gc' is declared in multiple files 正确的做法：只使用一个基础 JFC 文件（如 default.jfc 或 profile.jfc），然后通过命令行参数覆盖配置 JVM 参数和 jcmd 命令的区别：虽然 jfr configure 不能合并有冲突的 JFC 文件，但可以通过 JVM 参数（-XX:StartFlightRecording=settings=default,settings=profile）或 jcmd 命令（settings=default,settings=profile）合并，因为 JVM 运行时只读取最终的 setting 值，不关心 control 元素 5.5.2. 手动编辑 JFC 文件 # 可以直接编辑 XML 文件，但需要注意：\n必须符合 jfc.xsd Schema 建议使用支持 XML Schema 验证的编辑器 修改后可以使用 jfr configure --input \u0026lt;file\u0026gt; 验证格式 验证 JFC 文件：\n# 方法 1：尝试加载并输出到临时文件（验证后删除） # 注意：输出文件必须以 .jfc 结尾，不能使用 /dev/null jfr configure --input custom.jfc --output /tmp/validate.jfc \u0026amp;\u0026amp; rm /tmp/validate.jfc # 方法 2：输出到当前目录的临时文件（验证后删除） jfr configure --input custom.jfc --output validate-temp.jfc \u0026amp;\u0026amp; rm validate-temp.jfc 验证原理：\njfr configure 命令在解析和保存 JFC 文件时会进行格式验证 如果 JFC 文件格式错误（如 XML 格式错误、不符合 Schema、control 引用错误等），会在解析或保存阶段抛出异常 输出文件必须是以 .jfc 结尾的有效文件路径，不能使用 /dev/null 等特殊设备文件 5.6. 使用 JFC 配置文件 # 5.6.1. 通过 JVM 参数使用 # 指定单个 JFC 文件：\n# 使用预定义配置（default.jfc） -XX:StartFlightRecording=settings=default # 使用预定义配置（profile.jfc） -XX:StartFlightRecording=settings=profile # 使用自定义 JFC 文件（完整路径） -XX:StartFlightRecording=settings=/path/to/custom.jfc # 使用自定义 JFC 文件（相对路径，相对于当前工作目录） -XX:StartFlightRecording=settings=./custom.jfc # 不使用任何预定义配置（从空白配置开始） -XX:StartFlightRecording=settings=none 指定多个 JFC 文件（合并配置）：\n# 合并多个 JFC 文件，后面的配置会覆盖前面的同名配置 # 注意：只能合并不包含相同 control 名称的 JFC 文件 -XX:StartFlightRecording=settings=default,settings=profile # 合并自定义配置和预定义配置 -XX:StartFlightRecording=settings=default,settings=/path/to/custom.jfc 合并规则：\n多个 JFC 文件按顺序加载和合并 后面文件中的配置会覆盖前面文件中的同名配置 如果多个文件定义了同一个事件的同一个配置项，最后一个文件的值生效 重要限制：控制（control）元素不能重复定义。如果多个文件定义了相同的 control 名称（如 gc、method-profiling 等），jfr configure 命令会抛出异常：Control with 'gc' is declared in multiple files 实际影响：由于 default.jfc 和 profile.jfc 都定义了相同的 control 名称（如 gc、method-profiling 等），它们不能通过 jfr configure --input default.jfc,profile.jfc 合并。但可以通过 JVM 参数或 jcmd 命令的 settings=default,settings=profile 方式合并，因为 JVM 在运行时只读取最终的 setting 值，不关心 control 元素 同时指定 JFC 文件和覆盖配置：\n# 使用 default.jfc，并覆盖特定事件的配置 -XX:StartFlightRecording=settings=default,jdk.ThreadSleep#threshold=50ms,jdk.CPULoad#period=2s 5.6.2. 通过 jcmd 命令使用 # # 启动记录，使用 default.jfc jcmd \u0026lt;pid\u0026gt; JFR.start settings=default # 启动记录，使用自定义 JFC 文件 jcmd \u0026lt;pid\u0026gt; JFR.start settings=/path/to/custom.jfc # 启动记录，使用多个 JFC 文件 # 注意：虽然 default.jfc 和 profile.jfc 不能通过 jfr configure 合并（因为 control 名称冲突）， # 但可以通过 JVM 参数或 jcmd 命令合并，因为 JVM 运行时只读取最终的 setting 值 jcmd \u0026lt;pid\u0026gt; JFR.start settings=default,settings=profile # 启动记录，使用 JFC 文件并覆盖配置 jcmd \u0026lt;pid\u0026gt; JFR.start settings=default,jdk.ThreadSleep#threshold=50ms 5.6.3. 通过 JDK API 使用 # import jdk.jfr.Configuration; import jdk.jfr.Recording; // 加载预定义配置 Configuration config = Configuration.getConfiguration(\u0026#34;default\u0026#34;); // 从文件加载配置 Configuration config = Configuration.create(Path.of(\u0026#34;/path/to/custom.jfc\u0026#34;)); // 从 Reader 加载配置 Configuration config = Configuration.create(new FileReader(\u0026#34;/path/to/custom.jfc\u0026#34;)); // 使用配置创建记录 Recording recording = new Recording(config); recording.start(); 5.6.4. 通过 JMX 使用 # import javax.management.MBeanServer; import com.sun.management.HotSpotDiagnosticMXBean; // 获取 FlightRecorderMXBean MBeanServer server = ManagementFactory.getPlatformMBeanServer(); FlightRecorderMXBean frBean = ManagementFactory.newPlatformMXBeanProxy( server, \u0026#34;jdk.management.jfr:type=FlightRecorder\u0026#34;, FlightRecorderMXBean.class ); // 启动记录，使用 default.jfc long recordingId = frBean.newRecording(); frBean.setConfiguration(recordingId, \u0026#34;default\u0026#34;); frBean.startRecording(recordingId); 5.7. JFC 文件查找路径 # 当指定 JFC 文件时，JFR 按以下顺序查找：\n预定义配置名称：如果名称是 \u0026quot;default\u0026quot; 或 \u0026quot;profile\u0026quot;，从 JAVA_HOME/lib/jfr/ 目录加载 JAVA_HOME/lib/jfr/ 目录：如果文件在 JAVA_HOME/lib/jfr/ 目录中，可以直接使用文件名（不需要完整路径） 完整路径：如果提供了完整路径，直接使用该路径 相对路径：如果提供了相对路径，相对于当前工作目录 示例：\n# 以下方式等价（如果 custom.jfc 在 JAVA_HOME/lib/jfr/ 目录中） -XX:StartFlightRecording=settings=custom -XX:StartFlightRecording=settings=custom.jfc -XX:StartFlightRecording=settings=$JAVA_HOME/lib/jfr/custom.jfc # 使用完整路径 -XX:StartFlightRecording=settings=/home/user/custom.jfc # 使用相对路径（相对于当前工作目录） -XX:StartFlightRecording=settings=./config/custom.jfc 5.8. 常用配置示例 # 5.8.1. 生产环境低开销配置 # 基于 default.jfc，禁用高开销事件：\n\u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;Production\u0026#34; description=\u0026#34;Low overhead production configuration\u0026#34;\u0026gt; \u0026lt;!-- 禁用方法执行采样（开销较大） --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ExecutionSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 禁用对象分配采样（开销较大） --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ObjectAllocationSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 启用关键事件，但提高阈值以减少事件数量 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ThreadSleep\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34;\u0026gt;100 ms\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 5.8.2. 性能分析配置 # 基于 profile.jfc，启用更多事件和堆栈跟踪：\n\u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;Profiling\u0026#34; description=\u0026#34;Performance profiling configuration\u0026#34;\u0026gt; \u0026lt;!-- 启用方法执行采样，降低采样频率 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ExecutionSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;throttle\u0026#34;\u0026gt;50/s\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 启用对象分配采样 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.ObjectAllocationSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;throttle\u0026#34;\u0026gt;100/s\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 降低锁等待阈值，捕获更多锁竞争 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.JavaMonitorEnter\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34;\u0026gt;1 ms\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 5.8.3. 内存泄漏分析配置 # 启用 OldObjectSample 事件并配置 GC root 路径：\n\u0026lt;configuration version=\u0026#34;2.0\u0026#34; label=\u0026#34;Memory Leak Analysis\u0026#34; description=\u0026#34;Configuration for memory leak analysis\u0026#34;\u0026gt; \u0026lt;!-- 启用旧对象采样 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.OldObjectSample\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;cutoff\u0026#34;\u0026gt;infinity\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;!-- 启用 GC 相关事件 --\u0026gt; \u0026lt;event name=\u0026#34;jdk.GCPhase\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 使用方式：\n# 启动记录时指定 path-to-gc-roots=true（在记录级别配置中） -XX:StartFlightRecording=settings=memory-leak.jfc,path-to-gc-roots=true 5.9. 最佳实践 # 生产环境：\n使用 default.jfc 或基于它创建的低开销配置 禁用高开销事件（如 ExecutionSample、ObjectAllocationSample） 提高事件阈值，减少事件数量 禁用不必要的堆栈跟踪 问题诊断：\n使用 profile.jfc 或基于它创建的配置 启用相关事件和堆栈跟踪 根据需要调整采样频率和阈值 配置管理：\n将自定义 JFC 文件纳入版本控制 为不同场景创建不同的配置文件 使用 jfr configure 命令创建和修改配置，避免手动编辑 XML 配置验证：\n使用 jfr configure --input \u0026lt;file\u0026gt; 验证配置文件格式 在测试环境验证配置效果后再用于生产环境 6. 通过 JDK API 使用 JFR # JDK 提供了完整的 JFR API，允许在应用程序中直接控制 JFR 记录、创建自定义事件、读取和分析 JFR 数据。JFR API 位于 jdk.jfr 包中，主要包含以下核心类：\njdk.jfr.Recording：用于创建、配置、启动和停止 JFR 记录 jdk.jfr.FlightRecorder：用于访问 Flight Recorder 实例和管理记录 jdk.jfr.Configuration：用于加载和管理 JFC 配置文件 jdk.jfr.Event：用于定义自定义事件 jdk.jfr.consumer.*：用于读取和分析 JFR 数据 6.1. 基本 API 类 # 6.1.1. FlightRecorder 类 # FlightRecorder 是访问 Flight Recorder 实例的入口点。\n主要方法：\nstatic FlightRecorder getFlightRecorder()：获取 Flight Recorder 实例 List\u0026lt;Recording\u0026gt; getRecordings()：获取所有正在运行的记录 Recording takeSnapshot()：创建所有记录数据的快照 static void register(Class\u0026lt;? extends Event\u0026gt; eventClass)：注册自定义事件类 static void unregister(Class\u0026lt;? extends Event\u0026gt; eventClass)：注销事件类 static boolean isAvailable()：检查 JFR 是否可用 static boolean isInitialized()：检查 JFR 是否已初始化 使用示例：\nimport jdk.jfr.FlightRecorder; import jdk.jfr.Recording; // 检查 JFR 是否可用 if (!FlightRecorder.isAvailable()) { System.out.println(\u0026#34;JFR is not available\u0026#34;); return; } // 获取 Flight Recorder 实例 FlightRecorder recorder = FlightRecorder.getFlightRecorder(); // 获取所有记录 List\u0026lt;Recording\u0026gt; recordings = recorder.getRecordings(); System.out.println(\u0026#34;Active recordings: \u0026#34; + recordings.size()); // 创建快照 Recording snapshot = recorder.takeSnapshot(); 6.1.2. Recording 类 # Recording 类用于创建和管理 JFR 记录。\n记录状态（RecordingState 枚举）：\nNEW：初始状态，记录已创建但未启动 DELAYED：已调度延迟启动 RUNNING：正在运行 STOPPED：已停止，数据仍可用 CLOSED：已关闭，资源已释放 主要方法：\nvoid start()：启动记录 void scheduleStart(Duration delay)：延迟启动 boolean stop()：停止记录 void close()：关闭记录并释放资源 void dump(Path destination)：转储记录数据到文件 Recording copy(boolean stop)：创建记录的副本 void setSettings(Map\u0026lt;String, String\u0026gt; settings)：设置事件配置 Map\u0026lt;String, String\u0026gt; getSettings()：获取当前配置 void setName(String name)：设置记录名称 void setDestination(Path destination)：设置停止时转储的目标路径 void setMaxAge(Duration maxAge)：设置最大保留时间 void setMaxSize(long maxSize)：设置最大大小 RecordingState getState()：获取记录状态 6.2. 创建和启动记录 # 6.2.1. 使用默认配置创建记录 # import jdk.jfr.Recording; import java.nio.file.Path; import java.nio.file.Paths; // 创建记录（使用默认配置） Recording recording = new Recording(); // 设置记录名称 recording.setName(\u0026#34;My Recording\u0026#34;); // 启动记录 recording.start(); // ... 应用程序运行 ... // 停止记录 recording.stop(); // 转储到文件 Path destination = Paths.get(\u0026#34;recording.jfr\u0026#34;); recording.dump(destination); // 关闭记录 recording.close(); 6.2.2. 使用预定义配置创建记录 # import jdk.jfr.Configuration; import jdk.jfr.Recording; import java.io.IOException; import java.text.ParseException; try { // 加载预定义配置 Configuration config = Configuration.getConfiguration(\u0026#34;default\u0026#34;); // 使用配置创建记录 Recording recording = new Recording(config); recording.setName(\u0026#34;Default Configuration Recording\u0026#34;); recording.start(); // ... 应用程序运行 ... recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); } catch (IOException | ParseException e) { e.printStackTrace(); } 6.2.3. 使用自定义配置创建记录 # import jdk.jfr.Configuration; import jdk.jfr.Recording; import java.nio.file.Path; import java.nio.file.Paths; import java.io.IOException; import java.text.ParseException; try { // 从文件加载配置 Path configPath = Paths.get(\u0026#34;custom.jfc\u0026#34;); Configuration config = Configuration.create(configPath); // 使用配置创建记录 Recording recording = new Recording(config); recording.setName(\u0026#34;Custom Configuration Recording\u0026#34;); recording.start(); // ... 应用程序运行 ... recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); } catch (IOException | ParseException e) { e.printStackTrace(); } 6.2.4. 使用 Map 配置创建记录 # import jdk.jfr.Recording; import java.util.HashMap; import java.util.Map; // 创建配置 Map Map\u0026lt;String, String\u0026gt; settings = new HashMap\u0026lt;\u0026gt;(); settings.put(\u0026#34;jdk.ThreadSleep#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#threshold\u0026#34;, \u0026#34;20 ms\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#period\u0026#34;, \u0026#34;1 s\u0026#34;); // 使用配置创建记录 Recording recording = new Recording(settings); recording.setName(\u0026#34;Custom Settings Recording\u0026#34;); recording.start(); // ... 应用程序运行 ... recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); 6.3. 配置记录 # 6.3.1. 使用 EventSettings API 配置事件 # import jdk.jfr.Recording; import java.time.Duration; Recording recording = new Recording(); // 启用事件并配置 recording.enable(\u0026#34;jdk.ThreadSleep\u0026#34;) .withThreshold(Duration.ofMillis(20)) .withStackTrace(); recording.enable(\u0026#34;jdk.CPULoad\u0026#34;) .withPeriod(Duration.ofSeconds(1)); // 禁用事件 recording.disable(\u0026#34;jdk.ExecutionSample\u0026#34;); recording.start(); // ... 应用程序运行 ... recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); 6.3.2. 使用 Map 配置事件 # import jdk.jfr.Recording; import java.util.HashMap; import java.util.Map; Recording recording = new Recording(); // 获取当前配置 Map\u0026lt;String, String\u0026gt; settings = recording.getSettings(); // 添加或修改配置 settings.put(\u0026#34;jdk.ThreadSleep#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#threshold\u0026#34;, \u0026#34;20 ms\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#stackTrace\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#period\u0026#34;, \u0026#34;1 s\u0026#34;); // 应用配置 recording.setSettings(settings); recording.start(); // ... 应用程序运行 ... recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); 6.3.3. 配置记录选项 # import jdk.jfr.Recording; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; Recording recording = new Recording(); // 设置记录名称 recording.setName(\u0026#34;Production Monitoring\u0026#34;); // 设置最大保留时间（2 天） recording.setMaxAge(Duration.ofDays(2)); // 设置最大大小（500MB） recording.setMaxSize(500 * 1024 * 1024); // 设置停止时转储的目标路径 recording.setDestination(Paths.get(\u0026#34;production-recording.jfr\u0026#34;)); // 启动记录 recording.start(); // ... 应用程序运行 ... // 停止记录（会自动转储到 destination） recording.stop(); recording.close(); 6.4. 延迟启动和自动停止 # import jdk.jfr.Recording; import java.time.Duration; Recording recording = new Recording(); recording.setName(\u0026#34;Delayed Recording\u0026#34;); // 延迟 5 分钟后启动 recording.scheduleStart(Duration.ofMinutes(5)); // 或者立即启动，但设置自动停止时间 recording.start(); // 注意：Recording 类本身不提供自动停止功能 // 需要在单独的线程中等待指定时间后调用 stop() // 在单独线程中自动停止 new Thread(() -\u0026gt; { try { Thread.sleep(Duration.ofMinutes(30).toMillis()); recording.stop(); recording.dump(Paths.get(\u0026#34;recording.jfr\u0026#34;)); recording.close(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); 6.5. 创建记录快照 # import jdk.jfr.FlightRecorder; import jdk.jfr.Recording; import java.nio.file.Paths; FlightRecorder recorder = FlightRecorder.getFlightRecorder(); // 创建所有记录数据的快照 Recording snapshot = recorder.takeSnapshot(); snapshot.setName(\u0026#34;Snapshot\u0026#34;); // 转储快照 snapshot.dump(Paths.get(\u0026#34;snapshot.jfr\u0026#34;)); snapshot.close(); 6.6. 复制记录 # import jdk.jfr.Recording; Recording recording = new Recording(); recording.setName(\u0026#34;Original Recording\u0026#34;); recording.start(); // ... 应用程序运行 ... // 创建记录的副本（不停止原记录） Recording copy = recording.copy(false); copy.setName(\u0026#34;Copy of Original\u0026#34;); copy.dump(Paths.get(\u0026#34;copy.jfr\u0026#34;)); copy.close(); // 原记录继续运行 // ... recording.stop(); recording.dump(Paths.get(\u0026#34;original.jfr\u0026#34;)); recording.close(); 6.7. 创建自定义事件 # 6.7.1. 基本自定义事件 # import jdk.jfr.Event; import jdk.jfr.Label; import jdk.jfr.Description; @Label(\u0026#34;User Action\u0026#34;) @Description(\u0026#34;Records user actions in the application\u0026#34;) public class UserActionEvent extends Event { @Label(\u0026#34;Action Type\u0026#34;) @Description(\u0026#34;Type of user action\u0026#34;) public String actionType; @Label(\u0026#34;User ID\u0026#34;) @Description(\u0026#34;Identifier of the user\u0026#34;) public String userId; @Label(\u0026#34;Timestamp\u0026#34;) @Description(\u0026#34;When the action occurred\u0026#34;) public long timestamp; } // 使用事件 public void performUserAction(String userId, String actionType) { UserActionEvent event = new UserActionEvent(); event.actionType = actionType; event.userId = userId; event.timestamp = System.currentTimeMillis(); event.commit(); // 提交事件 } 6.7.2. 持续时间事件 # import jdk.jfr.Event; import jdk.jfr.Label; import jdk.jfr.Description; @Label(\u0026#34;Database Query\u0026#34;) @Description(\u0026#34;Records database query execution\u0026#34;) public class DatabaseQueryEvent extends Event { @Label(\u0026#34;Query SQL\u0026#34;) public String sql; @Label(\u0026#34;Rows Returned\u0026#34;) public int rowsReturned; public void begin() { super.begin(); } public void end() { super.end(); } } // 使用持续时间事件 public List\u0026lt;Row\u0026gt; executeQuery(String sql) { DatabaseQueryEvent event = new DatabaseQueryEvent(); event.sql = sql; event.begin(); try { List\u0026lt;Row\u0026gt; rows = database.execute(sql); event.rowsReturned = rows.size(); return rows; } finally { event.end(); event.commit(); } } 6.7.3. 使用 shouldCommit() 优化性能 # import jdk.jfr.Event; public class ExpensiveEvent extends Event { public String expensiveData; public void recordExpensiveOperation() { ExpensiveEvent event = new ExpensiveEvent(); // 检查事件是否会被记录 if (event.shouldCommit()) { // 只有会被记录时才执行昂贵的操作 event.expensiveData = computeExpensiveData(); event.commit(); } } private String computeExpensiveData() { // 昂贵的计算操作 return \u0026#34;expensive data\u0026#34;; } } 6.7.4. 注册自定义事件 # import jdk.jfr.FlightRecorder; import jdk.jfr.Event; // 注册自定义事件类（可选，通常会自动注册） FlightRecorder.register(UserActionEvent.class); // 使用事件 UserActionEvent event = new UserActionEvent(); event.actionType = \u0026#34;login\u0026#34;; event.userId = \u0026#34;user123\u0026#34;; event.commit(); 6.8. 读取 JFR 文件 # 6.8.1. 使用 RecordingFile 读取事件 # import jdk.jfr.consumer.RecordingFile; import jdk.jfr.consumer.RecordedEvent; import java.nio.file.Path; import java.nio.file.Paths; import java.io.IOException; Path recordingPath = Paths.get(\u0026#34;recording.jfr\u0026#34;); try (RecordingFile recordingFile = new RecordingFile(recordingPath)) { // 读取所有事件 while (recordingFile.hasMoreEvents()) { RecordedEvent event = recordingFile.readEvent(); // 获取事件信息 String eventName = event.getEventType().getName(); long startTime = event.getStartTime().toEpochMilli(); // 获取事件字段 if (eventName.equals(\u0026#34;jdk.ThreadSleep\u0026#34;)) { Duration duration = event.getDuration(); System.out.println(\u0026#34;Thread sleep: \u0026#34; + duration.toMillis() + \u0026#34; ms\u0026#34;); } else if (eventName.equals(\u0026#34;jdk.CPULoad\u0026#34;)) { double jvmUser = event.getDouble(\u0026#34;jvmUser\u0026#34;); double jvmSystem = event.getDouble(\u0026#34;jvmSystem\u0026#34;); System.out.println(\u0026#34;CPU Load - User: \u0026#34; + jvmUser + \u0026#34;%, System: \u0026#34; + jvmSystem + \u0026#34;%\u0026#34;); } } } catch (IOException e) { e.printStackTrace(); } 6.8.2. 过滤特定事件 # import jdk.jfr.consumer.RecordingFile; import jdk.jfr.consumer.RecordedEvent; import java.nio.file.Paths; import java.io.IOException; try (RecordingFile recordingFile = new RecordingFile(Paths.get(\u0026#34;recording.jfr\u0026#34;))) { while (recordingFile.hasMoreEvents()) { RecordedEvent event = recordingFile.readEvent(); // 只处理特定事件 String eventName = event.getEventType().getName(); if (eventName.equals(\u0026#34;jdk.GarbageCollection\u0026#34;)) { String gcName = event.getString(\u0026#34;name\u0026#34;); Duration duration = event.getDuration(); System.out.println(\u0026#34;GC: \u0026#34; + gcName + \u0026#34;, Duration: \u0026#34; + duration.toMillis() + \u0026#34; ms\u0026#34;); } } } catch (IOException e) { e.printStackTrace(); } 6.8.3. 读取事件类型信息 # import jdk.jfr.consumer.RecordingFile; import jdk.jfr.EventType; import java.nio.file.Paths; import java.io.IOException; import java.util.List; try (RecordingFile recordingFile = new RecordingFile(Paths.get(\u0026#34;recording.jfr\u0026#34;))) { // 获取所有事件类型 List\u0026lt;EventType\u0026gt; eventTypes = recordingFile.readEventTypes(); for (EventType eventType : eventTypes) { System.out.println(\u0026#34;Event: \u0026#34; + eventType.getName()); System.out.println(\u0026#34; Label: \u0026#34; + eventType.getLabel()); System.out.println(\u0026#34; Description: \u0026#34; + eventType.getDescription()); } } catch (IOException e) { e.printStackTrace(); } 6.9. 流式处理（RecordingStream） # RecordingStream（JDK 14+）提供了流式处理 JFR 事件的能力，可以从当前 JVM 实时读取事件。\n6.9.1. 基本流式处理 # import jdk.jfr.consumer.RecordingStream; import jdk.jfr.consumer.RecordedEvent; import java.time.Duration; try (RecordingStream stream = new RecordingStream()) { // 启用事件 stream.enable(\u0026#34;jdk.ThreadSleep\u0026#34;); stream.enable(\u0026#34;jdk.CPULoad\u0026#34;).withPeriod(Duration.ofSeconds(1)); // 注册事件处理器 stream.onEvent(\u0026#34;jdk.ThreadSleep\u0026#34;, event -\u0026gt; { Duration duration = event.getDuration(); System.out.println(\u0026#34;Thread sleep: \u0026#34; + duration.toMillis() + \u0026#34; ms\u0026#34;); }); stream.onEvent(\u0026#34;jdk.CPULoad\u0026#34;, event -\u0026gt; { double jvmUser = event.getDouble(\u0026#34;jvmUser\u0026#34;); System.out.println(\u0026#34;CPU Load: \u0026#34; + jvmUser + \u0026#34;%\u0026#34;); }); // 启动流（异步处理） stream.startAsync(); // 运行一段时间 Thread.sleep(Duration.ofMinutes(5).toMillis()); // 停止流 stream.stop(); } catch (Exception e) { e.printStackTrace(); } 6.9.2. 使用配置创建流 # import jdk.jfr.Configuration; import jdk.jfr.consumer.RecordingStream; import jdk.jfr.consumer.RecordedEvent; import java.io.IOException; import java.text.ParseException; try { // 加载配置 Configuration config = Configuration.getConfiguration(\u0026#34;default\u0026#34;); // 使用配置创建流 try (RecordingStream stream = new RecordingStream(config)) { // 注册所有事件处理器 stream.onEvent(event -\u0026gt; { System.out.println(\u0026#34;Event: \u0026#34; + event.getEventType().getName()); }); // 设置最大保留时间 stream.setMaxAge(Duration.ofMinutes(10)); // 启动流 stream.startAsync(); // 运行 Thread.sleep(Duration.ofMinutes(5).toMillis()); stream.stop(); } } catch (IOException | ParseException e) { e.printStackTrace(); } 6.9.3. 流式处理并转储 # import jdk.jfr.consumer.RecordingStream; import jdk.jfr.consumer.RecordedEvent; import java.nio.file.Paths; import java.time.Duration; try (RecordingStream stream = new RecordingStream()) { stream.enable(\u0026#34;jdk.ThreadSleep\u0026#34;); stream.enable(\u0026#34;jdk.CPULoad\u0026#34;); // 设置最大保留时间 stream.setMaxAge(Duration.ofMinutes(10)); // 注册事件处理器 stream.onEvent(event -\u0026gt; { // 处理事件 System.out.println(\u0026#34;Event: \u0026#34; + event.getEventType().getName()); }); // 启动流 stream.start(); // 运行一段时间 Thread.sleep(Duration.ofMinutes(5).toMillis()); // 转储到文件 stream.dump(Paths.get(\u0026#34;stream-recording.jfr\u0026#34;)); // 停止流 stream.stop(); } catch (Exception e) { e.printStackTrace(); } 6.10. 完整示例 # 以下是一个完整的示例，展示了如何使用 JFR API 进行性能监控：\nimport jdk.jfr.*; import jdk.jfr.consumer.*; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.HashMap; import java.util.Map; public class JFRExample { // 自定义事件 @Label(\u0026#34;Business Operation\u0026#34;) @Description(\u0026#34;Records business operations\u0026#34;) static class BusinessOperationEvent extends Event { @Label(\u0026#34;Operation Name\u0026#34;) String operationName; @Label(\u0026#34;Result\u0026#34;) String result; } public static void main(String[] args) throws Exception { // 1. 创建记录 Recording recording = new Recording(); recording.setName(\u0026#34;Performance Monitoring\u0026#34;); // 2. 配置事件 recording.enable(\u0026#34;jdk.ThreadSleep\u0026#34;) .withThreshold(Duration.ofMillis(20)) .withStackTrace(); recording.enable(\u0026#34;jdk.CPULoad\u0026#34;) .withPeriod(Duration.ofSeconds(1)); // 3. 设置记录选项 recording.setMaxAge(Duration.ofHours(1)); recording.setMaxSize(100 * 1024 * 1024); // 100MB // 4. 启动记录 recording.start(); System.out.println(\u0026#34;Recording started\u0026#34;); // 5. 执行业务逻辑 performBusinessOperations(); // 6. 停止记录 recording.stop(); System.out.println(\u0026#34;Recording stopped\u0026#34;); // 7. 转储记录 Path destination = Paths.get(\u0026#34;performance.jfr\u0026#34;); recording.dump(destination); System.out.println(\u0026#34;Recording dumped to: \u0026#34; + destination); // 8. 读取和分析记录 analyzeRecording(destination); // 9. 关闭记录 recording.close(); } private static void performBusinessOperations() throws InterruptedException { // 模拟业务操作 for (int i = 0; i \u0026lt; 10; i++) { BusinessOperationEvent event = new BusinessOperationEvent(); event.begin(); // 模拟操作 Thread.sleep(100); event.operationName = \u0026#34;Operation \u0026#34; + i; event.result = \u0026#34;Success\u0026#34;; event.end(); event.commit(); } } private static void analyzeRecording(Path recordingPath) { try (RecordingFile recordingFile = new RecordingFile(recordingPath)) { int threadSleepCount = 0; int businessOpCount = 0; while (recordingFile.hasMoreEvents()) { RecordedEvent event = recordingFile.readEvent(); String eventName = event.getEventType().getName(); if (eventName.equals(\u0026#34;jdk.ThreadSleep\u0026#34;)) { threadSleepCount++; } else if (eventName.equals(\u0026#34;Business Operation\u0026#34;)) { businessOpCount++; String operationName = event.getString(\u0026#34;operationName\u0026#34;); System.out.println(\u0026#34;Business Operation: \u0026#34; + operationName); } } System.out.println(\u0026#34;Thread Sleep events: \u0026#34; + threadSleepCount); System.out.println(\u0026#34;Business Operation events: \u0026#34; + businessOpCount); } catch (Exception e) { e.printStackTrace(); } } } 6.11. 注意事项 # 资源管理：Recording 和 RecordingFile 实现了 Closeable 接口，应该使用 try-with-resources 语句确保资源被正确释放\n记录状态：在调用方法前检查记录状态，某些操作只能在特定状态下执行\n性能开销：虽然 JFR 开销很低，但在高频率路径中创建事件对象仍会有开销，使用 shouldCommit() 可以优化性能\n事件字段类型：事件字段只支持特定类型（基本类型、String、Thread、Class），其他类型会被忽略\n线程安全：Recording 类的方法不是线程安全的，如果从多个线程访问，需要同步\n流式处理：RecordingStream 适合实时监控场景，但要注意设置 maxAge 或 maxSize 以避免内存占用过大\n7. 通过 JMX 使用 JFR # JMX（Java Management Extensions）是 Java 平台提供的管理和监控框架。JFR 通过 FlightRecorderMXBean 接口暴露了完整的 JMX 管理接口，允许通过 JMX 远程或本地管理 JFR 记录。\nObjectName：jdk.management.jfr:type=FlightRecorder\n7.1. 获取 FlightRecorderMXBean # 7.1.1. 本地获取（同一 JVM 进程） # import java.lang.management.ManagementFactory; import jdk.management.jfr.FlightRecorderMXBean; // 通过 ManagementFactory 获取（推荐） FlightRecorderMXBean bean = ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class); // 或者通过 MBeanServer 获取 MBeanServer server = ManagementFactory.getPlatformMBeanServer(); ObjectName objectName = new ObjectName(\u0026#34;jdk.management.jfr:type=FlightRecorder\u0026#34;); FlightRecorderMXBean bean = JMX.newMXBeanProxy(server, objectName, FlightRecorderMXBean.class); 7.1.2. 远程获取（不同 JVM 进程） # 方式 1：通过 JMX Service URL 连接\nimport javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.management.JMX; import javax.management.ObjectName; import jdk.management.jfr.FlightRecorderMXBean; // 创建 JMX Service URL String host = \u0026#34;localhost\u0026#34;; int port = 9999; String url = \u0026#34;service:jmx:rmi:///jndi/rmi://\u0026#34; + host + \u0026#34;:\u0026#34; + port + \u0026#34;/jmxrmi\u0026#34;; // 连接并获取 MBeanServerConnection JMXServiceURL jmxURL = new JMXServiceURL(url); JMXConnector connector = JMXConnectorFactory.connect(jmxURL); MBeanServerConnection connection = connector.getMBeanServerConnection(); // 获取 FlightRecorderMXBean ObjectName objectName = new ObjectName(\u0026#34;jdk.management.jfr:type=FlightRecorder\u0026#34;); FlightRecorderMXBean bean = JMX.newMXBeanProxy(connection, objectName, FlightRecorderMXBean.class); 方式 2：通过 Attach API 连接（本地进程）\nimport com.sun.tools.attach.VirtualMachine; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.management.JMX; import javax.management.ObjectName; import jdk.management.jfr.FlightRecorderMXBean; // 通过 PID 附加到目标 JVM String pid = \u0026#34;12345\u0026#34;; VirtualMachine vm = VirtualMachine.attach(pid); // 启动本地管理代理并获取 JMX Service URL String jmxServiceUrl = vm.startLocalManagementAgent(); // 连接并获取 FlightRecorderMXBean JMXServiceURL jmxURL = new JMXServiceURL(jmxServiceUrl); JMXConnector connector = JMXConnectorFactory.connect(jmxURL); MBeanServerConnection connection = connector.getMBeanServerConnection(); ObjectName objectName = new ObjectName(\u0026#34;jdk.management.jfr:type=FlightRecorder\u0026#34;); FlightRecorderMXBean bean = JMX.newMXBeanProxy(connection, objectName, FlightRecorderMXBean.class); 启用远程 JMX 连接：\n目标 JVM 需要启用 JMX 远程连接：\n-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false 7.2. 记录管理 # 7.2.1. 创建记录 # // 创建新记录（不启动） long recordingId = bean.newRecording(); 7.2.2. 启动和停止记录 # // 启动记录 bean.startRecording(recordingId); // 停止记录（返回 true 表示成功停止） boolean stopped = bean.stopRecording(recordingId); 7.2.3. 关闭记录 # // 关闭记录并释放资源 bean.closeRecording(recordingId); 7.2.4. 复制记录 # // 复制记录（不停止原记录） long cloneId = bean.cloneRecording(recordingId, false); // 复制记录并停止副本 long cloneId = bean.cloneRecording(recordingId, true); 7.2.5. 创建快照 # // 创建所有记录数据的快照 long snapshotId = bean.takeSnapshot(); 7.3. 记录配置 # 7.3.1. 记录选项（Recording Options） # 记录选项控制记录的行为（如持续时间、最大大小、转储路径等）。\n支持的选项：\nname：记录名称（String） maxAge：最大保留时间（格式：\u0026quot;2 h\u0026quot;、\u0026quot;24 h\u0026quot;、\u0026quot;2 d\u0026quot;、\u0026quot;0\u0026quot; 表示无限制） maxSize：最大总大小（格式：字节数，如 \u0026quot;1000000000\u0026quot;，\u0026quot;0\u0026quot; 表示无限制） dumpOnExit：JVM 退出时是否转储（\u0026quot;true\u0026quot; 或 \u0026quot;false\u0026quot;） destination：转储文件路径（String，相对路径相对于 JVM 启动目录） disk：是否写入磁盘（\u0026quot;true\u0026quot; 或 \u0026quot;false\u0026quot;） duration：记录持续时间（格式：\u0026quot;60 s\u0026quot;、\u0026quot;10 m\u0026quot;、\u0026quot;4 h\u0026quot;、\u0026quot;0\u0026quot; 表示无限制） 设置记录选项：\nimport java.util.HashMap; import java.util.Map; Map\u0026lt;String, String\u0026gt; options = new HashMap\u0026lt;\u0026gt;(); options.put(\u0026#34;name\u0026#34;, \u0026#34;My Recording\u0026#34;); options.put(\u0026#34;maxAge\u0026#34;, \u0026#34;2 h\u0026#34;); options.put(\u0026#34;maxSize\u0026#34;, \u0026#34;500000000\u0026#34;); // 500MB options.put(\u0026#34;dumpOnExit\u0026#34;, \u0026#34;true\u0026#34;); options.put(\u0026#34;destination\u0026#34;, \u0026#34;/path/to/recording.jfr\u0026#34;); options.put(\u0026#34;disk\u0026#34;, \u0026#34;true\u0026#34;); options.put(\u0026#34;duration\u0026#34;, \u0026#34;1 h\u0026#34;); bean.setRecordingOptions(recordingId, options); 获取记录选项：\nMap\u0026lt;String, String\u0026gt; options = bean.getRecordingOptions(recordingId); System.out.println(\u0026#34;Name: \u0026#34; + options.get(\u0026#34;name\u0026#34;)); System.out.println(\u0026#34;Max Age: \u0026#34; + options.get(\u0026#34;maxAge\u0026#34;)); System.out.println(\u0026#34;Max Size: \u0026#34; + options.get(\u0026#34;maxSize\u0026#34;)); 7.3.2. 记录设置（Recording Settings） # 记录设置控制哪些事件被记录以及如何记录（如事件阈值、采样间隔等）。\n设置格式：\u0026lt;event-name\u0026gt;#\u0026lt;setting-name\u0026gt;=\u0026lt;value\u0026gt;\n设置记录设置：\nMap\u0026lt;String, String\u0026gt; settings = new HashMap\u0026lt;\u0026gt;(); settings.put(\u0026#34;jdk.ThreadSleep#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#threshold\u0026#34;, \u0026#34;20 ms\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#stackTrace\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#period\u0026#34;, \u0026#34;1 s\u0026#34;); bean.setRecordingSettings(recordingId, settings); 获取记录设置：\nMap\u0026lt;String, String\u0026gt; settings = bean.getRecordingSettings(recordingId); for (Map.Entry\u0026lt;String, String\u0026gt; entry : settings.entrySet()) { System.out.println(entry.getKey() + \u0026#34; = \u0026#34; + entry.getValue()); } 7.3.3. 使用预定义配置 # // 使用预定义配置（如 \u0026#34;default\u0026#34; 或 \u0026#34;profile\u0026#34;） bean.setPredefinedConfiguration(recordingId, \u0026#34;default\u0026#34;); 7.3.4. 使用自定义配置（JFC 文件内容） # // 从字符串加载 JFC 配置内容 String jfcContents = \u0026#34;\u0026lt;?xml version=\\\u0026#34;1.0\\\u0026#34; encoding=\\\u0026#34;UTF-8\\\u0026#34;?\u0026gt;\\n\u0026#34; + \u0026#34;\u0026lt;configuration version=\\\u0026#34;2.0\\\u0026#34; label=\\\u0026#34;Custom\\\u0026#34; description=\\\u0026#34;Custom configuration\\\u0026#34;\u0026gt;\\n\u0026#34; + \u0026#34; \u0026lt;event name=\\\u0026#34;jdk.ThreadSleep\\\u0026#34;\u0026gt;\\n\u0026#34; + \u0026#34; \u0026lt;setting name=\\\u0026#34;enabled\\\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt;\\n\u0026#34; + \u0026#34; \u0026lt;setting name=\\\u0026#34;threshold\\\u0026#34;\u0026gt;20 ms\u0026lt;/setting\u0026gt;\\n\u0026#34; + \u0026#34; \u0026lt;/event\u0026gt;\\n\u0026#34; + \u0026#34;\u0026lt;/configuration\u0026gt;\u0026#34;; bean.setConfiguration(recordingId, jfcContents); 7.4. 查询信息 # 7.4.1. 获取所有记录 # import java.util.List; import jdk.management.jfr.RecordingInfo; List\u0026lt;RecordingInfo\u0026gt; recordings = bean.getRecordings(); for (RecordingInfo info : recordings) { System.out.println(\u0026#34;ID: \u0026#34; + info.getId()); System.out.println(\u0026#34;Name: \u0026#34; + info.getName()); System.out.println(\u0026#34;State: \u0026#34; + info.getState()); System.out.println(\u0026#34;Size: \u0026#34; + info.getSize()); System.out.println(\u0026#34;Start Time: \u0026#34; + info.getStartTime()); } RecordingInfo 字段：\nid：记录 ID name：记录名称 state：记录状态（NEW、DELAYED、RUNNING、STOPPED、CLOSED） size：记录大小（字节） startTime：开始时间（毫秒时间戳） stopTime：停止时间（毫秒时间戳） duration：持续时间（秒） maxAge：最大保留时间（秒） maxSize：最大大小（字节） dumpOnExit：是否在退出时转储 toDisk：是否写入磁盘 destination：转储目标路径 settings：事件设置（Map\u0026lt;String, String\u0026gt;） 7.4.2. 获取预定义配置 # import java.util.List; import jdk.management.jfr.ConfigurationInfo; List\u0026lt;ConfigurationInfo\u0026gt; configs = bean.getConfigurations(); for (ConfigurationInfo config : configs) { System.out.println(\u0026#34;Name: \u0026#34; + config.getName()); System.out.println(\u0026#34;Label: \u0026#34; + config.getLabel()); System.out.println(\u0026#34;Description: \u0026#34; + config.getDescription()); } ConfigurationInfo 字段：\nname：配置名称（如 \u0026quot;default\u0026quot;、\u0026quot;profile\u0026quot;） label：显示标签 description：描述信息 provider：提供者（如 \u0026quot;OpenJDK\u0026quot;） contents：JFC 文件内容（XML 字符串） settings：配置的事件设置（Map\u0026lt;String, String\u0026gt;） 7.4.3. 获取事件类型 # import java.util.List; import jdk.management.jfr.EventTypeInfo; List\u0026lt;EventTypeInfo\u0026gt; eventTypes = bean.getEventTypes(); for (EventTypeInfo eventType : eventTypes) { System.out.println(\u0026#34;Name: \u0026#34; + eventType.getName()); System.out.println(\u0026#34;Label: \u0026#34; + eventType.getLabel()); System.out.println(\u0026#34;Description: \u0026#34; + eventType.getDescription()); // 获取事件设置描述符 List\u0026lt;SettingDescriptorInfo\u0026gt; settings = eventType.getSettingDescriptors(); for (SettingDescriptorInfo setting : settings) { System.out.println(\u0026#34; Setting: \u0026#34; + setting.getName() + \u0026#34; (default: \u0026#34; + setting.getDefaultValue() + \u0026#34;)\u0026#34;); } } EventTypeInfo 字段：\nid：事件类型 ID name：事件名称（如 \u0026quot;jdk.ThreadSleep\u0026quot;） label：显示标签 description：描述信息 categoryNames：分类名称列表 settingDescriptors：设置描述符列表 7.5. 流式处理（Streaming） # JMX 提供了流式处理接口，可以从正在运行的记录中实时读取数据。\n7.5.1. 打开流 # import java.util.HashMap; import java.util.Map; import java.time.Instant; // 流选项 Map\u0026lt;String, String\u0026gt; streamOptions = new HashMap\u0026lt;\u0026gt;(); streamOptions.put(\u0026#34;startTime\u0026#34;, \u0026#34;2020-03-17T09:00:00\u0026#34;); // ISO-8601 格式 streamOptions.put(\u0026#34;endTime\u0026#34;, \u0026#34;2020-03-17T10:00:00\u0026#34;); streamOptions.put(\u0026#34;blockSize\u0026#34;, \u0026#34;50000\u0026#34;); // 每次读取的最大字节数 streamOptions.put(\u0026#34;streamVersion\u0026#34;, \u0026#34;1.0\u0026#34;); // 必须为 \u0026#34;1.0\u0026#34; 才能读取正在运行的记录 // 打开流（recordingId=0 表示读取所有记录的数据） long streamId = bean.openStream(recordingId, streamOptions); 流选项：\nstartTime：开始时间（ISO-8601 格式或毫秒时间戳，默认：Instant.MIN） endTime：结束时间（ISO-8601 格式或毫秒时间戳，默认：Instant.MAX） blockSize：每次读取的最大字节数（默认：50000） streamVersion：流版本（\u0026quot;1.0\u0026quot; 表示可以从正在运行的记录读取数据） 注意：\n如果 streamVersion 为 \u0026quot;1.0\u0026quot;，可以从正在运行的记录读取数据 如果未指定 streamVersion，记录必须已停止才能打开流 7.5.2. 读取流数据 # import java.io.FileOutputStream; import java.io.IOException; // 读取流数据并写入文件 try (FileOutputStream fos = new FileOutputStream(\u0026#34;recording.jfr\u0026#34;)) { while (true) { byte[] data = bean.readStream(streamId); if (data == null) { break; // 没有更多数据 } fos.write(data); } } 7.5.3. 关闭流 # bean.closeStream(streamId); 7.6. 转储记录 # // 转储记录到文件（在目标 JVM 的机器上） bean.copyTo(recordingId, \u0026#34;/path/to/recording.jfr\u0026#34;); 注意：如果通过远程 JMX 调用，文件会写入到目标 JVM 运行的机器上，而不是客户端机器。\n7.7. 通知机制 # FlightRecorderMXBean 实现了 NotificationEmitter 接口，可以监听记录状态变化。\nimport javax.management.Notification; import javax.management.NotificationListener; import javax.management.NotificationFilter; // 创建通知监听器 NotificationListener listener = new NotificationListener() { @Override public void handleNotification(Notification notification, Object handback) { System.out.println(\u0026#34;Notification: \u0026#34; + notification.getMessage()); System.out.println(\u0026#34;Type: \u0026#34; + notification.getType()); System.out.println(\u0026#34;User Data: \u0026#34; + notification.getUserData()); } }; // 添加通知监听器 bean.addNotificationListener(listener, null, null); // 移除通知监听器 bean.removeNotificationListener(listener); 通知类型：AttributeChangeNotification.ATTRIBUTE_CHANGE\n通知内容：当记录状态改变时（如启动、停止、关闭），会发送通知。\n7.8. RemoteRecordingStream（JDK 16+） # RemoteRecordingStream 提供了更高级的流式处理接口，类似于本地的 RecordingStream，但可以通过 JMX 远程连接使用。\n7.8.1. 基本使用 # import jdk.management.jfr.RemoteRecordingStream; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.management.MBeanServerConnection; import java.time.Duration; // 连接远程 JVM String host = \u0026#34;localhost\u0026#34;; int port = 9999; String url = \u0026#34;service:jmx:rmi:///jndi/rmi://\u0026#34; + host + \u0026#34;:\u0026#34; + port + \u0026#34;/jmxrmi\u0026#34;; JMXServiceURL jmxURL = new JMXServiceURL(url); JMXConnector connector = JMXConnectorFactory.connect(jmxURL); MBeanServerConnection connection = connector.getMBeanServerConnection(); // 创建远程记录流 try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) { // 启用事件 stream.enable(\u0026#34;jdk.GCPhasePause\u0026#34;).withoutThreshold(); stream.enable(\u0026#34;jdk.CPULoad\u0026#34;).withPeriod(Duration.ofSeconds(1)); // 注册事件处理器 stream.onEvent(\u0026#34;jdk.CPULoad\u0026#34;, event -\u0026gt; { System.out.println(\u0026#34;CPU Load: \u0026#34; + event); }); stream.onEvent(\u0026#34;jdk.GCPhasePause\u0026#34;, event -\u0026gt; { System.out.println(\u0026#34;GC Pause: \u0026#34; + event); }); // 启动流 stream.start(); // 运行一段时间 Thread.sleep(Duration.ofMinutes(5).toMillis()); } 7.8.2. 使用配置 # try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) { // 监听元数据事件以获取配置 stream.onMetadata(metadataEvent -\u0026gt; { for (Configuration config : metadataEvent.getConfigurations()) { if (config.getName().equals(\u0026#34;default\u0026#34;)) { stream.setSettings(config.getSettings()); } } }); stream.onEvent(System.out::println); stream.start(); Thread.sleep(Duration.ofMinutes(5).toMillis()); } 7.8.3. 指定临时目录 # import java.nio.file.Path; import java.nio.file.Paths; Path tempDir = Paths.get(\u0026#34;/tmp/jfr-remote\u0026#34;); try (RemoteRecordingStream stream = new RemoteRecordingStream(connection, tempDir)) { stream.enable(\u0026#34;jdk.ThreadSleep\u0026#34;); stream.onEvent(System.out::println); stream.start(); Thread.sleep(Duration.ofMinutes(5).toMillis()); } 7.9. 完整示例 # 以下是一个完整的示例，展示了如何通过 JMX 管理 JFR 记录：\nimport java.lang.management.ManagementFactory; import java.util.HashMap; import java.util.List; import java.util.Map; import jdk.management.jfr.FlightRecorderMXBean; import jdk.management.jfr.RecordingInfo; public class JFRJMXExample { public static void main(String[] args) throws Exception { // 1. 获取 FlightRecorderMXBean FlightRecorderMXBean bean = ManagementFactory.getPlatformMXBean(FlightRecorderMXBean.class); // 2. 创建新记录 long recordingId = bean.newRecording(); System.out.println(\u0026#34;Created recording: \u0026#34; + recordingId); // 3. 配置记录选项 Map\u0026lt;String, String\u0026gt; options = new HashMap\u0026lt;\u0026gt;(); options.put(\u0026#34;name\u0026#34;, \u0026#34;JMX Recording\u0026#34;); options.put(\u0026#34;maxAge\u0026#34;, \u0026#34;1 h\u0026#34;); options.put(\u0026#34;maxSize\u0026#34;, \u0026#34;100000000\u0026#34;); // 100MB options.put(\u0026#34;dumpOnExit\u0026#34;, \u0026#34;true\u0026#34;); options.put(\u0026#34;destination\u0026#34;, \u0026#34;jmx-recording.jfr\u0026#34;); options.put(\u0026#34;disk\u0026#34;, \u0026#34;true\u0026#34;); options.put(\u0026#34;duration\u0026#34;, \u0026#34;30 m\u0026#34;); bean.setRecordingOptions(recordingId, options); // 4. 配置记录设置 Map\u0026lt;String, String\u0026gt; settings = new HashMap\u0026lt;\u0026gt;(); settings.put(\u0026#34;jdk.ThreadSleep#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#threshold\u0026#34;, \u0026#34;20 ms\u0026#34;); settings.put(\u0026#34;jdk.ThreadSleep#stackTrace\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#enabled\u0026#34;, \u0026#34;true\u0026#34;); settings.put(\u0026#34;jdk.CPULoad#period\u0026#34;, \u0026#34;1 s\u0026#34;); bean.setRecordingSettings(recordingId, settings); // 5. 启动记录 bean.startRecording(recordingId); System.out.println(\u0026#34;Recording started\u0026#34;); // 6. 查询记录信息 List\u0026lt;RecordingInfo\u0026gt; recordings = bean.getRecordings(); for (RecordingInfo info : recordings) { if (info.getId() == recordingId) { System.out.println(\u0026#34;Recording Name: \u0026#34; + info.getName()); System.out.println(\u0026#34;Recording State: \u0026#34; + info.getState()); System.out.println(\u0026#34;Recording Size: \u0026#34; + info.getSize()); } } // 7. 运行一段时间 Thread.sleep(60000); // 1 分钟 // 8. 停止记录 bean.stopRecording(recordingId); System.out.println(\u0026#34;Recording stopped\u0026#34;); // 9. 转储记录 bean.copyTo(recordingId, \u0026#34;jmx-recording.jfr\u0026#34;); System.out.println(\u0026#34;Recording dumped to jmx-recording.jfr\u0026#34;); // 10. 关闭记录 bean.closeRecording(recordingId); System.out.println(\u0026#34;Recording closed\u0026#34;); } } 7.10. 注意事项 # 远程文件路径：通过远程 JMX 调用 copyTo() 时，文件会写入到目标 JVM 运行的机器上，而不是客户端机器。\n流版本：要从正在运行的记录读取数据，必须设置 streamVersion=\u0026quot;1.0\u0026quot;。\n资源管理：使用完流后，应该调用 closeStream() 释放资源。\n通知监听器：如果不再需要监听通知，应该移除监听器以避免内存泄漏。\n异常处理：JMX 操作可能抛出 IOException、IllegalArgumentException、IllegalStateException 等异常，需要适当处理。\n连接管理：使用远程 JMX 时，需要管理 JMXConnector 的生命周期，使用完后应该关闭连接。\n权限要求：远程 JMX 连接可能需要配置认证和 SSL，具体取决于目标 JVM 的配置。\nRemoteRecordingStream：RemoteRecordingStream 实现了 AutoCloseable 接口，应该使用 try-with-resources 语句确保资源被正确释放。\n8. 通过 jfr 工具分析 JFR 文件 # jfr 是 JDK 提供的命令行工具，用于打印、分析和操作 JFR 记录文件（.jfr）。jfr 工具的主要功能是将二进制格式的 JFR 文件转换为人类可读的格式，并提供过滤、汇总、清理、合并和拆分等功能。\n基本语法：\njfr \u0026lt;command\u0026gt; [options] [file] 查看帮助信息：\n# 查看所有可用命令 jfr help # 查看特定命令的帮助 jfr help print jfr help view jfr help summary jfr help metadata jfr help scrub jfr help assemble jfr help disassemble jfr help configure # 查看版本信息 jfr version # 或 jfr --version 8.1. jfr print（打印事件） # jfr print 命令用于将 JFR 记录文件的内容打印到标准输出。\n语法：\njfr print [--xml|--json|--exact] [--categories \u0026lt;filter\u0026gt;] [--events \u0026lt;filter\u0026gt;] [--stack-depth \u0026lt;depth\u0026gt;] \u0026lt;file\u0026gt; 选项：\n--xml：以 XML 格式打印记录 --json：以 JSON 格式打印记录 --exact：以完整精度打印数字和时间戳 --categories \u0026lt;filter\u0026gt;：选择匹配分类名称的事件 过滤器是逗号分隔的名称列表，支持简单名称、限定名称和引用的 glob 模式 示例：--categories \u0026quot;GC,JVM,Java*\u0026quot; --events \u0026lt;filter\u0026gt;：选择匹配事件名称的事件 过滤器是逗号分隔的名称列表，支持简单名称、限定名称和引用的 glob 模式 示例：--events \u0026quot;jdk.ThreadSleep,jdk.CPULoad\u0026quot;、--events \u0026quot;jdk.*\u0026quot; --stack-depth \u0026lt;depth\u0026gt;：堆栈跟踪的帧数（默认：5） 默认格式：如果不指定 --xml 或 --json，默认使用人类可读格式。\n过滤器说明：\n过滤器可以基于事件的符号名称（通过 @Name 注解设置）或分类名称（通过 @Category 注解设置） 如果使用多个过滤器，会包含所有匹配的事件（并集） 如果同时使用分类过滤器和事件过滤器，选择的事件是两个过滤器的并集 如果不使用过滤器，会打印所有事件 使用示例：\n# 打印所有事件（默认格式） jfr print recording.jfr # 打印特定事件 jfr print --events jdk.ThreadSleep recording.jfr # 打印多个事件 jfr print --events CPULoad,GarbageCollection recording.jfr # 打印特定分类的事件 jfr print --categories \u0026#34;GC,JVM,Java*\u0026#34; recording.jfr # 同时使用分类和事件过滤器 jfr print --categories GC --events CPULoad recording.jfr # 使用 glob 模式 jfr print --events \u0026#34;jdk.*\u0026#34; --stack-depth 64 recording.jfr # 以 JSON 格式输出 jfr print --json --events CPULoad recording.jfr # 以 XML 格式输出 jfr print --xml --events jdk.ThreadSleep recording.jfr # 使用完整精度 jfr print --exact --events \u0026#34;jdk.*\u0026#34; --stack-depth 64 recording.jfr 输出格式说明：\n人类可读格式（默认）：格式化的事件值，例如带有 @Percentage 注解的字段值 0.52 会显示为 52% XML 格式：机器可读的 XML 格式，可以进一步解析或处理 JSON 格式：机器可读的 JSON 格式，可以进一步解析或处理 堆栈跟踪：默认情况下，堆栈跟踪被截断为 5 帧，可以通过 --stack-depth 选项增加或减少。\n8.2. jfr view（查看聚合视图） # jfr view 命令用于以预定义的视图格式聚合和显示事件数据。\n语法：\njfr view [--verbose] [--width \u0026lt;integer\u0026gt;] [--truncate \u0026lt;mode\u0026gt;] [--cell-height \u0026lt;integer\u0026gt;] \u0026lt;view\u0026gt; \u0026lt;file\u0026gt; 选项：\n--verbose：显示构成视图的查询信息 --width \u0026lt;integer\u0026gt;：视图的宽度（字符数，默认值取决于视图） --truncate \u0026lt;mode\u0026gt;：如何截断超出表格单元格空间的内容 beginning：从开头截断 end：从结尾截断（默认） --cell-height \u0026lt;integer\u0026gt;：表格单元格的最大行数（默认值取决于视图） 视图参数：\n\u0026lt;view\u0026gt;（必需）：视图名称或事件类型名称 预定义视图：gc、hot-methods、allocation-by-class、contention-by-site 等 事件类型：jdk.GarbageCollection、jdk.ThreadStart 等 特殊值： types：列出所有可用的事件类型 all-views：显示所有预定义视图 all-events：显示所有事件 预定义视图分类：\nJVM 视图（jvm.*）：\ngc：垃圾回收统计 gc-pauses：GC 暂停统计 gc-configuration：GC 配置信息 gc-parallel-phases：并行 GC 阶段 gc-concurrent-phases：并发 GC 阶段 gc-pause-phases：GC 暂停阶段 gc-references：GC 引用统计 gc-allocation-trigger：GC 分配触发 gc-cpu-time：GC CPU 时间 heap-configuration：堆配置 compiler-configuration：编译器配置 compiler-statistics：编译器统计 compiler-phases：编译器阶段 longest-compilations：最长编译 safepoints：安全点 vm-operations：VM 操作 deoptimizations-by-reason：按原因的反优化 deoptimizations-by-site：按位置的反优化 class-modifications：类修改 blocked-by-system-gc：被 System.gc() 阻塞 native-memory-committed：已提交的本地内存 native-memory-reserved：保留的本地内存 tlabs：线程本地分配缓冲区 环境视图（environment.*）：\ncpu-load：CPU 负载 cpu-load-samples：CPU 负载样本 cpu-information：CPU 信息 cpu-tsc：CPU 时间戳计数器 system-information：系统信息 system-properties：系统属性 system-processes：系统进程 environment-variables：环境变量 network-utilization：网络利用率 native-libraries：本地库 native-library-failures：本地库加载/卸载失败 container-configuration：容器配置 container-cpu-usage：容器 CPU 使用 container-memory-usage：容器内存使用 container-io-usage：容器 I/O 使用 container-cpu-throttling：容器 CPU 节流 recording：记录信息 active-recordings：活动记录 active-settings：活动设置 jvm-flags：JVM 标志 jvm-information：JVM 信息 events-by-count：按计数的事件类型 events-by-name：按名称的事件类型 应用视图（application.*）：\nhot-methods：热点方法（执行最多的 Java 方法） cpu-time-hot-methods：CPU 时间热点方法 cpu-time-statistics：CPU 时间采样统计 allocation-by-class：按类分配 allocation-by-thread：按线程分配 allocation-by-site：按位置分配 contention-by-thread：按线程的锁竞争 contention-by-class：按锁类的锁竞争 contention-by-site：按位置的锁竞争 contention-by-address：按监视器地址的锁竞争 exception-count：异常统计 exception-by-type：按类型的异常 exception-by-message：按消息的异常 exception-by-site：按位置的异常 memory-leaks-by-class：按类的内存泄漏候选 memory-leaks-by-site：按位置的内存泄漏候选 thread-count：Java 线程统计 thread-allocation：线程分配统计 thread-cpu-load：线程 CPU 负载 thread-start：平台线程启动 pinned-threads：固定的虚拟线程 file-reads-by-path：按路径的文件读取 file-writes-by-path：按路径的文件写入 socket-reads-by-host：按主机的套接字读取 socket-writes-by-host：按主机的套接字写入 class-loaders：类加载器 longest-class-loading：最长的类加载 modules：模块 monitor-inflation：监视器膨胀 native-methods：等待或执行本地方法 object-statistics：占用超过 1% 的对象 finalizers：终结器 deprecated-methods-for-removal：标记为移除的废弃方法 method-timing：方法计时 method-calls：方法调用 latencies-by-type：按类型的延迟 使用示例：\n# 查看 GC 视图 jfr view gc recording.jfr # 查看热点方法视图 jfr view hot-methods recording.jfr # 查看分配视图（按类） jfr view allocation-by-class recording.jfr # 查看锁竞争视图（按位置） jfr view contention-by-site recording.jfr # 查看指定事件类型 jfr view jdk.GarbageCollection recording.jfr # 查看所有可用视图 jfr view all-views recording.jfr # 查看所有事件类型 jfr view types recording.jfr # 查看所有事件 jfr view all-events recording.jfr # 自定义视图参数 jfr view --width 160 hot-methods recording.jfr # 显示视图的查询信息 jfr view --verbose allocation-by-class recording.jfr # 设置截断模式 jfr view --truncate beginning SystemProcess recording.jfr # 设置单元格高度 jfr view --cell-height 10 ThreadStart recording.jfr 注意事项：\n视图查询：每个视图都基于一个查询语句，可以通过 --verbose 选项查看 事件类型：可以直接使用事件类型名称作为视图，会显示该事件类型的所有事件 性能：对于大型记录文件，某些视图可能需要较长时间处理 8.3. jfr summary（摘要统计） # jfr summary 命令用于打印记录的统计信息，例如记录的事件数量和它们使用的磁盘空间。\n语法：\njfr summary \u0026lt;file\u0026gt; 输出内容：\n版本信息：JFR 文件格式版本（如 2.1） Chunks 数量：记录文件包含的 chunk 数量 开始时间：记录开始时间（UTC） 持续时间：记录持续时间（秒） 事件统计表： 事件类型名称 事件数量（Count） 事件大小（Size，字节） 使用示例：\n# 查看记录摘要 jfr summary recording.jfr 输出示例：\nVersion: 2.1 Chunks: 1 Start: 2025-11-17 09:41:00 (UTC) Duration: 165 s Event Type Count Size (bytes) ============================================================= jdk.NativeMethodSample 6992 79112 jdk.GCPhaseParallel 6127 150540 jdk.ModuleExport 1615 19253 jdk.SystemProcess 1032 119761 jdk.NativeLibrary 877 76574 ... 注意事项：\n文件完整性：如果查看的是 repository 中的最后一个文件，可能会提示文件损坏（因为元数据还没有 flush），这是正常的，默认 1 秒执行一次 flush，多试几次就能看到正确的结果 排序：事件统计表按事件数量降序排序 8.4. jfr metadata（元数据信息） # jfr metadata 命令用于显示事件元数据信息，如事件名称、分类和字段布局。\n语法：\njfr metadata [--categories \u0026lt;filter\u0026gt;] [--events \u0026lt;filter\u0026gt;] [\u0026lt;file\u0026gt;] 选项：\n--categories \u0026lt;filter\u0026gt;：选择匹配分类名称的事件 --events \u0026lt;filter\u0026gt;：选择匹配事件名称的事件 \u0026lt;file\u0026gt;（可选）：JFR 记录文件路径 如果省略，使用运行 jfr 工具的 JDK 中的元数据 输出内容：\n事件类型信息： 事件名称、标签、描述 分类名称 字段信息（字段名、类型、标签、描述） 设置描述符（如 enabled、threshold、period 等） 使用示例：\n# 显示所有事件元数据（使用 JDK 中的元数据） jfr metadata # 显示记录文件中的事件元数据 jfr metadata recording.jfr # 显示特定事件的元数据 jfr metadata --events jdk.ThreadStart recording.jfr # 显示多个事件的元数据 jfr metadata --events CPULoad,GarbageCollection recording.jfr # 显示特定分类的事件元数据 jfr metadata --categories \u0026#34;GC,JVM,Java*\u0026#34; recording.jfr # 使用 glob 模式 jfr metadata --events \u0026#34;Thread*\u0026#34; recording.jfr 注意事项：\n元数据来源：如果不指定文件，使用运行 jfr 工具的 JDK 中的元数据；如果指定文件，使用文件中的元数据 字段布局：元数据包含事件的完整字段布局信息，有助于理解事件结构 8.5. jfr scrub（清理记录） # jfr scrub 命令用于从记录文件中移除敏感内容或减少文件大小。\n语法：\njfr scrub [--include-events \u0026lt;filter\u0026gt;] [--exclude-events \u0026lt;filter\u0026gt;] [--include-categories \u0026lt;filter\u0026gt;] [--exclude-categories \u0026lt;filter\u0026gt;] [--include-threads \u0026lt;filter\u0026gt;] [--exclude-threads \u0026lt;filter\u0026gt;] \u0026lt;input-file\u0026gt; [\u0026lt;output-file\u0026gt;] 选项：\n--include-events \u0026lt;filter\u0026gt;：选择匹配事件名称的事件（只保留这些事件） --exclude-events \u0026lt;filter\u0026gt;：排除匹配事件名称的事件（移除这些事件） --include-categories \u0026lt;filter\u0026gt;：选择匹配分类名称的事件（只保留这些事件） --exclude-categories \u0026lt;filter\u0026gt;：排除匹配分类名称的事件（移除这些事件） --include-threads \u0026lt;filter\u0026gt;：选择匹配线程名称的事件（只保留这些线程的事件） --exclude-threads \u0026lt;filter\u0026gt;：排除匹配线程名称的事件（移除这些线程的事件） \u0026lt;input-file\u0026gt;（必需）：输入文件路径 \u0026lt;output-file\u0026gt;（可选）：输出文件路径 如果未指定，会在输入文件路径后追加 -scrubbed 作为输出文件名 过滤器说明：\n过滤器是逗号分隔的名称列表，支持简单名称、限定名称和引用的 glob 模式 如果使用多个过滤器，会按指定顺序应用 include 和 exclude 可以组合使用 使用示例：\n# 只保留套接字相关事件 jfr scrub --include-events \u0026#39;jdk.Socket*\u0026#39; recording.jfr socket-only.jfr # 移除包含密码的环境变量事件 jfr scrub --exclude-events InitialEnvironmentVariable recording.jfr no-psw.jfr # 只保留主线程的事件 jfr scrub --include-threads main recording.jfr # 排除特定线程的事件 jfr scrub --exclude-threads \u0026#39;Foo*\u0026#39; recording.jfr # 只保留特定分类的事件 jfr scrub --include-categories \u0026#39;My App\u0026#39; recording.jfr # 排除特定分类的事件 jfr scrub --exclude-categories JVM,OS recording.jfr # 组合使用多个过滤器 jfr scrub --include-events \u0026#39;jdk.Socket*\u0026#39; --exclude-threads \u0026#39;Worker*\u0026#39; recording.jfr filtered.jfr 输出信息：\n命令执行后会显示：\n清理后的文件路径 移除的事件统计（事件名称和移除的百分比） 注意事项：\n文件大小：清理后的文件通常比原文件小，因为移除了不需要的事件 数据完整性：清理后的文件仍然是有效的 JFR 文件，可以正常使用其他 jfr 命令分析 敏感信息：可以用于移除包含敏感信息的事件（如环境变量、系统属性等） 8.6. jfr assemble（组装记录） # jfr assemble 命令用于将 repository 中的 chunk 文件组装成完整的 JFR 记录文件。\n语法：\njfr assemble \u0026lt;repository\u0026gt; \u0026lt;file\u0026gt; 参数：\n\u0026lt;repository\u0026gt;（必需）：包含 chunk 文件的 repository 目录路径 \u0026lt;file\u0026gt;（必需）：要创建的 JFR 记录文件路径（.jfr 扩展名） 工作原理：\n扫描 repository 目录中的所有 .jfr chunk 文件 按文件名排序（保持时间顺序） 排除未完成的 chunk 文件（.part 文件） 按时间顺序连接所有 chunk 文件 生成完整的 JFR 记录文件 使用场景：\nJVM 崩溃恢复：如果 JVM 崩溃，repository 中的 chunk 文件可以恢复并组装成完整的记录文件 手动组装：从 repository 目录手动创建记录文件 使用示例：\n# 从 repository 目录组装记录文件 jfr assemble ./2025_11_19_17_41_00_3833 assembled-recording.jfr # 从指定路径的 repository 组装 jfr assemble /path/to/repository recording.jfr 注意事项：\nChunk 文件：只会处理完整的 .jfr 文件，.part 文件会被排除 时间顺序：Chunk 文件按文件名排序，确保时间顺序正确 文件完整性：组装后的文件是有效的 JFR 文件，可以正常使用其他 jfr 命令分析 8.7. jfr disassemble（拆分记录） # jfr disassemble 命令用于将 JFR 记录文件拆分成多个较小的文件/chunk。\n语法：\njfr disassemble [--output \u0026lt;directory\u0026gt;] [--max-chunks \u0026lt;chunks\u0026gt;] [--max-size \u0026lt;size\u0026gt;] \u0026lt;file\u0026gt; 选项：\n--output \u0026lt;directory\u0026gt;：输出目录（默认：当前目录） --max-chunks \u0026lt;chunks\u0026gt;：每个拆分文件的最大 chunk 数量（默认：5） Chunk 大小因记录而异，通常约为 15 MB --max-size \u0026lt;size\u0026gt;：每个拆分文件的最大字节数 \u0026lt;file\u0026gt;（必需）：要拆分的 JFR 记录文件路径 工作原理：\n读取记录文件，识别所有 chunk 根据 --max-chunks 或 --max-size 参数将 chunk 分组 为每个分组创建一个新的 JFR 文件 文件命名：\u0026lt;原文件名\u0026gt;_1.jfr、\u0026lt;原文件名\u0026gt;_2.jfr 等 如果 chunk 数量超过 100，文件名会补零以保持时间顺序（如 myfile_001.jfr） 使用场景：\n修复损坏文件：通过移除损坏的 chunk 来修复损坏的记录文件 减小文件大小：将过大的文件拆分成较小的文件，便于传输 分段分析：将大文件拆分成小文件，便于分段分析 使用示例：\n# 拆分记录文件（使用默认设置：每个文件最多 5 个 chunk） jfr disassemble recording.jfr # 指定输出目录 jfr disassemble --output ./split recording.jfr # 指定每个文件的最大 chunk 数量 jfr disassemble --max-chunks 10 recording.jfr # 指定每个文件的最大大小（字节） jfr disassemble --max-size 50000000 recording.jfr # 组合使用多个选项 jfr disassemble --output ./split --max-chunks 3 recording.jfr 输出文件命名：\n如果记录包含 ≤ 100 个 chunk：recording_1.jfr、recording_2.jfr、\u0026hellip; 如果记录包含 \u0026gt; 100 个 chunk：recording_001.jfr、recording_002.jfr、\u0026hellip; 注意事项：\n文件完整性：拆分后的每个文件都是有效的 JFR 文件，可以独立分析 Chunk 边界：拆分在 chunk 边界进行，不会破坏 chunk 的完整性 时间顺序：文件名中的数字保持时间顺序 8.8. jfr configure（配置 JFC 文件） # jfr configure 命令用于创建和编辑 JFC 配置文件。此命令在第 3.5 节中已有详细介绍，这里仅作简要说明。\n语法：\njfr configure [--interactive] [--verbose] [--input \u0026lt;files\u0026gt;] [--output \u0026lt;file\u0026gt;] [option=value]* [event-setting=value]* 主要选项：\n--interactive：交互式模式 --verbose：显示修改的设置 --input \u0026lt;files\u0026gt;：输入 JFC 文件（逗号分隔） --output \u0026lt;file\u0026gt;：输出文件名（默认：custom.jfc） option=value：修改 JFC 选项（如 gc=high） event-setting=value：修改事件设置（如 jdk.ThreadSleep#threshold=50ms） 使用示例：\n# 交互式创建配置（需要 --interactive 参数） jfr configure --interactive --input default.jfc --output custom.jfc # 命令行创建配置 jfr configure --input default.jfc --output custom.jfc \\ jdk.ThreadSleep#threshold=50ms \\ jdk.CPULoad#period=2s 详细说明请参考第 3.5 节。\n8.9. 完整使用流程示例 # 以下是一个完整的使用流程示例：\n# 1. 查看记录摘要 jfr summary recording.jfr # 2. 查看所有可用视图 jfr view all-views recording.jfr # 3. 查看 GC 视图 jfr view gc recording.jfr # 4. 查看热点方法 jfr view hot-methods recording.jfr # 5. 打印特定事件 jfr print --events jdk.ThreadSleep recording.jfr # 6. 以 JSON 格式导出事件 jfr print --json --events jdk.CPULoad recording.jfr \u0026gt; cpuload.json # 7. 查看事件元数据 jfr metadata --events jdk.ThreadSleep recording.jfr # 8. 清理记录（移除敏感信息） jfr scrub --exclude-events InitialEnvironmentVariable recording.jfr cleaned.jfr # 9. 从 repository 组装记录 jfr assemble ./repository assembled.jfr # 10. 拆分大文件 jfr disassemble --max-chunks 5 large-recording.jfr 8.10. 注意事项 # 文件路径：文件路径可以是绝对路径或相对路径（相对于当前工作目录）\n文件格式：输入文件必须是有效的 JFR 文件（.jfr 扩展名）\n过滤器语法：\n支持简单名称：CPULoad 支持限定名称：jdk.CPULoad 支持 glob 模式：jdk.*、Thread* 多个值用逗号分隔：CPULoad,GarbageCollection 包含特殊字符时使用引号：\u0026quot;GC,JVM,Java*\u0026quot; 输出格式：\njfr print 默认输出到标准输出，可以重定向到文件 jfr view 输出到标准输出 jfr scrub、jfr assemble、jfr disassemble 会创建新文件 性能考虑：\n对于大型记录文件，某些操作（如 jfr print、jfr view）可能需要较长时间 使用过滤器可以减少处理的数据量，提高性能 文件完整性：\n如果查看的是 repository 中正在写入的文件，可能会提示文件损坏，这是正常的 等待文件写入完成后再查看，或使用 jfr assemble 组装完整的文件 帮助信息：使用 jfr help \u0026lt;command\u0026gt; 可以查看每个命令的详细帮助信息\n","date":"2025年12月1日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-7-jfr-conf-usage/","section":"文章","summary":"全面解析 JFR（Java Flight Recorder）的配置体系、使用方式和事件采集机制。涵盖 JFR 配置体系（全局配置、记录级别配置、JFC 配置文件）、多种使用方式（JVM 参数、jcmd 命令、JDK API、JMX）、jfr 工具分析、事件类型分类与配置适用性，以及从 JDK 11 到 JDK 25 的核心变化和最佳实践。","title":"全网最硬核 JDK 解析 - 7. JFR 事件采集原理与演进","type":"posts"},{"content":"","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/tags/flight-recorder/","section":"Tags","summary":"","title":"Flight Recorder","type":"tags"},{"content":"","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/categories/jfr/","section":"Categories","summary":"","title":"JFR","type":"categories"},{"content":"","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/tags/memory-leak/","section":"Tags","summary":"","title":"Memory Leak","type":"tags"},{"content":"","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/categories/oom/","section":"Categories","summary":"","title":"OOM","type":"categories"},{"content":"","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/tags/oom/","section":"Tags","summary":"","title":"OOM","type":"tags"},{"content":" 0. 观前提醒 # 上一篇文章我们解析了Heap dump 与错误处理诊断相关演进与最佳实践，其中我们提到了，我们不建议打开 -XX:+HeapDumpOnOutOfMemoryError 选项而是通过 JFR 快速定位 Java 堆 OOM。这一篇我们就来详细讲解一下如何通过 JFR 快速定位 Java 堆 OOM 的实战与底层原理。\n首先需要准备好如下工具或者环境：\nJava 25（其实 Java 21+ 就行，我们为了虚拟线程特性） JDK Mission Control（JMC） 8.3+ Jmeter 代码库：https://github.com/spring-projects/spring-petclinic.git maven 或者 gradle 1. 我们需要模拟哪些场景？ # 我们这里就模拟在打开 -XX:+HeapDumpOnOutOfMemoryError 选项下，Java 堆 OOM 会触发 Heap Dump 的场景：\njava.lang.OutOfMemoryError: Java heap space：Java 对象堆空间耗尽（Java 对象堆 OOM）和 java.lang.OutOfMemoryError: GC overhead limit exceeded：GC 开销超限（Java 对象堆相关 OOM） 场景 1: 某个请求有 bug，导致全表扫描，冲爆了 Java 对象堆内存。抛出了 OutOfMemoryError，但是这是异常情况，可能无法输出堆栈日志，在茫茫众多的请求中很难找到这个请求。 场景 2: 用户累计订单量随着你的系统成熟越来越多，大历史订单量的用户越来越多。之前的代码有 bug，用户订单列表实际是拉取每个用户的所有订单内存分页。可能两个大历史订单量的用户同时查询的时候就会抛出 OutOfMemoryError，就算不抛出也会频繁 GC 影响性能。 场景 3: 某个请求会触发分配一个小对象放入类似于缓存的地方，但是这个小对象一直没有被回收，日积月累导致 FullGC 越来越频繁，最后 OutOfMemoryError。 java.lang.OutOfMemoryError: Metaspace / Compressed class space：元空间耗尽（会触发 Java 对象堆转储，因为加载的类在 Java 对象堆上有 Class 对象） 场景 4: 动态生成类（如 CGLIB、Javassist 等）过多，最终导致元空间耗尽：这个会有更直接的异常展现出来，不会被淹没。所以也不需要 JFR 来定位。 场景 5：本身元空间不足，动态加载类失败：这个也不会被淹没，不需要 JFR 来定位。 java.lang.OutOfMemoryError: Requested array size exceeds VM limit：数组大小超限（会触发 Java 对象堆转储，因为通常表示集合过大） 场景 6: 某个请求有 bug，导致分配了一个超大数组，最终导致数组大小超限：这个也不会被淹没，不需要 JFR 来定位。 我们来分析需要 JFR 定位的场景 1～3\n2. 场景 1: 某个请求有 bug，导致全表扫描，冲爆了 Java 对象堆内存 # git clone 下来代码库后，我们使用 maven 构建。打开项目根目录的 pom.xml，确保 Java 语言级别为 21 及以上的版本：\n修改 application.properties 开启虚拟线程（spring.threads.virtual.enabled=true）\n增加一个会返回大量数据的查询，模拟问题：\npublic interface OwnerRepository extends JpaRepository\u0026lt;Owner, Integer\u0026gt; { // ... 省略其他现有代码 ... @Query( value = \u0026#34;SELECT REPEAT(\u0026#39;A\u0026#39;, :count) as repeated_string\u0026#34;, nativeQuery = true ) String repeatString(@Param(\u0026#34;count\u0026#34;) int count); } 增加一个 Controller 方法调用这个查询：\n@Controller class CrashController { // ... 省略其他现有代码 ... @Autowired private OwnerRepository repository; @ResponseBody @PostMapping(\u0026#34;/large-oom-query\u0026#34;) public String triggerLargeOomQuery() { return repository.repeatString(1024 * 1024 * 1024); } } 之后找到 jmeter 脚本，位于 src/test/jmeter/petclinic.jmx：\n我们将线程数量改为 50，循环次数无限，模拟持续不断的请求的环境，类似于一个不断被访问的线上环境：\n增加 jfc 配置文件，开启一些 JFR 事件，名字命名为 test-oom.jfc：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration version=\u0026#34;2.0\u0026#34;\u0026gt; \u0026lt;event name=\u0026#34;jdk.AllocationRequiringGC\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;gc-enabled-high\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.ZAllocationStall\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34;\u0026gt;0 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 启动 Spring Boot 应用，JVM 参数：\n-XX:+UseG1GC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true -XX:+UseG1GC：使用 G1 垃圾收集器，目前我用的是 JDK 25，默认垃圾收集器就是 G1 -Xmx256m：限制最大堆内存为 256MB，方便我们触发 Java 对象堆 OOM -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./test-oom.jfc：开启 JFR，设置最大大小为 5000MB，最大保存时间为 2 天，使用我们自定义的配置文件 test-oom.jfc，并且启动 JFR 事件的临时文件 chunk -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true：设置 JFR chunk 的最大大小为 128MB，JFR 事件临时文件存储位置为当前目录，堆栈深度为 256，并且保留 JFR 事件临时文件 chunk 启动 jmeter 脚本，模拟持续不断的请求。\n之后，发送请求： POST http://localhost:8080/large-oom-query ，就可以触发 Java 对象堆 OOM，可以从日志中看到大量异常输出，OutOfMemoryError: Java heap space 已经被淹没：\n2025-11-09T10:47:01.322+08:00 INFO 6708 --- [at-handler-2731] [ ] o.h.e.internal.DefaultLoadEventListener : HHH000327: Error performing load command org.hibernate.exception.JDBCConnectionException: JDBC exception executing SQL [The database has been closed [90098-232]] [select o1_0.id,o1_0.address,o1_0.city,o1_0.first_name,o1_0.last_name,o1_0.telephone,p1_0.owner_id,p1_0.id,p1_0.birth_date,p1_0.name,t1_0.id,t1_0.name from owners o1_0 left join pets p1_0 on o1_0.id=p1_0.owner_id left join types t1_0 on t1_0.id=p1_0.type_id where o1_0.id=? order by p1_0.name] at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:49) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:34) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:115) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:304) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:200) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.\u0026lt;init\u0026gt;(JdbcValuesResultSetImpl.java:72) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValues(JdbcSelectExecutorStandardImpl.java:372) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValuesSource(JdbcSelectExecutorStandardImpl.java:332) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:135) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:100) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:64) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:138) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:143) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.ast.internal.SingleIdLoadPlan.load(SingleIdLoadPlan.java:115) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.ast.internal.SingleIdEntityLoaderStandardImpl.load(SingleIdEntityLoaderStandardImpl.java:66) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:3532) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3521) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:596) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.loadFromCacheOrDatasource(DefaultLoadEventListener.java:570) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:554) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:538) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:204) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.loadWithRegularProxy(DefaultLoadEventListener.java:284) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:236) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:109) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:68) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:151) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1287) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1274) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.internal.SessionImpl.load(SessionImpl.java:1256) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.internal.IdentifierLoadAccessImpl.doLoad(IdentifierLoadAccessImpl.java:181) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.internal.IdentifierLoadAccessImpl.lambda$load$1(IdentifierLoadAccessImpl.java:167) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.internal.IdentifierLoadAccessImpl.perform(IdentifierLoadAccessImpl.java:134) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.loader.internal.IdentifierLoadAccessImpl.load(IdentifierLoadAccessImpl.java:167) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2403) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2377) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na] at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:317) ~[spring-orm-7.0.0-M9.jar:7.0.0-M9] at jdk.proxy2/jdk.proxy2.$Proxy166.find(Unknown Source) ~[na:na] at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById(SimpleJpaRepository.java:344) ~[spring-data-jpa-4.0.0-M6.jar:4.0.0-M6] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na] at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:278) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:169) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:545) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:290) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:708) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:171) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:146) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:69) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:369) ~[spring-tx-7.0.0-M9.jar:7.0.0-M9] at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-7.0.0-M9.jar:7.0.0-M9] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:135) ~[spring-tx-7.0.0-M9.jar:7.0.0-M9] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:167) ~[spring-data-jpa-4.0.0-M6.jar:4.0.0-M6] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.data.util.NullnessMethodInvocationValidator.invoke(NullnessMethodInvocationValidator.java:99) ~[spring-data-commons-4.0.0-M6.jar:4.0.0-M6] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:222) ~[spring-aop-7.0.0-M9.jar:7.0.0-M9] at jdk.proxy2/jdk.proxy2.$Proxy176.findById(Unknown Source) ~[na:na] at org.springframework.samples.petclinic.owner.PetController.findOwner(PetController.java:69) ~[classes/:na] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:565) ~[na:na] at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:256) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.method.annotation.ModelFactory.invokeModelAttributeMethods(ModelFactory.java:142) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:111) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:918) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:852) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:86) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:866) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:892) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[tomcat-embed-core-11.0.11.jar:6.1] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:874) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710) ~[tomcat-embed-core-11.0.11.jar:6.1] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:130) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-11.0.11.jar:11.0.11] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.springframework.web.servlet.resource.ResourceUrlEncodingFilter.doFilter(ResourceUrlEncodingFilter.java:66) ~[spring-webmvc-7.0.0-M9.jar:7.0.0-M9] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:110) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:199) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-7.0.0-M9.jar:7.0.0-M9] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:79) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:396) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1780) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-11.0.11.jar:11.0.11] at java.base/java.lang.VirtualThread.run(VirtualThread.java:456) ~[na:na] Caused by: org.h2.jdbc.JdbcSQLNonTransientConnectionException: The database has been closed [90098-232] at org.h2.message.DbException.getJdbcSQLException(DbException.java:690) ~[h2-2.3.232.jar:2.3.232] at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) ~[h2-2.3.232.jar:2.3.232] at org.h2.message.DbException.get(DbException.java:212) ~[h2-2.3.232.jar:2.3.232] at org.h2.engine.SessionLocal.getTransaction(SessionLocal.java:1616) ~[h2-2.3.232.jar:2.3.232] at org.h2.engine.SessionLocal.startStatementWithinTransaction(SessionLocal.java:1637) ~[h2-2.3.232.jar:2.3.232] at org.h2.command.Command.executeQuery(Command.java:190) ~[h2-2.3.232.jar:2.3.232] at org.h2.jdbc.JdbcPreparedStatement.executeQuery(JdbcPreparedStatement.java:130) ~[h2-2.3.232.jar:2.3.232] at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52) ~[HikariCP-7.0.2.jar:na] at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java) ~[HikariCP-7.0.2.jar:na] at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:285) ~[hibernate-core-7.1.1.Final.jar:7.1.1.Final] ... 110 common frames omitted ...... 大量日志省略 先不要停止进程，我们将 JFR 事件本地 chunk 临时文件复制出来，目录结构类似于：\n- 2025_11_09_10_54_11_7246 ｜ 2025_11_09_10_54_11.jfr 使用 JMC 打开这个 jfr 文件，查看 Allocation Requiring GC 事件，按照大小倒序排列，可以找到到最大的对象分配请求（不论分配是否成功，都会在分配前生成这个事件）：\n我们再试试改成用 ZGC：\n-XX:+UseZGC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 由于 ZGC 不会有 Allocation Requiring GC 事件，我们可以查看 ZAllocationStall 事件（这个事件机制类似，不论分配是否成功，都会在分配前生成这个事件），按照大小倒序排列，同样可以找到到最大的对象分配请求：\n可以看出，针对情况一，我们可以通过 JFR 快速定位到触发 Java 对象堆 OOM 的请求，进而定位到代码位置进行修复，而不需要依赖于 -XX:+HeapDumpOnOutOfMemoryError 选项生成的 Heap Dump 文件进行离线分析。\n3. 场景 2: 用户累计订单量随着你的系统成熟越来越多，大历史订单量的用户越来越多 # 增加一个新的 Controller 方法，模拟大历史订单量的用户查询：\n@ResponseBody @GetMapping(\u0026#34;/large-user-query\u0026#34;) public String largeUserQuery() { return repository.repeatString(32 * 1024 * 1024); } 然后修改 jmeter 脚本，增加一个新的请求，模拟大量用户访问这个接口：\n启动 Spring Boot 应用，JVM 参数同上：\n-XX:+UseG1GC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 启动 jmeter 脚本，模拟持续不断的请求。一段时间就会看到 Java 对象堆 OOM，可以从日志中看到大量异常输出，OutOfMemoryError: Java heap space 已经被淹没。\n同样的，将 JFR 事件本地 chunk 临时文件复制出来，使用 JMC 打开这个 jfr 文件，查看 Allocation Requiring GC 事件，按照大小倒序排列。针对这个场景，可以找到大量的 Allocation Requiring GC 事件，对象大小大概是 32MB 左右，对应我们模拟的请求：\n再试试改成用 ZGC：\n-XX:+UseZGC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 由于 ZGC 不会有 Allocation Requiring GC 事件，我们可以查看 ZAllocationStall 事件，按照大小倒序排列，同样可以找到大量的 ZAllocationStall 事件，对象大小大概是 32MB 左右，对应我们模拟的请求：\n可以看出，针对情况二，我们同样可以通过 JFR 快速定位到触发 Java 对象堆 OOM 的请求，进而定位到代码位置进行修复，而不需要依赖于 -XX:+HeapDumpOnOutOfMemoryError 选项生成的 Heap Dump 文件进行离线分析。\n4. 场景 3: 某个请求会触发分配一个小对象放入类似于缓存的地方，但是这个小对象一直没有被回收 # 这个缓存，一般是一个容器，比如 Map、List、Queue 等等。JDK 中的这个内置实现，底层都会涉及数组，并且都是动态扩容的。而且，这个扩容在容器已经比较大的时候一般还是每次扩容到原来的 1.5～2 倍。\n4.1. List 容器扩容实现 # 4.1.1. ArrayList # JDK 11： jdk-jdk-11-28/src/java.base/share/classes/java/util/ArrayList.java\nprivate int newCapacity(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); if (newCapacity - minCapacity \u0026lt;= 0) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) return Math.max(DEFAULT_CAPACITY, minCapacity); if (minCapacity \u0026lt; 0) // overflow throw new OutOfMemoryError(); return minCapacity; } return (newCapacity - MAX_ARRAY_SIZE \u0026lt;= 0) ? newCapacity : hugeCapacity(minCapacity); } JDK 17+： jdk-jdk-17-35/src/java.base/share/classes/java/util/ArrayList.java\nprivate Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity \u0026gt; 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity \u0026gt;\u0026gt; 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } 扩容机制：\n扩容倍数：1.5 倍（oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)） 默认初始容量：10 版本差异： JDK 11：直接计算新容量 JDK 17+：使用 ArraysSupport.newLength 统一处理，逻辑相同但代码更统一 4.1.2. Vector # JDK 11： jdk-jdk-11-28/src/java.base/share/classes/java/util/Vector.java\nprivate int newCapacity(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement \u0026gt; 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity \u0026lt;= 0) { if (minCapacity \u0026lt; 0) // overflow throw new OutOfMemoryError(); return minCapacity; } return (newCapacity - MAX_ARRAY_SIZE \u0026lt;= 0) ? newCapacity : hugeCapacity(minCapacity); } 扩容机制：\n扩容策略： 如果 capacityIncrement \u0026gt; 0：按增量扩容（oldCapacity + capacityIncrement） 如果 capacityIncrement \u0026lt;= 0：2 倍扩容（oldCapacity + oldCapacity） 默认初始容量：10 默认 capacityIncrement：0（因此默认是 2 倍扩容） 4.1.3. ArrayDeque # JDK 11： jdk-jdk-11-28/src/java.base/share/classes/java/util/ArrayDeque.java\nprivate void grow(int needed) { // overflow-conscious code final int oldCapacity = elements.length; int newCapacity; // Double capacity if small; else grow by 50% int jump = (oldCapacity \u0026lt; 64) ? (oldCapacity + 2) : (oldCapacity \u0026gt;\u0026gt; 1); if (jump \u0026lt; needed || (newCapacity = (oldCapacity + jump)) - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = newCapacity(needed, jump); final Object[] es = elements = Arrays.copyOf(elements, newCapacity); // Exceptionally, here tail == head needs to be disambiguated if (tail \u0026lt; head || (tail == head \u0026amp;\u0026amp; es[head] != null)) { // wrap around; slide first leg forward to end of array int newSpace = newCapacity - oldCapacity; System.arraycopy(es, head, es, head + newSpace, oldCapacity - head); for (int i = head, to = (head += newSpace); i \u0026lt; to; i++) es[i] = null; } } 扩容机制：\n扩容策略： 容量 \u0026lt; 64：增加 2（oldCapacity + 2） 容量 \u0026gt;= 64：1.5 倍扩容（oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)） 默认初始容量：16 4.1.4. CopyOnWriteArrayList # JDK 11-25： jdk-jdk-11-28/src/java.base/share/classes/java/util/concurrent/CopyOnWriteArrayList.java\npublic boolean add(E e) { synchronized (lock) { Object[] es = getArray(); int len = es.length; es = Arrays.copyOf(es, len + 1); es[len] = e; setArray(es); return true; } } 扩容机制：\n扩容策略：精确扩容，每次只增加 1（len + 1） 特点：写时复制（Copy-On-Write），每次修改操作都会创建新数组 初始容量：0（空数组） 内存影响：由于每次写操作都复制整个数组，频繁写入会导致大量内存分配和 GC 压力 4.2. Queue 容器扩容实现 # 4.2.1. PriorityQueue # JDK 11： jdk-jdk-11-28/src/java.base/share/classes/java/util/PriorityQueue.java\nprivate void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity \u0026lt; 64) ? (oldCapacity + 2) : (oldCapacity \u0026gt;\u0026gt; 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); } JDK 17+： jdk-jdk-17-35/src/java.base/share/classes/java/util/PriorityQueue.java\nprivate void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity \u0026lt; 64 ? oldCapacity + 2 : oldCapacity \u0026gt;\u0026gt; 1 /* preferred growth */); queue = Arrays.copyOf(queue, newCapacity); } 扩容机制：\n扩容策略： 容量 \u0026lt; 64：增加 oldCapacity + 2（接近 2 倍） 容量 \u0026gt;= 64：1.5 倍扩容（oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)） 默认初始容量：11 版本差异： JDK 11：直接计算新容量 JDK 17+：使用 ArraysSupport.newLength 统一处理 4.2.2. PriorityBlockingQueue # JDK 11： jdk-jdk-11-28/src/java.base/share/classes/java/util/concurrent/PriorityBlockingQueue.java\nprivate void tryGrow(Object[] array, int oldCap) { lock.unlock(); // must release and then re-acquire main lock Object[] newArray = null; if (allocationSpinLock == 0 \u0026amp;\u0026amp; ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) { try { int newCap = oldCap + ((oldCap \u0026lt; 64) ? (oldCap + 2) : // grow faster if small (oldCap \u0026gt;\u0026gt; 1)); if (newCap - MAX_ARRAY_SIZE \u0026gt; 0) { // possible overflow int minCap = oldCap + 1; if (minCap \u0026lt; 0 || minCap \u0026gt; MAX_ARRAY_SIZE) throw new OutOfMemoryError(); newCap = MAX_ARRAY_SIZE; } if (newCap \u0026gt; oldCap \u0026amp;\u0026amp; queue == array) newArray = new Object[newCap]; } finally { allocationSpinLock = 0; } } JDK 17+： jdk-jdk-17-35/src/java.base/share/classes/java/util/concurrent/PriorityBlockingQueue.java\nprivate void tryGrow(Object[] array, int oldCap) { lock.unlock(); // must release and then re-acquire main lock Object[] newArray = null; if (allocationSpinLock == 0 \u0026amp;\u0026amp; ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) { try { int growth = (oldCap \u0026lt; 64) ? (oldCap + 2) // grow faster if small : (oldCap \u0026gt;\u0026gt; 1); int newCap = ArraysSupport.newLength(oldCap, 1, growth); if (queue == array) newArray = new Object[newCap]; } finally { allocationSpinLock = 0; } } if (newArray == null) // back off if another thread is allocating Thread.yield(); lock.lock(); if (newArray != null \u0026amp;\u0026amp; queue == array) { queue = newArray; System.arraycopy(array, 0, newArray, 0, oldCap); } } 扩容机制：\n扩容策略：与 PriorityQueue 相同 容量 \u0026lt; 64：增加 oldCapacity + 2 容量 \u0026gt;= 64：1.5 倍扩容 默认初始容量：11 线程安全：使用 CAS 和锁机制保证并发安全 版本差异： JDK 11：直接计算新容量 JDK 17+：使用 ArraysSupport.newLength 统一处理 4.2.3. ArrayBlockingQueue # JDK 11-25： jdk-jdk-11-28/src/java.base/share/classes/java/util/concurrent/ArrayBlockingQueue.java\n扩容机制：\n扩容策略：固定容量，不支持扩容 特点：构造时指定容量，之后容量不可变 用途：有界队列，用于生产者-消费者模式，防止队列无限增长 4.2.4. LinkedBlockingQueue # JDK 11-25： jdk-jdk-11-28/src/java.base/share/classes/java/util/concurrent/LinkedBlockingQueue.java\n扩容机制：\n扩容策略：基于链表实现，不涉及数组扩容 特点：每个元素都是独立的 Node 对象，动态创建和销毁 容量限制：可指定最大容量，默认 Integer.MAX_VALUE（无界） 内存影响：每个节点都有额外的对象头开销，内存利用率低于数组实现 4.2.5. ConcurrentLinkedQueue # 扩容机制：\n扩容策略：基于链表实现，不涉及数组扩容 特点：无锁并发队列，使用 CAS 操作 容量限制：无界队列 4.3. Map 容器扩容实现 # 4.3.1. HashMap # JDK 11-25： jdk-jdk-11-28/src/java.base/share/classes/java/util/HashMap.java\nfinal Node\u0026lt;K,V\u0026gt;[] resize() { Node\u0026lt;K,V\u0026gt;[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap \u0026gt; 0) { if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold } else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({\u0026#34;rawtypes\u0026#34;,\u0026#34;unchecked\u0026#34;}) Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j \u0026lt; oldCap; ++j) { Node\u0026lt;K,V\u0026gt; e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash \u0026amp; (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode\u0026lt;K,V\u0026gt;)e).split(this, newTab, j, oldCap); else { // preserve order Node\u0026lt;K,V\u0026gt; loHead = null, loTail = null; Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; do { next = e.next; if ((e.hash \u0026amp; oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } 扩容机制：\n扩容倍数：2 倍（newCap = oldCap \u0026lt;\u0026lt; 1） 默认初始容量：16 负载因子：0.75 扩容触发条件：size \u0026gt; threshold（threshold = capacity * loadFactor） 4.3.2. LinkedHashMap # LinkedHashMap 继承自 HashMap，扩容机制与 HashMap 相同，都是 2 倍扩容。\n4.3.3. Hashtable # Hashtable 的扩容机制与 HashMap 类似，也是 2 倍扩容，但它是线程安全的。\n4.3.4. ConcurrentHashMap # ConcurrentHashMap 的扩容机制也是 2 倍扩容，但实现更复杂，支持多线程并发扩容。\n4.4. 版本对比总结 # 容器类型 JDK 11 JDK 17+ 扩容策略 默认初始容量 说明 List 容器 ArrayList 直接计算 ArraysSupport.newLength 1.5 倍 10 Vector 直接计算 ArraysSupport.newLength 2 倍（默认）或按增量 10 ArrayDeque 直接计算 直接计算 \u0026lt; 64: +2, \u0026gt;= 64: 1.5 倍 16 CopyOnWriteArrayList 精确扩容 精确扩容 每次 +1 0 写时复制，每次写操作都创建新数组 LinkedList 链表实现 链表实现 不涉及数组扩容 - 基于链表，动态创建节点 Queue 容器 PriorityQueue 直接计算 ArraysSupport.newLength \u0026lt; 64: +oldCap+2, \u0026gt;= 64: 1.5 倍 11 PriorityBlockingQueue 直接计算 ArraysSupport.newLength \u0026lt; 64: +oldCap+2, \u0026gt;= 64: 1.5 倍 11 线程安全版本 ArrayBlockingQueue 固定容量 固定容量 不支持扩容 构造时指定 有界队列 LinkedBlockingQueue 链表实现 链表实现 不涉及数组扩容 Integer.MAX_VALUE 基于链表 ConcurrentLinkedQueue 链表实现 链表实现 不涉及数组扩容 无界 无锁并发队列 Map 容器 HashMap 直接计算 直接计算 2 倍 16 LinkedHashMap 继承 HashMap 继承 HashMap 2 倍 16 Hashtable 直接计算 直接计算 2 倍 11 ConcurrentHashMap 直接计算 直接计算 2 倍 16 支持并发扩容 版本演进特点：\nJDK 17+：ArrayList、Vector、PriorityQueue、PriorityBlockingQueue 使用 ArraysSupport.newLength 统一处理扩容计算，提高了代码复用性和可维护性 扩容倍数： 大多数容器采用 2 倍扩容（HashMap、Hashtable、ConcurrentHashMap 等） ArrayList 和 ArrayDeque（大容量时）、PriorityQueue（大容量时）采用 1.5 倍扩容 CopyOnWriteArrayList 采用精确扩容（每次 +1），但每次写操作都复制整个数组 特殊容器： ArrayBlockingQueue：固定容量，不支持扩容 LinkedList、LinkedBlockingQueue、ConcurrentLinkedQueue：基于链表，不涉及数组扩容 扩容策略：倍数扩容可以减少扩容次数，但会导致内存浪费，特别是在内存泄漏场景下 大部分我们常用的缓存容器，底层实现都有基于数组的动态扩容机制。而在内存泄漏的场景下，这些容器会不断扩容，最终肯定会有比较大的数组扩容请求，被记录到 JFR 事件中，从而帮助我们快速定位问题。\n4.5. 通过 JFR 定位容器扩容触发的 OOM # 继续使用上面的 Spring Boot 应用，增加一个新的 Controller 方法，模拟一个缓存容器不断扩容的场景：\nprivate ConcurrentHashMap cache = new ConcurrentHashMap(); @ResponseBody @GetMapping(\u0026#34;/small-cumulative-task\u0026#34;) public String smallCumulativeTask() { //模拟一个请求，向缓存中放入大量小对象 for (int i = 0; i \u0026lt; 100_000; i++) { cache.put(new Object(), new Object()); } return \u0026#34;OK\u0026#34;; } 然后修改 jmeter 脚本，增加一个新的请求，模拟大量用户访问这个接口： 这次，我们需要修改下 JFR 采集事件配置，增加上 Allocation Outside TLAB 事件（由于本次是小内存持续泄漏，其他种类的并发请求很多，可能扩容的数组那次请求没有触发 Allocation Requiring GC 或者 ZAllocationStall 事件，但是扩容的历程一定能在 Allocation Outside TLAB 中体现出来）：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration version=\u0026#34;2.0\u0026#34;\u0026gt; \u0026lt;event name=\u0026#34;jdk.ObjectAllocationOutsideTLAB\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.AllocationRequiringGC\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34; control=\u0026#34;gc-enabled-high\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.ZAllocationStall\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;threshold\u0026#34;\u0026gt;0 ms\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;/configuration\u0026gt; 启动 Spring Boot 应用，JVM 参数同上：\n-XX:+UseG1GC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 启动 jmeter 脚本，模拟持续不断的请求。一段时间就会看到 Java 对象堆 OOM，并且没有堆栈信息，因为这种容器扩容触发的 OutOfMemoryError 更严重无法恢复。前面的两个场景触发 OutOfMemoryError 异常的一般是大分配请求，抛出 OutOfMemoryError 之后分配就被放弃内存就被释放了。但是这种场景触发 OutOfMemoryError 异常的是大量小对象持续分配，导致容器不断扩容，最终触发 OutOfMemoryError，这种情况下内存无法被释放，最后看到的控制台会类似于下面的：\n同样的，将 JFR 事件本地 chunk 临时文件复制出来，使用 JMC 打开这个 jfr 文件，首先查看 Allocation Requiring GC 事件，按照大小倒序排列，可能找不到明显的线索：\n接着查看 Allocation Outside TLAB 事件，按照大小倒序排列，可以看出明显的扩容请求线索：\n接下来，我们继续试试改成用 ZGC：\n-XX:+UseZGC -Xmx256m -XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2,settings=./test-oom.jfc -XX:FlightRecorderOptions=maxchunksize=128m,repository=./,stackdepth=256,preserve-repository=true 由于 ZGC 不会有 Allocation Requiring GC 事件，我们可以查看 ZAllocationStall 事件，发现也不明显：\n但是查看 Allocation Outside TLAB 事件，按照大小倒序排列，同样可以看到明显的扩容请求线索：\n可以看出，针对情况三，我们同样可以通过 JFR 快速定位到触发 Java 对象堆 OOM 的请求，进而定位到代码位置进行修复，而不需要依赖于 -XX:+HeapDumpOnOutOfMemoryError 选项生成的 Heap Dump 文件进行离线分析。\n5. 总结 # 下面是这三个 JFR 事件的采集时机：\njdk.ObjectAllocationOutsideTLAB：当一个对象分配请求无法在 TLAB（Thread-Local Allocation Buffer）中满足时触发。TLAB 是每个线程专用的一块内存区域，用于快速分配小对象。当一个对象太大或者当前 TLAB 空间不足以容纳该对象时，JVM 会尝试在堆的其他区域分配内存，并触发该事件。 JVM 中 TLAB 原理可以参考我的另一篇文章：TLAB 全面解析。和另外两个事件的区别是，只有分配成功的才会被记录到这个事件中。通过这个事件，可以明显看出 JDK 内部容器的扩容趋势，从而定位到内存泄漏的代码位置。 jdk.AllocationRequiringGC：当一个对象分配请求无法在堆中满足时触发，这通常是因为堆内存不足以容纳该对象。JVM 会尝试触发垃圾回收（GC）以释放内存，然后重新尝试分配。如果 GC 后仍然无法满足分配请求，JVM 会抛出 OutOfMemoryError 异常。这个事件无论是否分配成功都会记录。通过这个事件，可以大概率在非 ZGC、ShenandoahGC 的 GC 情况下看到一次性的大内存请求异常导致的 Java 堆 OOM。 jdk.ZAllocationStall：当使用 ZGC 时，如果一个对象分配请求无法在堆中满足，通常发生在 GC 回收速度不满足分配速度的时候。这个事件无论是否分配成功都会记录。通过这个事件，可以大概率在 ZGC 的 GC 情况下看到一次性的大内存请求异常导致的 Java 堆 OOM。 通过这三个事件，我们可以覆盖大部分 Java 对象堆 OOM 的场景，快速定位到触发 OOM 的代码位置，从而进行修复，而不需要依赖于 -XX:+HeapDumpOnOutOfMemoryError 选项生成的 Heap Dump 文件进行离线分析，极大地提升了排查效率。\n","date":"2025年11月18日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-6-jfr-track-oom/","section":"文章","summary":"深入讲解如何通过 JFR（JDK Flight Recorder）快速定位 Java 堆 OOM 的实战方法与底层原理，涵盖三种典型 OOM 场景的 JFR 事件分析，包括 Allocation Requiring GC、ZAllocationStall、ObjectAllocationOutsideTLAB 等关键事件的采集与分析技巧。","title":"全网最硬核 JDK 解析 - 6. 通过 JFR 快速定位 Java 堆 OOM 实战与底层原理","type":"posts"},{"content":"","date":"2025年11月11日","externalUrl":null,"permalink":"/zh-cn/tags/error-handling/","section":"Tags","summary":"","title":"Error Handling","type":"tags"},{"content":"","date":"2025年11月11日","externalUrl":null,"permalink":"/zh-cn/tags/heapdump/","section":"Tags","summary":"","title":"HeapDump","type":"tags"},{"content":"","date":"2025年11月11日","externalUrl":null,"permalink":"/zh-cn/tags/jvm-flags/","section":"Tags","summary":"","title":"JVM Flags","type":"tags"},{"content":" 0. 观前提醒 # 本文深入分析 JVM 错误处理和诊断相关参数的设计原理、实现机制和版本演进。文章涵盖以下内容：\nHeap Dump 相关参数：HeapDumpOnOutOfMemoryError、HeapDumpBeforeFullGC、HeapDumpAfterFullGC 等 OutOfMemoryError 处理参数：OnOutOfMemoryError、CrashOnOutOfMemoryError、ExitOnOutOfMemoryError 错误日志和诊断参数：ErrorFile、ShowMessageBoxOnError、CreateCoredumpOnCrash 等 其他诊断参数：MaxJavaStackTraceDepth、SelfDestructTimer 本文涉及 JVM 内部实现细节，包括 C++ 源码分析、安全点机制、VM_Operation 机制等。文章中的设计思想和实现原理分析主要基于个人对源码的理解，如有不准确之处，欢迎指正！本文主要基于 JDK 11、17、21、25 的 HotSpot 源码进行分析。\n这些里面我们可能最常见的就是 -XX:+HeapDumpOnOutOfMemoryError，但是无论哪个版本，都不推荐在生产环境开启任何 Heap Dump 参数。后续章节会说明原因，并告诉你如何不依赖这个参数去定位 Java 对象堆 OOM 问题。\n我们最常见的需要定位的错误就是 java.lang.OutOfMemoryError，在引入虚拟线程之后，这个问题会更加明显。原来的时候没有虚拟线程，因为 I/O 阻塞线程，阻塞任务队列后，就卡住了吞吐量。引入虚拟线程之后，I/O 阻塞线程会被挂起，继续调度其他虚拟线程去跑任务，这样就会让吞吐量提升很多。但是目前大部分框架针对这个背压问题以及限流问题还没有形成一个比较成熟的方案，所以在高并发场景下，虚拟线程可能会让内存消耗更快，从而更容易触发 OutOfMemoryError。 并且，发生 OutOfMemoryError 时，JVM 状态已经不健康了，这个 OutOfMemoryError 可能在任意一个触发对象分配的代码中抛出来，但是不论是哪里的 Java 代码，就算是 JDK 内部的 Java 代码也没有通过 catch (Throwable t) 的方式捕获 OutOfMemoryError，这就造成了某些内部状态不一致的问题。比如 JDK 内部的 HashMap，在 put 的时候触发 OutOfMemoryError，这个 put 操作可能会导致 HashMap 内部的数组扩容，而扩容过程中如果发生 OutOfMemoryError，那么这个 HashMap 可能就处于一个不一致的状态，导致后面这个 HashMap 无法正常使用。如果是业务代码，可能会有更严重的问题。发生 OutOfMemoryError 后，我们最安全的做法就是尽快让这个进程退出，避免继续运行导致更多的问题。而 JDK 内部的参数机制正好可以让我们实现这一点。\n1. 概述 # 1.1. 错误处理和诊断参数分类 # JVM 的错误处理和诊断参数可以分为以下几类：\n1.1.1. Heap Dump 相关参数 # 用于在特定时机转储 Java 对象堆内存快照，便于后续分析：\n-XX:+HeapDumpOnOutOfMemoryError：Java 对象堆 OOM 时转储 Java 对象堆 JDK 1.6 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-6280629 默认值：false（JDK 11、17、21、25） -XX:+HeapDumpBeforeFullGC：Full GC 前转储 Java 对象堆 JDK 1.7 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-6797870 默认值：false（JDK 11、17、21、25） -XX:+HeapDumpAfterFullGC：Full GC 后转储 Java 对象堆 JDK 1.7 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-6797870 默认值：false（JDK 11、17、21、25） -XX:HeapDumpPath=\u0026lt;path\u0026gt;：Java 对象堆转储文件路径 JDK 1.6 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-6280629 默认值：NULL（JDK 11、17），nullptr（JDK 21、25） -XX:HeapDumpGzipLevel=\u0026lt;level\u0026gt;：Java 对象堆转储文件压缩级别 JDK 17 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-8260282 默认值：不支持（JDK 11），0（JDK 17、21、25） -XX:FullGCHeapDumpLimit=\u0026lt;count\u0026gt;：Full GC Java 对象堆转储次数限制 JDK 23 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-8321442 默认值：不支持（JDK 11、17、21），0（JDK 25） 1.1.2. OutOfMemoryError 处理参数 # 用于在发生 Java 对象堆 OOM 时执行特定操作：\n-XX:OnOutOfMemoryError=\u0026lt;command\u0026gt;：执行用户定义的命令或脚本 JDK 1.6 引入（不考虑 Back Port） 默认值：\u0026quot;\u0026quot;（空字符串，JDK 11、17、21、25） -XX:+CrashOnOutOfMemoryError：Java 对象堆 OOM 时触发 JVM 崩溃 JDK 9 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-8138745 默认值：false（JDK 11、17、21、25） -XX:+ExitOnOutOfMemoryError：Java 对象堆 OOM 时退出 JVM JDK 9 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-8138745 默认值：false（JDK 11、17、21、25） 1.1.3. 错误日志和诊断参数 # 用于控制错误日志输出和诊断行为：\n-XX:ErrorFile=\u0026lt;file\u0026gt;：错误日志文件路径 JDK 1.6 引入（不考虑 Back Port）：https://bugs.openjdk.org/browse/JDK-6872355 默认值：NULL（JDK 11、17、21、25） -XX:+ShowMessageBoxOnError：在致命错误时显示消息框，这个一般用不到了 JDK 1.4 引入（不考虑 Back Port） 默认值：false（JDK 11、17、21、25） -XX:+CreateCoredumpOnCrash / -XX:-CreateCoredumpOnCrash：在致命错误时创建核心转储 早期版本引入 默认值：true（JDK 11、17、21、25） -XX:+SuppressFatalErrorMessage：抑制致命错误消息 早期版本引入 默认值：false（JDK 11、17、21、25） -XX:OnError=\u0026lt;command\u0026gt;：在致命错误时执行用户定义的命令 早期版本引入 默认值：\u0026quot;\u0026quot;（空字符串，JDK 11、17、21、25） -XX:ErrorLogTimeout=\u0026lt;seconds\u0026gt;：错误日志写入超时时间 早期版本引入 默认值：120（2 分钟，JDK 11、17、21、25） 1.1.4. 其他诊断参数 # -XX:MaxJavaStackTraceDepth=\u0026lt;depth\u0026gt;：Java 异常堆栈跟踪最大深度 早期版本引入 默认值：1024（JDK 11、17、21、25） -XX:SelfDestructTimer=\u0026lt;minutes\u0026gt;：自毁定时器（用于测试） 早期版本引入 默认值：0（关闭，JDK 11、17、21、25） 1.2. 版本演进概览 # 版本 主要特性 关键改进 JDK 11 基础功能 基本的 Heap Dump 和 Java 对象堆 OOM 处理支持 JDK 17 压缩支持 新增 gzip 压缩，改进错误处理 JDK 21 快速退出 使用 _exit 快速退出，C++11 现代化 JDK 25 并行转储 支持并行转储，%p 占位符，栈上分配，FullGC 转储次数限制 2. Heap Dump 相关参数 # 2.1. 为何不推荐开启 Heap Dump 参数？ # 重要提醒：无论哪个版本，都不推荐在生产环境开启任何 Heap Dump 参数。下面会说明原因，后续章节会告诉你如何不依赖这个参数去定位 Java 对象堆 OOM 问题。\n2.1.1. 磁盘 IO 性能限制 # Heap Dump 需要输出到硬盘，而硬盘 IO 速度相对有限：\nHDD：顺序存储 100–200 MB/s SSD：顺序存储 500 MB/s 到数 GB/s 不等 Heap Dump 是顺序写入，但是 Java 对象堆可能非常大，比如 8GB、16GB，甚至更大。如果 Java 对象堆非常大，写入时间会非常长。而且你跑 Java 应用的机器，不可能每台都配备高速 SSD，并且现在云环境、容器环境下，磁盘 IO 可能会被限速和共享。\n2.1.2. 压缩开销 # JDK 17 虽然引入了 gzip 压缩 Java 对象堆的功能，并且 JDK 25 引入了多线程 Heap Dump，可以多线程压缩，但是压缩是 CPU 密集型操作。假设 4 个 CPU，一个经验指标是：\n级别 0/1（最快） 压缩比：2.3×–2.7× 吞吐：1.0–3.0 GB/s 每 GB 耗时：0.33–1.0 s 级别 3 压缩比：2.5×–2.9× 吞吐：0.9–2.2 GB/s 每 GB 耗时：0.45–1.1 s 级别 5（性价比常用） 压缩比：2.7×–3.1× 吞吐：0.7–1.8 GB/s 每 GB 耗时：0.55–1.4 s 级别 6（默认/稳妥） 压缩比：2.8×–3.2× 吞吐：0.6–1.6 GB/s 每 GB 耗时：0.62–1.7 s 级别 7 压缩比：2.9×–3.3× 吞吐：0.45–1.2 GB/s 每 GB 耗时：0.83–2.2 s 级别 8 压缩比：2.9×–3.4× 吞吐：0.35–0.9 GB/s 每 GB 耗时：1.1–2.9 s 级别 9（极致比） 压缩比：3.0×–3.5× 吞吐：0.25–0.7 GB/s 每 GB 耗时：1.4–4.0 s 假设设置级别是 5，4 CPU 的情况下，针对 8GB Java 对象堆，压缩时间大概 4.4–11.2 秒，压缩后文件大概 2.6–3.2 GB，写入 HDD 大概 13–32 秒，一共大概 17.4–43.2 秒。这是理想情况下的估计，实际情况可能更差。\n2.1.3. 执行顺序影响 # 一般线上会用 -XX:OnOutOfMemoryError=xxxxx 指定一个脚本，来做一些让 Java 进程微服务在注册中心下线的行为，因为一般发生 OutOfMemoryError 的进程都是不再健康的，运行业务可能有问题。但是，如果你开启了 HeapDumpOnOutOfMemoryError，那么必须等 HeapDumpOnOutOfMemoryError 执行完才能做 OnOutOfMemoryError 的脚本。\n这意味着：服务无法更及时从注册中心下线，不健康的服务可能继续接收更多的请求。越早执行脚本下线越好，越晚下线可能会影响更多的请求。\n2.2. HeapDumpOnOutOfMemoryError # 2.2.1. 参数说明 # 说明：当发生 java.lang.OutOfMemoryError 时，自动转储 Java 对象堆内存到文件。\n默认：false\n类型：manageable（JDK 11）或 product + MANAGEABLE（JDK 17+），可通过 JMX 动态修改\n举例：-XX:+HeapDumpOnOutOfMemoryError\n2.2.2. 触发场景 # 重要说明：HeapDumpOnOutOfMemoryError 只会在调用 report_java_out_of_memory 的 OOM 类型时触发 Heap Dump。以下类型的 OOM 会触发 Heap Dump：\njava.lang.OutOfMemoryError: Java heap space：Java 对象堆空间耗尽（Java 对象堆 OOM） java.lang.OutOfMemoryError: GC overhead limit exceeded：GC 开销超限（Java 对象堆相关 OOM） java.lang.OutOfMemoryError: Metaspace / Compressed class space：元空间耗尽（会触发 Java 对象堆转储，因为加载的类在 Java 对象堆上有 Class 对象） java.lang.OutOfMemoryError: Requested array size exceeds VM limit：数组大小超限（会触发 Java 对象堆转储，因为通常表示集合过大） 不会触发 Heap Dump 的 OOM 类型：\n线程创建失败（unable to create native thread） Unicode 字符串分配失败 类验证器内存不足 Unsafe 分配失败 反优化时重新分配对象失败（realloc_objects） 可重试分配失败（retry，JDK 17+） 内部 OOM 标记场景（JDK 25+） 详细说明见 2.2.3.1 节。\n2.2.3. 实现机制 # 2.2.3.1. OutOfMemoryError 类型与 Heap Dump 触发关系 # JVM 中定义了多种 OutOfMemoryError 类型，但只有调用 report_java_out_of_memory 的类型才会触发 Heap Dump：\n会触发 Heap Dump 的 OutOfMemoryError（✅）：\njava.lang.OutOfMemoryError: Java heap space - Java 对象堆空间耗尽 java.lang.OutOfMemoryError: GC overhead limit exceeded - GC 开销超限 java.lang.OutOfMemoryError: Metaspace - 元空间耗尽 java.lang.OutOfMemoryError: Compressed class space - 压缩类空间耗尽 java.lang.OutOfMemoryError: Requested array size exceeds VM limit - 数组大小超限 不会触发 Heap Dump 的 OutOfMemoryError（❌）：\njava.lang.OutOfMemoryError: realloc_objects - 反优化时重新分配对象失败（直接抛出，不调用 report_java_out_of_memory） java.lang.OutOfMemoryError: retry（JDK 17+）- 可重试分配失败（内部机制，不调用 report_java_out_of_memory） java.lang.OutOfMemoryError: Java heap space（无堆栈跟踪）（JDK 25+）- 内部 OOM 标记场景（不调用 report_java_out_of_memory） java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached - 线程创建失败（使用 THROW_MSG，不调用 report_java_out_of_memory） java.lang.OutOfMemoryError: could not allocate Unicode string - Unicode 字符串分配失败（使用 THROW_MSG_0，不调用 report_java_out_of_memory） java.lang.OutOfMemoryError（类验证器内存不足） - 类验证过程中内存不足（使用 THROW_MSG_，不调用 report_java_out_of_memory） java.lang.OutOfMemoryError（Unsafe 分配失败） - Unsafe 操作中内存分配失败（使用 THROW_0，不调用 report_java_out_of_memory） 其他内存不足情况（不抛出 OutOfMemoryError）：\nCodeCache 内存不足 - 直接调用 vm_exit_out_of_memory，VM 直接退出，不抛出异常 触发机制：只有调用 report_java_out_of_memory 的 OOM 类型才会检查 HeapDumpOnOutOfMemoryError 标志并触发 Heap Dump。其他 OOM 类型直接抛出异常，不经过 report_java_out_of_memory 函数。\n2.2.3.1.1. 不会触发 Heap Dump 的 OOM 类型源码说明 # 线程创建失败： jdk-jdk-11-28/src/hotspot/share/prims/jvm.cpp\nif (native_thread-\u0026gt;osthread() == NULL) { // 线程创建失败，直接抛出 OutOfMemoryError，不调用 report_java_out_of_memory THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(), os::native_thread_creation_failed_msg()); } Unicode 字符串分配失败： jdk-jdk-11-28/src/hotspot/share/classfile/javaClasses.cpp\n// Unicode 字符串分配失败，直接抛出 OutOfMemoryError THROW_MSG_0(vmSymbols::java_lang_OutOfMemoryError(), \u0026#34;could not allocate Unicode string\u0026#34;); 类验证器内存不足： jdk-jdk-11-28/src/hotspot/share/classfile/verifier.cpp\n// 类验证过程中内存不足，直接抛出 OutOfMemoryError THROW_MSG_(vmSymbols::java_lang_OutOfMemoryError(), message, NULL); Unsafe 分配失败： jdk-jdk-11-28/src/hotspot/share/prims/unsafe.cpp\nu1* class_bytes = NEW_C_HEAP_ARRAY(u1, length, mtInternal); if (class_bytes == NULL) { // Unsafe 操作中内存分配失败，直接抛出 OutOfMemoryError THROW_0(vmSymbols::java_lang_OutOfMemoryError()); } CodeCache 内存不足（不抛出异常）： jdk-jdk-11-28/src/hotspot/share/code/codeBlob.cpp\nif (blob == NULL) { // CodeCache 内存不足，直接退出 VM，不抛出异常 vm_exit_out_of_memory(size, OOM_MALLOC_ERROR, \u0026#34;CodeCache: no room for method handle adapter blob\u0026#34;); } 2.2.3.2. OOM 检测与报告流程图 # flowchart TD A[各种资源分配场景] --\u003e B{资源类型} B --\u003e|Java Heap| C[MemAllocator::check_out_of_memory] B --\u003e|Metaspace| D[Metaspace::allocate] B --\u003e|数组大小| E1[TypeArrayKlass::allocateJDK 11] B --\u003e|数组大小| E2[Klass::check_array_allocation_lengthJDK 17+] B --\u003e|其他资源| F[其他分配路径如: realloc_objects不触发HeapDump] C --\u003e G{分配失败原因} G --\u003e|堆空间不足| H1{可重试分配检查JDK 17+: in_retryable_allocationJDK 25+: is_in_internal_oome_mark} G --\u003e|GC开销超限| I1{可重试分配检查JDK 17+: in_retryable_allocationJDK 25+: is_in_internal_oome_mark} H1 --\u003e|否| H[report_java_out_of_memoryJava heap space] H1 --\u003e|是 JDK 17+| H2[抛出 retry OOM不触发HeapDump] H1 --\u003e|是 JDK 25+| H3[抛出无堆栈跟踪 OOM不触发HeapDump] I1 --\u003e|否| I[report_java_out_of_memoryGC overhead limit exceeded] I1 --\u003e|是 JDK 17+| I2[抛出 retry OOM不触发HeapDump] I1 --\u003e|是 JDK 25+| I3[抛出无堆栈跟踪 OOM不触发HeapDump] D --\u003e J[report_java_out_of_memoryMetaspace或Compressed class space] E1 --\u003e K1[report_java_out_of_memoryRequested array sizeexceeds VM limit] E2 --\u003e K2{可重试分配检查JDK 17+: in_retryable_allocation} K2 --\u003e|否| K[report_java_out_of_memoryRequested array sizeexceeds VM limit] K2 --\u003e|是| K3[抛出 retry OOM不触发HeapDump] H --\u003e L[report_java_out_of_memory函数] I --\u003e L J --\u003e L K --\u003e L K1 --\u003e L L --\u003e M1{Atomic::cmpxchgJDK 11: cmpxchg 1, addr, 0JDK 17+: cmpxchg addr, 0, 1} M1 --\u003e|已报告| Z[跳过] M1 --\u003e|首次报告| N{HeapDumpOnOutOfMemoryError是否启用?} N --\u003e|是| O[打印OOM消息] N --\u003e|否| P[检查OnOutOfMemoryError] O --\u003e Q[HeapDumper::dump_heap_from_oome] Q --\u003e R[HeapDumper::dump_heap true] R --\u003e S1[构建dump文件路径JDK 11-21: os::mallocJDK 25: 栈上分配] S1 --\u003e S2{HeapDumpPathJDK 25: 支持%p占位符} S2 --\u003e T[创建HeapDumper对象] T --\u003e U1[VMThread::execute触发安全点] U1 --\u003e U2[VM_HeapDumper::doit在安全点执行转储] U2 --\u003e U3{压缩JDK 17+: HeapDumpGzipLevel} U3 --\u003e|启用| U4[并行压缩JDK 25: 多线程] U3 --\u003e|禁用| U5[直接写入] U4 --\u003e P U5 --\u003e P P --\u003e V{其他OOM处理选项} V --\u003e W[CrashOnOutOfMemoryError] V --\u003e X[ExitOnOutOfMemoryError] W --\u003e Y1[fatalJDK 11] W --\u003e Y2[report_fatalJDK 17+] X --\u003e AA1[os::exit 3JDK 11-17] X --\u003e AA2[os::_exit 3JDK 21+快速退出] style H fill:#90EE90 style I fill:#90EE90 style J fill:#FFB6C1 style K fill:#FFB6C1 style K1 fill:#FFB6C1 style Q fill:#87CEEB style U2 fill:#87CEEB style H2 fill:#FFE4B5 style I2 fill:#FFE4B5 style K3 fill:#FFE4B5 style H3 fill:#FFE4B5 style I3 fill:#FFE4B5 2.2.3.3. OOM 检测与报告源码分析 # 2.2.3.3.1. Java heap space / GC overhead limit exceeded # jdk-jdk-11-28/src/hotspot/share/gc/shared/memAllocator.cpp\nJDK 11：\nbool MemAllocator::Allocation::check_out_of_memory() { // ... 省略分配失败检查 ... if (!_overhead_limit_exceeded) { // Java 对象堆空间不足，调用 report_java_out_of_memory 触发 Heap Dump report_java_out_of_memory(\u0026#34;Java heap space\u0026#34;); // ... 省略 JVMTI 相关代码 ... THROW_OOP_(Universe::out_of_memory_error_java_heap(), true); } else { // GC 开销超限，调用 report_java_out_of_memory 触发 Heap Dump report_java_out_of_memory(\u0026#34;GC overhead limit exceeded\u0026#34;); // ... 省略 JVMTI 相关代码 ... THROW_OOP_(Universe::out_of_memory_error_gc_overhead_limit(), true); } } JDK 17+：\nbool MemAllocator::Allocation::check_out_of_memory() { // ... 省略分配失败检查 ... const char* message = _overhead_limit_exceeded ? \u0026#34;GC overhead limit exceeded\u0026#34; : \u0026#34;Java heap space\u0026#34;; // JDK 17+ 新增可重试分配检查，避免在可重试场景触发 Heap Dump if (!_thread-\u0026gt;in_retryable_allocation()) { // JDK 17-21 // if (!_thread-\u0026gt;is_in_internal_oome_mark()) { // JDK 25+ report_java_out_of_memory(message); // ... 省略 JVMTI 相关代码 ... THROW_OOP_(exception, true); } else { // 可重试分配失败，抛出 retry OOM（不触发 Heap Dump） THROW_OOP_(Universe::out_of_memory_error_retry(), true); // JDK 17-21 // THROW_OOP_(Universe::out_of_memory_error_java_heap_without_backtrace(), true); // JDK 25+ } } 版本差异：\nJDK 17+：新增 in_retryable_allocation() 检查，可重试分配失败不触发 Heap Dump JDK 25+：使用 is_in_internal_oome_mark() 替代，内部 OOM 标记场景不触发 Heap Dump JDK 25+ 内部 OOM 标记场景说明：\nInternalOOMEMark 是一个 RAII（Resource Acquisition Is Initialization）标记类，用于标识线程处于 JVM 内部操作中的内存分配场景。在这些场景中，如果内存分配失败，不应该触发 Heap Dump 和完整的 OOM 处理流程，因为：发生在 JVM 内部（如反优化、编译、类加载等），不是用户代码直接触发的。用户代码无法捕获这些异常，并且可能可以快速恢复，或者如果是核心操作则可以快速失败退出。\n使用场景：\n反优化时重新分配对象：反优化过程中需要重新分配对象，如果失败，抛出无堆栈跟踪的 OOM 编译时解析字符串常量：JIT 编译过程中解析字符串常量时分配失败 类加载时的数组分配：类加载过程中分配数组失败 JVMCI 相关操作：JVMCI 编译器相关操作中的内存分配失败 在这些场景中，如果分配失败，会抛出 out_of_memory_error_java_heap_without_backtrace()，这是一个无堆栈跟踪的 OOM 异常，不调用 report_java_out_of_memory，因此不会触发 Heap Dump。\n源码示例：\njdk-jdk-25-36/src/hotspot/share/gc/shared/memAllocator.hpp\n// RAII 类，用于标记线程处于内部 OOM 处理场景 // 在这些场景中，OOM 不会传播到用户代码，因此不需要堆栈跟踪 class InternalOOMEMark: public StackObj { explicit InternalOOMEMark(JavaThread* thread) { _outer = thread-\u0026gt;is_in_internal_oome_mark(); thread-\u0026gt;set_is_in_internal_oome_mark(true); // 设置标记 _thread = thread; } ~InternalOOMEMark() { _thread-\u0026gt;set_is_in_internal_oome_mark(_outer); // 恢复标记 } }; jdk-jdk-25-36/src/hotspot/share/runtime/deoptimization.cpp\n// 反优化时重新分配对象，使用 InternalOOMEMark 标记 if (obj == nullptr \u0026amp;\u0026amp; !cache_init_error) { InternalOOMEMark iom(THREAD); // 进入内部 OOM 标记场景 obj = ik-\u0026gt;allocate_instance(THREAD); // 如果失败，抛出无堆栈跟踪的 OOM } 2.2.3.3.2. Metaspace / Compressed class space # jdk-jdk-11-28/src/hotspot/share/memory/metaspace.cpp\nvoid Metaspace::allocate(ClassLoaderData* cld, size_t word_size, bool read_only, MetaspaceObj::Type type, Metaspace** result, TRAPS) { // ... 省略元空间分配逻辑 ... // 元空间或压缩类空间耗尽，调用 report_java_out_of_memory 触发 Heap Dump // 合理性：加载的类在 Java 对象堆上有 Class 对象，Heap Dump 有助于分析类加载问题 const char* space_string = out_of_compressed_class_space ? \u0026#34;Compressed class space\u0026#34; : \u0026#34;Metaspace\u0026#34;; report_java_out_of_memory(space_string); // ... 省略异常抛出 ... } 2.2.3.3.3. Requested array size exceeds VM limit # JDK 11： jdk-jdk-11-28/src/hotspot/share/oops/typeArrayKlass.cpp\ntypeArrayOop TypeArrayKlass::allocate(int length, TRAPS) { // ... 省略数组分配逻辑 ... if (length \u0026gt; max_length()) { // 数组大小超过 VM 限制，调用 report_java_out_of_memory 触发 Heap Dump // 合理性：通常表示集合过大，可能存在内存泄漏 report_java_out_of_memory(\u0026#34;Requested array size exceeds VM limit\u0026#34;); JvmtiExport::post_array_size_exhausted(); THROW_OOP_0(Universe::out_of_memory_error_array_size()); } } JDK 17+： jdk-jdk-17-35/src/hotspot/share/oops/klass.cpp\nvoid Klass::check_array_allocation_length(int length, int max_length, TRAPS) { if (length \u0026gt; max_length) { // JDK 17+ 新增可重试分配检查 if (!THREAD-\u0026gt;in_retryable_allocation()) { report_java_out_of_memory(\u0026#34;Requested array size exceeds VM limit\u0026#34;); JvmtiExport::post_array_size_exhausted(); THROW_OOP(Universe::out_of_memory_error_array_size()); } else { // 可重试分配失败，抛出 retry OOM（不触发 Heap Dump） THROW_OOP(Universe::out_of_memory_error_retry()); } } } 2.2.3.4. OOM 报告处理 # jdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp\nJDK 11：\nvoid report_java_out_of_memory(const char* message) { static int out_of_memory_reported = 0; // 多个线程可能同时尝试报告 OutOfMemoryError，使用原子操作确保只执行一次 // JDK 11: Atomic::cmpxchg(expected, addr, new_value) 返回旧值 if (Atomic::cmpxchg(1, \u0026amp;out_of_memory_reported, 0) == 0) { // 在 OnOutOfMemoryError 命令执行之前创建 Java 对象堆转储 if (HeapDumpOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;java.lang.OutOfMemoryError: %s\u0026#34;, message); HeapDumper::dump_heap_from_oome(); } // ... 省略 OnOutOfMemoryError 处理 ... if (CrashOnOutOfMemoryError) { fatal(\u0026#34;OutOfMemory encountered: %s\u0026#34;, message); } if (ExitOnOutOfMemoryError) { os::exit(3); } } } JDK 17+：\nvoid report_java_out_of_memory(const char* message) { static int out_of_memory_reported = 0; // JDK 17+: Atomic::cmpxchg(addr, expected, new_value) 参数顺序变化 if (Atomic::cmpxchg(\u0026amp;out_of_memory_reported, 0, 1) == 0) { if (HeapDumpOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;java.lang.OutOfMemoryError: %s\u0026#34;, message); HeapDumper::dump_heap_from_oome(); } // ... 省略 OnOutOfMemoryError 处理 ... if (CrashOnOutOfMemoryError) { // JDK 17+: 使用 report_fatal 替代 fatal report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, \u0026#34;OutOfMemory encountered: %s\u0026#34;, message); } if (ExitOnOutOfMemoryError) { os::exit(3); // JDK 17 // os::_exit(3); // JDK 21+: 快速退出，不运行清理钩子 } } } 关键点：\n原子操作版本差异：JDK 11 使用 cmpxchg(1, \u0026amp;addr, 0)，JDK 17+ 使用 cmpxchg(\u0026amp;addr, 0, 1)（参数顺序变化） 退出机制差异：JDK 21+ 使用 os::_exit(3) 快速退出，不运行清理钩子 注意：所有调用 report_java_out_of_memory 的地方都会检查 HeapDumpOnOutOfMemoryError，没有对 message 类型进行过滤 2.2.3.5. 安全点执行机制 # jdk-jdk-11-28/src/hotspot/share/services/heapDumper.cpp\n// 由 OOM 错误报告调用，在 Java 线程中，不在安全点 void HeapDumper::dump_heap_from_oome() { HeapDumper::dump_heap(true); } // 由错误报告调用（不在安全点）或由 VM 线程在 GC 安全点调用 void HeapDumper::dump(const char* path) { // ... 省略 writer 创建等代码 ... // 创建 VM_HeapDumper 操作对象 VM_HeapDumper dumper(\u0026amp;writer, _gc_before_heap_dump, _oome); if (Thread::current()-\u0026gt;is_VM_thread()) { // 如果已经是 VM 线程，必须在安全点 assert(SafepointSynchronize::is_at_safepoint(), \u0026#34;Expected to be called at a safepoint\u0026#34;); dumper.doit(); } else { // 否则通过 VMThread::execute 触发安全点，所有 Java 线程暂停 VMThread::execute(\u0026amp;dumper); } // ... 省略其他代码 ... } 安全点执行机制：\nOOM 场景：report_java_out_of_memory 由 Java 线程调用，不在安全点 转储触发：调用 HeapDumper::dump_heap_from_oome() → HeapDumper::dump_heap() 安全点进入：通过 VMThread::execute(\u0026amp;dumper) 触发安全点，所有 Java 线程暂停 转储执行：VM_thread 在安全点执行 VM_HeapDumper::doit()，遍历 Java 对象堆 关键点：\n✅ Java 对象堆转储在安全点执行，确保 Java 对象堆状态一致性 ✅ 通过 VM_Operation 机制，由 VM_thread 在安全点执行 ✅ 所有 Java 线程在转储期间暂停，避免并发修改 2.2.3.6. Java 对象堆转储执行 # JDK 11： jdk-jdk-11-28/src/hotspot/share/services/heapDumper.cpp\nvoid HeapDumper::dump_heap(bool oome) { static char base_path[JVM_MAXPATHLEN] = {\u0026#39;\\0\u0026#39;}; static uint dump_file_seq = 0; char* my_path; const char* dump_file_name = \u0026#34;java_pid\u0026#34;; const char* dump_file_ext = \u0026#34;.hprof\u0026#34;; if (dump_file_seq == 0) { // 首次调用：处理 HeapDumpPath，构建基础路径 // ... 省略路径验证和目录检查 ... // 如果未指定 HeapDumpPath 或为目录，使用默认文件名 if (use_default_filename) { jio_snprintf(\u0026amp;base_path[dlen], sizeof(base_path)-dlen, \u0026#34;%s%d%s\u0026#34;, dump_file_name, os::current_process_id(), dump_file_ext); } // JDK 11-21: 使用 os::malloc 从原生堆分配路径内存 my_path = (char*)os::malloc(len, mtInternal); if (my_path == NULL) { warning(\u0026#34;Cannot create heap dump file. Out of system memory.\u0026#34;); return; } strncpy(my_path, base_path, len); } else { // 后续转储：追加序列号 jio_snprintf(my_path, len, \u0026#34;%s.%d\u0026#34;, base_path, dump_file_seq); } dump_file_seq++; // 创建 HeapDumper 对象并执行转储（不支持压缩） HeapDumper dumper(false, true, oome); dumper.dump(my_path); os::free(my_path); } JDK 17+：\nvoid HeapDumper::dump_heap(bool oome) { // ... 省略路径处理逻辑（类似 JDK 11） ... // JDK 17+: 根据 HeapDumpGzipLevel 决定文件扩展名 const char* dump_file_ext = (HeapDumpGzipLevel \u0026gt; 0) ? \u0026#34;.hprof.gz\u0026#34; : \u0026#34;.hprof\u0026#34;; // ... 省略路径构建 ... // 创建 HeapDumper 对象，支持压缩 HeapDumper dumper(false, true, oome); dumper.dump(my_path); os::free(my_path); } JDK 25+：\nvoid HeapDumper::dump_heap(bool oome) { static char base_path[JVM_MAXPATHLEN] = {\u0026#39;\\0\u0026#39;}; static uint dump_file_seq = 0; // JDK 25+: 使用栈上分配，避免在 OOM 场景下分配失败 char my_path[JVM_MAXPATHLEN]; if (dump_file_seq == 0) { // JDK 25+: 支持 %p 占位符，使用 Arguments::copy_expand_pid 展开 if (HeapDumpPath != nullptr \u0026amp;\u0026amp; strstr(HeapDumpPath, \u0026#34;%p\u0026#34;) != nullptr) { Arguments::copy_expand_pid(HeapDumpPath, my_path, JVM_MAXPATHLEN); } // ... 省略其他路径处理 ... } // JDK 25+: 支持并行转储和压缩 HeapDumper dumper(false, true, oome); dumper.dump(my_path); } 版本差异总结：\nJDK 11：不支持压缩，使用 os::malloc 分配路径内存 JDK 17+：支持 gzip 压缩（HeapDumpGzipLevel），文件扩展名为 .hprof 或 .hprof.gz JDK 21+：使用 nullptr 替代 NULL（C++11 风格） JDK 25+：支持栈上分配路径（避免 OOM 场景分配失败），支持 %p 占位符，支持并行转储和压缩 2.2.3.7. JDK 25+ 并行转储和压缩机制详解 # JDK 25 引入了并行转储机制，可以充分利用多核 CPU 加速大 Java 对象堆的转储过程。并行转储采用分段写入 + 合并的两阶段设计。\n2.2.3.7.1. 并行转储流程图 # flowchart TD A[HeapDumper::dump] --\u003e B[创建 DumpWriter和 GZipCompressor] B --\u003e C[VM_HeapDumper 构造传入 num_dump_threads] C --\u003e D[VMThread::execute触发安全点] D --\u003e E[VM_HeapDumper::doit在安全点执行] E --\u003e F[可选: GC before dump] F --\u003e G[prepare_parallel_dump确定实际线程数] G --\u003e H{并行转储?} H --\u003e|否| I[单线程转储work VMDumperId] H --\u003e|是| J[创建 ParallelObjectIterator初始化堆切分] J --\u003e K[workers-\u003erun_task启动多个 worker 线程] K --\u003e L1[Worker 0: VM Dumper] K --\u003e L2[Worker 1-N: Parallel Dumpers] L1 --\u003e M1[lock_global_writer获取全局写入锁] M1 --\u003e N1[写入文件头HPROF_HEADER] N1 --\u003e O1[写入 UTF8 记录HPROF_UTF8] O1 --\u003e P1[写入类加载记录HPROF_LOAD_CLASS] P1 --\u003e Q1[写入堆栈跟踪HPROF_TRACE] Q1 --\u003e R1[unlock_global_writer释放全局写入锁] L2 --\u003e M2[wait_for_start_signal等待 VM Dumper 完成非堆数据] M2 --\u003e N2[创建段文件base_path.p1, .p2, ...] R1 --\u003e S[并行阶段开始] S --\u003e T1[VM Dumper: 写入类转储HPROF_GC_CLASS_DUMP] S --\u003e T2[VM Dumper: 写入线程对象HPROF_GC_ROOT_THREAD_OBJ] S --\u003e T3[VM Dumper: 写入 JNI 全局引用HPROF_GC_ROOT_JNI_GLOBAL] T1 --\u003e U[并行遍历堆对象] T2 --\u003e U T3 --\u003e U U --\u003e V1[Worker 0: ParallelObjectIterator遍历堆区域 0] U --\u003e V2[Worker 1: ParallelObjectIterator遍历堆区域 1] U --\u003e V3[Worker N: ParallelObjectIterator遍历堆区域 N] V1 --\u003e W1[写入实例转储HPROF_GC_INSTANCE_DUMP到段文件 .p0] V2 --\u003e W2[写入实例转储HPROF_GC_INSTANCE_DUMP到段文件 .p1] V3 --\u003e W3[写入实例转储HPROF_GC_INSTANCE_DUMP到段文件 .pN] W1 --\u003e X1{压缩启用?} W2 --\u003e X2{压缩启用?} W3 --\u003e X3{压缩启用?} X1 --\u003e|是| Y1[GZip 压缩段数据独立压缩缓冲区] X1 --\u003e|否| Z1[直接写入段文件] X2 --\u003e|是| Y2[GZip 压缩段数据独立压缩缓冲区] X2 --\u003e|否| Z2[直接写入段文件] X3 --\u003e|是| Y3[GZip 压缩段数据独立压缩缓冲区] X3 --\u003e|否| Z3[直接写入段文件] Y1 --\u003e AA1[finish_dump_segment完成段写入] Y2 --\u003e AA2[finish_dump_segment完成段写入] Y3 --\u003e AA3[finish_dump_segment完成段写入] Z1 --\u003e AA1 Z2 --\u003e AA2 Z3 --\u003e AA3 AA1 --\u003e BB1[dumper_complete通知完成] AA2 --\u003e BB2[dumper_complete通知完成] AA3 --\u003e BB3[dumper_complete通知完成] BB1 --\u003e CC[VM Dumper 等待wait_all_dumpers_complete] BB2 --\u003e CC BB3 --\u003e CC CC --\u003e DD[安全点结束返回调用线程] DD --\u003e EE[DumpMerger::do_merge合并段文件] EE --\u003e FF[读取段文件 .p0, .p1, ..., .pN] FF --\u003e GG{Linux?} GG --\u003e|是| HH[sendfile 系统调用零拷贝合并] GG --\u003e|否| II[read + write常规合并] HH --\u003e JJ[写入 HPROF_HEAP_DUMP_END] II --\u003e JJ JJ --\u003e KK[删除临时段文件] KK --\u003e LL[完成合并生成最终 .hprof 文件] style L1 fill:#87CEEB style L2 fill:#90EE90 style U fill:#FFB6C1 style EE fill:#FFE4B5 style HH fill:#DDA0DD 2.2.3.7.2. 线程数量确定 # jdk-jdk-25-36/src/hotspot/share/services/heapDumper.hpp\n// 默认线程数：CPU 核心数的 3/8 static uint default_num_of_dump_threads() { return MAX2\u0026lt;uint\u0026gt;(1, (uint)os::initial_active_processor_count() * 3 / 8); } jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp\nint HeapDumper::dump(const char* path, outputStream* out, int compression, bool overwrite, uint num_dump_threads) { // OOM 场景下的线程数限制：每个线程需要约 20MB 内存 if (_oome \u0026amp;\u0026amp; num_dump_threads \u0026gt; 1) { // DumpWriter buffer, DumperClassCacheTable, GZipCompressor buffers julong max_threads = os::free_memory() / (20 * M); if (num_dump_threads \u0026gt; max_threads) { num_dump_threads = MAX2\u0026lt;uint\u0026gt;(1, (uint)max_threads); } } // ... 省略其他代码 ... } void VM_HeapDumper::prepare_parallel_dump(WorkerThreads* workers) { uint num_active_workers = workers != nullptr ? workers-\u0026gt;active_workers() : 0; uint num_requested_dump_threads = _num_dumper_threads; // 检查是否可以并行转储 if (num_active_workers \u0026lt;= 1 || num_requested_dump_threads \u0026lt;= 1) { _num_dumper_threads = 1; // 单线程转储 } else { // 限制在 2 到 active_workers 之间 _num_dumper_threads = clamp(num_requested_dump_threads, 2U, num_active_workers); } _dumper_controller = new (std::nothrow) DumperController(_num_dumper_threads); // ... 记录日志 ... } 线程数量确定规则：\n默认值：CPU 核心数 * 3 / 8（例如 8 核 CPU 默认 3 个线程） OOM 场景限制：根据可用内存动态调整，每个线程需要约 20MB（DumpWriter buffer、DumperClassCacheTable、GZipCompressor buffers） 实际线程数：clamp(请求线程数, 2, active_workers)，至少 2 个线程才启用并行，最多不超过 GC worker 线程数 单线程回退：如果 active_workers \u0026lt;= 1 或 请求线程数 \u0026lt;= 1，回退到单线程转储 2.2.3.7.3. 堆切分方式 # 并行转储使用 ParallelObjectIterator 来切分堆，不同 GC 实现有不同的切分策略：\nG1 GC 切分方式： jdk-jdk-25-36/src/hotspot/share/gc/g1/g1CollectedHeap.cpp\nclass G1ParallelObjectIterator : public ParallelObjectIteratorImpl { G1HeapRegionClaimer _claimer; // 按 region 切分 public: G1ParallelObjectIterator(uint thread_num) : _heap(G1CollectedHeap::heap()), _claimer(thread_num == 0 ? G1CollectedHeap::heap()-\u0026gt;workers()-\u0026gt;active_workers() : thread_num) {} virtual void object_iterate(ObjectClosure* cl, uint worker_id) { // 每个 worker 处理不同的 region _heap-\u0026gt;object_iterate_parallel(cl, worker_id, \u0026amp;_claimer); } }; 切分策略：\nG1 GC：按 Heap Region 切分，每个 worker 线程处理不同的 region ZGC：按页面（Page）切分 Shenandoah：按 region 切分，使用任务队列 Parallel GC：按堆区域切分 关键点：\n每个 worker 线程独立遍历分配的堆区域 使用 G1HeapRegionClaimer 等机制确保 region 不重复处理 切分粒度取决于 GC 实现，通常与 GC 的并行策略一致 2.2.3.7.4. 并行转储执行流程 # jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp\nvoid VM_HeapDumper::doit() { CollectedHeap* ch = Universe::heap(); // 可选：转储前执行 GC if (_gc_before_heap_dump) { ch-\u0026gt;collect_as_vm_thread(GCCause::_heap_dump); } WorkerThreads* workers = ch-\u0026gt;safepoint_workers(); prepare_parallel_dump(workers); // 确定实际线程数 if (!is_parallel_dump()) { // 单线程转储 work(VMDumperId); } else { // 并行转储：创建 ParallelObjectIterator 并启动多个 worker ParallelObjectIterator poi(_num_dumper_threads); _poi = \u0026amp;poi; workers-\u0026gt;run_task(this, _num_dumper_threads); // 启动 worker 线程 _poi = nullptr; } } void VM_HeapDumper::work(uint worker_id) { int dumper_id = get_next_dumper_id(); // 原子获取 dumper ID if (is_vm_dumper(dumper_id)) { // VM Dumper (worker_id=0)：负责非堆数据 _dumper_controller-\u0026gt;lock_global_writer(); _dumper_controller-\u0026gt;signal_start(); // 写入文件头、UTF8、类加载、堆栈跟踪等 writer()-\u0026gt;write_raw(\u0026#34;JAVA PROFILE 1.0.2\u0026#34;, ...); SymbolTable::symbols_do(\u0026amp;sym_dumper); ClassLoaderDataGraph::classes_do(\u0026amp;loaded_class_dumper); dump_stack_traces(writer()); _dumper_controller-\u0026gt;unlock_global_writer(); // 释放锁，允许并行 dumpers 开始 } else { // Parallel Dumpers (worker_id=1-N)：等待 VM Dumper 完成非堆数据 _dumper_controller-\u0026gt;wait_for_start_signal(); } // 创建段文件：base_path.p0, .p1, .p2, ... DumpWriter segment_writer(DumpMerger::get_writer_path(writer()-\u0026gt;get_file_path(), dumper_id), writer()-\u0026gt;is_overwrite(), writer()-\u0026gt;compressor()); if (is_vm_dumper(dumper_id)) { // VM Dumper 还负责写入类转储、线程对象、JNI 全局引用等 ClassDumper class_dumper(\u0026amp;segment_writer); ClassLoaderDataGraph::classes_do(\u0026amp;class_dumper); dump_threads(\u0026amp;segment_writer); // ... 省略其他 GC root ... } // 并行遍历堆对象 HeapObjectDumper obj_dumper(\u0026amp;segment_writer, this); if (!is_parallel_dump()) { Universe::heap()-\u0026gt;object_iterate(\u0026amp;obj_dumper); } else { // 并行转储：每个 worker 遍历分配的堆区域 _poi-\u0026gt;object_iterate(\u0026amp;obj_dumper, worker_id); } segment_writer.finish_dump_segment(); segment_writer.flush(); _dumper_controller-\u0026gt;dumper_complete(\u0026amp;segment_writer, writer()); if (is_vm_dumper(dumper_id)) { // VM Dumper 等待所有并行 dumpers 完成 _dumper_controller-\u0026gt;wait_all_dumpers_complete(); writer()-\u0026gt;flush(); // 此时所有段文件已写入，需要在安全点外合并 } } 执行流程关键点：\nVM Dumper（worker_id=0）：\n获取全局写入锁，写入非堆数据（文件头、UTF8、类信息、堆栈跟踪） 释放锁后，继续写入类转储、线程对象等 最后等待所有并行 dumpers 完成 Parallel Dumpers（worker_id=1-N）：\n等待 VM Dumper 完成非堆数据写入 创建独立的段文件（.p1, .p2, \u0026hellip;, .pN） 并行遍历分配的堆区域，写入实例转储记录 每个段文件独立压缩（如果启用） 同步机制：\n使用 DumperController 协调多个 dumpers lock_global_writer / unlock_global_writer：保护非堆数据写入 wait_for_start_signal / signal_start：确保并行 dumpers 在 VM Dumper 完成后开始 wait_all_dumpers_complete：VM Dumper 等待所有并行 dumpers 完成 2.2.3.7.5. 并行压缩机制 # jdk-jdk-25-36/src/hotspot/share/services/heapDumperCompression.hpp\nclass GZipCompressor : public AbstractCompressor { int _level; // 压缩级别 0-9 size_t _block_size; // 压缩块大小 bool _is_first; // 是否为第一个块 public: virtual char const* compress(char* in, size_t in_size, char* out, size_t out_size, char* tmp, size_t tmp_size, size_t* compressed_size) { // 每个段文件独立压缩，使用独立的压缩缓冲区 if (_is_first) { // 第一个块写入块大小注释 jio_snprintf(buf, sizeof(buf), \u0026#34;HPROF BLOCKSIZE=%zu\u0026#34;, _block_size); *compressed_size = ZipLibrary::compress(..., buf, ...); _is_first = false; } else { *compressed_size = ZipLibrary::compress(..., nullptr, ...); } } }; 并行压缩特点：\n独立压缩：每个段文件使用独立的 GZipCompressor 实例和压缩缓冲区 块级压缩：数据按块压缩，每个块独立处理 无全局同步：各 worker 线程独立压缩，无需同步 内存开销：每个线程需要独立的压缩缓冲区（约 20MB/线程） 2.2.3.7.6. 段文件合并机制 # jdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp\nint HeapDumper::dump(const char* path, ...) { // Phase 1: 在安全点内并行写入段文件 VM_HeapDumper dumper(\u0026amp;writer, _gc_before_heap_dump, _oome, num_dump_threads); VMThread::execute(\u0026amp;dumper); // Phase 2: 在安全点外合并段文件（不占用 VM Thread） DumpMerger merger(path, \u0026amp;writer, dumper.dump_seq()); merger.do_merge(); } void DumpMerger::do_merge() { // 合并时不需要再次压缩（段文件已压缩） AbstractCompressor* saved_compressor = _writer-\u0026gt;compressor(); _writer-\u0026gt;set_compressor(nullptr); // 按顺序合并所有段文件 for (int i = 0; i \u0026lt; _dump_seq; i++) { const char* path = get_writer_path(_path, i); // base_path.p0, .p1, ... merge_file(path); remove(path); // 删除临时段文件 } _writer-\u0026gt;set_compressor(saved_compressor); // 写入 HPROF_HEAP_DUMP_END 记录 DumperSupport::end_of_dump(_writer); } #ifdef LINUX void DumpMerger::merge_file(const char* path) { // Linux: 使用 sendfile 零拷贝合并（高效） int segment_fd = os::open(path, O_RDONLY, 0); os::Linux::sendfile(_writer-\u0026gt;get_fd(), segment_fd, \u0026amp;offset, st.st_size); ::close(segment_fd); } #else void DumpMerger::merge_file(const char* path) { // 其他平台: 使用 read + write 合并 fileStream segment_fs(path, \u0026#34;rb\u0026#34;); while ((cnt = segment_fs.read(_writer-\u0026gt;buffer(), 1, _writer-\u0026gt;buffer_size())) != 0) { _writer-\u0026gt;set_position(cnt); _writer-\u0026gt;flush(); } } #endif 合并机制特点：\n两阶段设计：\nPhase 1：在安全点内并行写入段文件（.p0, .p1, \u0026hellip;, .pN） Phase 2：在安全点外合并段文件（不占用 VM Thread，不影响 GC） 合并优化：\nLinux：使用 sendfile 系统调用实现零拷贝合并 其他平台：使用 read + write 常规合并 合并时不需要再次压缩（段文件已压缩） 文件命名：\n段文件：base_path.p0, base_path.p1, \u0026hellip;, base_path.pN 最终文件：base_path（合并后删除所有段文件） 2.2.3.7.7. 设计与限制 # 设计考虑：\n并行遍历：多个线程同时遍历堆的不同区域，充分利用多核 CPU 并行压缩：每个线程独立压缩，压缩速度随线程数线性提升（理想情况） 零拷贝合并：Linux 平台使用 sendfile 高效合并段文件 限制：\n内存开销：每个线程需要约 20MB 内存（DumpWriter buffer、压缩缓冲区等） OOM 场景限制：OOM 时可用内存有限，可能无法启用并行转储 磁盘 IO 瓶颈：即使并行转储，最终仍受磁盘 IO 速度限制 合并开销：合并阶段需要读取所有段文件并写入最终文件 2.3. HeapDumpBeforeFullGC / HeapDumpAfterFullGC # 2.3.1. 参数说明 # 说明：\nHeapDumpBeforeFullGC：在 Full GC 之前转储 Java 对象堆 HeapDumpAfterFullGC：在 Full GC 之后转储 Java 对象堆 默认：false\n类型：manageable（JDK 11）或 product + MANAGEABLE（JDK 17+），可通过 JMX 动态修改\n举例：\n-XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC 2.3.2. 实现机制 # 在 Full GC 前后调用 CollectedHeap::full_gc_dump()，如果相应 flag 启用，则触发 Java 对象堆转储。\nJDK 11-24 实现：\njdk-jdk-11-28/src/hotspot/share/gc/shared/collectedHeap.cpp\nvoid CollectedHeap::full_gc_dump(GCTimer* timer, bool before) { assert(timer != NULL, \u0026#34;timer is null\u0026#34;); // 检查是否启用相应的 Heap Dump 参数 if ((HeapDumpBeforeFullGC \u0026amp;\u0026amp; before) || (HeapDumpAfterFullGC \u0026amp;\u0026amp; !before)) { // 记录 GC 日志并触发 Java 对象堆转储 GCTraceTime(Info, gc) tm(before ? \u0026#34;Heap Dump (before full gc)\u0026#34; : \u0026#34;Heap Dump (after full gc)\u0026#34;, timer); HeapDumper::dump_heap(); } // ... 省略其他代码 ... } JDK 25+ 变化：增加了 FullGCHeapDumpLimit 限制，防止频繁 Full GC 导致过多转储文件：\njdk-jdk-25-36/src/hotspot/share/gc/shared/collectedHeap.cpp\nvoid CollectedHeap::full_gc_dump(GCTimer* timer, bool before) { assert(timer != nullptr, \u0026#34;timer is null\u0026#34;); static uint count = 0; // 静态计数器，记录已转储次数 // 检查是否启用相应的 Heap Dump 参数 if ((HeapDumpBeforeFullGC \u0026amp;\u0026amp; before) || (HeapDumpAfterFullGC \u0026amp;\u0026amp; !before)) { // 检查转储次数限制：0 表示无限制，否则检查是否超过限制 if (FullGCHeapDumpLimit == 0 || count \u0026lt; FullGCHeapDumpLimit) { GCTraceTime(Info, gc) tm(before ? \u0026#34;Heap Dump (before full gc)\u0026#34; : \u0026#34;Heap Dump (after full gc)\u0026#34;, timer); HeapDumper::dump_heap(); count++; // 转储后递增计数器 } } // ... 省略其他代码 ... } 关键点：\nFull GC Java 对象堆转储在 Full GC 的安全点期间执行 JDK 25 新增转储次数限制，防止频繁 Full GC 导致过多转储文件 2.4. HeapDumpPath # 2.4.1. 参数说明 # 说明：指定 Java 对象堆转储文件的路径（文件名或目录）。\n默认：NULL（JDK 11-17）或 nullptr（JDK 21+）\n类型：manageable（JDK 11）或 product + MANAGEABLE（JDK 17+），可通过 JMX 动态修改\n举例：\n-XX:HeapDumpPath=/path/to/dump.hprof（指定文件） -XX:HeapDumpPath=/path/to/dumps/（指定目录，会自动生成文件名） 2.4.2. 文件命名规则 # 如果未指定或为空，默认文件名为 java_pid\u0026lt;pid\u0026gt;.hprof 如果指定为目录，会在目录下创建默认文件名 如果多次转储，后续文件会追加序列号：java_pid\u0026lt;pid\u0026gt;.hprof.1、java_pid\u0026lt;pid\u0026gt;.hprof.2 等 JDK 25 新增：支持 %p 占位符，会自动替换为进程 ID。\n2.4.3. 实现机制 # 2.4.3.1. 参数定义与解析 # 参数定义：\njdk-jdk-17-35/src/hotspot/share/runtime/globals.hpp\nproduct(ccstr, HeapDumpPath, NULL, MANAGEABLE, \u0026#34;When HeapDumpOnOutOfMemoryError is on, the path (filename or \u0026#34; \u0026#34;directory) of the dump file (defaults to java_pid\u0026lt;pid\u0026gt;.hprof \u0026#34; \u0026#34;in the current working directory)\u0026#34;) 参数类型说明：\nccstr：C 字符串类型，存储指向字符串的指针 MANAGEABLE：可通过 JMX 动态修改 默认值：NULL（JDK 11-17）或 nullptr（JDK 21+） 参数解析：\nJVM 启动时通过 -XX:HeapDumpPath=\u0026lt;path\u0026gt; 指定 运行时可通过 JMX HotSpotDiagnosticMXBean.setVMOption() 动态修改 参数值存储在全局变量 HeapDumpPath 中 2.4.3.2. 路径处理逻辑 # JDK 11-21 实现：\njdk-jdk-17-35/src/hotspot/share/services/heapDumper.cpp\nvoid HeapDumper::dump_heap(bool oome) { static char base_path[JVM_MAXPATHLEN] = {\u0026#39;\\0\u0026#39;}; static uint dump_file_seq = 0; char* my_path; const char* dump_file_name = \u0026#34;java_pid\u0026#34;; const char* dump_file_ext = HeapDumpGzipLevel \u0026gt; 0 ? \u0026#34;.hprof.gz\u0026#34; : \u0026#34;.hprof\u0026#34;; if (dump_file_seq == 0) { // 首次调用：计算最长路径长度并验证 const size_t total_length = (HeapDumpPath == NULL ? 0 : strlen(HeapDumpPath)) + strlen(os::file_separator()) + max_digit_chars + strlen(dump_file_name) + strlen(dump_file_ext) + 1; if (total_length \u0026gt; sizeof(base_path)) { warning(\u0026#34;Cannot create heap dump file. HeapDumpPath is too long.\u0026#34;); return; } bool use_default_filename = true; if (HeapDumpPath == NULL || HeapDumpPath[0] == \u0026#39;\\0\u0026#39;) { // 未指定 HeapDumpPath，使用默认文件名 } else { strcpy(base_path, HeapDumpPath); // 检查路径是否为已存在的目录 DIR* dir = os::opendir(base_path); if (dir == NULL) { // 不是目录，视为文件名 use_default_filename = false; } else { // 是目录，追加文件分隔符（如果需要） os::closedir(dir); size_t fs_len = strlen(os::file_separator()); if (strlen(base_path) \u0026gt;= fs_len) { char* end = base_path; end += (strlen(base_path) - fs_len); if (strcmp(end, os::file_separator()) != 0) { strcat(base_path, os::file_separator()); } } } } // 如果 HeapDumpPath 不是文件名，则追加默认文件名 if (use_default_filename) { const size_t dlen = strlen(base_path); jio_snprintf(\u0026amp;base_path[dlen], sizeof(base_path)-dlen, \u0026#34;%s%d%s\u0026#34;, dump_file_name, os::current_process_id(), dump_file_ext); } // 使用 os::malloc 从原生堆分配路径内存 const size_t len = strlen(base_path) + 1; my_path = (char*)os::malloc(len, mtInternal); if (my_path == NULL) { warning(\u0026#34;Cannot create heap dump file. Out of system memory.\u0026#34;); return; } strncpy(my_path, base_path, len); } else { // 后续转储：追加序列号 const size_t len = strlen(base_path) + max_digit_chars + 2; // for \u0026#39;.\u0026#39; and \\0 my_path = (char*)os::malloc(len, mtInternal); if (my_path == NULL) { warning(\u0026#34;Cannot create heap dump file. Out of system memory.\u0026#34;); return; } jio_snprintf(my_path, len, \u0026#34;%s.%d\u0026#34;, base_path, dump_file_seq); } dump_file_seq++; // ... 执行转储 ... os::free(my_path); } 关键处理逻辑：\n路径长度验证：首次调用时计算最长可能路径长度，超过 JVM_MAXPATHLEN 则警告并返回 目录检测：使用 os::opendir() 检查路径是否为已存在的目录 文件分隔符处理：如果是目录，自动追加文件分隔符（如果需要） 默认文件名生成：未指定或为目录时，生成 java_pid\u0026lt;pid\u0026gt;.hprof 格式的文件名 序列号追加：多次转储时，在基础路径后追加 .1、.2 等序列号 内存分配：使用 os::malloc() 从原生堆分配路径内存，转储完成后释放 JDK 25+ 变化：支持 %p 占位符和栈上分配路径这个变量：\njdk-jdk-25-36/src/hotspot/share/services/heapDumper.cpp\nvoid HeapDumper::dump_heap(bool oome) { static char base_path[JVM_MAXPATHLEN] = {\u0026#39;\\0\u0026#39;}; static uint dump_file_seq = 0; char my_path[JVM_MAXPATHLEN]; // JDK 25+: 使用栈上分配，避免 OOM 场景分配失败 const int max_digit_chars = 20; const char* dump_file_name = HeapDumpGzipLevel \u0026gt; 0 ? \u0026#34;java_pid%p.hprof.gz\u0026#34; : \u0026#34;java_pid%p.hprof\u0026#34;; if (dump_file_seq == 0) { // 设置基础路径（文件名或目录，默认或自定义，不含序列号），执行 %p 替换 const char *path_src = (HeapDumpPath != nullptr \u0026amp;\u0026amp; HeapDumpPath[0] != \u0026#39;\\0\u0026#39;) ? HeapDumpPath : dump_file_name; // 使用 Arguments::copy_expand_pid 展开 %p 占位符 if (!Arguments::copy_expand_pid(path_src, strlen(path_src), base_path, JVM_MAXPATHLEN - max_digit_chars)) { warning(\u0026#34;Cannot create heap dump file. HeapDumpPath is too long.\u0026#34;); return; } // 检查路径是否为已存在的目录 DIR* dir = os::opendir(base_path); if (dir != nullptr) { os::closedir(dir); // 路径是目录，追加文件分隔符（如果需要） size_t fs_len = strlen(os::file_separator()); if (strlen(base_path) \u0026gt;= fs_len) { char* end = base_path; end += (strlen(base_path) - fs_len); if (strcmp(end, os::file_separator()) != 0) { strcat(base_path, os::file_separator()); } } // 然后添加默认文件名，执行 %p 替换。使用 my_path 临时存储 if (!Arguments::copy_expand_pid(dump_file_name, strlen(dump_file_name), my_path, JVM_MAXPATHLEN - max_digit_chars)) { warning(\u0026#34;Cannot create heap dump file. HeapDumpPath is too long.\u0026#34;); return; } const size_t dlen = strlen(base_path); jio_snprintf(\u0026amp;base_path[dlen], sizeof(base_path) - dlen, \u0026#34;%s\u0026#34;, my_path); } strncpy(my_path, base_path, JVM_MAXPATHLEN); } else { // 后续转储：追加序列号 const size_t len = strlen(base_path) + max_digit_chars + 2; // for \u0026#39;.\u0026#39; and \\0 jio_snprintf(my_path, len, \u0026#34;%s.%d\u0026#34;, base_path, dump_file_seq); } dump_file_seq++; // ... 执行转储 ... } JDK 25+ 关键变化：\n栈上分配：使用栈上数组 char my_path[JVM_MAXPATHLEN] 替代 os::malloc()，避免 OOM 场景下内存分配失败 %p 占位符支持：使用 Arguments::copy_expand_pid() 展开 %p 占位符为进程 ID 默认文件名包含 %p：默认文件名格式改为 java_pid%p.hprof，在展开时替换为实际进程 ID 2.4.3.3. %p 占位符展开机制 # JDK 25+ 引入：Arguments::copy_expand_pid() 函数用于展开路径中的 %p 占位符：\njdk-jdk-17-35/src/hotspot/share/runtime/arguments.cpp\nbool Arguments::copy_expand_pid(const char* src, size_t srclen, char* buf, size_t buflen) { const char* p = src; char* b = buf; const char* src_end = \u0026amp;src[srclen]; char* buf_end = \u0026amp;buf[buflen - 1]; while (p \u0026lt; src_end \u0026amp;\u0026amp; b \u0026lt; buf_end) { if (*p == \u0026#39;%\u0026#39;) { switch (*(++p)) { case \u0026#39;%\u0026#39;: // \u0026#34;%%\u0026#34; ==\u0026gt; \u0026#34;%\u0026#34;（转义） *b++ = *p++; break; case \u0026#39;p\u0026#39;: { // \u0026#34;%p\u0026#34; ==\u0026gt; 当前进程 ID // 计算可用缓冲区大小 size_t buf_sz = buf_end - b + 1; // 使用 jio_snprintf 格式化进程 ID int ret = jio_snprintf(b, buf_sz, \u0026#34;%d\u0026#34;, os::current_process_id()); // 如果格式化失败或缓冲区不足，返回 false if (ret \u0026lt; 0 || ret \u0026gt;= (int)buf_sz) { return false; } else { b += ret; // 移动缓冲区指针 assert(*b == \u0026#39;\\0\u0026#39;, \u0026#34;fail in copy_expand_pid\u0026#34;); if (p == src_end \u0026amp;\u0026amp; b == buf_end + 1) { // 到达缓冲区末尾 return true; } } p++; // 跳过 \u0026#39;p\u0026#39; break; } default: // 未知占位符，原样复制 *b++ = \u0026#39;%\u0026#39;; *b++ = *p++; break; } } else { // 普通字符，直接复制 *b++ = *p++; } } *b = \u0026#39;\\0\u0026#39;; // 确保字符串以 null 结尾 return true; } 占位符展开规则：\n%p：替换为当前进程 ID（通过 os::current_process_id() 获取） %%：转义为单个 % 字符 其他 %X：未知占位符原样保留（% + X） 缓冲区检查：确保展开后的路径不超过缓冲区大小，否则返回 false 使用示例：\n-XX:HeapDumpPath=./dump_%p.hprof → ./dump_12345.hprof（假设进程 ID 为 12345） -XX:HeapDumpPath=/var/log/java_pid%p.hprof → /var/log/java_pid12345.hprof -XX:HeapDumpPath=/tmp/dumps/ → /tmp/dumps/java_pid12345.hprof（目录 + 默认文件名） 关键点：\n路径长度限制：展开后的路径长度不能超过 JVM_MAXPATHLEN - max_digit_chars（为序列号预留空间） 目录检测时机：%p 展开在目录检测之前执行，因此可以在目录路径中使用 %p 多次转储：首次转储时展开 %p，后续转储使用已展开的基础路径追加序列号 2.5. HeapDumpGzipLevel # 2.5.1. 参数说明 # 说明：指定 Java 对象堆转储文件的 gzip 压缩级别（0-9）。\n默认：0（禁用压缩）\n类型：product + MANAGEABLE（JDK 17+），可通过 JMX 动态修改\n范围：0-9\n0：禁用压缩 1-9：压缩级别，数字越大压缩比越高，但压缩时间越长 举例：-XX:HeapDumpGzipLevel=5\n2.5.2. 压缩性能参考 # 假设 4 个 CPU，针对 8GB Java 对象堆的压缩时间估算：\n级别 5（性价比常用）： 压缩比：2.7×–3.1× 压缩时间：4.4–11.2 秒 压缩后文件：2.6–3.2 GB 写入 HDD：13–32 秒 总耗时：17.4–43.2 秒 注意：这是理想情况下的估计，实际情况可能更差。\n2.6. FullGCHeapDumpLimit # 2.6.1. 参数说明 # 说明：限制 Full GC 触发的 Java 对象堆转储次数。\n默认：0（无限制）\n类型：product + MANAGEABLE（JDK 25+），可通过 JMX 动态修改\n举例：-XX:FullGCHeapDumpLimit=5（最多转储 5 次）\n2.6.2. 使用场景 # 防止频繁 Full GC 导致过多 Java 对象堆转储文件，特别是在调试阶段。\n3. OutOfMemoryError 处理相关参数 # 3.1. OnOutOfMemoryError # 3.1.1. 参数说明 # 说明：在第一个 java.lang.OutOfMemoryError 发生时，执行用户定义的命令或脚本。\n默认：空字符串（不执行）\n类型：product，不可通过 JMX 修改\n举例：\n-XX:OnOutOfMemoryError=\u0026#34;kill -9 %p\u0026#34; -XX:OnOutOfMemoryError=\u0026#34;/path/to/script.sh\u0026#34; 3.1.2. 实现机制 # 3.1.2.1. 参数定义与解析 # 参数定义：\njdk-jdk-17-35/src/hotspot/share/runtime/globals.hpp\nproduct(ccstrlist, OnOutOfMemoryError, \u0026#34;\u0026#34;, \u0026#34;Run user-defined commands on first java.lang.OutOfMemoryError \u0026#34; \u0026#34;(see VMError::report_java_out_of_memory)\u0026#34;) 参数类型说明：\nccstrlist：C 字符串列表类型，支持分号分隔的多个命令 默认值：空字符串（不执行任何命令） 不可通过 JMX 修改：product 类型，只能在启动时指定 参数解析：\nJVM 启动时通过 -XX:OnOutOfMemoryError=\u0026quot;\u0026lt;command\u0026gt;\u0026quot; 指定 支持多个命令，使用分号（;）分隔 支持 %p 占位符，会自动替换为进程 ID 参数值存储在全局变量 OnOutOfMemoryError 中 使用示例：\n# 单个命令 -XX:OnOutOfMemoryError=\u0026#34;kill -9 %p\u0026#34; # 多个命令（分号分隔） -XX:OnOutOfMemoryError=\u0026#34;echo OOM occurred; kill -9 %p\u0026#34; # 执行脚本 -XX:OnOutOfMemoryError=\u0026#34;/path/to/script.sh %p\u0026#34; 3.1.2.2. 命令解析机制 # 命令解析函数：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\nstatic char* next_OnError_command(char* buf, int buflen, const char** ptr) { if (ptr == NULL || *ptr == NULL) return NULL; const char* cmd = *ptr; // 跳过前导空格或分号 while (*cmd == \u0026#39; \u0026#39; || *cmd == \u0026#39;;\u0026#39;) cmd++; // 如果到达字符串末尾，返回 NULL if (*cmd == \u0026#39;\\0\u0026#39;) return NULL; // 查找命令结束位置（分号或字符串末尾） const char * cmdend = cmd; while (*cmdend != \u0026#39;\\0\u0026#39; \u0026amp;\u0026amp; *cmdend != \u0026#39;;\u0026#39;) cmdend++; // 展开 %p 占位符并复制到缓冲区 Arguments::copy_expand_pid(cmd, cmdend - cmd, buf, buflen); // 更新指针位置（跳过当前命令和分号） *ptr = (*cmdend == \u0026#39;\\0\u0026#39; ? cmdend : cmdend + 1); return buf; } 解析规则：\n分号分隔：使用分号（;）分隔多个命令 空格处理：自动跳过前导空格和分号 %p 占位符展开：使用 Arguments::copy_expand_pid() 将 %p 替换为进程 ID 顺序解析：按顺序解析每个命令，直到字符串末尾 解析示例：\n\u0026quot;cmd1; cmd2; cmd3\u0026quot; → 解析为 3 个命令：cmd1、cmd2、cmd3 \u0026quot;kill -9 %p\u0026quot; → 展开为 kill -9 12345（假设进程 ID 为 12345） \u0026quot; cmd1 ; cmd2 \u0026quot; → 自动去除空格，解析为 cmd1、cmd2 3.1.2.3. 命令执行机制 # 触发入口：\njdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { static int out_of_memory_reported = 0; // 使用原子操作确保只执行一次（多个线程可能同时触发） if (Atomic::cmpxchg(\u0026amp;out_of_memory_reported, 0, 1) == 0) { // 1. 先执行 Java 对象堆转储（如果启用） if (HeapDumpOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;java.lang.OutOfMemoryError: %s\u0026#34;, message); HeapDumper::dump_heap_from_oome(); } // 2. 执行 OnOutOfMemoryError 命令（如果启用） if (OnOutOfMemoryError \u0026amp;\u0026amp; OnOutOfMemoryError[0]) { VMError::report_java_out_of_memory(message); } // 3. 触发崩溃（如果启用） if (CrashOnOutOfMemoryError) { // ... 省略 ... } // 4. 退出 JVM（如果启用） if (ExitOnOutOfMemoryError) { // ... 省略 ... } } } VM_ReportJavaOutOfMemory 实现：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\nclass VM_ReportJavaOutOfMemory : public VM_Operation { private: const char* _message; public: VM_ReportJavaOutOfMemory(const char* message) { _message = message; } VMOp_Type type() const { return VMOp_ReportJavaOutOfMemory; } void doit(); }; void VM_ReportJavaOutOfMemory::doit() { // 不在栈上分配大缓冲区 static char buffer[O_BUFLEN]; // 打印错误信息和命令信息 tty-\u0026gt;print_cr(\u0026#34;#\u0026#34;); tty-\u0026gt;print_cr(\u0026#34;# java.lang.OutOfMemoryError: %s\u0026#34;, _message); tty-\u0026gt;print_cr(\u0026#34;# -XX:OnOutOfMemoryError=\\\u0026#34;%s\\\u0026#34;\u0026#34;, OnOutOfMemoryError); // 确保 Java 对象堆可解析（不需要 retire TLABs） Universe::heap()-\u0026gt;ensure_parsability(false); // 解析并执行每个命令 char* cmd; const char* ptr = OnOutOfMemoryError; while ((cmd = next_OnError_command(buffer, sizeof(buffer), \u0026amp;ptr)) != NULL) { tty-\u0026gt;print(\u0026#34;# Executing \u0026#34;); #if defined(LINUX) tty-\u0026gt;print(\u0026#34;/bin/sh -c \u0026#34;); // Linux 使用 /bin/sh -c 执行 #endif tty-\u0026gt;print_cr(\u0026#34;\\\u0026#34;%s\\\u0026#34;...\u0026#34;, cmd); // 执行命令（fork 子进程执行，不等待完成） if (os::fork_and_exec(cmd, true) \u0026lt; 0) { // 执行失败时打印错误信息，但继续执行后续命令 tty-\u0026gt;print_cr(\u0026#34;os::fork_and_exec failed: %s (%s=%d)\u0026#34;, os::strerror(errno), os::errno_name(errno), errno); } } } void VMError::report_java_out_of_memory(const char* message) { if (OnOutOfMemoryError \u0026amp;\u0026amp; OnOutOfMemoryError[0]) { // 获取 Heap_lock 锁，确保线程安全 MutexLocker ml(Heap_lock); // 创建 VM_Operation 并通过 VMThread 在安全点执行 VM_ReportJavaOutOfMemory op(message); VMThread::execute(\u0026amp;op); } } 执行机制关键点：\n安全点执行：通过 VMThread::execute() 在安全点执行，确保 Java 对象堆状态一致 堆可解析性：执行前调用 Universe::heap()-\u0026gt;ensure_parsability(false)，确保堆可被工具（如 jmap）解析 异步执行：使用 os::fork_and_exec() 在子进程中执行命令，不阻塞主线程 顺序执行：多个命令按顺序执行，但每个命令都是异步的（不等待完成） Linux 平台：使用 /bin/sh -c 执行命令，支持 shell 语法 3.1.2.4. 错误处理机制 # 错误处理策略：\nif (os::fork_and_exec(cmd, true) \u0026lt; 0) { // 执行失败时打印错误信息，但继续执行后续命令 tty-\u0026gt;print_cr(\u0026#34;os::fork_and_exec failed: %s (%s=%d)\u0026#34;, os::strerror(errno), os::errno_name(errno), errno); } 错误处理特点：\n非阻塞：命令执行失败不会阻止后续命令的执行 错误日志：执行失败时打印错误信息到控制台（tty），包括： 错误描述（os::strerror(errno)） 错误名称（os::errno_name(errno)） 错误码（errno） 继续执行：即使某个命令失败，仍会继续执行后续命令 返回值忽略：os::fork_and_exec() 的返回值被忽略，命令在子进程中异步执行 常见错误场景：\n命令不存在：ENOENT（No such file or directory） 权限不足：EACCES（Permission denied） 路径过长：ENAMETOOLONG（File name too long） 资源不足：ENOMEM（Out of memory）或 EAGAIN（Resource temporarily unavailable） 3.1.2.5. 执行顺序与时机 # 在 OOM 处理流程中的位置：\njdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { static int out_of_memory_reported = 0; // 原子操作确保只执行一次 if (Atomic::cmpxchg(\u0026amp;out_of_memory_reported, 0, 1) == 0) { // 1. 最先执行：Java 对象堆转储（如果启用） if (HeapDumpOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;java.lang.OutOfMemoryError: %s\u0026#34;, message); HeapDumper::dump_heap_from_oome(); } // 2. 然后执行：OnOutOfMemoryError 命令（如果启用） if (OnOutOfMemoryError \u0026amp;\u0026amp; OnOutOfMemoryError[0]) { VMError::report_java_out_of_memory(message); } // 3. 接着执行：触发崩溃（如果启用） if (CrashOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Aborting due to java.lang.OutOfMemoryError: %s\u0026#34;, message); report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, \u0026#34;OutOfMemory encountered: %s\u0026#34;, message); } // 4. 最后执行：退出 JVM（如果启用） if (ExitOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Terminating due to java.lang.OutOfMemoryError: %s\u0026#34;, message); os::exit(3); // JDK 17 // os::_exit(3); // JDK 21+ } } } 执行时机：\n安全点执行：OnOutOfMemoryError 命令在安全点执行，确保堆状态一致 只执行一次：使用原子操作 Atomic::cmpxchg 确保多个线程同时触发时只执行一次 异步执行：命令在子进程中异步执行，不阻塞主线程，但主线程会继续执行后续操作（如崩溃或退出） 3.2. CrashOnOutOfMemoryError # 3.2.1. 参数说明 # 说明：在第一个 java.lang.OutOfMemoryError 发生时，触发 JVM 崩溃，生成错误日志和核心转储。\n默认：false\n类型：product，不可通过 JMX 修改\n举例：-XX:+CrashOnOutOfMemoryError\n3.2.2. 实现机制 # 3.2.2.1. 触发入口 # JDK 11 实现：\njdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { // ... 省略其他代码 ... if (CrashOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Aborting due to java.lang.OutOfMemoryError: %s\u0026#34;, message); fatal(\u0026#34;OutOfMemory encountered: %s\u0026#34;, message); } } JDK 17+ 实现：\njdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { // ... 省略其他代码 ... if (CrashOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Aborting due to java.lang.OutOfMemoryError: %s\u0026#34;, message); // JDK 17+: 使用 report_fatal 替代 fatal，提供更详细的错误信息 report_fatal(OOM_JAVA_HEAP_FATAL, __FILE__, __LINE__, \u0026#34;OutOfMemory encountered: %s\u0026#34;, message); } } 关键变化：\nJDK 11：使用 fatal() 函数，直接触发崩溃 JDK 17+：使用 report_fatal() 函数，提供错误类型、文件名、行号等详细信息 实现机制：CrashOnOutOfMemoryError 触发崩溃后，会执行完整的错误报告流程，包括生成错误日志和核心转储。详细实现机制请参考第四章\u0026quot;错误日志和诊断相关参数\u0026quot;中的相关章节。\n3.3. ExitOnOutOfMemoryError # 3.3.1. 参数说明 # 说明：在第一个 java.lang.OutOfMemoryError 发生时，退出 JVM。\n默认：false\n类型：product，不可通过 JMX 修改\n举例：-XX:+ExitOnOutOfMemoryError\n3.3.2. 实现机制 # 3.3.2.1. 触发入口 # JDK 11-20 实现：\njdk-jdk-11-28/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { // ... 省略其他代码 ... if (ExitOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Terminating due to java.lang.OutOfMemoryError: %s\u0026#34;, message); os::exit(3); // 调用 os::exit，会执行清理钩子 } } JDK 21+ 实现：\njdk-jdk-21-35/src/hotspot/share/utilities/debug.cpp\nvoid report_java_out_of_memory(const char* message) { // ... 省略其他代码 ... if (ExitOnOutOfMemoryError) { tty-\u0026gt;print_cr(\u0026#34;Terminating due to java.lang.OutOfMemoryError: %s\u0026#34;, message); os::_exit(3); // JDK 21+: 快速退出，不运行清理钩子 } } 关键变化：\nJDK 11-20：使用 os::exit(3)，会执行清理钩子（shutdown hooks） JDK 21+：使用 os::_exit(3)，直接退出，不执行清理钩子 3.3.2.2. Shutdown Hooks 机制 # Shutdown Hooks 定义：\nShutdown Hooks（清理钩子）是 JVM 在正常关闭时执行的清理任务。通过 Runtime.addShutdownHook() 注册的线程会在 JVM 退出前执行。\nShutdown Hooks 注册：\njdk-jdk-17-35/src/java.base/share/classes/java/lang/Runtime.java\npublic void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission(\u0026#34;shutdownHooks\u0026#34;)); } ApplicationShutdownHooks.add(hook); } Shutdown Hooks 执行顺序：\njdk-jdk-17-35/src/java.base/share/classes/java/lang/Shutdown.java\nclass Shutdown { // 系统 shutdown hooks 按预定义槽位注册，执行顺序如下： // (0) Console restore hook - 恢复控制台状态 // (1) ApplicationShutdownHooks - 执行所有应用注册的 shutdown hooks // (2) DeleteOnExit hook - 删除标记为退出时删除的文件 private static final int MAX_SYSTEM_HOOKS = 10; private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS]; private static void runHooks() { // 按顺序执行所有系统 shutdown hooks for (int i=0; i \u0026lt; MAX_SYSTEM_HOOKS; i++) { Runnable hook = hooks[i]; if (hook != null) hook.run(); } // 设置 shutdown 状态 VM.shutdown(); } static void exit(int status) { synchronized (Shutdown.class) { beforeHalt(); // 通知 VM 准备退出 runHooks(); // 执行所有 shutdown hooks halt(status); // 调用 native halt 终止进程 } } } ApplicationShutdownHooks 执行：\njdk-jdk-17-35/src/java.base/share/classes/java/lang/ApplicationShutdownHooks.java\nclass ApplicationShutdownHooks { private static IdentityHashMap\u0026lt;Thread, Thread\u0026gt; hooks; static void runHooks() { Collection\u0026lt;Thread\u0026gt; threads; synchronized(ApplicationShutdownHooks.class) { threads = hooks.keySet(); hooks = null; // 清空 hooks，防止重复执行 } // 启动所有应用 shutdown hooks（并发执行） for (Thread hook : threads) { hook.start(); } // 等待所有 hooks 完成 for (Thread hook : threads) { while (true) { try { hook.join(); break; } catch (InterruptedException ignored) { } } } } } Shutdown Hooks 常见用途：\n资源清理：关闭文件、网络连接、数据库连接等 临时文件删除：删除程序运行期间创建的临时文件 日志刷新：确保日志缓冲区内容写入磁盘 服务注销：从注册中心注销服务 状态保存：保存应用状态到持久化存储 3.3.2.3. os::exit 与 os::_exit 的区别 # os::exit 实现：\njdk-jdk-17-35/src/hotspot/os/posix/os_posix.cpp\nvoid os::exit(int num) { ::exit(num); // 调用标准 C 库的 exit()，会执行 atexit 注册的函数 } os::exit 调用链：\njdk-jdk-17-35/src/hotspot/share/runtime/java.cpp\nvoid vm_direct_exit(int code) { notify_vm_shutdown(); // 通知 VM 准备关闭 os::wait_for_keypress_at_exit(); os::exit(code); // 调用 os::exit } // Runtime.exit() 最终调用 void Java_java_lang_Shutdown_beforeHalt() { // 调用 Shutdown.beforeHalt()，触发 shutdown hooks 执行 Shutdown::exit(status); } os::_exit 实现（JDK 21+）：\njdk-jdk-21-35/src/hotspot/os/posix/os_posix.cpp\nvoid os::_exit(int num) { ::_exit(num); // 调用系统调用 _exit()，直接终止进程，不执行任何清理 } 关键区别：\n特性 os::exit(3) os::_exit(3) 标准 C 库函数 exit() _exit() 执行 atexit 函数 ✅ 是 ❌ 否 执行 shutdown hooks ✅ 是 ❌ 否 刷新标准流缓冲区 ✅ 是 ❌ 否 关闭文件描述符 ✅ 是 ❌ 否（由操作系统关闭） 执行顺序 1. atexit 函数2. shutdown hooks3. 刷新缓冲区4. 关闭文件5. 退出进程 直接退出进程 退出速度 较慢（需要执行清理） 快速（立即退出） 适用场景 正常退出，需要清理资源 异常退出（如 OOM），避免清理时再次分配内存 3.3.2.4. 清理操作详解 # os::exit(3) 执行的清理操作：\nJava 层清理：\nShutdown Hooks：执行所有注册的 shutdown hooks（包括应用 hooks 和系统 hooks） DeleteOnExit Hook：删除通过 File.deleteOnExit() 标记的文件 Console Restore Hook：恢复控制台状态（如果使用了 Console） JVM 层清理：\nVM Shutdown 通知：调用 notify_vm_shutdown() 通知各个子系统 PerfMemory 清理：清理性能内存资源 Attach Listener 清理：清理 Attach API 相关资源 输出流刷新：刷新所有输出流缓冲区 系统层清理：\natexit 函数：执行通过 atexit() 注册的函数 标准流刷新：刷新 stdout、stderr 缓冲区 文件描述符关闭：关闭所有打开的文件描述符 os::_exit(3) 不执行的清理操作：\n不执行 Shutdown Hooks：\n应用注册的 shutdown hooks 不会执行 系统 shutdown hooks（如 DeleteOnExit）不会执行 可能导致资源泄漏（如文件未关闭、连接未释放） 不执行 atexit 函数：\n通过 atexit() 注册的函数不会执行 可能导致某些全局资源未清理 不刷新缓冲区：\n标准输出/错误流的缓冲区不会刷新 可能导致部分日志丢失 不关闭文件描述符：\n文件描述符由操作系统自动关闭 但不会触发文件关闭时的清理逻辑 os::shutdown() 执行的清理：\njdk-jdk-17-35/src/hotspot/os/posix/os_posix.cpp\nvoid os::shutdown() { // 允许 PerfMemory 尝试清理持久化资源 perfMemory_exit(); // 移除文件系统中的对象 AttachListener::abort(); // 刷新缓冲输出，完成日志文件 ostream_abort(); // 检查是否有 abort hook abort_hook_t abort_hook = Arguments::abort_hook(); if (abort_hook != NULL) { abort_hook(); } } 注意：os::shutdown() 在 os::abort() 中也会被调用，但在 os::_exit() 中不会被调用。\n3.3.2.5. OOM 场景下的问题 # os::exit(3) 在 OOM 场景下的风险：\nShutdown Hooks 可能触发新的内存分配：\nShutdown hooks 是 Java 线程，执行时可能需要分配内存 在 Java 对象堆 OOM 的情况下，执行 shutdown hooks 可能再次触发 OOM 导致退出过程卡住或失败 清理操作可能失败：\n某些清理操作（如文件写入、网络请求）可能需要分配内存 在内存不足的情况下，这些操作可能失败或阻塞 退出延迟：\nShutdown hooks 的执行时间不确定 如果某个 hook 阻塞或执行时间过长，会延迟进程退出 os::_exit(3) 的优势：\n快速退出：直接调用系统调用 _exit()，立即终止进程，不执行任何清理 避免二次 OOM：不执行 shutdown hooks，避免在 OOM 场景下再次分配内存 确定性强：退出时间确定，不会因为清理操作阻塞 JDK 21+ 改进原因：\n在 Java 对象堆 OOM 的场景下，使用 os::_exit(3) 可以：\n避免 shutdown hooks 执行时再次触发 OOM 快速退出，让容器编排系统（如 Kubernetes）能够快速重启服务 减少退出过程中的不确定性 注意事项：\n使用 os::_exit(3) 的代价是：\n不会执行任何清理操作，可能导致资源泄漏 不会刷新缓冲区，可能导致日志丢失 不会删除临时文件（除非通过 File.deleteOnExit() 标记，但该 hook 也不会执行） 因此，os::_exit(3) 仅适用于异常退出场景（如 OOM），不适合正常退出。\n3.4. 执行顺序和优先级 # 3.4.1. 执行流程图 # flowchart TD A[发生 OutOfMemoryError包括 Java 对象堆 OOM] --\u003e B[report_java_out_of_memory] B --\u003e C{Atomic::cmpxchg检查是否已报告} C --\u003e|已报告| Z[跳过] C --\u003e|首次报告| D{HeapDumpOnOutOfMemoryError?} D --\u003e|是| E[执行 Java 对象堆 Heap Dump] D --\u003e|否| F{OnOutOfMemoryError?} E --\u003e F F --\u003e|是| G[执行用户脚本在安全点执行] F --\u003e|否| H{CrashOnOutOfMemoryError?} G --\u003e H H --\u003e|是| I[触发 fatal生成错误日志和核心转储] H --\u003e|否| J{ExitOnOutOfMemoryError?} I --\u003e K[退出] J --\u003e|是| L[os::_exit 3快速退出] J --\u003e|否| M[抛出 OutOfMemoryError] L --\u003e K M --\u003e N[Java 代码捕获异常] style E fill:#87CEEB style G fill:#90EE90 style I fill:#FFB6C1 style L fill:#FFB6C1 3.4.2. 执行顺序总结 # HeapDumpOnOutOfMemoryError：最先执行（如果启用） OnOutOfMemoryError：在 Java 对象堆转储后执行（如果启用） CrashOnOutOfMemoryError：在脚本后执行（如果启用） ExitOnOutOfMemoryError：最后执行（如果启用） 注意：这些参数可以同时启用，会按顺序执行。\n4. 错误日志和诊断相关参数 # 4.1. ErrorFile # 4.1.1. 参数说明 # 说明：指定错误日志文件的路径。当发生致命错误时，JVM 会将错误信息写入此文件。\n默认：NULL（使用默认文件名 hs_err_pid%p.log）\n类型：product，不可通过 JMX 修改\n举例：-XX:ErrorFile=/var/log/jvm/hs_err_pid%p.log\n占位符：支持 %p 占位符，会自动替换为进程 ID。\n4.1.2. 错误日志内容 # 错误日志文件（hs_err_pid*.log）包含以下信息：\nJVM 版本和配置信息 错误类型和原因 线程堆栈跟踪 Java 对象堆内存信息 本地变量信息 系统信息 4.1.3. 实现机制 # 4.1.3.1. 错误日志生成流程 # 当发生致命错误（如 CrashOnOutOfMemoryError 触发）时，JVM 会调用 report_fatal() 函数，最终通过 VMError::report_and_die() 生成错误日志。\nreport_fatal 实现：\njdk-jdk-17-35/src/hotspot/share/utilities/debug.cpp\nvoid report_fatal(VMErrorType error_type, const char* file, int line, const char* detail_fmt, ...) { if (Debugging || error_is_suppressed(file, line)) return; va_list detail_args; va_start(detail_args, detail_fmt); void* context = NULL; // 打印单元测试错误信息 print_error_for_unit_test(\u0026#34;fatal error\u0026#34;, detail_fmt, detail_args); // 调用 VMError::report_and_die 执行实际的错误报告和崩溃 VMError::report_and_die(error_type, \u0026#34;fatal error\u0026#34;, detail_fmt, detail_args, Thread::current_or_null(), NULL, NULL, context, file, line, 0); va_end(detail_args); } 函数作用：\n参数验证：检查是否处于调试模式或错误被抑制 格式化错误信息：使用 va_list 格式化详细错误信息 调用报告函数：调用 VMError::report_and_die() 执行实际的错误报告和崩溃流程 VMError::report_and_die 主流程：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\nvoid VMError::report_and_die(int id, const char* message, const char* detail_fmt, va_list detail_args, Thread* thread, address pc, void* siginfo, void* context, const char* filename, int lineno, size_t size) { static char buffer[O_BUFLEN]; static const int fd_out = 1; // stdout static int fd_log = -1; // 错误日志文件描述符 fdStream out(fd_out); fdStream log(fd_log); // 使用原子操作确保只执行一次（多个线程可能同时触发） intptr_t mytid = os::current_thread_id(); if (_first_error_tid == -1 \u0026amp;\u0026amp; Atomic::cmpxchg(\u0026amp;_first_error_tid, (intptr_t)-1, mytid) == -1) { // 如果 SuppressFatalErrorMessage 启用，直接退出 if (SuppressFatalErrorMessage) { os::abort(CreateCoredumpOnCrash); } // 初始化时间戳 out.time_stamp().update_to(1); log.time_stamp().update_to(1); // 保存错误信息 _id = id; _message = message; _thread = thread; _pc = pc; _siginfo = siginfo; _context = context; _filename = filename; _lineno = lineno; _size = size; jio_vsnprintf(_detail_msg, sizeof(_detail_msg), detail_fmt, detail_args); // 记录报告开始时间 reporting_started(); record_reporting_start_time(); // 如果启用 ShowMessageBoxOnError，显示消息框 if (ShowMessageBoxOnError || PauseAtExit) { show_message_box(buffer, sizeof(buffer)); ShowMessageBoxOnError = false; } // 检查转储限制 os::check_dump_limit(buffer, sizeof(buffer)); // 安装辅助信号处理器 install_secondary_signal_handler(); } // Part 1: 打印简要版本到标准输出（verbose = false） if (!out_done) { if (!(ErrorFileToStdout \u0026amp;\u0026amp; out.fd() == 1)) { report(\u0026amp;out, false); // 打印简要错误信息 } out_done = true; } // Part 2: 打印完整错误日志到文件（verbose = true） if (!log_done) { // 打开错误日志文件 if (!log.is_open()) { if (ErrorFileToStdout) { fd_log = 1; // 输出到标准输出 } else if (ErrorFileToStderr) { fd_log = 2; // 输出到标准错误 } else { // 准备错误日志文件（默认：hs_err_pid%p.log） fd_log = prepare_log_file(ErrorFile, \u0026#34;hs_err_pid%p.log\u0026#34;, true, buffer, sizeof(buffer)); if (fd_log != -1) { out.print_raw(\u0026#34;# An error report file with more information is saved as:\\n# \u0026#34;); out.print_raw_cr(buffer); } else { out.print_raw_cr(\u0026#34;# Can not save log file, dump to screen..\u0026#34;); fd_log = 1; // 无法创建文件，输出到标准输出 } } log.set_fd(fd_log); } // 生成完整错误日志 report(\u0026amp;log, true); log_done = true; // 关闭日志文件 if (fd_log \u0026gt; 3) { close(fd_log); fd_log = -1; } log.set_fd(-1); } // 打印 NMT 统计信息（如果启用） if (PrintNMTStatistics) { fdStream fds(fd_out); MemTracker::final_report(\u0026amp;fds); } // 执行 OnError 命令（如果启用） // ... 省略 OnError 处理 ... // 生成核心转储并退出 os::abort(dump_core \u0026amp;\u0026amp; CreateCoredumpOnCrash, _siginfo, _context); } 执行流程关键步骤：\n原子检查：使用原子操作确保只执行一次错误报告 保存错误信息：保存错误 ID、消息、线程、PC、上下文等信息 打印简要信息：先打印简要错误信息到标准输出 生成错误日志：创建 hs_err_pid%p.log 文件并写入完整错误信息 生成核心转储：如果 CreateCoredumpOnCrash 启用，生成核心转储文件 退出进程：调用 os::abort() 终止进程 4.1.3.2. 错误日志文件生成 # 错误日志文件生成：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\n// 准备错误日志文件 fd_log = prepare_log_file(ErrorFile, \u0026#34;hs_err_pid%p.log\u0026#34;, true, buffer, sizeof(buffer)); 错误日志文件命名：\n默认文件名：hs_err_pid\u0026lt;pid\u0026gt;.log（%p 占位符会被替换为进程 ID） 自定义路径：可通过 -XX:ErrorFile=\u0026lt;path\u0026gt; 指定 文件位置：默认在当前工作目录，如果无法写入则输出到标准输出 4.1.3.3. 错误日志详细内容 # 错误日志包含的内容（通过 VMError::report() 生成）：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\nvoid VMError::report(outputStream* st, bool _verbose) { // 1. 错误类型和消息 st-\u0026gt;print_cr(\u0026#34;# A fatal error has been detected by the Java Runtime Environment:\u0026#34;); // 或 st-\u0026gt;print_cr(\u0026#34;# There is insufficient memory for the Java Runtime Environment to continue.\u0026#34;); // 2. 错误详情（信号/异常名称、PC、PID、TID） st-\u0026gt;print(\u0026#34;# Out of Memory Error (debug.cpp:364)\u0026#34;); st-\u0026gt;print(\u0026#34;, pid=%d\u0026#34;, os::current_process_id()); st-\u0026gt;print(\u0026#34;, tid=\u0026#34; UINTX_FORMAT, os::current_thread_id()); // 3. JVM 版本信息 report_vm_version(st, buf, sizeof(buf)); // 4. 问题帧信息（如果有上下文） if (_context) { st-\u0026gt;print_cr(\u0026#34;# Problematic frame:\u0026#34;); frame fr = os::fetch_frame_from_context(_context); fr.print_on_error(st, buf, sizeof(buf)); } // 5. 核心转储信息 if (CreateCoredumpOnCrash) { st-\u0026gt;print(\u0026#34;Core dump will be written. Default location: %s\u0026#34;, coredump_message); } // 6. 摘要信息（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- S U M M A R Y ------------\u0026#34;); // VM 选项摘要 Arguments::print_summary_on(st); // 机器和 OS 信息 os::print_summary_info(st, buf, sizeof(buf)); // 日期和时间 os::print_date_and_time(st, buf, sizeof(buf)); } // 7. 线程信息（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- T H R E A D ---------------\u0026#34;); if (_thread) { _thread-\u0026gt;print_on_error(st, buf, sizeof(buf)); } // 堆栈边界 // 寄存器信息 // 堆栈跟踪 } // 8. Java 对象堆信息（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- H E A P ---------------\u0026#34;); Universe::heap()-\u0026gt;print_on_error(st); } // 9. 所有线程信息（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- T H R E A D S ---------------\u0026#34;); Threads::print_on_error(st, buf, sizeof(buf)); } // 10. 动态库信息（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- D Y N A M I C L I B R A R I E S ---------------\u0026#34;); os::print_dll_info(st); } // 11. 环境变量（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- E N V I R O N M E N T V A R I A B L E S ---------------\u0026#34;); os::print_environment_variables(st, buf, sizeof(buf)); } // 12. 信号处理器（verbose 模式） if (_verbose) { st-\u0026gt;print_cr(\u0026#34;--------------- S I G N A L S ---------------\u0026#34;); os::print_signal_handlers(st, buf, sizeof(buf)); } } 错误日志主要内容：\n错误类型和消息：致命错误类型和详细错误消息 进程和线程信息：进程 ID（PID）、线程 ID（TID） JVM 版本信息：Java 版本、JVM 版本、构建信息 问题帧信息：发生错误的代码位置和堆栈帧 核心转储信息：是否生成核心转储及位置 VM 选项摘要：所有 JVM 启动参数 系统信息：操作系统、CPU、内存信息 当前线程信息：线程状态、堆栈边界、寄存器、堆栈跟踪 Java 对象堆信息：堆大小、使用情况、GC 信息 所有线程信息：所有 Java 线程和本地线程的堆栈跟踪 动态库信息：加载的共享库列表 环境变量：JVM 相关的环境变量 信号处理器：已安装的信号处理器信息 4.2. ShowMessageBoxOnError # 4.2.1. 参数说明 # 说明：在 VM 致命错误时显示消息框，保持进程存活，等待用户交互。\n默认：false\n类型：product，不可通过 JMX 修改\n举例：-XX:+ShowMessageBoxOnError\n4.2.2. 使用场景 # 主要用于桌面应用程序的调试，现在一般不用了。\n4.3. CreateCoredumpOnCrash # 4.3.1. 参数说明 # 说明：在 VM 致命错误时创建核心转储（core dump）或迷你转储（minidump）。\n默认：true\n类型：product，不可通过 JMX 修改\n举例：\n-XX:+CreateCoredumpOnCrash（默认，可省略） -XX:-CreateCoredumpOnCrash（禁用） 4.3.2. 平台差异 # Linux：生成 core dump 文件 Windows：生成 minidump 文件 macOS：生成 core dump 文件 4.3.3. 实现机制 # 核心转储生成：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\n// 在 report_and_die 的最后 os::abort(dump_core \u0026amp;\u0026amp; CreateCoredumpOnCrash, _siginfo, _context); os::abort 实现：\njdk-jdk-17-35/src/hotspot/share/runtime/os.cpp\nvoid os::abort(bool dump_core) { // 如果启用转储核心，调用平台特定的核心转储生成函数 if (dump_core) { // Linux: 调用 abort() 系统调用，触发 SIGABRT，由系统生成 core dump // Windows: 调用 MiniDumpWriteDump() 生成 minidump // macOS: 调用 abort() 系统调用，触发 SIGABRT，由系统生成 core dump os::abort(dump_core, NULL, NULL); } else { // 不生成核心转储，直接退出 os::die(); } } 核心转储生成条件：\nCreateCoredumpOnCrash 启用：默认启用（true） 系统配置允许：需要系统配置允许生成核心转储（如 ulimit -c unlimited） 磁盘空间充足：核心转储文件可能很大（与 Java 对象堆大小相关） 核心转储文件位置：\nLinux：默认在当前工作目录，文件名为 core 或 core.\u0026lt;pid\u0026gt; Windows：默认在当前工作目录，文件名为 hs_err_pid\u0026lt;pid\u0026gt;.mdmp macOS：默认在 /cores/ 目录，文件名为 core.\u0026lt;pid\u0026gt; 核心转储文件大小：\n核心转储包含进程的完整内存映像，大小通常等于进程的虚拟内存大小 对于大 Java 对象堆的进程，核心转储文件可能达到数 GB 甚至数十 GB 核心转储用途：\n使用调试器（如 gdb）分析崩溃原因 查看崩溃时的内存状态、寄存器值、堆栈信息 分析内存泄漏、内存损坏等问题 4.4. SuppressFatalErrorMessage # 4.4.1. 参数说明 # 说明：抑制致命错误消息的输出，避免死锁。\n默认：false\n类型：product，不可通过 JMX 修改\n举例：-XX:+SuppressFatalErrorMessage\n4.4.2. 使用场景 # 在某些特殊场景下，错误报告可能导致死锁，此时可以启用此参数抑制错误消息。\n4.5. OnError # 4.5.1. 参数说明 # 说明：在 VM 致命错误时执行用户定义的命令或脚本。\n默认：空字符串（不执行）\n类型：product，不可通过 JMX 修改\n举例：\n-XX:OnError=\u0026#34;gdb -batch -ex \u0026#39;thread apply all bt\u0026#39; %p\u0026#34; -XX:OnError=\u0026#34;/path/to/error_handler.sh\u0026#34; 4.5.2. 实现机制 # OnError 命令执行：\njdk-jdk-17-35/src/hotspot/share/utilities/vmError.cpp\nstatic bool skip_OnError = false; if (!skip_OnError \u0026amp;\u0026amp; OnError \u0026amp;\u0026amp; OnError[0]) { skip_OnError = true; // 使用静态变量确保只执行一次 // ... 省略其他代码 ... // 解析并执行每个命令（使用分号分隔） const char* ptr = OnError; while ((cmd = next_OnError_command(buffer, sizeof(buffer), \u0026amp;ptr)) != NULL) { // 在子进程中异步执行命令，不阻塞主线程 os::fork_and_exec(cmd); } } 关键点：\n只执行一次：使用静态变量 skip_OnError 确保只执行一次 支持多个命令：使用分号分隔多个命令，通过 next_OnError_command() 解析 执行时机：在错误报告过程中执行，在生成错误日志后、JVM 退出前执行 异步执行：使用 os::fork_and_exec() 在子进程中执行，不阻塞主线程 4.6. ErrorLogTimeout # 4.6.1. 参数说明 # 说明：限制错误日志写入的超时时间（秒）。如果超时，错误报告会中断。\n默认：120（2 分钟）\n类型：product，不可通过 JMX 修改\n范围：0 到 max_jlong/1000\n0：无超时限制 举例：-XX:ErrorLogTimeout=60\n4.6.2. 使用场景 # 在某些场景下，错误报告可能耗时过长（如大 Java 对象堆的堆栈跟踪），可以设置超时时间限制。\n5. 其他诊断参数 # 5.1. MaxJavaStackTraceDepth # 5.1.1. 参数说明 # 说明：限制 Java 异常堆栈跟踪的最大深度（行数）。\n默认：1024\n类型：product，不可通过 JMX 修改\n范围：0 到 max_jint/2\n0：无限制（打印所有堆栈） 举例：-XX:MaxJavaStackTraceDepth=512\n5.1.2. 实现机制 # 堆栈深度限制检查：\njdk-jdk-17-35/src/hotspot/share/runtime/thread.cpp\n// 在打印堆栈跟踪时检查深度限制 if (MaxJavaStackTraceDepth \u0026gt; 0 \u0026amp;\u0026amp; MaxJavaStackTraceDepth == count) { return; // 达到限制，停止打印后续堆栈信息 } 关键点：\n当堆栈深度达到 MaxJavaStackTraceDepth 限制时，停止打印后续堆栈信息 MaxJavaStackTraceDepth == 0 表示无限制，打印所有堆栈 限制检查在每次打印堆栈帧时进行，确保不会超过限制 5.1.3. 使用场景 # 用于限制异常堆栈跟踪的输出长度，特别是在深度递归场景下。\n5.2. SelfDestructTimer # 5.2.1. 参数说明 # 说明：自毁定时器，在指定时间（分钟）后导致 VM 终止。\n默认：0（关闭）\n类型：product，不可通过 JMX 修改\n范围：0 到 max_intx\n0：关闭 举例：-XX:SelfDestructTimer=60（60 分钟后终止）\n5.2.2. 实现机制 # 自毁定时器检查：\njdk-jdk-17-35/src/hotspot/share/runtime/vmThread.cpp\n// 在 VMThread 的循环中定期检查 if ((SelfDestructTimer != 0) \u0026amp;\u0026amp; !VMError::is_error_reported() \u0026amp;\u0026amp; (os::elapsedTime() \u0026gt; (double)SelfDestructTimer * 60.0)) { // 达到定时器时间，终止 VM vm_exit(0); } 关键点：\n在 VMThread 的循环中定期检查自毁定时器 检查条件：SelfDestructTimer != 0（已启用）且没有错误已报告 时间计算：os::elapsedTime() 返回秒数，与 SelfDestructTimer * 60.0（分钟转秒）比较 达到时间后调用 vm_exit(0) 正常退出 VM 6. 版本对比总结 # 6.1. 各版本特性对比表 # 6.1.1. Heap Dump 相关参数 # 特性 JDK 11 JDK 17 JDK 21 JDK 25 HeapDumpGzipLevel ❌ 不支持 ✅ 引入 ✅ ✅ 并行转储 ❌ ❌ ❌ ✅ 引入 并行压缩 ❌ ❌ ❌ ✅ 引入 HeapDumpPath %p 占位符 ❌ ❌ ❌ ✅ 引入 路径内存分配方式 os::malloc os::malloc os::malloc ✅ 栈上分配 HeapDumpPath 默认值 NULL NULL nullptr nullptr FullGCHeapDumpLimit ❌ ❌ ❌ ✅ 引入 可重试分配检查 ❌ ✅ in_retryable_allocation() ✅ in_retryable_allocation() ✅ is_in_internal_oome_mark() 内部 OOM 标记机制 ❌ ❌ ❌ ✅ InternalOOMEMark 6.1.2. OutOfMemoryError 处理相关参数 # 特性 JDK 11 JDK 17 JDK 21 JDK 25 CrashOnOutOfMemoryError 实现 fatal() ✅ report_fatal() report_fatal() report_fatal() ExitOnOutOfMemoryError 实现 os::exit(3) os::exit(3) ✅ os::_exit(3) os::_exit(3) OnOutOfMemoryError %p 占位符 ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持 Atomic::cmpxchg 参数顺序 cmpxchg(1, \u0026amp;addr, 0) ✅ cmpxchg(\u0026amp;addr, 0, 1) cmpxchg(\u0026amp;addr, 0, 1) cmpxchg(\u0026amp;addr, 0, 1) 6.1.3. 错误日志和诊断相关参数 # 特性 JDK 11 JDK 17 JDK 21 JDK 25 ErrorFile %p 占位符 ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持 CreateCoredumpOnCrash ✅ 默认启用 ✅ 默认启用 ✅ 默认启用 ✅ 默认启用 OnError %p 占位符 ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持 ErrorLogTimeout ✅ 默认 120 秒 ✅ 默认 120 秒 ✅ 默认 120 秒 ✅ 默认 120 秒 6.1.4. 其他诊断参数 # 特性 JDK 11 JDK 17 JDK 21 JDK 25 MaxJavaStackTraceDepth ✅ 默认 1024 ✅ 默认 1024 ✅ 默认 1024 ✅ 默认 1024 SelfDestructTimer ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持 6.2. 关键变化点分析 # 6.2.1. JDK 17 主要变化 # 6.2.1.1. Heap Dump 压缩支持 # 新增参数：-XX:HeapDumpGzipLevel=\u0026lt;level\u0026gt;\n引入版本：JDK 17 JBS 链接：https://bugs.openjdk.org/browse/JDK-8260282 功能：支持 gzip 压缩 Java 对象堆转储文件，压缩级别 0-9 默认值：0（禁用压缩） 影响：可以显著减少转储文件大小，但会增加转储时间 6.2.1.2. 错误处理改进 # CrashOnOutOfMemoryError 实现变化：\nJDK 11：使用 fatal() 函数，直接触发崩溃 JDK 17+：使用 report_fatal() 函数，提供错误类型、文件名、行号等详细信息 影响：错误日志更详细，便于问题诊断 6.2.1.3. 可重试分配机制 # 新增检查：in_retryable_allocation()\n功能：识别可重试的分配失败场景，避免在这些场景触发 Heap Dump 触发场景： GC 后重试分配 堆扩展后重试分配 影响：减少不必要的 Heap Dump，提高性能 6.2.1.4. 原子操作 API 变化 # Atomic::cmpxchg 参数顺序变化：\nJDK 11：Atomic::cmpxchg(1, \u0026amp;addr, 0) - 参数顺序：(expected, addr, new_value) JDK 17+：Atomic::cmpxchg(\u0026amp;addr, 0, 1) - 参数顺序：(addr, expected, new_value) 影响：与 C++11 标准库 std::atomic::compare_exchange_weak 保持一致 6.2.2. JDK 21 主要变化 # 6.2.2.1. 快速退出机制 # ExitOnOutOfMemoryError 实现变化：\nJDK 11-20：使用 os::exit(3)，会执行清理钩子（shutdown hooks） JDK 21+：使用 os::_exit(3)，直接退出，不执行清理钩子 原因：在 Java 对象堆 OOM 场景下，执行 shutdown hooks 可能再次触发 OOM，导致退出过程卡住 影响：快速退出，让容器编排系统能够快速重启服务 6.2.2.2. C++11 现代化 # 代码风格改进：\nHeapDumpPath 默认值：NULL → nullptr 影响：代码更符合 C++11 标准，类型安全 6.2.3. JDK 25 主要变化 # 6.2.3.1. 并行转储和压缩支持 # 并行转储机制：\n功能：使用多个线程并行遍历堆对象并写入段文件，最后合并 线程数量：默认 CPU 核心数 * 3 / 8，OOM 场景下根据可用内存动态调整 堆切分方式：不同 GC 使用不同切分策略（G1 按 region，ZGC 按页面等） 性能提升：对于大 Java 对象堆（8GB+），可以显著减少转储时间 并行压缩：\n功能：每个转储线程独立压缩，压缩速度随线程数线性提升（理想情况） 限制：受 CPU 核心数和内存带宽限制 6.2.3.2. %p 占位符支持 # HeapDumpPath 支持 %p 占位符：\n功能：在路径中使用 %p 占位符，自动替换为进程 ID 实现：使用 Arguments::copy_expand_pid() 函数展开占位符 使用示例：-XX:HeapDumpPath=./dump_%p.hprof → ./dump_12345.hprof 影响：更灵活的文件命名，避免多进程转储时文件名冲突 6.2.3.3. 栈上分配路径 # 路径内存分配方式变化：\nJDK 11-21：使用 os::malloc() 从原生堆分配路径内存 JDK 25+：使用栈上数组 char my_path[JVM_MAXPATHLEN] 原因：在 OOM 场景下，os::malloc() 可能分配失败，导致无法生成转储文件 影响：提高 OOM 场景下转储成功率 6.2.3.4. FullGC 转储次数限制 # 新增参数：-XX:FullGCHeapDumpLimit=\u0026lt;count\u0026gt;\n引入版本：JDK 25（JDK 23 引入，但本文分析 LTS 版本） JBS 链接：https://bugs.openjdk.org/browse/JDK-8321442 功能：限制 Full GC 触发的 Java 对象堆转储次数 默认值：0（无限制） 影响：防止频繁 Full GC 导致过多转储文件 6.2.3.5. 内部 OOM 标记机制 # 新增机制：InternalOOMEMark（RAII 类）\n功能：标记内部 JVM 内存分配路径，这些路径中的 OOM 不应触发完整的 Heap Dump 或堆栈跟踪 使用场景： 类加载过程中的内存分配 异常对象创建 其他内部 JVM 操作 实现：使用 is_in_internal_oome_mark() 替代 in_retryable_allocation() 影响：更精确地识别内部 OOM 场景，避免不必要的 Heap Dump 和性能开销 6.3. 版本演进总结 # 6.3.1. 演进趋势 # 性能优化：\nJDK 17：引入压缩支持，减少文件大小 JDK 25：引入并行转储，提高转储速度 可靠性提升：\nJDK 17：可重试分配检查，避免不必要的 Heap Dump JDK 21：快速退出机制，避免 OOM 场景下二次 OOM JDK 25：栈上分配路径，提高 OOM 场景下转储成功率 功能增强：\nJDK 25：%p 占位符支持，更灵活的文件命名 JDK 25：FullGC 转储次数限制，防止过多转储文件 JDK 25：内部 OOM 标记机制，更精确的 OOM 识别 代码现代化：\nJDK 21：C++11 风格（nullptr 替代 NULL） JDK 17+：原子操作 API 与 C++11 标准库保持一致 7. 最佳实践和建议 # 7.1. 生产环境配置建议 # 7.1.1. 推荐配置 # 今天我们分析的参数，在线上我们仅使用 OnOutOfMemoryError。\n-XX:StartFlightRecording=disk=true,maxsize=5000m,maxage=2d,dumpOnExit=true,filename=JFRdump文件名.jfr -XX:+FlightRecorder -XX:FlightRecorderOptions=maxchunksize=128m,repository=/jfr临时文件目录,preserve-repository=true -XX:OnOutOfMemoryError=\u0026#34;curl -X POST http://registry/unregister?service=my-service; cp /jfr临时文件目录 /容器挂载目录;\u0026#34; 这样配置：\n开启 JFR，同时指定了会把 JFR 事件写到磁盘的临时文件目录，并且最多保存 5000 MB，最长跨度为最近 2 天的事件。 JDK 14 之后，引入了定时 JFR 事件元数据写入临时文件 chunk 的机制，避免 Java 堆 OOM 时因为内存不足导致最后一个 chunk 的 JFR 文件格式非法。参考：https://bugs.openjdk.org/browse/JDK-8184193 开启了 JFR 的 dumpOnExit 功能，确保 JVM 退出时会把 JFR 事件写入文件。 但是 Java 对象 OOM 的时候，可能 dumpOnExit 无法完成。 保底是通过 OnOutOfMemoryError 指定执行命令，把 JFR 临时文件目录拷贝到容器挂载目录，确保能拿到 JFR 文件进行后续分析。 同时，OnOutOfMemoryError 还会执行服务下线等操作。 如前面分析，OnOutOfMemoryError 是异步执行，一条命令一个子进程执行。 这样，我们可以最大程度保证线上 Java 对象堆 OOM 时能拿到 JFR 文件进行分析。通过 JFR 文件，我们基本可以分析出 Java 对象堆 OOM 的原因，这篇文章我们不会详细讲解 JFR 文件的分析方法，下一篇文章我们会详细分析。基本通过 JFR 文件我们可以快速找到几乎一切 Java 对象堆 OOM 的原因。\n7.2. 常见问题解答 # 7.2.1. 为什么 OutOfMemoryError 的 JVM 实例最好下线？ # 发生 OutOfMemoryError 时，JVM 状态已经不健康了，这个 OutOfMemoryError 可能在任意一个触发对象分配的代码中抛出来，但是不论是哪里的 Java 代码，就算是 JDK 内部的 Java 代码也没有通过 catch (Throwable t) 的方式捕获 OutOfMemoryError，这就造成了某些内部状态不一致的问题**。比如 JDK 内部的 HashMap，在 put 的时候触发 OutOfMemoryError，这个 put 操作可能会导致 HashMap 内部的数组扩容，而扩容过程中如果发生 OutOfMemoryError，那么这个 HashMap 可能就处于一个不一致的状态，导致后面这个 HashMap 无法正常使用。如果是业务代码，可能会有更严重的问题。\n7.2.2. 为什么 Heap Dump 不推荐在生产环境使用？ # OnOutOfMemoryError 脚本的尽快执行更重要，但是这个脚本是在 Heap Dump 生成之后才执行的，如果生成 Heap Dump 很慢，可能会导致脚本执行更晚，影响业务下线等操作。 即使有了多线程 + Gzip 压缩的支持，Heap Dump 生成依然可能非常慢，特别是对于大 Java 对象堆（比如 8 GB 以上），生成 Heap Dump 可能需要几分钟甚至更久的时间，这个时间对于业务下线来说太长了。 几乎所有 Java 对象堆 OOM 的原因都可以通过 JFR 文件快速分析出来，Heap Dump 并不是必须的。 7.2.3. OnOutOfMemoryError 和 OnError 的区别是什么？ # OnOutOfMemoryError：只在 java.lang.OutOfMemoryError 时执行（包括 Java 对象堆 OOM 和其他类型的 OOM） OnError：在 VM 致命错误导致 Crash 的时候执行（Java 对象堆 OOM 如果开启了 CrashOnOutOfMemoryError 也会触发） 7.2.4. ExitOnOutOfMemoryError 和 CrashOnOutOfMemoryError 的区别是什么？ # ExitOnOutOfMemoryError：直接退出，退出码为 3，不触发崩溃生成错误日志，但是 JDK 21+ 会使用 _exit(3) 快速退出，避免执行清理钩子 CrashOnOutOfMemoryError：触发 JVM 崩溃，生成错误日志和核心转储，然后退出 ","date":"2025年11月11日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-5-heap-dump-diagnostics/","section":"文章","summary":"深入分析 JVM 错误处理和诊断相关参数的设计原理、实现机制和版本演进，涵盖 Heap Dump、Java 对象堆 OOM 处理、错误日志等关键参数的源码解析和最佳实践。","title":"全网最硬核 JDK 解析 - 5. Heap dump 与错误处理诊断相关演进与最佳实践解析","type":"posts"},{"content":"","date":"1 November 2025","externalUrl":null,"permalink":"/categories/brief-look/","section":"Categories","summary":"","title":"Brief Look","type":"categories"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/concurrency/","section":"Tags","summary":"","title":"Concurrency","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/categories/jep/","section":"Categories","summary":"","title":"JEP","type":"categories"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/jep-502/","section":"Tags","summary":"","title":"Jep-502","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/jit/","section":"Tags","summary":"","title":"Jit","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/lazy-initialization/","section":"Tags","summary":"","title":"Lazy-Initialization","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/optimization/","section":"Tags","summary":"","title":"Optimization","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/tags/stable-value/","section":"Tags","summary":"","title":"Stable-Value","type":"tags"},{"content":"","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/categories/%E6%B5%85%E5%B0%9D%E8%BE%84%E6%AD%A2/","section":"Categories","summary":"","title":"浅尝辄止","type":"categories"},{"content":"JEP 502 是一个预览版的 JDK 增强提案。对于预览版 JEP，我们浅尝辄止；对于稳定版 JEP，我们深入浅出。这是对 JEP 502：Stable Value（预览）的浅尝辄止。\nJEP 链接：https://openjdk.org/jeps/502\n1. 概述 # JEP 502 引入的 StableValue API 旨在解决 Java 开发中的一个根本性矛盾：不可变性与初始化灵活性之间的权衡。\n1.1. 传统方案的局限性 # 我们经常会遇到这样一个程序设计场景：某个字段在初始化后不应再改变，但其初始化时机可能并不确定，可能需要在运行时动态决定。传统上，Java 提供了两种主要方式来实现这一目标：\nfinal 字段的限制：\n必须在构造时或静态初始化时设置，但是实际上我们再编写程序的时候，往往无法在构造函数中就确定某个值 初始化顺序受声明顺序限制 导致应用启动时的\u0026quot;急切初始化\u0026quot;问题 可变字段的问题：\n失去常量折叠优化机会 需要复杂的同步机制（需要双重检查锁定 DCL），并且在某些并发访问的场景下字段必须是 volatile 或者通过 Varhandle 的 getRelease() 和 setAcquire() 方法来保证可见性。 DCL 带来的 volatile 字段带来的性能，即使字段被初始化了，访问时仍然需要读取 volatile 字段，volatile 读有额外的内存屏障，但是实际上这个字段其实不再改变了。 1.2. StableValue 的设计 # StableValue 通过以下设计原则解决了这些问题：\n延迟不可变性（Deferred Immutability）：值可以在任何时候设置，但一旦设置就不可变 最多一次初始化（At-Most-Once Semantics）：即使在并发环境下，也只初始化一次 JVM 优化友好：利用 @Stable 注解，让 JVM 可以进行常量折叠优化 1.3. StableValue API 设计 # 核心接口：StableValue\u0026lt;T\u0026gt;\npublic sealed interface StableValue\u0026lt;T\u0026gt; permits StableValueImpl { // 设置操作 boolean trySet(T contents); void setOrThrow(T contents); // 读取操作 T orElseThrow(); T orElse(T other); T orElseSet(Supplier\u0026lt;? extends T\u0026gt; supplier); // 状态查询 boolean isSet(); // 工厂方法 static \u0026lt;T\u0026gt; StableValue\u0026lt;T\u0026gt; of(); static \u0026lt;T\u0026gt; StableValue\u0026lt;T\u0026gt; of(T contents); static \u0026lt;T\u0026gt; Supplier\u0026lt;T\u0026gt; supplier(Supplier\u0026lt;? extends T\u0026gt; underlying); static \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; list(int size, IntFunction\u0026lt;? extends T\u0026gt; function); static \u0026lt;K, V\u0026gt; Map\u0026lt;K, V\u0026gt; map(Set\u0026lt;K\u0026gt; keys, Function\u0026lt;? super K, ? extends V\u0026gt; function); static \u0026lt;T\u0026gt; IntFunction\u0026lt;T\u0026gt; intFunction(int size, IntFunction\u0026lt;? extends T\u0026gt; function); static \u0026lt;T\u0026gt; Function\u0026lt;T, R\u0026gt; function(Set\u0026lt;T\u0026gt; keys, Function\u0026lt;? super T, ? extends R\u0026gt; function); } 简单看来：这个接口使用 sealed 接口限制实现类，确保：\n只有 StableValueImpl 可以实现该接口 防止外部不安全的实现 便于 JVM 进行特殊优化 1.3.1. orElseSet() - 最核心的方法 # T orElseSet(Supplier\u0026lt;? extends T\u0026gt; supplier); 设计要点：\n保证最多一次执行：即使在高并发场景下，supplier 也只会执行一次 线程安全：内部使用同步机制确保线程安全（其实也是 DCL） 异常处理：如果 supplier 抛出异常，值不会被设置，异常会传播给调用者 防递归：检测并防止递归初始化（同一个线程重入的时候，即在 orElseSet 里面重复调用 orElseSet，会抛出 IllegalStateException） 使用场景对比：\n// 传统方式：需要 DCL 手动同步 private Logger logger = null; private final Object lock = new Object(); Logger getLogger() { if (logger == null) { synchronized (lock) { if (logger == null) { logger = Logger.create(...); } } } return logger; } // StableValue 方式：简洁且安全 private final StableValue\u0026lt;Logger\u0026gt; logger = StableValue.of(); Logger getLogger() { return logger.orElseSet(() -\u0026gt; Logger.create(...)); } 1.3.2. trySet() vs setOrThrow() # 两种设置方法的设计差异：\nboolean trySet(T contents); // 返回 false 如果已设置 void setOrThrow(T contents); // 抛出异常如果已设置 设计考虑：\ntrySet()：适合\u0026quot;尝试设置，失败也无妨\u0026quot;的场景 setOrThrow()：适合\u0026quot;必须设置成功\u0026quot;的场景，提供明确的错误信息 1.3.3. 高级抽象：Stable Supplier、Function、Collection # 1.3.3.1. Stable Supplier # static \u0026lt;T\u0026gt; Supplier\u0026lt;T\u0026gt; supplier(Supplier\u0026lt;? extends T\u0026gt; underlying); 将初始化和访问逻辑封装在声明处 客户端代码更简洁：logger.get() vs getLogger() 减少样板代码（不需要单独的 getter 方法） 实现方式：\nrecord StableSupplier\u0026lt;T\u0026gt;(StableValueImpl\u0026lt;T\u0026gt; delegate, Supplier\u0026lt;? extends T\u0026gt; original) implements Supplier\u0026lt;T\u0026gt; { @ForceInline @Override public T get() { return delegate.orElseSet(original); } } 底层还是使用 orElseSet() 实现的。\n1.3.3.2. Stable List # static \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; list(int size, IntFunction\u0026lt;? extends T\u0026gt; function); 固定大小的不可变列表 每个元素独立延迟初始化 支持随机访问（实现 RandomAccess 接口） 适合对象池模式 使用示例：\n// 创建对象池 static final List\u0026lt;OrderController\u0026gt; POOL = StableValue.list(POOL_SIZE, index -\u0026gt; new OrderController()); // 按线程 ID 分配 OrderController getController() { int index = (int)(Thread.currentThread().getId() % POOL_SIZE); return POOL.get(index); } 1.3.3.3. Stable Function # static \u0026lt;T, R\u0026gt; Function\u0026lt;T, R\u0026gt; function(Set\u0026lt;T\u0026gt; keys, Function\u0026lt;? super T, ? extends R\u0026gt; function); 部分函数（Partial Function）：只对预定义的键集合有效 适合缓存计算结果（如查找表、转换表） 支持常量折叠优化 使用示例：\n// 计算对数查找表 private static final Set\u0026lt;Integer\u0026gt; KEYS = Set.of(1, 2, 4, 8, 16, 32); private static final Function\u0026lt;Integer, Integer\u0026gt; LOG2 = StableValue.function(KEYS, i -\u0026gt; 31 - Integer.numberOfLeadingZeros(i)); public static int log2(int n) { return LOG2.apply(n); // 如果入参属于 KEYS，会被 JIT 折叠为常量 } 2. 核心实现源码简要分析 # StableValue 的核心实现类是 StableValueImpl\n2.1. StableValueImpl 的核心字段 # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java\n// Unsafe 允许在启动序列早期使用 StableValue static final Unsafe UNSAFE = Unsafe.getUnsafe(); // 用于直接字段访问的偏移量 private static final long CONTENTS_OFFSET = UNSAFE.objectFieldOffset(StableValueImpl.class, \u0026#34;contents\u0026#34;); // 用于表示 null 值的哨兵对象 private static final Object NULL_SENTINEL = new Object(); // 使用 @Stable 注解标记的核心字段 // | Value | Meaning | // | -------------- | ------------ | // | null | Unset | // | NULL_SENTINEL | Set(null) | // | other | Set(other) | @Stable private Object contents; 使用 Unsafe 的原因：\n允许在 JVM 启动早期使用（不依赖反射、MethodHandles） 可以精确控制内存语义（acquire/release） 避免方法调用的开销 NULL_SENTINEL 设计：\nJava 中 null 无法区分\u0026quot;未设置\u0026quot;和\u0026quot;设置为 null\u0026quot; 使用哨兵对象解决这个问题 增加少量内存开销，但保证语义正确性 JDK 内部使用的 @Stable 注解的作用：\n告诉 JVM 该字段在初始化后值稳定 允许 JIT 进行常量折叠优化 2.2. 核心方法实现分析 # 2.2.1. 读取操作：wrappedContentsAcquire() # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java\n@ForceInline public Object wrappedContentsAcquire() { return UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET); } Acquire 语义：确保在读取 contents 之前，所有之前的内存操作都已完成。Release/Acquire 语义一般成对出现，内存屏障比 volatile 更轻量： Release/Acquire：JMM 抽象上，Release 之前放 LoadStore 和 StoreStore 屏障；Acquire 之后放 LoadLoad 和 LoadStroe 屏障 volatile：JMM 抽象上，volatile write 之前放 LoadStore 和 StoreStore 屏障，之后放StroeLoad；volatile read 之后放 LoadLoad 和 LoadStroe 屏障。 StoreLoad 屏障开销一般比较大，一般 CPU 与操作系统中都是全屏障。 使用 @ForceInline 提示 JVM 内联此方法，减少调用开销 2.2.2. 设置操作：trySet() 和 wrapAndSet() # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java\n@ForceInline @Override public boolean trySet(T contents) { // 快速路径：如果已设置，直接返回 false if (wrappedContentsAcquire() != null) { return false; } // 防止递归初始化 preventReentry(); // 互斥锁保护，与 orElseSet 协调 synchronized (this) { return wrapAndSet(contents); } } private void preventReentry() { if (Thread.holdsLock(this)) { throw new IllegalStateException(\u0026#34;Recursive initialization...\u0026#34;); } } @ForceInline private boolean wrapAndSet(T newValue) { assert Thread.holdsLock(this); // 在持有锁的情况下，普通语义足够 if (contents == null) { // 使用 Release 语义写入，确保之前的所有操作可见 UNSAFE.putReferenceRelease(this, CONTENTS_OFFSET, wrap(newValue)); return true; } return false; } 双重检查锁定模式（DCL）：\n第一次检查：wrappedContentsAcquire() != null（快速路径） 加锁后再次检查：contents == null（慢速路径） 避免不必要的锁竞争 内存屏障顺序：\n读取：使用 getReferenceAcquire（acquire 语义） 写入：使用 putReferenceRelease（release 语义） 形成 happens-before 关系 防递归机制：防止同一个线程递归调用导致重复初始化。\n2.2.3. 延迟初始化：orElseSet() # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java\n@ForceInline @Override public T orElseSet(Supplier\u0026lt;? extends T\u0026gt; supplier) { Objects.requireNonNull(supplier); // 快速路径：如果已设置，直接返回 final Object t = wrappedContentsAcquire(); return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t); } @DontInline // 明确标记不要内联，保持方法边界清晰 private T orElseSetSlowPath(Supplier\u0026lt;? extends T\u0026gt; supplier) { preventReentry(); // 防止递归 synchronized (this) { // 在锁内再次检查（双重检查） final Object t = contents; // 普通语义足够（已持有锁） if (t == null) { // 执行 supplier，可能耗时较长 final T newValue = supplier.get(); // 设置值并返回 wrapAndSet(newValue); return newValue; } // 其他线程已设置，直接返回 return unwrap(t); } } 快速路径优化：\n大部分情况下值已设置，快速返回 使用 @ForceInline 内联主方法 避免不必要的同步开销 慢速路径分离：\n使用 @DontInline 标记慢速路径 保持方法边界，便于调试和性能分析 避免过度内联导致代码膨胀 2.2.4. null 值处理机制 # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java\n// 将 null 值包装为哨兵对象 @ForceInline private static Object wrap(Object t) { return (t == null) ? NULL_SENTINEL : t; } // 将哨兵对象解包为 null @SuppressWarnings(\u0026#34;unchecked\u0026#34;) @ForceInline private static \u0026lt;T\u0026gt; T unwrap(Object t) { return t != NULL_SENTINEL ? (T) t : null; } 为什么需要这个机制？\n在 Java 中，对象引用字段无法区分以下两种情况：\n字段未初始化（null） 字段已初始化为 null 值 StableValue 需要区分这两种状态，因此：\n使用 null 表示\u0026quot;未设置\u0026quot; 使用 NULL_SENTINEL 表示\u0026quot;已设置为 null\u0026quot; 内存开销：\n每个 StableValue 实例：一个对象引用（8 字节） NULL_SENTINEL：全局共享对象（每个 JVM 一个实例） 总开销：几乎可以忽略 2.2.5. 高级抽象实现 # 2.2.5.1. StableSupplier 实现 # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java\npublic record StableSupplier\u0026lt;T\u0026gt;(StableValueImpl\u0026lt;T\u0026gt; delegate, Supplier\u0026lt;? extends T\u0026gt; original) implements Supplier\u0026lt;T\u0026gt; { @ForceInline @Override public T get() { return delegate.orElseSet(original); } // 基于身份的 equals 和 hashCode @Override public int hashCode() { return System.identityHashCode(this); } @Override public boolean equals(Object obj) { return obj == this; } } 底层仍然使用 StableValueImpl 的 orElseSet() 方法实现延迟初始化。\n2.2.5.2. StableFunction 实现 # 文件位置：src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java\npublic record StableFunction\u0026lt;T, R\u0026gt;( Map\u0026lt;? extends T, StableValueImpl\u0026lt;R\u0026gt;\u0026gt; values, Function\u0026lt;? super T, ? extends R\u0026gt; original) implements Function\u0026lt;T, R\u0026gt; { @ForceInline @Override public R apply(T value) { // 查找对应的 StableValue final StableValueImpl\u0026lt;R\u0026gt; stable = values.get(value); if (stable == null) { throw new IllegalArgumentException(\u0026#34;Input not allowed: \u0026#34; + value); } // 延迟初始化并返回 return stable.orElseSet(new Supplier\u0026lt;R\u0026gt;() { @Override public R get() { return original.apply(value); } }); } } Map 结构：\n每个输入值对应一个 StableValueImpl，底层还是使用 orElseSet() 实现延迟初始化 独立的延迟初始化 支持部分函数（只允许预定义的输入） 异常处理：\n不允许的输入立即抛出异常 不创建不必要的 StableValue 实例 提供清晰的错误信息 3. JIT 优化 # JDK 层面通过 StableValue 使用了 @Stable 注解，JIT 层面通过识别 StableValue 的使用模式，进行优化。\n3.1. @Stable 注解的作用 # @Stable 字段优化支持：\nC1 编译器：支持基本的常量折叠，但优化程度有限 C2 编译器：支持完整的常量折叠和更激进的优化（死代码消除、循环展开等） flowchart TD A[Java 字节码: 读取字段] --\u003e B{编译器接口 CIciField.cppC1 和 C2 共享} B --\u003e C[检查字段属性is_stable?] C --\u003e D{字段是Stable注解的?} D --\u003e|是| E[标记为常量_is_constant = trueC1 和 C2 共享] D --\u003e|否| F[普通字段处理] E --\u003e G{编译器选择} G --\u003e|C1 路径| H[C1 GraphBuilderc1_GraphBuilder.cpp] G --\u003e|C2 路径| I[C2 Parserparse3.cpp] H --\u003e J[创建 Constant 节点make_constant支持 StableArrayConstant] I --\u003e K[创建常量节点make_constant_from_field跳过内存读取] J --\u003e L[C1 优化阶段基本常量折叠] K --\u003e M[C2 优化阶段激进优化] M --\u003e N[消除内存别名分析死代码消除循环展开内联优化] L --\u003e O[生成机器码值直接嵌入] N --\u003e O O --\u003e P[运行时执行零内存访问] style B fill:#e1f5ff style E fill:#fff4e1 style H fill:#ffe1e1 style I fill:#e1ffe1 style M fill:#e1ffe1 style N fill:#e1ffe1 3.2. JIT 优化后的 StableValue 核心方法 orElseSet() 的效果 # 原始代码：\n// 调用方代码 Logger logger = stableValue.orElseSet(() -\u0026gt; Logger.create(...)); // StableValueImpl.java 中的 orElseSet 方法 @ForceInline @Override public T orElseSet(Supplier\u0026lt;? extends T\u0026gt; supplier) { Objects.requireNonNull(supplier); final Object t = wrappedContentsAcquire(); return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t); } 如果代码进入 JIT 优化，一定是被调用了很多次，那么这个值一定被初始化了。首先是 C1 优化，第一步，方法内联：\n由于 @ForceInline 注解，C1 会内联所有 @ForceInline 的方法：\nLogger logger; { Objects.requireNonNull(supplier); // supplier 是 lambda final Object t = UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET); logger = (t == null) ? orElseSetSlowPath(supplier) : (t != NULL_SENTINEL ? (T) t : null); } 其中：\nwrappedContentsAcquire() 是 @ForceInline，内联为：UNSAFE.getReferenceAcquire(this, CONTENTS_OFFSET) unwrap(t) 是 @ForceInline，内联为：(t != NULL_SENTINEL ? (T) t : null) 接下来，C1 会进行常量折叠优化：\nLogger logger; { Objects.requireNonNull(supplier); final Object t = \u0026lt;常量值\u0026gt;; // 直接使用常量，无需内存读取 logger = (t == null) ? orElseSetSlowPath(supplier) : (t != NULL_SENTINEL ? (T) t : null); } 如果代码运行足够多次，进入 C2，C2 会做前面的优化，然后进行更激进的优化，首先是死代码消除（Dead Code Elimination）：\n// C2 死代码消除后 Logger logger; { Objects.requireNonNull(supplier); // 可能被消除（如果编译器能证明 supplier 非 null） final Object t = \u0026lt;常量值\u0026gt;; // 已知 != null // 分支 (t == null) 被证明为 false // orElseSetSlowPath(supplier) 调用被完全消除（死代码） logger = t != NULL_SENTINEL ? (T) t : null; // 只保留这个路径 } 如果 C2 能证明 t != NULL_SENTINEL（大多数情况），可以进一步优化：\n// C2 最终优化结果（t != NULL_SENTINEL） Logger logger = \u0026lt;常量值\u0026gt;; // 直接赋值，零开销 或者如果 t == NULL_SENTINEL：\n// C2 最终优化结果（t == NULL_SENTINEL） Logger logger = null; // 直接返回 null ","date":"2025年11月1日","externalUrl":null,"permalink":"/zh-cn/posts/brief-look-jep-502/","section":"文章","summary":"探索 JEP 502 的 StableValue API，它解决了 Java 开发中不变性与初始化灵活性之间的根本权衡。本文涵盖 API 设计、核心实现细节（包括双重检查锁定模式、内存语义）以及使用 @Stable 注解的 JIT 优化策略。","title":"浅尝辄止 JEP - JEP-502：Stable Value（预览）","type":"posts"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/anti-bot/","section":"Tags","summary":"","title":"Anti-Bot","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/categories/api/","section":"Categories","summary":"","title":"API","type":"categories"},{"content":" API 安全密钥交换 # 我们将客户端分为两种主要类型：web/wap（轻量级客户端）和 android/ios（可信客户端）。\n对于 web/wap 客户端： 过程首先通过 CSRF 保护获取 RSA 公钥，然后使用该 RSA 公钥解密服务器与当前会话关联的 AES Token。所有后续请求都使用此 AES Token 加密。\nCSRF token 必须直接嵌入页面中，而不是存储在 cookie 中 - 否则，安全收益很小。 这种方法有助于确保页面实际上由我们的服务器渲染。 CSRF token 需要加密，并且每次返回都应该是唯一的（通过嵌入时间戳），尽管它们映射到相同的后端 CSRF token。 公钥轮换至关重要：我们运行定时任务定期更新存储的 wap/web 密钥对，之前的密钥在 1 个月后过期（我们的前端会话公钥在没有续订的情况下在 2 周内过期，所以 1 个月提供了安全缓冲）。 对于 android/ios 客户端： 每个应用版本在构建时嵌入唯一的公钥，服务器记录该公钥。这直接解密当前会话的 AES Token，所有后续请求都使用此 AES Token 加密。\n由于移动应用通常是具有版本特定公钥的可信客户端，通常不需要 CSRF 保护。 此机制还可以促进强制版本升级。 为什么这很重要：\nHTTPS 防止 WiFi 劫持和 HTTP 标头和正文内容的数据包嗅探 API 加密防止机器人抓取、请求伪造和客户端模拟 Android/iOS 公钥有助于识别特定版本并验证可信客户端，而浏览器无法提供这种级别的保证 - 这就是为什么我们添加 CSRF token 以合理确保请求来自我们渲染的页面 安全标头 # Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n启用 HTTP 严格传输安全（HSTS），指示浏览器仅通过 HTTPS 访问网站 max-age=31536000 告诉浏览器记住仅 HTTPS 访问 31,536,000 秒（1 年） includeSubDomains 将此规则扩展到当前域的所有子域 preload 表示网站希望包含在浏览器的内置 HSTS 预加载列表中，即使在首次访问时也强制使用 HTTPS X-Content-Type-Options: nosniff\n防止浏览器尝试\u0026quot;嗅探\u0026quot;或猜测资源 MIME 类型的安全功能，强制遵守服务器提供的 Content-Type 标头 nosniff 选项防止 MIME 类型混淆攻击，例如浏览器将非脚本文件解释为可执行脚本 X-Frame-Options: SAMEORIGIN\n防止其他站点通过 \u0026lt;iframe\u0026gt;、\u0026lt;frame\u0026gt;、\u0026lt;embed\u0026gt; 或 \u0026lt;object\u0026gt; 嵌入页面，防止点击劫持攻击 SAMEORIGIN 仅允许来自同一源的页面在框架中嵌入当前页面 Content-Security-Policy: [此处省略大量内容]\n此阻止机制可能导致某些第三方跟踪像素、图像等加载失败，需要监控（通过在末尾添加 report-uri /api/csp-report-endpoint?version=5）。报告包括所有被阻止的内容：JS、CSS、JPG、持久连接、视频资源等。 此标头经常更改 - 例如，当搜索引擎重定向到你的网站时，它们嵌入 JS，广告渠道合作伙伴也是如此 你需要自己实现 /api/csp-report-endpoint 包含版本参数，以便在添加新的白名单条目并递增版本号后，你可以忽略来自旧版本的报告（因为此标头通常在 CDN 级别添加，如 Cloudflare，通常具有长缓存时间） 以下是允许所有 Google 和 Facebook 域的示例 Content-Security-Policy：\ndefault-src \u0026#39;self\u0026#39; data: \u0026#39;unsafe-inline\u0026#39; blob: \u0026#39;unsafe-eval\u0026#39; *.google-analytics.com *.googletagmanager.com *.gstatic.com *.googleapis.com *.google.co *.google.com *.google.ad *.google.ae *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za *.google.co.zm *.google.co.zw *.google.cat *.googleadservices.com facebook.net *.facebook.net facebook.com *.facebook.com; report-uri /api/csp-report-endpoint?version=5 反机器人保护机制 # 我们主要解决两种情况：\n对于未注册或已注销的用户： 为注册、验证码和类似端点实施反机器人措施，同时最小化用户摩擦。\n对于已认证的用户：\n为活动设置必要的参与阈值（如最近的交易量） 引入 MFA 后，将活动参与限制为用户绑定的 MFA 设备 场景 2 主要面向业务。MFA 机制不仅提供安全保证，还有助于验证设备真实性，实现基于设备的业务限制。\n对于场景 1，我们可以使用这些机制减少验证码摩擦：\n为注册和短信 OTP 等敏感端点实施 Google reCAPTCHA Enterprise（reCAPTCHA v3）或 hCAPTCHA Enterprise 等服务。每个请求包括 Google reCAPTCHA Enterprise 分数： reCAPTCHA v3 在浏览时持续评估用户行为，包括交互模式（鼠标移动、滚动、点击）、设备和浏览器信息，以及跨多个页面的会话范围行为分析。 基于此行为分析，reCAPTCHA v3 为每个用户请求分配 0.0 到 1.0 的分数。接近 1.0 的分数表示系统相信行为来自真实人类，而较低分数表明自动化脚本或机器人。以下是示例分数分布： 你的后端根据此分数响应（我们要求所有得分低于 0.8 的请求提供验证码）。有许多验证码实现选项可用。\n结果： 大多数用户甚至不需要在注册期间输入验证码。只有得分较低的用户面临挑战（验证码或其他挑战方法）。\n为什么我们不推荐 IP + 设备阻止或速率限制（速率限制意味着重定向或显示验证码，而不是阻止访问）：与上述方法相比，这会产生更多用户摩擦。此外，IP 和设备很容易被伪造（IP 通过 VPN，设备通过模拟），当前浏览器趋势朝着统一用户代理发展，暴露的信息更少：\nhttps://developers.google.com/privacy-sandbox/blog/user-agent-reduction-android-model-and-version\n","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/posts/safe-and-anti-bot/","section":"文章","summary":"深入探讨现代 API 安全实践，涵盖 Web 和移动客户端密钥交换机制、基本安全标头实现以及有效的反机器人保护策略，以保护你的应用同时保持出色的用户体验。","title":"API 安全和反机器人保护综合指南","type":"posts"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/api-security/","section":"Tags","summary":"","title":"Api-Security","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/csp/","section":"Tags","summary":"","title":"Csp","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/csrf/","section":"Tags","summary":"","title":"Csrf","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/hsts/","section":"Tags","summary":"","title":"Hsts","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/recaptcha/","section":"Tags","summary":"","title":"Recaptcha","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/rsa-encryption/","section":"Tags","summary":"","title":"Rsa-Encryption","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/tags/security-headers/","section":"Tags","summary":"","title":"Security-Headers","type":"tags"},{"content":"","date":"2025年5月28日","externalUrl":null,"permalink":"/zh-cn/categories/web-development/","section":"Categories","summary":"","title":"Web Development","type":"categories"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/categories/devops/","section":"Categories","summary":"","title":"DevOps","type":"categories"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/memory/","section":"Tags","summary":"","title":"Memory","type":"tags"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/microservices/","section":"Tags","summary":"","title":"Microservices","type":"tags"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/outofmemoryerror/","section":"Tags","summary":"","title":"Outofmemoryerror","type":"tags"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/performance/","section":"Tags","summary":"","title":"Performance","type":"tags"},{"content":"","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/production/","section":"Tags","summary":"","title":"Production","type":"tags"},{"content":" 1. 为什么我们不推荐启用 HeapDumpOnOutOfMemoryError # 1.1. 启用 HeapDumpOnOutOfMemoryError 后，哪些 OutOfMemoryError 实际会触发它？ # 这里有个有趣的事情 - 一旦你启用了 HeapDumpOnOutOfMemoryError，并不是每个 OutOfMemoryError 都会实际触发堆转储！让我们分解不同类型的 OutOfMemoryError 异常，看看哪些会配合：\nOutOfMemoryError: Java heap space 和 OutOfMemoryError: GC overhead limit exceeded：这两个都表示 Java 堆内存不足 - 一个在分配时剩余空间不足时发生，另一个达到特定阈值。这两个都会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: unable to create native thread：当系统无法创建新的平台线程时发生。这个不会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: Requested array size exceeds VM limit：当请求的数组大小超过堆内存限制时抛出。这会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: Compressed class space 和 OutOfMemoryError: Metaspace：两者都与元空间问题相关。两者都会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: Cannot reserve xxx bytes of direct buffer memory (allocated: xxx, limit: xxx)：在 DirectByteBuffer 中，系统首先从 Bits 类请求配额，该类维护一个全局 totalCapacity 变量跟踪所有 DirectByteBuffer 大小。你可以使用 -XX:MaxDirectMemorySize 限制这个。这不会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: map failed：在文件内存映射（MMAP）期间系统内存不足时发生。这不会触发 HeapDumpOnOutOfMemoryError\n还有一些额外情况：\nShenandoah 分配区域位图内存问题触发 OutOfMemoryError 会触发 HeapDumpOnOutOfMemoryError\nOutOfMemoryError: Native heap allocation failed：消息可能因操作系统而异，但通常包括\u0026quot;native heap\u0026quot;。这通常与 Java 对象堆无关，而是其他内存分配失败。这些不会触发 HeapDumpOnOutOfMemoryError\n1.2. 为什么我们建议不要启用 HeapDumpOnOutOfMemoryError # 让我们深入了解 HeapDumpOnOutOfMemoryError 实际如何工作：\nJVM 进入 safepoint，暂停所有应用线程。对于 HeapDumpOnOutOfMemoryError，它使用单线程转储（与可以使用多线程的 jcmd/jmap 不同）创建多个文件。然后退出 safepoint。\n然后将这些多个文件合并为一个并压缩。\n这里的主要瓶颈是第一步 - 写入过程 - 具体来说，是磁盘 I/O 性能。让我们看看一些真实的云存储性能标准：\nAWS EFS（标准存储）：https://docs.aws.amazon.com/efs/latest/ug/performance.html AWS EBS（SSD 等效）：https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html 对于 4GB 堆，使用 EFS（对应不到 100GB 磁盘），写入至少需要 4 * 1024 / 300 = 13.65 秒（这还是峰值性能！）。如果峰值性能已经在其他地方使用，你需要 4 * 1024 / 15 = 273 秒。即使使用 EBS，你仍然需要 4 * 1024 / 1000 = 4 秒。记住，这是你的应用线程在 stop-the-world 状态下完全冻结的时间！这甚至没有考虑同一台机器上的多个容器实例。从成本角度来看，我们不能给每个微服务 AWS EBS（SSD 等效）存储。\n所以我们的建议？完全跳过 HeapDumpOnOutOfMemoryError！\n2. 用什么替代 HeapDumpOnOutOfMemoryError？ # 2.1. 使用 JFR 进行内存泄漏检测 # 当我需要追踪 OutOfMemoryError 问题时，我通常依赖 JFR 的对象分配样本和旧对象样本数据来定位有问题的对象。只有当这些方法没有产生结果时，我才会考虑生成堆转储。\n2.2. 为什么遇到 OutOfMemoryError 的微服务应该重启？ # 事情是这样的 - 大多数代码，包括 JDK 源代码，不会在每个内存分配点考虑 OutOfMemoryError。这可能导致应用状态不一致。例如，在 HashMap 重新哈希操作期间，如果中途抛出 OutOfMemoryError，之前更新的状态就会损坏。大多数库很少捕获 Throwable - 它们通常只捕获 Exception。\n在每个内存分配点处理 OutOfMemoryError 根本不现实。为了防止 OutOfMemoryError 导致的意外一致性问题，最安全的方法是使服务离线并重启它。\n2.3. 如何实现遇到 OutOfMemoryError 的微服务的自动重启？ # 你可以使用 -XX:OnOutOfMemoryError=\u0026quot;/path/to/script.sh\u0026quot; 来指定一个处理以下内容的脚本：\n优雅的微服务关闭 微服务重启 对于 Spring Boot 应用，考虑启用对 /actuator/shutdown 的本地访问以优雅关闭微服务（尽管一些社区成员报告这在发生 OutOfMemoryError 时可能会挂起 - 这可能是由于启用了 HeapDumpOnOutOfMemoryError，如第 1.2 节所述）。Kubernetes 会自动启动一个新实例。\n","date":"2025年5月1日","externalUrl":null,"permalink":"/zh-cn/posts/java-oom/","section":"文章","summary":"全面指南，探讨为什么启用 HeapDumpOnOutOfMemoryError 会在生产环境中导致严重的性能问题，哪些 OutOfMemoryError 类型实际触发堆转储，以及使用 JFR 进行内存泄漏检测和自动服务重启策略等更好的替代方案。","title":"为什么应该避免在生产环境中启用 HeapDumpOnOutOfMemoryError","type":"posts"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/categories/database/","section":"Categories","summary":"","title":"Database","type":"categories"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/indexing/","section":"Tags","summary":"","title":"Indexing","type":"tags"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/mvcc/","section":"Tags","summary":"","title":"Mvcc","type":"tags"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/mysql/","section":"Tags","summary":"","title":"Mysql","type":"tags"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/oltp/","section":"Tags","summary":"","title":"Oltp","type":"tags"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/tags/postgresql/","section":"Tags","summary":"","title":"Postgresql","type":"tags"},{"content":" PostgreSQL vs MySQL：为你的 OLTP 工作负载找到合适的选择 # 在 OLTP 场景中选择 PostgreSQL 和 MySQL 之间（假设默认事务引擎和编码/压缩设置），决策通常归结为了解它们的根本架构差异。让我带你了解在实践中真正重要的关键区别。\n两种索引架构的故事 # 最显著的差异在于这些数据库如何处理二级索引，相信我，这比你预期的更能影响性能。\nMySQL 的巧妙方法：二级索引叶节点存储主键值（感谢 monstaxl 微信公众号的 LiZN 的澄清）。这个设计选择对于写密集型工作负载实际上非常聪明。\nPostgreSQL 的直接方法：二级索引叶节点直接指向记录位置，就像主索引一样。虽然这看起来很简单，但它创造了一个有趣的挑战。\n这就是它变得有趣的地方：当 PostgreSQL 更新一行时，特别是使用 MVCC 和可变长度字段时，行位置经常改变。这意味着所有二级索引都需要更新。MySQL 通过其主键引用方法完全避免了这个问题。\n现在，PostgreSQL 确实有一个巧妙的优化，称为堆仅元组（HOT），当只有非索引字段改变时可以避免索引更新（假设有足够的缓冲池）。然而，在真实的 OLTP 场景中，HOT 命中率并不像我们希望的那样令人印象深刻。\n底线：在高并发更新下，对于具有二级索引的表，MySQL 通常优于 PostgreSQL，特别是在处理可变长度字段更改（如扩展 varchar 字段）和重度插入工作负载时。\n权衡：MySQL 的二级索引读取需要额外的主键查找，使它们比 PostgreSQL 的直接访问方法慢。这都是关于选择你的战斗！\nMVCC：两种哲学，不同结果 # 第二个主要差异在于它们的多版本并发控制实现。\nPostgreSQL 的 xmin/xmax 舞蹈：PostgreSQL 使用优雅的 xmin/xmax 机制，其中：\n更新创建新的行版本，xmin 设置为当前事务 ID，同时标记旧版本的 xmax 删除只需将行的 xmax 设置为当前事务 ID MySQL 的撤销日志策略：MySQL 依赖于行锁和撤销日志，每条记录包含事务 ID（trx_id）和回滚指针（roll_pointer）的隐藏列。InnoDB 使用这些来定位每个事务的正确行版本。\nPostgreSQL 的读取优势：旧版本保持直接可访问，读取永远不会阻止同一行的更新。对于读密集型场景来说这很美妙。\n黑暗面：频繁更新导致表快速膨胀。Vacuum 操作有时无法跟上高速写入，在某些场景中 autovacuum 可能有问题，导致死元组积累，最终使查询极其 I/O 昂贵。需要手动 DBA 干预。插入性能也受到这种多版本开销的影响。\nMySQL 的写入优势：只有主动读取/写入的行被锁定，允许更高的并发写入而不受干扰。\n当橡胶遇到路面 # 两个数据库在大型表上的高频更新都面临挑战，但突破点不同。我们这里不是在谈论典型的订单或事务表，而是像用户余额表这样不断更新的场景。\nPostgreSQL 的 xmin/xmax MVCC 设计导致比 MySQL 的类似 Oracle 的重做日志方法更快的表膨胀。这意味着MySQL 通常在需要分片之前处理更大的数据集。\n有趣的是，PostgreSQL 在 2020 年左右探索了 zheap 项目以采用重做日志机制，但开发似乎已经停滞。你仍然可以在以下位置找到痕迹：https://wiki.postgresql.org/wiki/Zheap\n实际结论 # 对于纯 OLTP 工作负载，MySQL 通常证明更合适。现代云提供商已经通过极低延迟的只读副本在很大程度上解决了读取性能差距。以 Aurora 为例 - 你可以将多达 12 个只读实例附加到单个写入实例，延迟通常在 10 毫秒左右（在高峰流量期间偶尔会飙升到 300 毫秒）。\nPostgreSQL 在其丰富的生态系统和 OLAP 能力方面表现出色。许多分析数据库实际上使用 PostgreSQL 的线路协议，PostgreSQL 的开发轨迹越来越关注 OLAP 生态系统增强。\n真实世界验证 # Uber 在 2015 年从分片 PostgreSQL 迁移到分片 MySQL 用于其 OLTP 工作负载，提供了令人信服的真实世界证据。他们的详细分析值得一读：https://www.uber.com/en-HK/blog/postgres-to-mysql-migration/\n选择最终取决于你的特定工作负载特征，但了解这些根本差异有助于做出明智的决策，而不是跟随趋势或假设。\n","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/posts/pgsql-mysql/","section":"文章","summary":"PostgreSQL 和 MySQL 在 OLTP 场景下的全面比较，探讨它们在二级索引处理和 MVCC 实现方面的根本架构差异，以及何时选择每个数据库的实用见解。","title":"PostgreSQL vs MySQL：为你的 OLTP 工作负载找到合适的选择","type":"posts"},{"content":"","date":"2024年5月1日","externalUrl":null,"permalink":"/zh-cn/categories/technology/","section":"Categories","summary":"","title":"Technology","type":"categories"},{"content":"","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/tags/crac/","section":"Tags","summary":"","title":"Crac","type":"tags"},{"content":"","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/tags/graalvm/","section":"Tags","summary":"","title":"Graalvm","type":"tags"},{"content":" GraalVM Native Image 进程能否被 jps 检测到？ # 答案是肯定的，但只在特定条件下！如果你在编译时使用参数 --enable-monitoring=jmxserver,jmxclient,jvmstat 启用了 jstat 和 jmx 等监控功能，那么你的 native image 进程就会在 jps 中显示。如果没有启用这些标志，它们对 jps 来说仍然是不可见的。\n我们当前的策略：GraalVM vs JVM 使用 # 以下是我们如何在技术栈中战略性地利用这两种技术：\n1. Lambda 风格任务 → GraalVM Native Image # 对于像不频繁但数据量大的定时任务（想想每周报告或临时数据导出）这样的工作负载，我们全力投入 GraalVM Native Image。以下是为什么这样做完全合理：\n为什么不使用传统微服务？ # 资源浪费是真实存在的：你需要为峰值处理需求调整微服务大小，大部分时间让资源闲置 扩展的舞蹈：为了与持久化微服务保持成本效益，你需要在任务运行前以更高的内存/CPU 重启，然后在之后缩小规模——多么麻烦！ 为什么这些工作负载非常适合无服务器 # K8s CronJobs 和 AWS Lambda 是理想的平台，但它们要求极快的启动时间 Native Image 在这里大放异彩：快速启动时间加上更简单的依赖树，使迁移变得直接 2. 长期运行的微服务 → 坚持使用 JVM # 对于我们的常驻服务，JVM 仍然是我们首选，但有一些智能优化：\n存储密集型服务 # 对于管理大量存储 I/O 连接的微服务，我们采取谨慎的方法：\n暂时跳过 CRaC - 持久连接有太多移动部件 启用 CDS 以加快类加载 考虑 Graal JIT 作为 C2 编译器的即插即用替代品 无状态服务 → 全程使用 CRaC # 对于没有重度存储依赖的服务 - Web 引擎、API 网关和广告服务（具有重度本地缓存） - CRaC 是游戏规则改变者。这些正是需要在需求激增时快速扩展的流量敏感服务！\n这种混合方法为我们提供了两全其美：批处理工作负载的极快无服务器执行，以及实时流量的优化、快速扩展的微服务。\n","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/posts/graalvm-jvm/","section":"文章","summary":"了解 GraalVM Native Image 进程何时会在 jps 中显示，并学习我们在生产环境中在 GraalVM Native Image 和 JVM 之间选择的经过实战验证的方法。我们详细介绍了针对 Lambda 风格任务与长期运行的微服务的策略。","title":"GraalVM Native Image 进程能否被 jps 检测到？以及我们的生产策略","type":"posts"},{"content":"","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/tags/lambda/","section":"Tags","summary":"","title":"Lambda","type":"tags"},{"content":"","date":"2024年4月19日","externalUrl":null,"permalink":"/zh-cn/tags/native-image/","section":"Tags","summary":"","title":"Native-Image","type":"tags"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/tags/api/","section":"Tags","summary":"","title":"API","type":"tags"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/tags/async/","section":"Tags","summary":"","title":"Async","type":"tags"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/tags/testcontainers/","section":"Tags","summary":"","title":"TestContainers","type":"tags"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/categories/testing/","section":"Categories","summary":"","title":"Testing","type":"categories"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/tags/toxicproxy/","section":"Tags","summary":"","title":"Toxicproxy","type":"tags"},{"content":"","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/tags/webclient/","section":"Tags","summary":"","title":"WebClient","type":"tags"},{"content":" 最大化第三方 API 请求吞吐量：实用测试方法 # 一位社区成员最近提出了一个有趣的问题：\u0026ldquo;我如何尽可能快地向第三方 API 发送请求，而不担心压垮他们的服务器？开发和验证这种方法的最佳方式是什么？\u0026rdquo;\n以下是我在实践中发现非常有效的综合策略：\n游戏计划 # 1. 异步或回家 你肯定想使用 WebClient 的异步、非阻塞 I/O 功能。或者，像 Vert.x 这样的框架是绝佳选择。我现在会暂缓使用虚拟线程 - 虽然它们令人兴奋，但它们还没有完全准备好用于生产。\n2. 隔离你的测试环境 关键洞察：如果你只关心代码的性能并想最大化对目标 API 的压力，在开发期间永远不要直接测试第三方接口。你需要先隔离你的代码，确保它按预期执行。\n为什么？响应时间有太多变量和不稳定性。想想看 - 你和 API 之间的网络带宽、你的网卡性能、他们端的潜在速率限制，如果你不限制连接数，CDN 可能完全阻止你。此外，即使是非阻塞请求，如果你同时发出 10,000 个请求，许多请求无论如何都会在网络层排队。\n3. 使用真实模拟进行本地测试 为了测试你自己的代码同时模拟真实延迟，我通常在本地运行测试。我的首选方法使用带有 httpbin 镜像的 TestContainers（kennethreitz/httpbin:latest）。对于你的特定场景，你可以为每个请求添加时间并调用 /anything 端点来收集响应 - 此端点只是回显你发送的所有参数。\n想模拟带宽约束？添加一个 toxicproxy 镜像并通过它路由你的 httpbin 调用以进行带宽限制。需要模拟 API 延迟？使用 /delay/0.1 端点（用于 100 毫秒延迟）。\n代码示例 # 这是一个实用示例（快速测试设置，未微调，仅用于演示测试方法）。首先，让我们创建一个可重用的 TestContainer 基类：\nimport eu.rekawek.toxiproxy.Proxy; import eu.rekawek.toxiproxy.ToxiproxyClient; import eu.rekawek.toxiproxy.model.ToxicDirection; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.ToxiproxyContainer; import org.testcontainers.junit.jupiter.Testcontainers; import java.io.IOException; @Testcontainers public class CommonMicroServiceTest { private static final Network network = Network.newNetwork(); private static final String HTTPBIN = \u0026#34;httpbin\u0026#34;; public static final int HTTPBIN_PORT = 80; public static final GenericContainer\u0026lt;?\u0026gt; HTTPBIN_CONTAINER = new GenericContainer\u0026lt;\u0026gt;(\u0026#34;kennethreitz/httpbin:latest\u0026#34;) .withExposedPorts(HTTPBIN_PORT) .withNetwork(network) .withNetworkAliases(HTTPBIN); /** * \u0026lt;a href=\u0026#34;https://java.testcontainers.org/modules/toxiproxy/\u0026#34;\u0026gt;toxiproxy\u0026lt;/a\u0026gt; * 使用 toxiproxy 包装 httpbin * 可以使用 toxiproxy 模拟网络故障和其他条件 * 可用端口范围：8666～8697 */ private static final ToxiproxyContainer TOXIPROXY_CONTAINER = new ToxiproxyContainer(\u0026#34;ghcr.io/shopify/toxiproxy:2.5.0\u0026#34;) .withNetwork(network); private static final int GOOD_HTTPBIN_PROXY_PORT = 8666; private static final int READ_TIMEOUT_HTTPBIN_PROXY_PORT = 8667; private static final int RESET_PEER_HTTPBIN_PROXY_PORT = 8668; public static final String GOOD_HOST; public static final int GOOD_PORT; /** * 表示请求到达服务器但超时或无法响应的情况（例如，服务器重启） */ public static final String READ_TIMEOUT_HOST; public static final int READ_TIMEOUT_PORT; public static final String RESET_PEER_HOST; public static final int RESET_PEER_PORT; /** * 表示请求从未发出，TCP 连接无法建立的情况 */ public static final String CONNECT_TIMEOUT_HOST = \u0026#34;localhost\u0026#34;; /** * 端口 1 保证无法访问 */ public static final int CONNECT_TIMEOUT_PORT = 1; static { //不使用 @Container 注解进行生命周期管理，因为我们需要在静态块中生成代理 //不用担心容器清理 - testcontainers 启动一个 ryuk 容器来监控和关闭所有容器 HTTPBIN_CONTAINER.start(); TOXIPROXY_CONTAINER.start(); final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(TOXIPROXY_CONTAINER.getHost(), TOXIPROXY_CONTAINER.getControlPort()); try { Proxy proxy = toxiproxyClient.createProxy(\u0026#34;good\u0026#34;, \u0026#34;0.0.0.0:\u0026#34; + GOOD_HTTPBIN_PROXY_PORT, HTTPBIN + \u0026#34;:\u0026#34; + HTTPBIN_PORT); //禁用流量，将导致 READ TIMEOUT proxy = toxiproxyClient.createProxy(\u0026#34;read_timeout\u0026#34;, \u0026#34;0.0.0.0:\u0026#34; + READ_TIMEOUT_HTTPBIN_PROXY_PORT, HTTPBIN + \u0026#34;:\u0026#34; + HTTPBIN_PORT); proxy.toxics().bandwidth(\u0026#34;UP_DISABLE\u0026#34;, ToxicDirection.UPSTREAM, 0); proxy.toxics().bandwidth(\u0026#34;DOWN_DISABLE\u0026#34;, ToxicDirection.DOWNSTREAM, 0); proxy = toxiproxyClient.createProxy(\u0026#34;connect_timeout\u0026#34;, \u0026#34;0.0.0.0:\u0026#34; + RESET_PEER_HTTPBIN_PROXY_PORT, HTTPBIN + \u0026#34;:\u0026#34; + HTTPBIN_PORT); proxy.toxics().resetPeer(\u0026#34;UP_SLOW_CLOSE\u0026#34;, ToxicDirection.UPSTREAM, 1); proxy.toxics().resetPeer(\u0026#34;DOWN_SLOW_CLOSE\u0026#34;, ToxicDirection.DOWNSTREAM, 1); } catch (IOException e) { throw new RuntimeException(e); } GOOD_HOST = TOXIPROXY_CONTAINER.getHost(); GOOD_PORT = TOXIPROXY_CONTAINER.getMappedPort(GOOD_HTTPBIN_PROXY_PORT); READ_TIMEOUT_HOST = TOXIPROXY_CONTAINER.getHost(); READ_TIMEOUT_PORT = TOXIPROXY_CONTAINER.getMappedPort(READ_TIMEOUT_HTTPBIN_PROXY_PORT); RESET_PEER_HOST = TOXIPROXY_CONTAINER.getHost(); RESET_PEER_PORT = TOXIPROXY_CONTAINER.getMappedPort(RESET_PEER_HTTPBIN_PROXY_PORT); } } 这是实际的测试代码：\n@Test public void test() { // 创建自定义连接提供者 ConnectionProvider provider = ConnectionProvider.builder(\u0026#34;customConnectionProvider\u0026#34;) .maxConnections(100) // 增加最大连接数，但不要太高以避免 CDN DDoS 检测 .pendingAcquireMaxCount(10000) // 增加等待队列大小 .build(); HttpClient httpClient = HttpClient.create(provider) .responseTimeout(Duration.ofMillis(100000)); // 响应超时 WebClient build = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build(); List\u0026lt;Mono\u0026lt;String\u0026gt;\u0026gt; monos = Lists.newArrayList(); for (int i = 0; i \u0026lt; 10000; i++) { //使用 TestContainer 端口模拟 0.1s 延迟 Mono\u0026lt;String\u0026gt; stringMono = build.get().uri(\u0026#34;http://localhost:\u0026#34; + CommonMicroServiceTest.HTTPBIN_CONTAINER.getMappedPort(HTTPBIN_PORT) + \u0026#34;/delay/0.1\u0026#34;) .retrieve().bodyToMono(String.class); monos.add(stringMono); } long start = System.currentTimeMillis(); String block = Mono.zip(monos, objects -\u0026gt; { log.info(\u0026#34;{}\u0026#34;, objects); return \u0026#34;ok\u0026#34;; }).block(); log.info(\u0026#34;block: {} in {}ms\u0026#34;, block, System.currentTimeMillis() - start); } 测试结果： block: ok in 10362ms\n这与我们的预期完全一致：\n10,000 个请求，每个延迟 0.1 秒，连接池为 100 总时间 ≈ 0.1 × 10000/100 = 10 秒 专业提示 # 我经常使用 toxicproxy 来模拟各种故障场景：服务器断开连接、请求到达服务器但没有响应、请求从未到达服务器、部分请求传输失败等等。在构建健壮的微服务基础设施时，这个工具包非常有价值！\n这种方法的优点是你完全控制测试环境，同时仍保持真实条件。你可以将代码推向极限而不担心外部因素，然后自信地部署，确切知道你的系统在压力下的行为。\n","date":"2024年4月18日","externalUrl":null,"permalink":"/zh-cn/posts/http-client/","section":"文章","summary":"学习如何使用 WebClient、TestContainers 和 toxicproxy 开发和测试高性能 API 客户端。本综合指南涵盖异步请求处理、隔离测试环境和真实故障模拟，用于构建健壮的微服务。","title":"最大化第三方 API 请求吞吐量：实用测试方法","type":"posts"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/compressed-oops/","section":"Tags","summary":"","title":"Compressed-Oops","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/garbage-collection/","section":"Tags","summary":"","title":"Garbage-Collection","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/heap/","section":"Tags","summary":"","title":"Heap","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/hotspot/","section":"Tags","summary":"","title":"Hotspot","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/metaspace/","section":"Tags","summary":"","title":"Metaspace","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/native-memory-tracking/","section":"Tags","summary":"","title":"Native-Memory-Tracking","type":"tags"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/performance-tuning/","section":"Tags","summary":"","title":"Performance-Tuning","type":"tags"},{"content":" 本篇是关于 JVM 内存的详细分析。网上有很多关于 JVM 内存结构的分析以及图片，但是由于不是一手的资料亦或是人云亦云导致有很错误，造成了很多误解；并且，这里可能最容易混淆的是一边是 JVM Specification 的定义，一边是 Hotspot JVM 的实际实现，有时候人们一些部分说的是 JVM Specification，一部分说的是 Hotspot 实现，给人一种割裂感与误解。本篇主要从 Hotspot 实现出发，以 Linux x86 环境为主，紧密贴合 JVM 源码并且辅以各种 JVM 工具验证帮助大家理解 JVM 内存的结构。但是，本篇仅限于对于这些内存的用途，使用限制，相关参数的分析，有些地方可能比较深入，有些地方可能需要结合本身用这块内存涉及的 JVM 模块去说，会放在另一系列文章详细描述。最后，洗稿抄袭狗不得 house\n本篇全篇目录（以及涉及的 JVM 参数）：\n从 Native Memory Tracking 说起 Native Memory Tracking 的开启 Native Memory Tracking 的使用（涉及 JVM 参数：NativeMemoryTracking） Native Memory Tracking 的 summary 信息每部分含义 Native Memory Tracking 的 summary 信息的持续监控 为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed JVM 内存申请与使用流程 Linux 下内存管理模型简述 JVM commit 的内存与实际占用内存的差异 JVM commit 的内存与实际占用内存的差异 大页分配 UseLargePages Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs) Linux 大页分配方式 - Transparent Huge Pages (THP) JVM 大页分配相关参数与机制（涉及 JVM 参数：UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes） Java 堆内存相关设计 通用初始化与扩展流程 直接指定三个指标的方式（涉及 JVM 参数：MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms） 不手动指定三个指标的情况下，这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的 压缩对象指针相关机制（涉及 JVM 参数：UseCompressedOops） 压缩对象指针存在的意义（涉及 JVM 参数：ObjectAlignmentInBytes） 压缩对象指针与压缩类指针的关系演进（涉及 JVM 参数：UseCompressedOops,UseCompressedClassPointers） 压缩对象指针的不同模式与寻址优化机制（涉及 JVM 参数：ObjectAlignmentInBytes,HeapBaseMinAddress） 为何预留第 0 页，压缩对象指针 null 判断擦除的实现（涉及 JVM 参数：HeapBaseMinAddress） 结合压缩对象指针与前面提到的堆内存限制的初始化的关系（涉及 JVM 参数：HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize） 使用 jol + jhsdb + JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论 验证 32-bit 压缩指针模式 验证 Zero based 压缩指针模式 验证 Non-zero disjoint 压缩指针模式 验证 Non-zero based 压缩指针模式 堆大小的动态伸缩（涉及 JVM 参数：MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes） 适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap JVM 参数 AlwaysPreTouch 的作用 JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制 JVM 参数 SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用 JVM 元空间设计 什么是元数据，为什么需要元数据 什么时候用到元空间，元空间保存什么 什么时候用到元空间，以及释放时机 元空间保存什么 元空间的核心概念与设计 元空间的整体配置以及相关参数（涉及 JVM 参数：MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy） 元空间上下文 MetaspaceContext 虚拟内存空间节点列表 VirtualSpaceList 虚拟内存空间节点 VirtualSpaceNode 与 CompressedClassSpaceSize MetaChunk ChunkHeaderPool 池化 MetaChunk 对象 ChunkManager 管理空闲的 MetaChunk 类加载的入口 SystemDictionary 与保留所有 ClassLoaderData 的 ClassLoaderDataGraph 每个类加载器私有的 ClassLoaderData 以及 ClassLoaderMetaspace 管理正在使用的 MetaChunk 的 MetaspaceArena 元空间内存分配流程 类加载器到 MetaSpaceArena 的流程 从 MetaChunkArena 普通分配 - 整体流程 从 MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 与用于后续分配的流程 从 MetaChunkArena 普通分配 - 尝试从 FreeBlocks 分配 从 MetaChunkArena 普通分配 - 尝试扩容 current chunk 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 从 VirtualSpaceList 申请新的 RootMetaChunk 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 将 RootMetaChunk 切割成为需要的 MetaChunk MetaChunk 回收 - 不同情况下， MetaChunk 如何放入 FreeChunkListVector ClassLoaderData 回收 元空间分配与回收流程举例 首先类加载器 1 需要分配 1023 字节大小的内存，属于类空间 然后类加载器 1 还需要分配 1023 字节大小的内存，属于类空间 然后类加载器 1 需要分配 264 KB 大小的内存，属于类空间 然后类加载器 1 需要分配 2 MB 大小的内存，属于类空间 然后类加载器 1 需要分配 128KB 大小的内存，属于类空间 新来一个类加载器 2，需要分配 1023 Bytes 大小的内存，属于类空间 然后类加载器 1 被 GC 回收掉 然后类加载器 2 需要分配 1 MB 大小的内存，属于类空间 元空间大小限制与动态伸缩 CommitLimiter 的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC 每次 GC 之后，也会尝试重新计算 _capacity_until_GC jcmd VM.metaspace 元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解 jcmd \u0026lt;pid\u0026gt; VM.metaspace 元空间说明 元空间相关 JVM 日志 元空间 JFR 事件详解 jdk.MetaspaceSummary 元空间定时统计事件 jdk.MetaspaceAllocationFailure 元空间分配失败事件 jdk.MetaspaceOOM 元空间 OOM 事件 jdk.MetaspaceGCThreshold 元空间 GC 阈值变化事件 jdk.MetaspaceChunkFreeListSummary 元空间 Chunk FreeList 统计事件 JVM 线程内存设计（重点研究 Java 线程） JVM 中有哪几种线程，对应线程栈相关的参数是什么（涉及 JVM 参数：ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack） Java 线程栈内存的结构 Java 线程如何抛出的 StackOverflowError 解释执行与编译执行时候的判断（x86为例） 一个 Java 线程 Xss 最小能指定多大 1. 从 Native Memory Tracking 说起 # JVM 内存究竟包括哪些，可能网上众说纷纭。我们这里由官方提供的一个查看 JVM 内存占用的工具引入，即 Native Memory Tracking。不过要注意的一点是，这个只能监控 JVM 原生申请的内存大小，如果是通过 JDK 封装的系统 API 申请的内存，是统计不到的，例如 Java JDK 中的 DirectBuffer 以及 MappedByteBuffer 这两个（当然，对于这两个，我们后面也有其他的办法去看到当前使用的大小。当然xigao dog 啥都不会）。以及如果你自己封装 JNI 调用系统调用去申请内存，都是 Native Memory Tracking 无法涵盖的。这点要注意。\n1.1. Native Memory Tracking 的开启 # Native Memory Tracking 主要是用来通过在 JVM 向系统申请内存的时候进行埋点实现的。注意，这个埋点，并不是完全没有消耗的，我们后面会看到。由于需要埋点，并且 JVM 中申请内存的地方很多，这个埋点是有不小消耗的，这个 Native Memory Tracking 默认是不开启的，并且无法动态开启（因为这是埋点采集统计的，如果可以动态开启那么没开启的时候的内存分配没有记录无法知晓，所以无法动态开启），目前只能通过在启动 JVM 的时候通过启动参数开启。即通过 -XX:NativeMemoryTracking 开启：\n-XX:NativeMemoryTracking=off:这是默认值，即关闭 Native Memory Tracking -XX:NativeMemoryTracking=summary: 开启 Native Memory Tracking，但是仅仅按照各个 JVM 子系统去统计内存占用情况 -XX:NativeMemoryTracking=detail:开启 Native Memory Tracking，从每次 JVM 中申请内存的不同调用路径的维度去统计内存占用情况。注意，开启 detail 比开启 summary 的消耗要大不少，因为 detail 每次都要解析 CallSite 分辨调用位置。我们一般用不到这么详细的内容，除非是 JVM 开发。只有洗稿狗才会开启这个配置导致线上崩溃而自己又很懵。 开启之后，我们可以通过 jcmd 命令去查看 Native Memory Tracking 的信息，即jcmd \u0026lt;pid\u0026gt; VM.native_memory：\njcmd \u0026lt;pid\u0026gt; VM.native_memory或者jcmd \u0026lt;pid\u0026gt; VM.native_memory summary：两者是等价的，即查看 Native Memory Tracking 的 summary 信息。默认单位是 KB，可以指定单位为其他，例如 jcmd \u0026lt;pid\u0026gt; VM.native_memory summary scale=MB jcmd \u0026lt;pid\u0026gt; VM.native_memory detail：查看 Native Memory Tracking 的 detail 信息，包括 summary 信息，以及按照虚拟内存映射分组的内存使用信息，还有按照不同 CallSite 调用分组的内存使用情况。默认单位是 KB，可以指定单位为其他，例如 jcmd \u0026lt;pid\u0026gt; VM.native_memory detail scale=MB 1.2. Native Memory Tracking 的使用 # 对于我们这些 Java 开发以及 JVM 使用者而言（对于抄袭狗是没有好果汁吃的），我们只关心并且查看 Native Memory Tracking 的 summary 信息即可，detail 信息一般是供 JVM 开发人员使用的，我们不用太关心，我们后面的分析也只会涉及 Native Memory Tracking 的 summary 部分。\n一般地，只有遇到问题的时候，我们才会考虑开启 Native Memory Tracking，并且在定位出问题后，我们想把它关闭，可以通过 jcmd \u0026lt;pid\u0026gt; VM.native_memory shutdown 进行关闭并清理掉之前 Native Memory tracking 使用的埋点以及占用的内存。如前面所述，我们无法动态开启 Native Memory tracking，所以只要动态关闭了，这个进程就无法再开启了。\njcmd 本身提供了简单的对比功能，例如：\n使用 jcmd \u0026lt;pid\u0026gt; VM.native_memory baseline 记录当前内存占用信息 之后过一段时间 jcmd \u0026lt;pid\u0026gt; VM.native_memory summary.diff 会输出当前 Native Memory Tracking 的 summary 信息，如果与第一步 baseline 的有差异，会在对应位将差异输出 但是这个工具本身比较粗糙，我们有时候并不知道何时调用 jcmd \u0026lt;pid\u0026gt; VM.native_memory summary.diff 合适，因为我们不确定什么时候会有我们想看到的内存使用过大的问题。所以我们一般做成一种持续监控的方式\n1.3. Native Memory Tracking 的 summary 信息每部分含义 # 以下是一个 Native Memory Tracking 的示例输出：\nTotal: reserved=10575644KB, committed=443024KB - Java Heap (reserved=8323072KB, committed=192512KB) (mmap: reserved=8323072KB, committed=192512KB) - Class (reserved=1050202KB, committed=10522KB) (classes #15409) ( instance classes #14405, array classes #1004) (malloc=1626KB #33495) (mmap: reserved=1048576KB, committed=8896KB) ( Metadata: ) ( reserved=57344KB, committed=57216KB) ( used=56968KB) ( waste=248KB =0.43%) ( Class space:) ( reserved=1048576KB, committed=8896KB) ( used=8651KB) ( waste=245KB =2.75%) - Thread (reserved=669351KB, committed=41775KB) (thread #653) (stack: reserved=667648KB, committed=40072KB) (malloc=939KB #3932) (arena=764KB #1304) - Code (reserved=50742KB, committed=17786KB) (malloc=1206KB #9495) (mmap: reserved=49536KB, committed=16580KB) - GC (reserved=370980KB, committed=69260KB) (malloc=28516KB #8340) (mmap: reserved=342464KB, committed=40744KB) - Compiler (reserved=159KB, committed=159KB) (malloc=29KB #813) (arena=131KB #3) - Internal (reserved=1373KB, committed=1373KB) (malloc=1309KB #6135) (mmap: reserved=64KB, committed=64KB) - Other (reserved=12348KB, committed=12348KB) (malloc=12348KB #14) - Symbol (reserved=18629KB, committed=18629KB) (malloc=16479KB #445877) (arena=2150KB #1) - Native Memory Tracking (reserved=8426KB, committed=8426KB) (malloc=325KB #4777) (tracking overhead=8102KB) - Shared class space (reserved=12032KB, committed=12032KB) (mmap: reserved=12032KB, committed=12032KB) - Arena Chunk (reserved=187KB, committed=187KB) (malloc=187KB) - Tracing (reserved=32KB, committed=32KB) (arena=32KB #1) - Logging (reserved=5KB, committed=5KB) (malloc=5KB #216) - Arguments (reserved=31KB, committed=31KB) (malloc=31KB #90) - Module (reserved=403KB, committed=403KB) (malloc=403KB #2919) - Safepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) - Synchronization (reserved=56KB, committed=56KB) (malloc=56KB #789) - Serviceability (reserved=1KB, committed=1KB) (malloc=1KB #18) - Metaspace (reserved=57606KB, committed=57478KB) (malloc=262KB #180) (mmap: reserved=57344KB, committed=57216KB) - String Deduplication (reserved=1KB, committed=1KB) (malloc=1KB #8) 我们接下来将上面的信息按不同子系统分别简单分析下其含义：\n1.Java堆内存，所有 Java 对象分配占用内存的来源，由 JVM GC 管理回收，这是我们在第三章会重点分析的：\n//堆内存占用，reserve 了 8323072KB，当前 commit 了 192512KB 用于实际使用 Java Heap (reserved=8323072KB, committed=192512KB) //堆内存都是通过 mmap 系统调用方式分配的 (mmap: reserved=8323072KB, committed=192512KB) //chao xi 可耻 2.元空间，JVM 将类文件加载到内存中用于后续使用占用的空间，注意是 JVM C++ 层面的内存占用，主要包括类文件中在 JVM 解析为 C++ 的 Klass 类以及相关元素。对应的 Java 反射类 Class 还是在堆内存空间中：\n//Class 是类元空间总占用，reserve 了 1050202KB，当前 commit 了 10522KB 用于实际使用 //总共 reserved 1050202KB = mmap reserved 1048576KB + malloc 1626KB //总共 committed 10522KB = mmap committed 8896KB + malloc 1626KB Class (reserved=1050202KB, committed=10522KB) (classes #15409) //一共加载了 15409 个类 ( instance classes #14405, array classes #1004) //其中 14405 个实体类，1004 个数组类 (malloc=1626KB #33495) //通过 malloc 系统调用方式一共分配了 1626KB，一共调用了 33495 次 malloc (mmap: reserved=1048576KB, committed=8896KB) //通过 mmap 系统调用方式 reserve 了 1048576KB，当前 commit 了 8896KB 用于实际使用 ( Metadata: )//注意，MetaData 这块不属于类元空间，属于数据元空间，后面第四章会详细分析 ( reserved=57344KB, committed=57216KB) //数据元空间当前 reserve 了 57344KB，commit 了 57216KB 用于实际使用 ( used=56968KB) //但是实际从 MetaChunk 的角度去看使用，只有 56968KB 用于实际数据的分配，有 248KB 的浪费 ( waste=248KB =0.43%) ( Class space:) ( reserved=1048576KB, committed=8896KB) //类元空间当前 reserve 了 1048576KB，commit 了 8896KB 用于实际使用 ( used=8651KB) //但是实际从 MetaChunk 的角度去看使用，只有 8651KB 用于实际数据的分配，有 245KB 的浪费 ( waste=245KB =2.75%) 洗稿去shi Shared class space (reserved=12032KB, committed=12032KB) //共享类空间，当前 reserve 了 12032KB，commit 了 12032KB 用于实际使用，这块其实属于上面 Class 的一部分 (mmap: reserved=12032KB, committed=12032KB) Module (reserved=403KB, committed=403KB) //加载并记录模块占用空间，当前 reserve 了 403KB，commit 了 403KB 用于实际使用 (malloc=403KB #2919) Metaspace (reserved=57606KB, committed=57478KB) //等价于上面 Class 中的 MetaChunk（除了 malloc 的部分），当前 reserve 了 57606KB，commit 了 57478KB 用于实际使用 (malloc=262KB #180) (mmap: reserved=57344KB, committed=57216KB) 3.C++ 字符串即符号(Symbol)占用空间，前面加载类的时候，其实里面有很多字符串信息（注意不是 Java 字符串，是 JVM 层面 C++ 字符串），不同类的字符串信息可能会重复（维护原创打死潮汐犬）。所以统一放入符号表(Symbol table)复用。元空间中保存的是针对符号表中符号的引用。这不是本期内容的重点，我们不会详细分析\nSymbol (reserved=18629KB, committed=18629KB) (malloc=16479KB #445877) //通过 malloc 系统调用方式一共分配了 16479KB，一共调用了 445877 次 malloc (arena=2150KB #1) //通过 arena 系统调用方式一共分配了 2150KB，一共调用了 1 次 arena 4.线程占用内存，主要是每个线程的线程栈，我们也只会主要分析线程栈占用空间（在第五章），其他的管理线程占用的空间很小，可以忽略不计。\n//总共 reserve 了 669351KB，commit 了 41775KB Thread (reserved=669351KB, committed=41775KB) (thread #653)//当前线程数量是 653 (stack: reserved=667648KB, committed=40072KB) //线程栈占用的空间：我们没有指定 Xss，默认是 1MB，所以 reserved 是 653 * 1024 = 667648KB，当前 commit 了 40072KB 用于实际使用 (malloc=939KB #3932) //通过 malloc 系统调用方式一共分配了 939KB，一共调用了 3932 次 malloc (arena=764KB #1304) //通过 JVM 内部 Arena 分配的内存，一共分配了 764KB，一共调用了 1304 次 Arena 分配 5.JIT编译器本身占用的空间以及JIT编译器编译后的代码占用空间，这也不是本期内容的重点，我们不会详细分析\nCode (reserved=50742KB, committed=17786KB) (malloc=1206KB #9495) (mmap: reserved=49536KB, committed=16580KB) //chao xi 直接去火葬场炒 Compiler (reserved=159KB, committed=159KB) (malloc=29KB #813) (arena=131KB #3) 6.Arena 数据结构占用空间，我们看到 Native Memory Tracking 中有很多通过 arena 分配的内存，这个就是管理 Arena 数据结构占用空间。这不是本期内容的重点，我们不会详细分析\nArena Chunk (reserved=187KB, committed=187KB) (malloc=187KB) 7.JVM Tracing 占用内存，包括 JVM perf 以及 JFR 占用的空间。其中 JFR 占用的空间可能会比较大，我在我的另一个关于 JFR 的系列里面分析过 JVM 内存中占用的空间。这不是本期内容的重点，我们不会详细分析\nTracing (reserved=32KB, committed=32KB) (arena=32KB #1) 8.写 JVM 日志占用的内存（-Xlog 参数指定的日志输出，并且 Java 17 之后引入了异步 JVM 日志-Xlog:async，异步日志所需的 buffer 也在这里），这不是本期内容的重点，我们不会详细分析\nLogging (reserved=5KB, committed=5KB) (malloc=5KB #216) 9.JVM 参数占用内存，我们需要保存并处理当前的 JVM 参数以及用户启动 JVM 的是传入的各种参数（有时候称为 flag）。这不是本期内容的重点，我们不会详细分析\nArguments (reserved=31KB, committed=31KB) (malloc=31KB #90) 10.JVM 安全点占用内存，是固定的两页内存（我这里是一页是 4KB，后面第二章会分析这个页大小与操作系统相关），用于 JVM 安全点的实现，不会随着 JVM 运行时的内存占用而变化。JVM 安全点请期待本系列文章的下一系列：全网最硬核的 JVM 安全点与线程握手机制解析。这不是本期内容的重点，我们不会详细分析\nSafepoint (reserved=8KB, committed=8KB) (mmap: reserved=8KB, committed=8KB) 11.Java 同步机制（例如 synchronized，还有 AQS 的基础 LockSupport）底层依赖的 C++ 的数据结构，系统内部的 mutex 等占用的内存。这不是本期内容的重点，我们不会详细分析\nSynchronization (reserved=56KB, committed=56KB) (malloc=56KB #789) 12.JVM TI 相关内存，JVMTI 是 Java 虚拟机工具接口（Java Virtual Machine Tool Interface）的缩写。它是 Java 虚拟机（JVM）的一部分，提供了一组 API，使开发人员可以开发自己的 Java 工具和代理程序，以监视、分析和调试 Java 应用程序。JVMTI API 是一组 C/C++ 函数，可以通过 JVM TI Agent Library 和 JVM 进行交互。开发人员可以使用 JVMTI API 开发自己的 JVM 代理程序或工具，以监视和操作 Java 应用程序。例如，可以使用 JVMTI API 开发性能分析工具、代码覆盖率工具、内存泄漏检测工具等等。这里的内存就是调用了 JVMTI API 之后 JVM 为了生成数据占用的内存。这不是本期内容的重点，我们不会详细分析\nServiceability (reserved=1KB, committed=1KB) (malloc=1KB #18) 13.Java 字符串去重占用内存：Java 字符串去重机制可以减少应用程序中字符串对象的内存占用。 在 Java 应用程序中，字符串常量是不可变的，并且通常被使用多次。这意味着在应用程序中可能存在大量相同的字符串对象，这些对象占用了大量的内存。Java 字符串去重机制通过在堆中共享相同的字符串对象来解决这个问题。当一个字符串对象被创建时，JVM 会检查堆中是否已经存在相同的字符串对象。如果存在，那么新的字符串对象将被舍弃，而引用被返回给现有的对象。这样就可以减少应用程序中字符串对象的数量，从而减少内存占用。 但是这个机制一直在某些 GC 下表现不佳，尤其是 G1GC 以及 ZGC 中，所以默认是关闭的，可以通过 -XX:+UseStringDeduplication 来启用。这不是本期内容的重点，我们不会详细分析。\nString Deduplication (reserved=1KB, committed=1KB) (malloc=1KB #8) 14.JVM GC需要的数据结构与记录信息占用的空间，这块内存可能会比较大，尤其是对于那种专注于低延迟的 GC，例如 ZGC。其实 ZGC 是一种以空间换时间的思路，提高 CPU 消耗与内存占用，但是消灭全局暂停。之后的 ZGC 优化方向就是尽量降低 CPU 消耗与内存占用，相当于提高了性价比。这不是本期内容的重点，我们不会详细分析。\nGC (reserved=370980KB, committed=69260KB) (malloc=28516KB #8340) (mmap: reserved=342464KB, committed=40744KB) 15.JVM内部(不属于其他类的占用就会归到这一类)与其他占用(不是 JVM 本身而是操作系统的某些系统调用导致额外占的空间)，不会很大\nInternal (reserved=1373KB, committed=1373KB) (malloc=1309KB #6135) (mmap: reserved=64KB, committed=64KB) Other (reserved=12348KB, committed=12348KB) (malloc=12348KB #14) 16.开启 Native Memory Tracking 本身消耗的内存，这个就不用多说了吧\nNative Memory Tracking (reserved=8426KB, committed=8426KB) (malloc=325KB #4777) (tracking overhead=8102KB) 1.4. Native Memory Tracking 的 summary 信息的持续监控 # 现在 JVM 一般大部分部署在 k8s 这种云容器编排的环境中，每个 JVM 进程内存是受限的。如果超过限制，那么会触发 OOMKiller 将这个 JVM 进程杀掉。我们一般都是由于自己的 JVM 进程被 OOMKiller 杀掉，才会考虑打开 NativeMemoryTracking 去看看哪块内存占用比较多以及如何调整的。\nOOMKiller 是积分制，并不是你的 JVM 进程一超过限制就立刻会被杀掉，而是超过的话会累积分，累积到一定程度，就可能会被 OOMKiller 杀掉。所以，我们可以通过定时输出 Native Memory Tracking 的 summary 信息，从而抓到超过内存限制的点进行分析。\n但是，我们不能仅通过 Native Memory Tracking 的数据就判断 JVM 占用的内存，因为在后面的 JVM 内存申请与使用流程的分析我们会看到，JVM 通过 mmap 分配的大量内存都是先 reserve 再 commit 之后实际往里面写入数据的时候，才会真正分配物理内存。同时，JVM 还会动态释放一些内存，这些内存可能不会立刻被操作系统回收。Native Memory Tracking 是 JVM 认为自己向操作系统申请的内存，与实际操作系统分配的内存是有所差距的，所以我们不能只查看 Native Memory Tracking 去判断，我们还需要查看能体现真正内存占用指标。这里可以查看 linux 进程监控文件 smaps_rollup 看出具体的内存占用，例如 (一般不看 Rss，因为如果涉及多个虚拟地址映射同一个物理地址的话会有不准确，所以主要关注 Pss 即可，但是 Pss 更新不是实时的，但也差不多，这就可以理解为进程占用的实际物理内存)：\n\u0026gt; cat /proc/23/smaps_rollup 689000000-fffff53a9000 ---p 00000000 00:00 0 [rollup] Rss: 5870852 kB Pss: 5849120 kB Pss_Anon: 5842756 kB Pss_File: 6364 kB Pss_Shmem: 0 kB Shared_Clean: 27556 kB Shared_Dirty: 0 kB Private_Clean: 524 kB Private_Dirty: 5842772 kB Referenced: 5870148 kB Anonymous: 5842756 kB LazyFree: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB FilePmdMapped: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB SwapPss: 0 kB Locked: 0 kB 笔者通过在每个 Spring Cloud 微服务进程加入下面的代码，来实现定时的进程内存监控，主要通过 smaps_rollup 查看实际的物理内存占用找到内存超限的时间点，Native Memory Tracking 查看 JVM 每块内存占用的多少，用于指导优化参数。\nimport lombok.extern.log4j.Log4j2; import org.apache.commons.io.FileUtils; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME; @Log4j2 public class MonitorMemoryRSS implements ApplicationListener\u0026lt;ApplicationReadyEvent\u0026gt; { private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); private static final ScheduledThreadPoolExecutor sc = new ScheduledThreadPoolExecutor(1); @Override public void onApplicationEvent(ApplicationReadyEvent event) { if (isBootstrapContext(event)) { return; } synchronized (INITIALIZED) { if (INITIALIZED.get()) { return; } sc.scheduleAtFixedRate(() -\u0026gt; { long pid = ProcessHandle.current().pid(); try { //读取 smaps_rollup List\u0026lt;String\u0026gt; strings = FileUtils.readLines(new File(\u0026#34;/proc/\u0026#34; + pid + \u0026#34;/smaps_rollup\u0026#34;)); log.info(\u0026#34;MonitorMemoryRSS, smaps_rollup: {}\u0026#34;, strings.stream().collect(Collectors.joining(\u0026#34;\\n\u0026#34;))); //读取 Native Memory Tracking 信息 Process process = Runtime.getRuntime().exec(new String[]{\u0026#34;jcmd\u0026#34;, pid + \u0026#34;\u0026#34;, \u0026#34;VM.native_memory\u0026#34;}); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { log.info(\u0026#34;MonitorMemoryRSS, native_memory: {}\u0026#34;, reader.lines().collect(Collectors.joining(\u0026#34;\\n\u0026#34;))); } } catch (IOException e) { } }, 0, 30, TimeUnit.SECONDS); INITIALIZED.set(true); } } static boolean isBootstrapContext(ApplicationReadyEvent applicationEvent) { return applicationEvent.getApplicationContext().getEnvironment().getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME); } } 同时，笔者还将这些输出抽象为 JFR 事件，效果是：\n1.5. 为何 Native Memory Tracking 中申请的内存分为 reserved 和 committed # 这个会在第二章详细分析\n2. JVM 内存申请与使用流程 # 2.1. Linux 下内存管理模型简述 # Linux 内存管理模型不是咱们这个系列的讨论重点，我们这里只会简单提一些对于咱们这个系列需要了解到的，如果读者想要深入理解，建议大家查看 bin 神（公众号：bin 的技术小屋）的系列文章：一步一图带你深入理解 Linux 虚拟内存管理\nCPU 是通过寻址来访问内存的，目前大部分 CPU 都是 64 位的，即寻址范围是：0x0000 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF，即可以管理 16EB 的内存。但是，实际程序并不会直接通过 CPU 寻址访问到实际的物理内存，而是通过引入 MMU（Memory Management Unit 内存管理单元）与实际物理地址隔了一层虚拟内存的抽象。这样，程序申请以及访问的其实是虚拟内存地址，MMU 会将这个虚拟内存地址映射为实际的物理内存地址。同时，为了减少内存碎片，以及增加内存分配效率，在 MMU 的基础上 Linux 抽象了内存分页（Paging）的概念，将虚拟地址按固定大小分割成页（默认是 4K，如果平台支持更多更大的页大小 JVM 也是可以利用的，我们后面分析相关的 JVM 参数会看到），并在页被实际使用写入数据的时候，映射同样大小的实际的物理内存（页帧，Page Frame），或者是在物理内存不足的时候，将某些不常用的页转移到其他存储设备比如磁盘上。\n一般系统中会有多个进程使用内存，每个进程都有自己独立的虚拟内存空间，假设我们这里有三个进程，进程 A 访问的虚拟地址可以与进程 B 和进程 C 的虚拟地址相同，那么操作系统如何区分呢？即操作系统如何将这些虚拟地址转换为物理内存。这就需要页表了，页表也是每个进程独立的，操作系统会在给进程映射物理内存用来保存用户数据的时候，将物理内存保存到进程的页表里面。然后，进程访问虚拟内存空间的时候，通过页表找到物理内存：\n页表如何将一个虚拟内存地址（我们需要注意一点，目前虚拟内存地址，用户空间与内核空间可以使用从 0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF 的地址，即 256TB），转化为物理内存的呢？下面我们举一个在 x86，64 位环境下四级页表的结构视图：\n在这里，页表分为四个级别：PGD（Page Global Directory），PUD（Page Upper Directory），PMD（Page Middle Directory），PTE（Page Table Entry）。每个页表，里面的页表项，保存了指向下一个级别的页表的引用，除了最后一层的 PTE 里面的页表项保存的是指向用户数据内存的指针。如何将虚拟内存地址通过页表找到对应用户数据内存从而读取数据，过程是：\n取虚拟地址的 39 ~ 47 位（因为用户空间与内核空间可以使用从 0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF 的地址，即 47 位以下的地址）作为 offset，在唯一的 PGD 页面根据 offset 定位到 PGD 页表项 pgd_t 使用 pgd_t 定位到具体的 PUD 页面 取虚拟地址的 30 ~ 38 位作为 offset，在对应的 PUD 页面根据 offset 定位到 PUD 页表项 pud_t 使用 pud_t 定位到具体的 PMD 页面 取虚拟地址的 21 ~ 29 位作为 offset，在对应的 PMD 页面根据 offset 定位到 PMD 页表项 pmd_t 使用 pmd_t 定位到具体的 PTE 页面 取虚拟地址的 12 ~ 20 位作为 offset，在对应的 PTE 页面根据 offset 定位到 PTE 页表项 pte_t 使用 pte_t 定位到具体的用户数据物理内存页面 使用最后的 0 ~ 11 位作为 offset，对应到用户数据物理内存页面的对应 offset 如果每次访问虚拟内存，都需要访问这个页表翻译成实际物理内存的话，性能太差。所以一般 CPU 里面都有一个 TLB（Translation Lookaside Buffer，翻译后备缓冲）存在，一般它属于 CPU 的 MMU 的一部分。TLB 负责缓存虚拟内存与实际物理内存的映射关系，一般 TLB 容量很小。每次访问虚拟内存，先查看 TLB 中是否有缓存，如果没有才会去页表查询。\n默认情况下，TLB 缓存的 key 为地址的 12 ~ 47 位，value 是实际的物理内存页面。这样前面从第 1 到第 7 步就可以被替换成访问 TLB 了：\n取虚拟地址的 12 ~ 47 位作为 key，访问 TLB，定位到具体的用户数据物理内存页面。 使用最后的 0 ~ 11 位作为 offset，对应到用户数据物理内存页面的对应 offset。 TLB 一般很小，我们来看几个 CPU 中的 TLB 大小\n我们这里不用关心 iTLB，dTLB，sTLB 分别是什么意思，只要可以看出两点即可：1. TLB 整体可以容纳个数不多；2. 页大小越大，TLB 能容纳的个数越少。但是整体看，TLB 能容纳的页大小还是增多的（比如 Nehalem 的 iTLB，页大小 4K 的时候，一共可以容纳 128 * 4 = 512K 的内存，页大小 2M 的时候，一共可以容纳 2 * 7 = 14M 的内存）。\nJVM 中很多地方需要知道页大小，JVM 在初始化的时候，通过系统调用 sysconf(_SC_PAGESIZE) 读取出页大小，并保存下来以供后续使用。参考源码：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os/linux/os_linux.cpp：\n//设置全局默认页大小，通过 Linux::page_size() 可以获取全局默认页大小 Linux::set_page_size(sysconf(_SC_PAGESIZE)); if (Linux::page_size() == -1) { fatal(\u0026#34;os_linux.cpp: os::init: sysconf failed (%s)\u0026#34;, os::strerror(errno)); } //将默认页大小加入可选的页大小列表，在涉及大页分配的时候有用 _page_sizes.add(Linux::page_size()); 2.2. JVM 主要内存申请分配流程 # 第一步，JVM 的每个子系统（例如 Java 堆，元空间，JIT 代码缓存，GC 等等等等），如果需要的话，在初始化的时候首先 Reserve 要分配区域的最大限制大小的内存（这个最大大小，需要按照页大小对齐（即是页大小的整数倍），默认页大小是前面提到的 Linux::page_size()），例如对于 Java 堆，就是最大堆大小（通过 -Xmx 或者 -XX:MaxHeapSize限制），还有对于代码缓存，也是最大代码缓存大小（通过 -XX:ReservedCodeCacheSize 限制）。Reserve 的目的是在虚拟内存空间划出一块内存专门给某个区域使用，这样做的好处是：\n隔离每个 JVM 子系统使用的内存的虚拟空间，这样在 JVM 代码有 bug 的时候（例如发生 Segment Fault 异常），通过报错中的虚拟内存地址可以快速定位到是哪个子系统出了问题。 可以很方便的限制这个区域使用的最大内存大小。 便于管理，Reserve 不会触发操作系统分配映射实际物理内存，这个区域可以在 Reserve 的区域内按需伸缩。 便于一些 JIT 优化，例如我们故意将这个区域保留起来但是故意不将这个区域的虚拟内存映射物理内存，访问这块内存会造成 Segment Fault 异常。JVM 会预设 Segment Fault 异常的处理器，在处理器里面检查发生 Segment Fault 异常的内存地址属于哪个子系统的 Reserve 的区域，判断要做什么操作。后面我们会看到，null 检查抛出 NullPointerException 异常的优化，全局安全点，抛出 StackOverflowError 的实现，都和这个机制有关。 在 Linux 的环境下，Reserve 通过 mmap(2) 系统调用实现，参数传入 prot = PROT_NONE，PROT_NONE 代表不会使用，即不能做任何操作，包括读和写。为啥要打击抄袭，稿主被抄袭太多所以断更很久。如果 JVM 使用这块内存，会发生 Segment Fault 异常。Reserve 的源码，对应的是：\n入口为：https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/share/runtime/os.cpp\nchar* os::reserve_memory(size_t bytes, bool executable, MEMFLAGS flags) { //调用每个操作系统实现不同的 pd_reserve_memory 函数进行 reserve char* result = pd_reserve_memory(bytes, executable); if (result != NULL) { MemTracker::record_virtual_memory_reserve(result, bytes, CALLER_PC, flags); }不要偷取他人的劳动成果，也不要浪费自己的时间和精力，让我们一起做一个有良知的写作者。 return result; } 对应 linux 的实现是：https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/os/linux/os_linux.cpp\nchar* os::pd_reserve_memory(size_t bytes, bool exec) { return anon_mmap(nullptr, bytes); } static char* anon_mmap(char* requested_addr, size_t bytes) { const int flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS; //这里的关键是 PROT_NONE，代表仅仅是在虚拟空间保留，不实际映射物理内存 //fd 传入的是 -1，因为没有实际映射文件，我们这里目的是为了分配内存，不是将某个文件映射到内存中 char* addr = (char*)::mmap(requested_addr, bytes, PROT_NONE, flags, -1, 0); return addr == MAP_FAILED ? NULL : addr; } 第二步，JVM 的每个子系统，按照各自的策略，通过 Commit 第一步 Reserve 的区域的一部分扩展内存（大小也一般页大小对齐的），从而向操作系统申请映射物理内存，通过 Uncommit 已经 Commit 的内存来释放物理内存给操作系统。抄袭和xigao是文化的毒瘤，是对文化创造和发展的阻碍！\nCommit 的源码入口为：https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/share/runtime/os.cpp\nbool os::commit_memory(char* addr, size_t bytes, bool executable) { assert_nonempty_range(addr, bytes); //调用每个操作系统实现不同的 pd_commit_memory 函数进行 commit bool res = pd_commit_memory(addr, bytes, executable); if (res) { MemTracker::record_virtual_memory_commit((address)addr, bytes, CALLER_PC); } return res; } 对应 linux 的实现是：https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/os/linux/os_linux.cpp\nbool os::pd_commit_memory(char* addr, size_t size, bool exec) { return os::Linux::commit_memory_impl(addr, size, exec) == 0; } int os::Linux::commit_memory_impl(char* addr, size_t size, bool exec) { //这里的关键是 PROT_READ|PROT_WRITE，即申请需要读写这块内存 int prot = exec ? PROT_READ|PROT_WRITE|PROT_EXEC : PROT_READ|PROT_WRITE; uintptr_t res = (uintptr_t) ::mmap(addr, size, prot, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); if (res != (uintptr_t) MAP_FAILED) { if (UseNUMAInterleaving) { numa_make_global(addr, size); } return 0; } int err = errno; // save errno from mmap() call above if (!recoverable_mmap_error(err)) { warn_fail_commit_memory(addr, size, exec, err); vm_exit_out_of_memory(size, OOM_MMAP_ERROR, \u0026quot;committing reserved memory.\u0026quot;); } return err; } Commit 内存之后，并不是操作系统会立刻分配物理内存，而是在向 Commit 的内存里面写入数据的时候，操作系统才会实际映射内存。plagiarism和洗稿是恶意抄袭他人劳动成果的行为，是对劳动价值的漠视和践踏！JVM 有对应的参数，可以在 Commit 内存后立刻写入 0 来强制操作系统分配内存，即 AlwaysPreTouch 这个参数，这个参数我们后面会详细分析以及历史版本存在的缺陷。\n我们再来看下为什么先 Reserve 之后 Commit 这样好 Debug。看这个例子，如果我们没有第一步 Reserve，直接是第二步 Commit，那么我们可能分配的内存是这个样子的：\n假设此时，我们不小心在 JVM 中写了个 bug，导致洗稿狗没了妈，导致 MetaSpace 2 这块内存被回收了，这时候保留指向 MetaSpace 2 的内存的指针就会报 Segment Fault，但是通过 Segment Fault 里面带的地址，我们并不知道是这个地址属于哪里，除非我们有另外的内存结构保存每个子系统 Commit 内存的列表，但是这样效率太低了。如果我们先 Reserve 大块之后在里面 Commit，那么情况就不同了：\n这样，只需要判断 Segment Fault 里面带的地址处于的范围，就能知道是哪个子系统\n2.2.1. JVM commit 的内存与实际占用内存的差异 # 前面一节我们知道了，JVM 中大块内存，基本都是先 reserve 一大块，之后 commit 其中需要的一小块，然后开始读写处理内存，在 Linux 环境下，底层基于 mmap(2) 实现。但是需要注意一点的是，commit 之后，内存并不是立刻被分配了物理内存，而是真正往内存中 store 东西的时候，才会真正映射物理内存，如果是 load 读取也是可能不映射物理内存的。\n这其实是可能你平常看到但是忽略的现象，如果你使用的是 SerialGC，ParallelGC 或者 CMS GC，老年代的内存在有对象晋升到老年代之前，可能是不会映射物理内存的，虽然这块内存已经被 commit 了。并且年轻代可能也是随着使用才会映射物理内存。如果你用的是 ZGC，G1GC，或者 ShenandoahGC，那么内存用的会更激进些（主要因为分区算法划分导致内存被写入），这是你在换 GC 之后看到物理内存内存快速上涨的原因之一。JVM 有对应的参数，可以在 Commit 内存后立刻写入 0 来强制操作系统分配内存，即 AlwaysPreTouch 这个参数，这个参数我们后面会详细分析以及历史版本存在的缺陷。还有的差异，主要来源于在 uncommit 之后，系统可能还没有来的及将这块物理内存真正回收。\n所以，JVM 认为自己 commit 的内存，与实际系统分配的物理内存，可能是有差异的，可能 JVM 认为自己 commit 的内存比系统分配的物理内存多，也可能少。这就是为啥 Native Memory Tracking（JVM 认为自己 commit 的内存）与实际其他系统监控中体现的物理内存使用指标对不上的原因。\n2.3. 大页分配 UseLargePages # 前面提到了虚拟内存需要映射物理内存才能使用，这个映射关系被保存在内存中的页表（Page Table）。现代 CPU 架构中一般有 TLB （Translation Lookaside Buffer，翻译后备缓冲，也称为页表寄存器缓冲）存在，在里面保存了经常使用的页表映射项。TLB 的大小有限，一般 TLB 如果只能容纳小于 100 个页表映射项。 我们能让程序的虚拟内存对应的页表映射项都处于 TLB 中，那么能大大提升程序性能，这就要尽量减少页表映射项的个数：页表项个数 = 程序所需内存大小 / 页大小。我们要么缩小程序所需内存，要么增大页大小。我们一般会考虑增加页大小，这就大页分配的由来，JVM 对于堆内存分配也支持大页分配，用于优化大堆内存的分配。那么 Linux 环境中有哪些大页分配的方式呢？\n2.3.1. Linux 大页分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs) # 相关的 Linux 内核文档：https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt\n这是出现的比较早的大页分配方式，其实就是在之前提到的页表映射上面做文章：\n默认 4K 页大小：\nPMD 直接映射实际物理页面，页面大小为 4K * 2^9 = 2M：\nPUD 直接映射实际物理页面，页面大小为 2M * 2^9 = 1G：\n但是，要想使用这个特性，需要操作系统构建的时候开启 CONFIG_HUGETLBFS 以及 CONFIG_HUGETLB_PAGE。之后，大的页面通常是通过系统管理控制预先分配并放入池里面的。然后，可以通过 mmap 系统调用或者 shmget,shmat 这些 SysV 的共享内存系统调用使用大页分配方式从池中申请内存。\n这种大页分配的方式，需要系统预设开启大页，预分配大页之外，对于代码也是有一定侵入性的，在灵活性上面查一些。但是带来的好处就是，性能表现上更加可控。另一种灵活性很强的 Transparent Huge Pages (THP) 方式，总是可能在性能表现上有一些意想不到的情况。\n2.3.2. Linux 大页分配方式 - Transparent Huge Pages (THP) # 相关的 Linux 内核文档：https://www.kernel.org/doc/Documentation/vm/transhuge.txt\nTHP 是一种使用大页的第二种方法，它支持页面大小的自动升级和降级，这样对于用户使用代码基本没有侵入性，非常灵活。但是，前面也提到过，这种系统自己去做页面大小的升级降级，并且系统一般考虑通用性，所以在某些情况下会出现意想不到的性能瓶颈。\n2.3.3. JVM 大页分配相关参数与机制 # 相关的参数如下：\nUseLargePages：明确指定是否开启大页分配，如果关闭，那么下面的参数就都不生效。在 linux 下默认为 false。 UseHugeTLBFS：明确指定是否使用前面第一种大页分配方式 hugetlbfs 并且通过 mmap 系统调用分配内存。在 linux 下默认为 false。 UseSHM：明确指定是否使用前面第一种大页分配方式 hugetlbfs 并且通过 shmget,shmat 系统调用分配内存。在 linux 下默认为 false。 UseTransparentHugePages：明确指定是否使用前面第二种大页分配方式 THP。在 linux 下默认为 false。 LargePageSizeInBytes：指定明确的大页的大小，仅适用于前面第一种大页分配方式 hugetlbfs，并且必须属于操作系统支持的页大小否则不生效。默认为 0，即不指定 首先，需要对以上参数做一个简单的判断：如果没有指定 UseLargePages，那么使用对应系统的默认 UseLargePages 的值，在 linux 下是 false，那么就不会启用大页分配。如果启动参数明确指定 UseLargePages 不启用，那么也不会启用大页分配。如果读取 /proc/meminfo 获取默认大页大小读取不到或者为 0，则代表系统也不支大页分配，大页分配也不启用。\n那么如果大页分配启用的话，我们需要初始化并验证大页分配参数可行性，流程是：\n首先，JVM 会读取根据当前所处的平台与系统环境读取支持的页的大小，当然，这个是针对前面第一种大页分配方式 hugetlbfs 的。在 Linux 环境下，JVM 会从 /proc/meminfo 读取默认的 Hugepagesize，从 /sys/kernel/mm/hugepages 目录下检索所有支持的大页大小，这块可以参考源码：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os/linux/os_linux.cpp。有关这些文件或者目录的详细信息，请参考前面章节提到的 Linux 内核文档：https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt\n如果操作系统开启了 hugetlbfs，/sys/kernel/mm/hugepages 目录下的结构类似于：\n\u0026gt; tree /sys/kernel/mm/hugepages /sys/kernel/mm/hugepages ├── hugepages-1048576kB │ ├── free_hugepages │ ├── nr_hugepages │ ├── nr_hugepages_mempolicy │ ├── nr_overcommit_hugepages │ ├── resv_hugepages │ └── surplus_hugepages └── hugepages-2048kB ├── free_hugepages ├── nr_hugepages ├── nr_hugepages_mempolicy ├── nr_overcommit_hugepages ├── resv_hugepages └── surplus_hugepages 这个 hugepages-1048576kB 就代表支持大小为 1GB 的页，hugepages-2048kB 就代表支持大小为 2KB 的页。\n如果没有设置 UseHugeTLBFS，也没有设置 UseSHM，也没有设置 UseTransparentHugePages，那么其实就是走默认的，默认使用 hugetlbfs 方式，不使用 THP 方式，因为如前所述， THP 在某些场景下有意想不到的性能瓶颈表现，在大型应用中，稳定性优先于峰值性能。之后，默认优先尝试 UseHugeTLBFS（即使用 mmap 系统调用通过 hugetlbfs 方式大页分配），不行的话再尝试 UseSHM（即使用 shmget 系统调用通过 hugetlbfs 方式大页分配）。这里只是验证下这些大页内存的分配方式是否可用，只有可用后面真正分配内存的时候才会采用那种可用的大页内存分配方式。\n3. Java 堆内存相关设计 # 3.1. 通用初始化与扩展流程 # 目前最新的 JVM，主要根据三个指标初始化堆以及扩展或缩小堆：\n最大堆大小 最小堆大小 初始堆大小 不同的 GC 情况下，初始化以及扩展的流程可能在某些细节不太一样，但是，大体的思路都是：\n初始化阶段，reserve 最大堆大小，并且 commit 初始堆大小 在某些 GC 的某些阶段，根据上次 GC 的数据，动态扩展或者缩小堆大小，扩展就是 commit 更多，缩小就是 uncommit 一部分内存。但是，堆大小不会小于最小堆大小，也不会大于最大堆大小 3.2. 直接指定三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)的方式 # 这三个指标，直接对应的 JVM 参数是：\n最大堆大小：MaxHeapSize，如果没有指定的话会有默认预设值用于指导 JVM 计算这些指标的大小，下一章节会详细分析，预设值为 125MB 左右（96M*13/10） 最小堆大小：MinHeapSize，默认为 0，0 代表让 JVM 自己计算，下一章节会详细分析 初始堆大小：InitialHeapSize，默认为 0，0 代表让 JVM 自己计算，下一章节会详细分析 对应源码是：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp：\n#define ScaleForWordSize(x) align_down((x) * 13 / 10, HeapWordSize) product(size_t, MaxHeapSize, ScaleForWordSize(96*M), \\ \u0026quot;Maximum heap size (in bytes)\u0026quot;) \\ constraint(MaxHeapSizeConstraintFunc,AfterErgo) \\ product(size_t, MinHeapSize, 0, \\ \u0026quot;Minimum heap size (in bytes); zero means use ergonomics\u0026quot;) \\ constraint(MinHeapSizeConstraintFunc,AfterErgo) \\ product(size_t, InitialHeapSize, 0, \\ \u0026quot;Initial heap size (in bytes); zero means use ergonomics\u0026quot;) \\ constraint(InitialHeapSizeConstraintFunc,AfterErgo) \\ 我们可以通过类似于 -XX:MaxHeapSize=1G 这种启动参数对这三个指标进行设置，但是，我们经常看到的可能是 Xmx 以及 Xms 这两个参数设置这三个指标，这两个参数分别对应：\nXmx：对应 最大堆大小 等价于 MaxHeapSize Xms：相当于同时设置最小堆大小 MinHeapSize 和初始堆大小 InitialHeapSize 对应的 JVM 源码是：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/runtime/arguments.cpp：\n//如果设置了 Xms else if (match_option(option, \u0026quot;-Xms\u0026quot;, \u0026amp;tail)) { julong size = 0; //解析 Xms 大小 ArgsRange errcode = parse_memory_size(tail, \u0026amp;size, 0); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), \u0026quot;Invalid initial heap size: %s\\n\u0026quot;, option-\u0026gt;optionString); describe_range_error(errcode); return JNI_EINVAL; } //将解析的值设置到 MinHeapSize if (FLAG_SET_CMDLINE(MinHeapSize, (size_t)size) != JVMFlag::SUCCESS) { return JNI_EINVAL; } //将解析的值设置到 InitialHeapSize if (FLAG_SET_CMDLINE(InitialHeapSize, (size_t)size) != JVMFlag::SUCCESS) { return JNI_EINVAL; } //如果设置了 Xmx } else if (match_option(option, \u0026quot;-Xmx\u0026quot;, \u0026amp;tail) || match_option(option, \u0026quot;-XX:MaxHeapSize=\u0026quot;, \u0026amp;tail)) { julong long_max_heap_size = 0; //解析 Xmx 大小 ArgsRange errcode = parse_memory_size(tail, \u0026amp;long_max_heap_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), \u0026quot;Invalid maximum heap size: %s\\n\u0026quot;, option-\u0026gt;optionString); describe_range_error(errcode); return JNI_EINVAL; } //将解析的值设置到 MaxHeapSize if (FLAG_SET_CMDLINE(MaxHeapSize, (size_t)long_max_heap_size) != JVMFlag::SUCCESS) { return JNI_EINVAL; } } 最后提一句，JVM 启动参数，同一个参数可以多次出现，但是只有最后一个会生效，例如：\njava -XX:MaxHeapSize=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version 这个命令启动的 JVM MaxHeapSize 为 8MB。由于前面提到 Xmx 与 MaxHeapSize 是等价的，所以这么写也是可以的(虽然最后 MaxHeapSize 还是 8MB)：\njava -Xmx=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version 3.3. 不手动指定三个指标的情况下，这三个指标(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何计算的 # 上一章节我们提到我们可以手动指定这三个参数，如果不指定呢？JVM 会怎么计算这三个指标的大小？首先，当然，JVM 会读取 JVM 可用内存：首先 JVM 需要知道自己可用多少内存，我们称为可用内存。由此引入第一个 JVM 参数，MaxRAM，这个参数是用来明确指定 JVM 进程可用内存大小的，如果没有指定，JVM 会自己读取系统可用内存。这个可用内存用来指导 JVM 限制最大堆内存。后面我们会看到很多 JVM 参数与这个可用内存相关。\n前面我们还提到了，就算没有指定 MaxHeapSize 或者 Xmx，MaxHeapSize 也有自己预设的一个参考值。源码中这个预设参考值为 125MB 左右（96M*13/10）。但是一般最后不会以这个参考值为准，JVM 初始化的时候会有很复杂的计算计算出合适的值。比如你可以在你的电脑上执行下下面的命令，可以看到类似下面的输出：\n\u0026gt; java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version|grep MaxHeapSize size_t MaxHeapSize = 1572864000 {product} {ergonomic} size_t SoftMaxHeapSize = 1572864000 {manageable} {ergonomic} openjdk version \u0026quot;17.0.2\u0026quot; 2022-01-18 LTS OpenJDK Runtime Environment Corretto-17.0.2.8.1 (build 17.0.2+8-LTS) OpenJDK 64-Bit Server VM Corretto-17.0.2.8.1 (build 17.0.2+8-LTS, mixed mode, sharing) 可以看到 MaxHeapSize 的大小，以及它的值是通过 ergonomic 决定的。也就是非人工指定而是 JVM 自己算出来的。\n上面提到的那个 125MB 左右的初始参考值，一般用于 JVM 计算。我们接下来就会分析这个计算流程，首先是最大堆内存 MaxHeapSize 的计算流程：\n流程中涉及了以下几个参数，还有一些已经过期的参数，会被转换成未过期的参数：\nMinRAMPercentage：注意不要被名字迷惑，这个参数是在可用内存比较小的时候生效，即最大堆内存占用为可用内存的这个参数指定的百分比，默认为 50，即 50% MaxRAMPercentage：注意不要被名字迷惑，这个参数是在可用内存比较大的时候生效，即最大堆内存占用为可用内存的这个参数指定的百分比，默认为 25，即 25% ErgoHeapSizeLimit：通过自动计算，计算出的最大堆内存大小不超过这个参数指定的大小，默认为 0 即不限制 MinRAMFraction: 已过期，如果配置了会转化为 MinRAMPercentage 换算关系是：MinRAMPercentage = 100.0 / MinRAMFraction，默认是 2 MaxRAMFraction: 已过期，如果配置了会转化为 MaxRAMPercentage 换算关系是：MaxRAMPercentage = 100.0 / MaxRAMFraction，默认是 4 对应的源码是：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp：\nproduct(double, MinRAMPercentage, 50.0, \\ \u0026#34;Minimum percentage of real memory used for maximum heap\u0026#34; \\ \u0026#34;size on systems with small physical memory size\u0026#34;) \\ range(0.0, 100.0) \\ product(double, MaxRAMPercentage, 25.0, \\ \u0026#34;Maximum percentage of real memory used for maximum heap size\u0026#34;) \\ range(0.0, 100.0) \\ product(size_t, ErgoHeapSizeLimit, 0, \\ \u0026#34;Maximum ergonomically set heap size (in bytes); zero means use \u0026#34; \\ \u0026#34;MaxRAM * MaxRAMPercentage / 100\u0026#34;) \\ range(0, max_uintx) \\ product(uintx, MinRAMFraction, 2, \\ \u0026#34;Minimum fraction (1/n) of real memory used for maximum heap \u0026#34; \\ \u0026#34;size on systems with small physical memory size. \u0026#34; \\ \u0026#34;Deprecated, use MinRAMPercentage instead\u0026#34;) \\ range(1, max_uintx) \\ product(uintx, MaxRAMFraction, 4, \\ \u0026#34;Maximum fraction (1/n) of real memory used for maximum heap \u0026#34; \\ \u0026#34;size. \u0026#34; \\ \u0026#34;Deprecated, use MaxRAMPercentage instead\u0026#34;) \\ range(1, max_uintx) \\ 然后如果我们也没有设置 MinHeapSize 以及 InitialHeapSize，也会经过下面的计算过程计算出来：\n流程中涉及了以下几个参数，还有一些已经过期的参数，会被转换成未过期的参数：\nNewSize：初始新生代大小，预设值为 1.3MB 左右（1*13/10） OldSize：老年代大小，预设值为 5.2 MB 左右（4*13/10） InitialRAMPercentage：初始堆内存为可用内存的这个参数指定的百分比，默认为 1.5625，即 1.5625% InitialRAMFraction: 已过期，如果配置了会转化为 InitialRAMPercentage 换算关系是：InitialRAMPercentage = 100.0 / InitialRAMFraction 对应的源码是：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp：\nproduct(size_t, NewSize, ScaleForWordSize(1*M), \\ \u0026quot;Initial new generation size (in bytes)\u0026quot;) \\ constraint(NewSizeConstraintFunc,AfterErgo) \\ product(size_t, OldSize, ScaleForWordSize(4*M), \\ \u0026quot;Initial tenured generation size (in bytes)\u0026quot;) \\ range(0, max_uintx) \\ product(double, InitialRAMPercentage, 1.5625, \\ \u0026quot;Percentage of real memory used for initial heap size\u0026quot;) \\ range(0.0, 100.0) \\ product(uintx, InitialRAMFraction, 64, \\ \u0026quot;Fraction (1/n) of real memory used for initial heap size. \u0026quot; \\ \u0026quot;Deprecated, use InitialRAMPercentage instead\u0026quot;) \\ range(1, max_uintx) \\ 3.4. 压缩对象指针相关机制 - UseCompressedOops # 3.4.1. 压缩对象指针存在的意义 # 现代机器大部分是 64 位的，JVM 也从 9 开始仅提供 64 位的虚拟机。在 JVM 中，一个对象指针，对应进程存储这个对象的虚拟内存的起始位置，也是 64 位大小：\n我们知道，对于 32 位寻址，最大仅支持 4GB 内存的寻址，这在现在的 JVM 很可能不够用，可能仅仅堆大小就超过 4GB。所以目前对象指针一般是 64 位大小来支持大内存。但是，这相对 32 位指针寻址来说，性能上却有衰减。我们知道，CPU 仅能处理寄存器里面的数据，寄存器与内存之间，有很多层 CPU 缓存，虽然内存越来越便宜也越来越大，但是 CPU 缓存并没有变大，这就导致如果使用 64 位的指针寻址，相对于之前 32 位的，CPU 缓存能容纳的指针个数小了一倍。\nJava 是面向对象的语言，JVM 中最多的操作，就是对对象的操作，比如 load 一个对象的字段，store 一个对象的字段，这些都离不开访问对象指针。所以 JVM 想尽可能的优化对象指针，这就引入了压缩对象指针，让对象指针在条件满足的情况下保持原来的 32 位。\n对于 32 位的指针，假设每一个 1 代表 1 字节，那么可以描述 0~2^32-1 这 2^32 字节也就是 4 GB 的虚拟内存。\n如果我让每一个 1 代表 8 字节呢？也就是让这块虚拟内存是 8 字节对齐，也就是我在使用这块内存时候，最小的分配单元就是 8 字节。对于 Java 堆内存，也就是一个对象占用的空间，必须是 8 字节的整数倍，不足的话会填充到 8 字节的整数倍用于保证对齐。这样最多可以描述 2^32 * 8 字节也就是 32 GB 的虚拟内存。\n这就是压缩指针的原理，上面提到的相关 JVM 参数是：ObjectAlignmentInBytes，这个 JVM 参数表示 Java 堆中的每个对象，需要按照几字节对齐，也就是堆按照几字节对齐，值范围是 8 ~ 256，必须是 2 的 n 次方，因为 2 的 n 次方能简化很多运算，例如对于 2 的 n 次方取余数就可以简化成对于 2 的 n 次方减一取与运算，乘法和除法可以简化移位。\n如果配置最大堆内存超过 32 GB（当 JVM 是 8 字节对齐），那么压缩指针会失效（其实不是超过 32GB，会略小于 32GB 的时候就会失效，还有其他的因素影响，下一节会讲到）。 但是，这个 32 GB 是和字节对齐大小相关的，也就是 -XX:ObjectAlignmentInBytes=8 配置的大小(默认为8字节，也就是 Java 默认是 8 字节对齐)。如果你配置 -XX:ObjectAlignmentInBytes=16，那么最大堆内存超过 64 GB 压缩指针才会失效，如果你配置 -XX:ObjectAlignmentInBytes=32，那么最大堆内存超过 128 GB 压缩指针才会失效.\n3.4.2. 压缩对象指针与压缩类指针的关系演进 # 老版本中， UseCompressedClassPointers 取决于 UseCompressedOops，即压缩对象指针如果没开启，那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始， UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了，两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface（例如使用 GraalVM）。参考 JDK ISSUE：https://bugs.openjdk.java.net/browse/JDK-8241825 - Make compressed oops and compressed class pointers independent (x86_64, PPC, S390) 以及源码：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/x86/globalDefinitions_x86.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS EnableJVMCI 在 x86 CPU 上，UseCompressedClassPointers 是否依赖 UseCompressedOops 取决于是否启用了 JVMCI，默认使用的 JVM 发布版，EnableJVMCI 都是 false https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/arm/globalDefinitions_arm.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 ARM CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 PPC CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/s390/globalDefinitions_s390.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 S390 CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops 3.4.3. 压缩对象指针的不同模式与寻址优化机制 # 对象指针与压缩对象指针如何转化，我们先来思考一些问题。通过第二章的分析我们知道，每个进程有自己的虚拟地址空间，并且从 0 开始的一些低位空间，是给进程的一些系统调用保留的空间，例如 0x0000 0000 0000 0000 ~ 0x0000 0000 0040 0000 是保留区不可使用\n进程可以申请的空间，是上图所示的原生堆空间。所以，JVM 进程的虚拟内存空间，肯定不会从 0x0000 0000 0000 0000 开始。不同的操作系统，这个原生堆空间的起始不太一样，这里我们不关心具体的位置，我们仅知道一点：JVM 需要从虚拟内存的某一点开始申请内存，并且，需要预留出足够多的空间，给可能的一些系统调用机制使用，比如前面我们 native memory tracking 中看到的一些 malloc 内存，其实某些就在这个预留空间中分配的。一般的，JVM 会优先考虑 Java 堆的内存在原生堆分配，之后再在原生堆分配其他的，例如元空间，代码缓存空间等等。\nJVM 在 Reserve 分配 Java 堆空间的时候，会一下子 Reserve 最大 Java 堆空间的大小，然后在此基础上 Reserve 分配其他的存储空间。之后分配 Java 对象，在 Reserve 的 Java 堆内存空间内 Commit 然后写入数据映射物理内存分配 Java 对象。根据前面说的 Java 堆大小的伸缩策略，决定继续 Commit 占用更多物理内存还是 UnCommit 释放物理内存：\nJava 是一个面向对象的语言，JVM 中执行最多的就是访问这些对象，在 JVM 的各种机制中，必须无时无刻考虑怎么优化访问这些对象的速度，对于压缩对象指针，JVM 就考虑了很多优化。如果我们要使用压缩对象指针，那么需要将这个 64 位的地址，转换为 32 位的地址。然后在读取压缩对象指针所指向的对象信息的时候，需要将这个 32 位的地址，解析为 64 位的地址之后寻址读取。这个转换公式，如下所示：\n64 位地址 = 基址 + （压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移） 压缩对象指针 = (64 位地址 - 基址) \u0026gt;\u0026gt; 对象对齐偏移 基址其实就是对象地址的开始，注意，这个基址不一定是 Java 堆的开始地址，我们后面就会看到。对象对齐偏移与前面提到的 ObjectAlignmentInBytes 相关，例如 ObjectAlignmentInBytes=8 的情况下，对象对齐偏移就是 3 （因为 8 是 2 的 3 次方）。我们针对这个公式进行优化：\n首先，我们考虑把基址和对象对齐偏移去掉，那么压缩对象指针可以直接作为对象地址使用。什么情况下可以这样呢？那么就是对象地址从 0 开始算，并且最大堆内存 + Java 堆起始位置不大于 4GB。因为这种情况下，Java 堆中对象的最大地址不会超过 4GB，那么压缩对象指针的范围可以直接表示所有 Java 堆中的对象。可以直接使用压缩对象指针作为对象实际内存地址使用。这里为啥是最大堆内存 + Java 堆起始位置不大于 4GB？因为前面的分析，我们知道进程可以申请的空间，是原生堆空间。所以，Java 堆起始位置，肯定不会从 0x0000 0000 0000 0000 开始。\n如果最大堆内存 + Java 堆起始位置大于 4GB，第一种优化就不能用了，对象地址偏移就无法避免了。但是如果可以保证最大堆内存 + Java 堆起始位置小于 32位 * ObjectAlignmentInBytes，默认 ObjectAlignmentInBytes=8 的情况即 32GB，我们还是可以让基址等于 0，这样 64 位地址 = （压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移）\n但是，在ObjectAlignmentInBytes=8 的情况，如果最大堆内存太大，接近 32GB，想要保证最大堆内存 + Java 堆起始位置小于 32GB，那么 Java 堆起始位置其实就快接近 0 了，这显然不行。所以在最大堆内存接近 32GB 的时候，上面第二种优化也就失效了。但是我们可以让 Java 堆从一个与 32GB 地址完全不相交的地址开始，这样加法就可以优化为取或运算，即64 位地址 = 基址 |（压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移）\n最后，在ObjectAlignmentInBytes=8 的情况，如果用户通过 HeapBaseMinAddress 自己指定了 Java 堆开始的地址，并且与 32GB 地址相交，并最大堆内存 + Java 堆起始位置大于 32GB，但是最大堆内存没有超过 32GB，那么就无法优化了，只能 64 位地址 = 基址 + （压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移）\n总结下，上面我们说的那四种模式，对应 JVM 中的压缩对象指针的四种模式（以下叙述基于 ObjectAlignmentInBytes=8 的情况，即默认情况）：\n32-bit 压缩指针模式：最大堆内存 + Java 堆起始位置不大于 4GB（并且 Java 堆起始位置不能太小），64 位地址 = 压缩对象指针 Zero based 压缩指针模式：最大堆内存 + Java 堆起始位置不大于 32GB（并且 Java 堆起始位置不能太小），64 位地址 = （压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移） Non-zero disjoint 压缩指针模式：最大堆内存不大于 32GB，由于要保证 Java 堆起始位置不能太小，最大堆内存 + Java 堆起始位置大于 32GB，64 位地址 = 基址 |（压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移） Non-zero based 压缩指针模式：用户通过 HeapBaseMinAddress 自己指定了 Java 堆开始的地址，并且与 32GB 地址相交，并最大堆内存 + Java 堆起始位置大于 32GB，但是最大堆内存没有超过 32GB，64 位地址 = 基址 + （压缩对象指针 \u0026lt;\u0026lt; 对象对齐偏移） 3.5. 为何预留第 0 页，压缩对象指针 null 判断擦除的实现 # 前面我们知道，JVM 中的压缩对象指针有四种模式。对于地址非从 0 开始的那两种，即 Non-zero disjoint 和 Non-zero based 这两种，堆的实际地址并不是从 HeapBaseMinAddress 开始，而是有一页预留下来，被称为第 0 页，这一页不映射实际内存，如果访问这一页内部的地址，会有 Segment Fault 异常。那么为什么要预留这一页呢？主要是为了 null 判断优化，实现 null 判断擦除。\n我们都知道，Java 中如果对于一个 null 的引用变量进行成员字段或者方法的访问，会抛出 NullPointerException。但是，这个是如何实现的呢？我们的代码中没有明确的 null 判断，如果是 null 就抛出 NullPointerException，但是 JVM 还是针对 null 可以抛出 NullPointerException 这个 Java 异常。可以猜测出，JVM 可能在访问每个引用变量进行成员字段或者方法的时候，都会做这样一个判断：\nif (o == null) { throw new NullPoniterException(); } 但是，如果每次访问每个引用变量进行成员字段或者方法的时候都做这样一个判断，是很低效率的行为。所以，在解释执行的时候，可能每次访问每个引用变量进行成员字段或者方法的时候都做这样一个判断。在代码运行一定次数，进入 C1，C2 的编译优化之后，这些 null 判断可能会被擦除。可能擦除的包括：\n成员方法对于 this 的访问，可以将 this 的 null 判断擦除。 代码中明确判断了某个变量是否为 null，并且这个变量不是 volatile 的 前面已经有了 a.something() 类似的访问，并且 a 不是 volatile 的，后面 a.somethingElse() 就不用再做 null 检查了 等等等等\u0026hellip; 对于无法擦除的，JVM 倾向于做出一个假设，即这个变量大概率不会为 null，JIT 优化先直接将 null 判断擦除。Java 中的 null，对应压缩对象指针的值为 0：\nenum class narrowOop : uint32_t { null = 0 }; 对于压缩对象指针地址为 0 的地方进行访问，实际上就是针对前面我们讨论的压缩对象指针基址进行访问，在四种模式下：\n32-bit 压缩指针模式：就是对于 0x0000 0000 0000 0000 进行访问，但是前面我们知道，0x0000 0000 0000 0000 是保留区域，无法访问，会有 Segment Fault 错误，发出 SIGSEGV 信号 Zero based 压缩指针模式：就是对于 0x0000 0000 0000 0000 进行访问，但是前面我们知道，0x0000 0000 0000 0000 是保留区域，无法访问，会有 Segment Fault 错误，发出 SIGSEGV 信号 Non-zero disjoint 压缩指针模式：就是对于基址进行访问，但是前面我们知道，基址 + JVM 系统页大小为仅 Reserve 但是不会 commit 的预留区域，无法访问，会有 Segment Fault 错误，发出 SIGSEGV 信号 Non-zero based 压缩指针模式：就是对于基址进行访问，但是前面我们知道，基址 + JVM 系统页大小为仅 Reserve 但是不会 commit 的预留区域，无法访问，会有 Segment Fault 错误，发出 SIGSEGV 信号 对于非压缩对象指针的情况，更简单，非压缩对象指针 null 就是 0x0000 0000 0000 0000，就是对于 0x0000 0000 0000 0000 进行访问，但是前面我们知道，0x0000 0000 0000 0000 是保留区域，无法访问，会有 Segment Fault 错误，发出 SIGSEGV 信号\n可以看出，如果 JIT 优化将 null 判断擦除，那么在真的遇到 null 的时候，会有 Segment Fault 错误，发出 SIGSEGV 信号。JVM 有对于 SIGSEGV 信号的处理：\n//这是在 AMD64 CPU 下的代码 } else if ( //如果信号是 SIGSEGV sig == SIGSEGV \u0026amp;\u0026amp; //并且是由于遇到擦除 null 判断的地方遇到 null 导致的 SIGSEGV（后面我们看到很多其他地方用到了 SIGSEGV） MacroAssembler::uses_implicit_null_check(info-\u0026gt;si_addr) ) { // 如果是由于遇到 null 导致的 SIGSEGV，那么就需要评估下，是否要继续擦除这里的 null 判断了 stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL); } JVM 不仅 null 检查擦除使用了 SIGSEGV 信号，还有其他地方也用到了（例如后面我们会详细分析的 StackOverflowError 的实现）。所以，我们需要通过判断下发生 SIGSEGV 信号的地址，如果地址是我们上面列出的范围，则是擦除 null 判断的地方遇到 null 导致的 SIGSEGV：\nbool MacroAssembler::uses_implicit_null_check(void* address) { uintptr_t addr = reinterpret_cast\u0026lt;uintptr_t\u0026gt;(address); uintptr_t page_size = (uintptr_t)os::vm_page_size(); #ifdef _LP64 //如果压缩对象指针开启 if (UseCompressedOops \u0026amp;\u0026amp; CompressedOops::base() != NULL) { //如果存在预留页(第 0 页)，起点是基址 uintptr_t start = (uintptr_t)CompressedOops::base(); //如果存在预留页(第 0 页)，终点是基址 + 页大小 uintptr_t end = start + page_size; //如果地址范围在第 0 页，则是擦除 null 判断的地方遇到 null 导致的 `SIGSEGV` if (addr \u0026gt;= start \u0026amp;\u0026amp; addr \u0026lt; end) { return true; } } #endif //如果在整个虚拟空间的第 0 页，则是擦除 null 判断的地方遇到 null 导致的 `SIGSEGV` return addr \u0026lt; page_size; } 我们分别代入压缩对象指针的 4 种情况：\n32-bit 压缩指针模式：就是对于 0x0000 0000 0000 0000 进行访问，地址位于第 0 页，uses_implicit_null_check 返回 true Zero based 压缩指针模式：就是对于 0x0000 0000 0000 0000 进行访问，地址位于第 0 页，uses_implicit_null_check 返回 true Non-zero disjoint 压缩指针模式：就是对于基址进行访问，地址位于第 0 页，uses_implicit_null_check 返回 true Non-zero based 压缩指针模式：就是对于基址进行访问，地址位于第 0 页，uses_implicit_null_check 返回 true 对于非压缩对象指针的情况，更简单，非压缩对象指针 null 就是 0x0000 0000 0000 0000，就是对于基址进行访问，地址位于第 0 页，uses_implicit_null_check 返回 true\n这样，我们知道，JIT 可能会将 null 检查擦除，通过 SIGSEGV 信号抛出 NullPointerException。但是，通过 SIGSEGV 信号要经过系统调用，系统调用是一个很低效的行为，我们需要尽量避免（对于抄袭狗就不不必了）。但是这里的假设就是大概率不为 null，所以使用系统调用也无所谓。但是如果一个地方经常出现 null，JIT 就会考虑不这么优化了，将代码去优化并重新编译，不再擦除 null 检查而是使用显式 null 检查抛出。\n最后，我们知道了，要预留第 0 页，不映射内存，实际就是为了让对于基址进行访问可以触发 Segment Fault，JVM 会捕捉这个信号，查看触发这个信号的内存地址是否属于第一页，如果属于那么 JVM 就知道了这个是对象为 null 导致的。不过从前面看，我们其实只是为了不映射基址对应的地址，那为啥要保留一整页呢？这个是处于内存对齐与寻址访问速度的考量，里面映射物理内存都是以页为单位操作的，所以内存需要按页对齐。\n3.6. 结合压缩对象指针与前面提到的堆内存限制的初始化的关系 # 前面我们说明了不手动指定三个指标的情况下，这三个指标 (MinHeapSize,MaxHeapSize,InitialHeapSize) 是如何计算的，但是没有涉及压缩对象指针。如果压缩对象指针开启，那么堆内存限制的初始化之后，会根据参数确定压缩对象指针是否开启：\n首先，确定 Java 堆的起始位置： 第一步，在不同操作系统不同 CPU 环境下，HeapBaseMinAddress 的默认值不同，大部分环境下是 2GB，例如对于 Linux x86 环境，查看源码：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp：define_pd_global(size_t, HeapBaseMinAddress, 2*G); 将 DefaultHeapBaseMinAddress 设置为 HeapBaseMinAddress 的默认值，即 2GB 如果用户在启动参数中指定了 HeapBaseMinAddress，如果 HeapBaseMinAddress 小于 DefaultHeapBaseMinAddress，将 HeapBaseMinAddress 设置为 DefaultHeapBaseMinAddress 计算压缩对象指针堆的最大堆大小： 读取对象对齐大小 ObjectAlignmentInBytes 参数的值，默认为 8 对 ObjectAlignmentInBytes 取 2 的对数，记为 LogMinObjAlignmentInBytes 将 32 位左移 LogMinObjAlignmentInBytes 得到 OopEncodingHeapMax 即不考虑预留区的最大堆大小 如果需要预留区，即 Non-Zero Based Disjoint 以及 Non-Zero Based 这两种模式下，需要刨除掉预留区即第 0 页的大小，即 OopEncodingHeapMax - 第 0 页的大小 读取当前 JVM 配置的最大堆大小（前面我们分析了最大堆大小如何计算出来的） 如果 JVM 配置的最大堆小于压缩对象指针堆的最大堆大小，并且没有通过 JVM 启动参数明确关闭压缩对象指针，则开启压缩对象指针。否则，关闭压缩对象指针。你洗稿的样子真丑。 如果压缩对象指针关闭，根据前面分析过的是否压缩类指针强依赖压缩对象指针，如果是，关闭压缩类指针 3.7. 使用 jol + jhsdb + JVM 日志查看压缩对象指针与 Java 堆验证我们前面的结论 # 引入 jol 依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.openjdk.jol\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jol-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 编写代码：\npackage test; import org.openjdk.jol.info.ClassLayout; public class TestClass { //TestClass 对象仅仅包含一个字段 next（洗稿狗滚） private String next = new String(); public static void main(String[] args) throws InterruptedException { //在栈上新建一个 tt 本地变量，指向一个在堆上新建的 TestClass 对象 final TestClass tt = new TestClass(); //使用 jol 输出 tt 指向的对象的结构（抄袭不得好死） System.out.println(ClassLayout.parseInstance(tt).toPrintable()); //无限等待，防止程序退出 Thread.currentThread().join(); } } 3.7.1. 验证 32-bit 压缩指针模式 # 接下来我们先测试第一种压缩对象指针模式（32-bit）的情况，即 Java 堆位于 0x0000 0000 0000 0000 ~ 0x 0000 0001 0000 0000（0~4GB） 之间的情况，使用下面的启动参数启动这个程序：\n-Xmx32M -Xlog:coops*=debug 其中 -Xlog:coops*=debug 代表查看 JVM 日志中带 coops 标签的 debug 日志。这个日志会告诉你堆的起始虚拟内存位置，以及堆 reserved 的空间大小，以及 压缩对象指针的模式。\n启动后，查看日志输出：\n[0.006s][debug][gc,heap,coops] Heap address: 0x00000000fe000000, size: 32 MB, Compressed Oops mode: 32-bit test.TestClass object internals:个人爱好钻研技术分享，请抄袭狗滚开。 OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0x00c01000 12 4 java.lang.String TestClass.next (object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 第一行日志告诉我们，堆的起始位置是 0x0000 0000 fe00 0000，大小是 32 MB，压缩对象指针模式是 32-bit。其中 0x0000 0000 fe00 0000 加上 32 MB，结果就是 4GB 0x0000 0001 0000 0000。可以看出之前说的 Java 堆会从界限减去最大堆大小的位置开始 reserve 的结论是对的。在这种情况下，0x0000 0000 0000 0000 ~ 0x0000 0000 fdff ffff 的内存就给之前所说的进程系统调用以及原生内存分配使用。\n后面的日志是关于 jol 输出对象结构的，可以看出目前这个对象包含一个 markword (0x0000000000000001)，一个压缩类指针(0x00c01000)，以及字段 next。我们使用 jhsdb 看一下进程的具体虚拟内存的内容验证下\n首先打开 jhsdb gui 模式：jhsdb hsdb\n之后 \u0026ldquo;File\u0026rdquo; -\u0026gt; \u0026ldquo;Attach to Hotspot Process\u0026rdquo;，输入你的 JVM 进程号：\n成功 Attach 之后，可以看到面板上有你的 JVM 进程的所有线程，目前我们就看 main 线程即可，点击 main 线程，之后点击下图红框的按钮（查看线程栈内存）：\n之后我们在 main 线程栈内存中可以找到代码中的本地变量 tt：\n这里我们可以看到变量 tt 存储的值，其实就是对象的地址，我们打开 \u0026ldquo;Tools\u0026rdquo; -\u0026gt; \u0026ldquo;Memory Viewer\u0026rdquo;，这个是进程虚拟内存查看器，可以查看内存地址的实际值。还有 \u0026ldquo;Tools\u0026rdquo; -\u0026gt; \u0026ldquo;Inspector\u0026rdquo;，将地址转换为 JVM 的 C++ 对应对象。在这两个窗口都输入上面在 main 线程栈内存看到的本地变量 tt 的值：\n从上图我们可以看到，tt 保存的对象，对象位置，也就是对象起始地址是 0x00000000ffec7450，对象头是 0x0000 0000 ffec 7450 ~ 0x0000 0000 ffec 7457，保存的值是 0x0000 0000 0000 0001，这个和上面 jol 输出的一模一样。压缩类指针是 0x0000 0000 ffec 7458 ~ 0x0000 0000 ffec 745b，保存的值是 0x00c0 1000，这个和上面 jol 输出的压缩类指针地址一模一样。之后是 next 字段值，范围是 0x0000 0000 ffec 745c ~ 0x0000 0000 ffec 745f，保存的值是 0xffec 7460，对应的字符串对象实际地址也是 0x0000 0000 ffec 7460。可以看出，和我们之前说的 32-bit 模式的压缩类指针的特点一模一样。\n3.7.2. 验证 Zero based 压缩指针模式 # 下一个我们尝试 Zero based 模式，使用参数 -Xmx2050M -Xlog:coops*=debug 启动程序（和你的平台相关，建议你查看下在你的平台 HeapBaseMinAddress 默认的大小，一般对于 x86 都是 2G，所以指定一个大于 4G - 2G = 2G 的最大堆内存大小的值即可），日志输出是：\n[0.006s][debug][gc,heap,coops] Heap address: 0x000000077fe00000, size: 2050 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 洗稿的狗也遇到不少 test.TestClass object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1) 8 4 (object header: class) 0x00c01000 12 4 java.lang.String TestClass.next (object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 这次我们发现，Java 堆从 0x0000 0007 7fe0 0000 开始了，如果你用 0x0000 0007 7fe0 0000 加上 2050 MB 就会发现正好等于 32GB，可以看出之前说的 Java 堆会从界限减去最大堆大小的位置开始 reserve 的结论是对的。\n后面的日志是关于 jol 输出对象结构的，可以看出目前这个对象包含一个 markword(0x0000000000000009，由于我的程序启动后输出 jol 日志之前经过了一次 GC，所以当前值与前面一个例子的不一样)，一个压缩类指针(0x00c01000)，以及字段 next。\n我们使用 jhsdb 看一下进程的具体虚拟内存的内容验证下目前的压缩对象指针的内容，前面的步骤与上一个例子一样，我们直接看最后的：\n如上图所示，tt 保存的对象，从 0x0000 0007 9df7 2640 开始，我们找到 next 字段，它保存的值是 0xf3be ed80，将其左移三位，就是 0x0000 0007 9df7 6c00（inspector 中显示的是帮你解压缩之后的对象地址，Memory Viewer 中是虚拟内存实际保存的值）\n接下来我们试一下通过 HeapBaseMinAddress 让第一个例子也变成 Zero based 模式。使用下面的启动参数 -Xmx32M -Xlog:coops*=debug -XX:HeapBaseMinAddress=4064M，其中 4064MB + 32MB = 4GB，启动后可以通过日志发现模式还是 32-bit：[0.005s][debug][gc,heap,coops] Heap address: 0x00000000fe000000, size: 32 MB, Compressed Oops mode: 32-bit。其中 0x00000000fe000000 就是 4064MB，与启动参数配置的一致。使用下面的启动参数 -Xmx32M -Xlog:coops*=debug -XX:HeapBaseMinAddress=4065M，可以看到日志：\n[0.005s][debug][gc,heap,coops] Heap address: 0x00000000fe200000, size: 32 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 test.TestClass object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0x00c01000 12 4 java.lang.String TestClass.next (object) Instance size: 16 bytes chaoxi你妹啊，抄袭能给你赚几个钱，别为了这点镚子败人品了 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 可以看模式变为 Zero based，堆的起始点是 0x00000000fe200000 等于 4066MB，与我们的启动参数不符，是因为这个起始位置有对齐策略导致的，与使用的 GC 也是相关的，这个等我们以后分析 GC 的时候再关心。\n3.7.3. 验证 Non-zero disjoint 压缩指针模式 # 接下来我们来看下一个模式 Non-zero disjoint，使用以下参数 -Xmx31G -Xlog:coops*=debug 启动程序，日志输出为：\n[0.007s][debug][gc,heap,coops] Protected page at the reserved heap base: 0x0000001000000000 / 16777216 bytes [0.007s][debug][gc,heap,coops] Heap address: 0x0000001001000000, size: 31744 MB, Compressed Oops mode: Non-zero disjoint base: 0x0000001000000000, Oop shift amount: 3 test.TestClass object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0x00c01000 12 4 java.lang.String TestClass.next (object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 可以看到，保护页大小为 16MB（16777216 bytes）chaoxi你妹啊，抄袭能给你赚几个钱，别为了这点镚子败人品了，实际 Java 堆开始的地址是 0x0000 0010 0100 0000。并且，基址也不再是 0（Non-zero disjoint base，而是与 32GB 完全不相交的地址 0x0000001000000000），可以将加法优化为或运算。后面 jol 输出对象结构，可以看出目前这个对象包含一个 markword(0x0000000000000001)，一个压缩类指针(0x00c01000)，以及字段 next。\n我们使用 jhsdb 看一下进程的具体虚拟内存的内容验证下目前的压缩对象指针的内容，前面的步骤与上一个例子一样，我们直接看最后的：\n如上图所示，tt 保存的对象，从 0x000000102045ab90 开始，我们找到 next 字段，它保存的值是 0x0408 b574，将其左移三位，就是 0x0000 0000 2045 aba0（inspector 中显示的是帮你解压缩之后的对象地址，Memory Viewer 中是虚拟内存实际保存的值），然后对基址 ``0x0000 0010 0000 0000取或运算，得到 next 指向的字符串对象的实际地址0x0000 0010 2045 aba0`，计算结果与 inspector 中显示的 next 解析结果一致。\n3.7.4. 验证 Non-zero based 压缩指针模式 # 最后，我们来看最后一种模式，即 Non-zero based，使用以下参数 -Xmx31G -Xlog:coops*=debug -XX:HeapBaseMinAddress=2G 启动程序，日志输出为：\n[0.005s][debug][gc,heap,coops] Protected page at the reserved heap base: 0x0000000080000000 / 16777216 bytes [0.005s][debug][gc,heap,coops] Heap address: 0x0000000081000000, size: 31744 MB, Compressed Oops mode: Non-zero based: 0x0000000080000000, Oop shift amount: 3 test.TestClass object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) 8 4 (object header: class) 0x00c01000 12 4 java.lang.String TestClass.next (object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 可以看到，保护页大小为 16MB（16777216 bytes），实际 Java 堆开始的地址是 0x0000 0000 8100 0000。并且，基址也不再是 0（Non-zero based：0x0000000080000000）。后面 jol 输出对象结构，可以看出目前这个对象包含一个 markword(0x0000000000000001)，一个压缩类指针(0x00c01000)，以及字段 next。\n我们使用 jhsdb 看一下进程的具体虚拟内存的内容验证下目前的压缩对象指针的内容，前面的步骤与上一个例子一样，我们直接看最后的：\n如上图所示，tt 保存的对象，从 0x00000000a0431f10 开始，我们找到 next 字段，它保存的值是 0x0408 63e4，将其左移三位，就是 0x0000 0000 2043 1f20（inspector 中显示的是帮你解压缩之后的对象地址，Memory Viewer 中是虚拟内存实际保存的值），然后加上基址 ``0x0000 0000 8000 0000（其实就是 2GB，是我们在-XX:HeapBaseMinAddress=2G指定的 ），得到 next 指向的字符串对象的实际地址0x0000 0000 a043 1f20`，计算结果与 inspector 中显示的 next 解析结果一致。不要偷取他人的劳动成果\n3.8. 堆大小的动态伸缩 # 不同的 GC 堆大小动态伸缩有很大很大的差异（比如 ParallelGC 涉及 UseAdaptiveSizePolicy 启用的动态堆大小策略以及相关的 UsePSAdaptiveSurvivorSizePolicy、UseAdaptiveGenerationSizePolicyAtMinorCollection 等等等等的参数参与决定计算最新堆大小的方式以及时机），在这个系列以后的章节我们详细分析每个 GC 的时候再详细分析这些不同 GC 的动态伸缩策略。我们这里仅涉及大多数 GC 通用的堆大小伸缩涉及的参数：MinHeapFreeRatio 与 MaxHeapFreeRatio：\nMinHeapFreeRatio：目标最小堆空闲比例，如果某次 GC 之后堆的某个区域（在某些 GC 是整个堆）空闲比例小于这个比例，那么就考虑将这个区域扩容。默认是 40，即默认是 40%，但是某些 GC 如果你不设置就会变成 0%。0% 代表从来不会因为没有达到目标最小堆空闲比例而扩容，配置为 0% 一般是为了堆的大小稳定。 MaxHeapFreeRatio：目标最大堆空闲比例，如果某次 GC 之后堆的某个区域（在某些 GC 是整个堆）空闲比例大于这个比例，那么就考虑将这个区域缩小。默认是 70，即默认是 70%，但是某些 GC 如果你不设置就会变成 100%。100% 代表从来不会因为没有达到目标最大堆空闲比例而扩容，配置为 100% 一般是为了堆的大小稳定。 MinHeapDeltaBytes：当扩容时，至少扩展多大的内存。默认是 166.4 KB（128*13/10） 对应的源码是：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/runtime/globals.hpp：\nproduct(uintx, MinHeapFreeRatio, 40, MANAGEABLE, \\ \u0026quot;The minimum percentage of heap free after GC to avoid expansion.\u0026quot;\\ \u0026quot; For most GCs this applies to the old generation. In G1 and\u0026quot; \\ \u0026quot; ParallelGC it applies to the whole heap.\u0026quot;) \\ range(0, 100) \\ constraint(MinHeapFreeRatioConstraintFunc,AfterErgo) \\ product(uintx, MaxHeapFreeRatio, 70, MANAGEABLE, \\ \u0026quot;The maximum percentage of heap free after GC to avoid shrinking.\u0026quot;\\ \u0026quot; For most GCs this applies to the old generation. In G1 and\u0026quot; \\ \u0026quot; ParallelGC it applies to the whole heap.\u0026quot;) \\ range(0, 100) \\ constraint(MaxHeapFreeRatioConstraintFunc,AfterErgo) \\ product(size_t, MinHeapDeltaBytes, ScaleForWordSize(128*K), \\ \u0026quot;The minimum change in heap space due to GC (in bytes)\u0026quot;) \\ range(0, max_uintx) \\ 这两个参数，在不同 GC 下的实际表现，如下：\nSerialGC：在 SerialGC 的情况下，MinHeapFreeRatio 与 MaxHeapFreeRatio 指的仅仅是老年代的目标空闲比例，仅对老年代生效。在触发涉及老年代的 GC 的时候（其实就是 FullGC），GC 结束时，会查看（抄袭和xigao是文化的毒瘤，是对文化创造和发展的阻碍）当前老年代的空闲比例，与 MinHeapFreeRatio 和 MaxHeapFreeRatio比较 判断是否扩容或者缩小老年代的大小（这里的源码参考：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/serial/tenuredGeneration.cpp）。 ParallelGC：在 ParallelGC 的情况下，MinHeapFreeRatio 与 MaxHeapFreeRatio 指的是整个堆的大小。并且，如果这两个 JVM 参数没有明确指定的话，那么 MinHeapFreeRatio 就是 0，MaxHeapFreeRatio 就是 100（这里的源码参考：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/parallel/parallelArguments.cpp），相当于不会根据这两个参数调整堆大小。并且，如果 UseAdaptiveSizePolicy 是 false 的话，这两个参数也不会生效。 G1GC：在 G1GC 的情况下，MinHeapFreeRatio 与 MaxHeapFreeRatio 指的是整个堆的大小。在触发涉及老年代的 GC 的时候，GC 结束时，会查看当前堆的空闲比例，与 MinHeapFreeRatio 和 MaxHeapFreeRatio比较判断是否扩容还是缩小堆，通过增加或者减少 Region 数量进行堆的扩容与缩小（这里的源码参考：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp）。 ShenandoahGC：这三个参数不生效 ZGC：这三个参数不生效 3.9. 适用于长期运行并且尽量将所有可用内存被堆使用的 JVM 参数 AggressiveHeap # AggressiveHeap 是一种激进地让 JVM 使用当前系统的剩余内存的一种配置，开启会根据系统可用内存，自动设置堆大小等内存参数，将内存的一半分配给堆，另一半留给堆外其他的子系统占用内存，通过强制使用 ParallelGC 这种不会占用太多堆外内存的 GC 算法这种类似的思路限制堆外内存的使用（只能使用这个 GC，你指定其他 GC 的话会启动报错 Error occurred during initialization of VM. Multiple garbage collectors selected）。默认为 false 即不开启，可以通过 -XX:+AggressiveHeap 开启。\n开启后，首先检查系统内存大小是否足够 256 MB，如果不够会报错，够得话，会计算出一个目标堆大小：\n目标堆大小 = Math.min(系统可用内存/2, 系统可用内存 - 160MB) 之后，开启这个参数会强制设置以下参数：\nMaxHeapSize 最大堆内存为目标堆大小 InitialHeapSize 初始堆内存为目标堆大小 NewSize 和 MaxNewSize 新生代为目标堆大小 * 八分之三 BaseFootPrintEstimate 堆外内存占用大小预估为目标堆大小，用于指导一些堆外内存结构的初始化 UseLargePages 为开启，使用大页内存分配，增加实际物理内存的连续性 TLABSize 为 256K，即初始 TLAB 大小为 256 K，但是下面我们设置了 ResizeTLAB 为 false，所以 TLAB 就会保持为 256K ResizeTLAB 为 false，也就是 TLAB 大小不再随着 GC 以及分配特征的改变而改变，减少没必要的计算，反正进程要长期存在了，就在初始就指定一个比较大的 TLAB 的值。如果对 TLAB 细节感兴趣，请参考系列的第一部：全网最硬核 JVM TLAB 解析 UseParallelGC 为 true，强制使用 ParallelGC ThresholdTolerance 为最大值 100，ThresholdTolerance 用于动态控制对象晋升到老年代需要存活过的 GC 次数，如果 1 + ThresholdTolerance/100 * MinorGC 时间大于 MajorGC 的时间，我们就认为 MinorGC 占比过大，需要将更多对象晋升到老年代。反之，如果 1 + ThresholdTolerance/100 * MajorGC 时间大于 MinorGC 的时间，就认为 MajorGC 时间占比过多，需要将更少的对象晋升到老年代。调整成 100 可以实现这个晋升界限基本不变保持稳定。 ScavengeBeforeFullGC 设置为 false，在 FullGC 之前，先尝试执行一次 YoungGC。因为长期运行的应用，会经常 YoungGC 并晋升对象，需要 FullGC 的时候一般 YoungGC 无法回收那么多内存避免 FullGC，关闭它更有利于避免无效扫描弄脏 CPU 缓存。 3.10. JVM 参数 AlwaysPreTouch 的作用 # 在第二章的分析中，我们知道了 JVM 申请内存的流程，内存并不是在 JVM commit 一块内存之后就立刻被操作系统分配实际的物理内存的，只有真正往里面写数据的时候，才会关联实际的物理内存。所以对于 JVM 堆内存，我们也可以推测出，堆内存随着对象的分配才会关联实际的物理内存。那我们有没有办法提前强制让 committed 的内存关联实际的物理内存呢？很简单，往这些 committed 的内存中写入假数据就行了（一般是填充 0）。\n对于不同的 GC，由于不同 GC 对于堆内存的设计不同，所以对于 AlwaysPreTouch 的处理也略有不同，在以后的系列我们详细解析每一种 GC 的时候，会详细分析每种 GC 的堆内存设计，这里我们就简单列举通用的 AlwaysPreTouch 处理。AlwaysPreTouch 打开后，所有新 commit 的堆内存，都会往里面填充 0，相当于写入空数据让 commit 的内存真正被分配。\n不同操作系统环境下填充 0 的实现方式不太一样，但是基本思路是通过原子的方式给内存地址加 0 实现：https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/runtime/os.cpp：\nvoid os::pretouch_memory(void* start, void* end, size_t page_size) { if (start \u0026lt; end) { //对齐起始与末尾 char* cur = static_cast\u0026lt;char*\u0026gt;(align_down(start, page_size)); void* last = align_down(static_cast\u0026lt;char*\u0026gt;(end) - 1, page_size); //对内存写入空数据，通过 Atomic::add for ( ; true; cur += page_size) { Atomic::add(reinterpret_cast\u0026lt;int*\u0026gt;(cur), 0, memory_order_relaxed); if (cur \u0026gt;= last) break; } } } 在 linux x86 环境下，Atomic::add 的实现是通过 xaddq 加 lock 指令实现： https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp：\ntemplate\u0026lt;\u0026gt; template\u0026lt;typename D, typename I\u0026gt; inline D Atomic::PlatformAdd\u0026lt;8\u0026gt;::fetch_and_add(D volatile* dest, I add_value, atomic_memory_order order) const { STATIC_ASSERT(8 == sizeof(I)); STATIC_ASSERT(8 == sizeof(D)); D old_value; __asm__ __volatile__ (\u0026quot;lock xaddq %0,(%2)\u0026quot; : \u0026quot;=r\u0026quot; (old_value) : \u0026quot;0\u0026quot; (add_value), \u0026quot;r\u0026quot; (dest) : \u0026quot;cc\u0026quot;, \u0026quot;memory\u0026quot;); return old_value; } 同时，如果只是串行地处理这些 Atomic::add，那是非常非常慢的。我们可以将要 preTouch 的内存分成不相交的区域，然后并发的填充这些不相交的内存区域，目前最新版本的 Java 都已经在各种不同的并发 GC 中实现了并发的 PreTouch，但是历史上不同 GC 出现过对于 AlwaysPreTouch 的不同问题，这里汇总下（Plagiarism真的可恶，滚开好么）：\nParallelGC： 从 Java 16 build 21 开始，ParallelGC 才实现并发 PreTouch： Bug：https://bugs.openjdk.org/browse/JDK-8252221 Commit：https://github.com/openjdk/jdk/commit/9359ff03ae6b9e09e7defef148864f40e949b669 G1GC： 在 Java 9 build 45 之前，AlwaysPreTouch 对于 G1GC 不生效，这是一个 bug： Bug：https://bugs.openjdk.org/browse/JDK-8067469 Commit：https://github.com/openjdk/jdk/commit/f2e110fe7793b20a21f91e8ef7451814db2c8d73 从 Java 9 build 139 开始，G1GC 才实现并发 PreTouch： Bug：https://bugs.openjdk.org/browse/JDK-8157952 Commit：https://github.com/openjdk/jdk/commit/317f1aa044a8a71c52cfe733f1f4baf656c22c4c ZGC： 从 Java 14 build 26 开始，ZGC 才实现并发 PreTouch： Bug：https://bugs.openjdk.org/browse/JDK-8234543 Commit：https://github.com/openjdk/jdk/commit/5e758d2368b58ceef5092e74d481b60867b5ab93 3.11. JVM 参数 UseContainerSupport - JVM 如何感知到容器内存限制 # 在前面的章节我们分析了 JVM 自动计算堆大小限制，其中第一步就是 JVM 读取系统内存信息。在容器的环境下，JVM 也能感知到当前是容器环境，并且读取对应的内存限制。让 JVM 感知容器环境的相关 JVM 参数是 UseContainerSupport，默认值为 true，即让 JVM 感知容器的配置，相关源码：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/os/linux/globals_linux.hpp：\nproduct(bool, UseContainerSupport, true, \\ \u0026quot;Enable detection and runtime container configuration support\u0026quot;) \\ 这个配置默认开启，在开启的情况下，JVM 会通过下面的流程读取内存限制：\n可以看出，针对 Cgroup V1 与 V2 的情况，以及没有限制 pod 的 Memory limit 的情况，都考虑到了。\n3.12. SoftMaxHeapSize - 用于平滑迁移更耗内存的 GC 使用 # 由于那种完并发的 GC（目标是完全无 Stop the world 暂停或者是亚毫秒暂停的 GC），例如 ZGC ，需要在堆外使用比 G1GC 以及 ParallelGC 多的多的空间（指的就是我们后面会分析到的 Native Memory Tracking 的 GC 部分占用的内存），并且由于 ZGC 这种目前是未分代的（Java 20 之后会引入分代 ZGC），导致 GC 在堆外占用的内存会更多。所以我们一般认为，在从 G1GC，或者 ParallelGC 切换到 ZGC 的时候，就算最大堆大小等各种 JVM 参数不变，JVM 也会需要更多的物理内存。但是，在实际的生产中，修改 JVM GC 是比较简单的，修改下启动参数就行了，但是给 JVM 加内存是比较困难的，因为是实际要消耗的资源。如果不修改 JVM 内存限制参数，也不加可用内存，线上可能会在换 GC 后经常出现被 OOMkiller 干掉的情况，还有剽窃狗被干掉了。\n为了能让大家更平滑的切换 GC，以及对于线上应用，我们可能实际不一定需要用原来配置的堆大小的空间，JVM 针对 ShenandoahGC 以及 ZGC 引入了 SoftMaxHeapSize 这个参数（目前这个参数只对于这种专注于避免全局暂停的 GC 生效）。这个参数虽然默认是 0，但是如果没有指定的话，会自动设置为前文提到的 MaxHeapSize 大小。参考源码：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/shared/gc_globals.hpp\nproduct(size_t, SoftMaxHeapSize, 0, MANAGEABLE, \\ \u0026quot;Soft limit for maximum heap size (in bytes)\u0026quot;) \\ constraint(SoftMaxHeapSizeConstraintFunc,AfterMemoryInit) \\ https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/shared/gcArguments.cpp\n//如果没有设置 SoftMaxHeapSize，自动设置为前文提到的 MaxHeapSize 大小 if (FLAG_IS_DEFAULT(SoftMaxHeapSize)) { FLAG_SET_ERGO(SoftMaxHeapSize, MaxHeapSize); } ZGC 与 ShenandoahGC 的堆设计，都有软最大大小限制的概念。这个软最大大小是随着时间与 GC 表现（例如分配速率，空闲率等）不断变化的，这两个 GC 会在堆扩展到软最大大小之后，尽量就不扩展堆大小，尽量通过激进的 GC 回收空间。只有在暂停世界都完全无法回收足够内存用以分配的时候，才会尝试扩展，这之后最大限制就到了 MaxHeapSize。SoftMaxHeapSize 会给这个软最大大小一个指导值，让软最大大小不要超过这个值。\n4. JVM 元空间设计 # 4.1. 什么是元数据，为什么需要元数据 # JVM 在执行 Java 应用程序时，将加载的 Java 类的许多细节记录在内存中，这些信息称为类元数据（Class MetaData）。这些元数据对于 Java 的很多灵活的语言以及虚拟机特性都是很重要的，比如动态类加载、JIT 实时编译、反射以及动态代理等等。不同的 JVM 加载类保存的内存信息是不一样的，它们通常在更低的内存占用与更快的执行速度之间进行权衡（类似于空间还是时间的权衡）。对于 OpenJDK Hotspot 使用的则是相对丰富的元数据模型来获得尽可能快的性能（时间优先，不影响速度的情况下尽量优化空间占用）。相比于 C,C++,Go 这些离线编译为可执行二进制文件的程序相比，像 JVM 这样的托管运行时动态解释执行或者编译执行的，则需要保留更多关于正在执行的代码的运行时信息。原因如下：\n依赖类库并不是一个确定的有限集：Java 可以动态加载类，并且还有 ASM 以及 Javassist 这些工具在运行时动态定义类并加载，还有 JVMTI agent 这样的机制来动态修改类。所以，JVM 通过类元数据保存：运行时中存在哪些类，它们包含哪些方法和字段，并能够在链接加载期间动态地解析从一个类到另一个类的引用。类的链接也需要考虑类的可见性和可访问性。类元数据与类加载器相关联，同时类元数据也包括类权限和包路径以及模块信息（Java 9之后引入的模块化），以确定可访问性 JVM 解释执行或者通过 JIT 实时编译执行 Java 代码的时候需要基于类元数据的很多信息才能执行：需要知道例如类与类之间的关系，类属性以及字段还有方法结构等等等等。例如在做强制转换的时候，需要检查类型的父子类关系确定是否可以强制转换等等。 JVM 需要一些统计数据决定哪些代码解释执行那些代码是热点代码需要 JIT 即时编译执行。 Java 有反射 API 供用户使用，这就需要运行时知道所有类的各种信息。洗稿也是一种侵权行为 4.2. 什么时候用到元空间，元空间保存什么 # 4.2.1. 什么时候用到元空间，以及释放时机 # 只要发生类加载，就会用到元空间。例如我们创建一个类对象时：这个类首先会被类加载器加载，在发生类加载的时候，对应类的元数据被存入元空间。元数据分为两部分存入元空间，一部分存入了元空间的类空间另一部分存入了元空间的非类空间。堆中新建的对象的对象头中的 Klass 指针部分，指向元空间中 Klass，同时，Klass 中各种字段都是指针，实际对象的地址，可能在非类空间，例如实现方法多态以及 virtual call 的 vtable 与 itable 保存着方法代码地址的引用指针。非类空间中存储着比较大的元数据，例如常量池，字节码，JIT 编译后的代码等等。由于编译后的代码可能非常大，以及 JVM 对于多语言支持的扩展可能动态加载很多类，所以将 MetaSpace 的类空间与非类空间区分开。如图所示：\nJVM 启动参数 -XX:CompressedClassSpaceSize 指定的是压缩类空间大小，默认是 1G。-XX:MaxMetaspaceSize控制的是 MetaSpace 的总大小。这两个参数，以及 MetaSpace 更多参数，我们会在后面的章节详细解释。\n当类加载器加载的所有类都没有任何实例，并且没有任何指向这些类对象(java.lang.Class)的引用，也没有指向这个类加载器的引用的时候，如果发生了 GC，这个类加载器使用的元空间就会被释放。但是这个释放并不一定是释放回操作系统，而是被标记为可以被其他类加载器使用了。\n4.2.2. 元空间保存什么 # 元空间保存的数据，目前分为两大类：\nJava 类数据：即加载的 Java 类对应 JVM 中的 Klass 对象（Klass 是 JVM 源码中的一个 c++ 类，你可以理解为类在 JVM 中的内存形式），但是这个 Klass 对象中存储的很多数据都是指针，具体的数据存储属于非 Java 类数据，一般非 Java 类数据远比 Java 类数据占用空间大。 非 Java 类数据：即被 Klass 对象引用的一些数据，例如：类中的各种方法，注解，执行采集与统计信息等等。不要偷取他人的劳动成果，也不要浪费自己的时间和精力，让我们一起做一个有良知的写作者。 如果是 64 位的 JVM 虚拟机（从 Java 9+ 开始只有 64 位的虚拟机了）并且开启了压缩类指针(-XX:+UseCompressedClassPointers，默认是开启的)，那么元空间会被划分成两部分：\n类元空间：存储上面说的Java 类数据的空间 数据元空间：存储上面说的非 Java 类数据的空间 基于是否开启了压缩类指针分为这两部分的原因是，（剽窃抄袭侵权 ）在对象头需要保留指向 Klass 的指针，如果我们能尽量压缩这个指针的大小，那么每个对象的大小也能得到压缩，这将节省很多堆空间。在 64 位虚拟机上面，指针默认都是 64 位大小的，开启压缩类指针(-XX:+UseCompressedClassPointers，默认是开启的)之后，类指针变为 32 位大小，最多能指向 2^32 也就是 4G 的空间，如果我们能保持 Klass 所处的空间占用不超过这个限制的话，就能使用压缩类指针了。所以我们把 Klass 单独提取到一个单独的区域进行分配。Klass 占用的空间并不会太大，虽然对于 Java 中的每一个类都会有一个 Klass，但是占用空间的方法内容以及动态编译信息等等，具体数据都在数据元空间中存储，Klass 中大部分都是指针。基本上很少会遇到 32 位指针不够用的情况。\n注意，老版本中， UseCompressedClassPointers 取决于 UseCompressedOops，即压缩对象指针如果没开启，那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始， UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了，两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface（例如使用 GraalVM）。参考 JDK ISSUE：https://bugs.openjdk.java.net/browse/JDK-8241825 - Make compressed oops and compressed class pointers independent (x86_64, PPC, S390) 以及源码：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/x86/globalDefinitions_x86.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS EnableJVMCI 在 x86 CPU 上，UseCompressedClassPointers 是否依赖 UseCompressedOops 取决于是否启用了 JVMCI，默认使用的 JVM 发布版，EnableJVMCI 都是 false https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/arm/globalDefinitions_arm.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 ARM CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 PPC CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/s390/globalDefinitions_s390.hpp：#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false 在 S390 CPU 上，UseCompressedClassPointers 不依赖 UseCompressedOops 在元空间分配的对象，都是调用 Metaspace::allocate 从元空间分配空间。调用这个方法的是 MetaspaceObj 的构造函数，对应源码：https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/memory/allocation.cpp\nvoid* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, MetaspaceObj::Type type, TRAPS) throw() { // Klass has its own operator new return Metaspace::allocate(loader_data, word_size, type, THREAD); }//你以为我想这样么？主要是抄袭狗太多 void* MetaspaceObj::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, MetaspaceObj::Type type) throw() { assert(!Thread::current()-\u0026gt;is_Java_thread(), \u0026quot;only allowed by non-Java thread\u0026quot;); return Metaspace::allocate(loader_data, word_size, type); } MetaspaceObj 的 Operator new 方法定义了从 MetaSpace 上分配内存，即所有 MetaspaceObj 的子类，只要没有明确覆盖从其他地方分配，就会从 MetaSpace 分配内存。MetaspaceObj 的子类包括：\n位于类元空间的：\nKlass：其实就是 Java 类的实例（每个 Java 的 class 有一个对应的对象实例，用来反射访问，这个就是那个对象实例），即 Java 对象头的类型指针指向的实例： InstanceKlass：普通对象类的 Klass： InstanceRefKlass：java.lang.ref.Reference 类以及子类对应的 Klass InstanceClassLoaderKlass：Java 类加载器对应的 Klass InstanceMirrorKlass：java.lang.Class 对应的 Klass ArrayKlass：Java 数组对应的 Klass ObjArrayKlass：普通对象数组对应的 Klass TypeArrayKlass：原始类型数组对应的 Klass 位于数据元空间的：\nSymbol：符号常量，即类中所有的符号字符串，例如类名称，方法名称，方法定义等等。 ConstantPool：运行时常量池，数据来自于类文件中的常量池。 ConstanPoolCache：运行时常量池缓存，用于加速常量池访问 ConstMethod：类文件中的方法解析后，静态信息放入 ConstMethod，这部分信息可以理解为是不变的，例如字节码，行号，方法异常表，本地变量表，参数表等等。 MethodCounters：方法的计数器相关数据。 MethodData：方法数据采集，动态编译相关数据。例如某个方法需要采集一些指标，决定是否采用 C1 C2 动态编译优化性能。 Method：Java 方法，包含以上 ConstMethod，MethodCounters，MethodData 的指针以及一些额外数据。 RecordComponent：对应 Java 14 新特性 Record，即从 Record 中解析出的关键信息。 以上这类型，我们在下一个系列全网最硬核 JVM 元空间解析中再详细说明。\n4.3. 元空间的核心概念与设计 # 4.3.1. 元空间的整体配置以及相关参数 # 元空间配置相关的参数：\nMetaspaceSize：初始元空间大小，也是最小元空间大小。后面元空间大小伸缩的时候，不会小于这个大小。默认是 21M。抄袭剽窃侵权 滚 MaxMetaspaceSize：最大元空间大小，默认是无符号 int 最大值。 MinMetaspaceExpansion：每次元空间大小伸缩的时候，至少改变的大小。默认是 256K。后文讲到元空间内存大小限制的时候会详细分析。 MaxMetaspaceExpansion：每次元空间大小伸缩的时候，最多改变的大小。默认是 4M。后文讲到元空间内存大小限制的时候会详细分析。 MaxMetaspaceFreeRatio：最大元空间空闲比例，默认是 70，即 70%。后文讲到元空间内存大小限制的时候会详细分析。 MinMetaspaceFreeRatio：最小元空间空闲比例，默认是 40，即 40%。后文讲到元空间内存大小限制的时候会详细分析。 UseCompressedClassPointers：前文提到过，是否开启压缩类指针。默认是开启的。老版本中， UseCompressedClassPointers 取决于 UseCompressedOops，即压缩对象指针如果没开启，那么压缩类指针也无法开启。但是从 Java 15 Build 23 开始， UseCompressedClassPointers 已经不再依赖 UseCompressedOops 了，两者在大部分情况下已经独立开来。除非在 x86 的 CPU 上面启用 JVM Compiler Interface（例如使用 GraalVM）。参考 JDK ISSUE：https://bugs.openjdk.java.net/browse/JDK-8241825 - Make compressed oops and compressed class pointers independent (x86_64, PPC, S390) CompressedClassSpaceSize：如果启用了压缩类指针，则元空间会分为类元空间和数据元空间，否则只有数据元空间。这个参数限制类元空间的大小，范围是 1M ~ 3G。默认大小是 1G，如果指定了 MaxMetaspaceSize，那么为 1G 与 MaxMetaspaceSize * 0.8 中比较小的那个值， CompressedClassSpaceBaseAddress：类元空间起始虚拟内存地址，这个一般不指定。作用和前文分析堆内存的堆起始位置的作用差不多。 MetaspaceReclaimPolicy：可以为：balanced, aggressive, 以及 none，需要注意一点的是 none 要被移除了（https://bugs.openjdk.org/browse/JDK-8302385）。默认是 balanced。具体主要是影响元空间底层相关的配置，下面我们会详细分析。 元空间底层相关的配置包括：\ncommit 粒度 - commit_granule：通过第二章的分析我们知道，JVM 的空间一般是先 reserve， 之后 commit 之前 reserve 的空间的一部分，然后才能使用的。这个 commit 粒度代表元空间中 commit 内存的最小粒度，元空间在扩容缩容的时候最小的大小单位是 commit 粒度。 虚拟内存空间节点内存大小 - virtual_space_node_default_word_size：这是后文我们会详细分析的 VirtualSpaceNode 的虚拟内存大小。大小在 64 位环境下是 64 MB。 虚拟内存空间节点内存对齐 - virtual_space_node_reserve_alignment_words：这是后文我们会详细分析的 VirtualSpaceNode 的虚拟内存大小需要对齐的大小，即整体大小需要大于这个对齐大小并且是这个对齐大小整数倍。这个大小就是 MetaChunk 的最大大小，即 4MB。 当前 MetaChunk 不足以分配的时候，是否尝试扩容当前 MetaChunk - enlarge_chunks_in_place：这个参数在正式 JVM 中是 true，并且不能修改。后文我们会详细分析什么是 MetaChunk。这里简单理解就是，元空间整体使用了和 Linux 伙伴分配算法类似的设计与抽象，其中内存分配的单元就是 Chunk，元空间中对应的就是 MetaChunk。 分配新的 MetaChunk 的时候，是否一下子 commit MetaChunk 所有的内存 - new_chunks_are_fully_committed：后文我们会详细分析什么是 MetaChunk。 在 MetaChunk 整个空间都没有使用的时候，是否将 MetaChunk 的内存全部释放回操作系统 - uncommit_free_chunks：后文我们会详细分析什么是 MetaChunk。 从 Java 16 开始，引入了弹性元空间。老的元空间由于设计上分配粒度比较大，并且没有很好地释放空间的策略设计，所以占用可能比较大。Java 16 开始，JEP 387: Elastic Metaspace 引入了弹性元空间的设计，也是我们这里要讨论的设计。这个弹性元空间也引入了一个重要的参数 -XX:MetaspaceReclaimPolicy。\nMetaspaceReclaimPolicy：可以为：balanced, aggressive, 以及 none，需要注意一点的是 none 要被移除了（https://bugs.openjdk.org/browse/JDK-8302385），这三个配置具体影响是：\n4.3.2. 元空间上下文 MetaspaceContext # MetaspaceContext 本身直接原生堆上面分配，Native Memory Tracking 中属于 Metaspace 那一类别，即元空间的抽象类占用的空间。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/metaspaceContext.hpp\nclass MetaspaceContext : public CHeapObj\u0026lt;mtMetaspace\u0026gt; JVM 元空间，会在全局建立两个元空间上下文（MetaspaceContext），一个用于类元空间（我们后面称为类元空间 MetaspaceContext），一个用于数据元空间（我们后面称为数据元空间 MetaspaceContext）。当然，在没有启用压缩类指针的时候，只会初始化一个数据元空间 MetaspaceContext，不会初始化类元空间 MetaspaceContext，之后使用分配的时候，也只会用数据元空间 MetaspaceContext 进行分配。但是我们在后面讨论的时候，只会讨论开启压缩类指针的情况，因为这是默认并且常用的情况。\n每个 MetaspaceContext 都会对应一个独立的 VirtualSpaceList，以及一个独立的 ChunkManager。\n这个 VirtualSpaceList 中的每一个元素都是一个 VirtualSpaceNode。顾名思义，VirtualSpaceNode 是从操作系统申请内存，与元空间内存划分的抽象隔离的中间层抽象。VirtualSpaceList 负责与操作系统交互，申请或者释放内存。元空间与 VirtualSpaceList 交互，使用内存。\nChunkManager 顾名思义，是管理所有 Chunk 的内存管理器。Chunk 这个概念经常出现在各种伙伴内存管理算法框架（Buddy Allocator）中，一般指内存管理分配的最小单元，这里的 Chunk 抽象对应的就是 MetaChunk。ChunkManager 从 VirtualSpaceList 上面获取一块连续比较大的内存的 MetaChunk（其实是 RootMetaChunk），然后将这个 RootMetaChunk 按照分配需求，连续对半分割成需要的大小，返回这个合适大小的 MetaChunk，剩下的分割出来的 MetaChunk 进入 FreeChunkListVector 用于下次分配 MetaChunk 的时候，直接返回合适的，就不再从 VirtualSpaceList 获取了。\n我们接下来仔细分析 VirtualSpaceList 与 ChunkManager\n4.3.3. 虚拟内存空间节点列表 VirtualSpaceList # VirtualSpaceList 本身直接原生堆上面分配，Native Memory Tracking 中属于 Class 那一类别，即元空间的加载类占用的空间。其实本人感觉这么设计不太合理，应该和 MetaspaceContext 属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode 上面标记的内存分配的，这是下一小节要分析的内容。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/virtualSpaceList.hpp\nclass VirtualSpaceList : public CHeapObj\u0026lt;mtClass\u0026gt; 首先提一点，类元空间 MetaspaceContext 与数据元空间 MetaspaceContext 略有不同：类元空间 MetaspaceContext 的 VirtualSpaceList 是不可以扩展申请新的内存的，但是数据元空间 MetaspaceContext 的 VirtualSpaceList 是可以的。也就是说：类元空间 MetaspaceContext 的 VirtualSpaceList 其实只有一个 VirtualSpaceNode，但是数据元空间 MetaspaceContext 的 VirtualSpaceList 是一个包含多个 VirtualSpaceNode 的列表。\n4.3.4. 虚拟内存空间节点 VirtualSpaceNode 与 CompressedClassSpaceSize # VirtualSpaceNode 本身直接原生堆上面分配，Native Memory Tracking 中属于 Class 那一类别，即元空间的加载类占用的空间。其实本人感觉这么设计不太合理，应该和 MetaspaceContext 属于同一个类别才比较合理。真正分配加载的类的占用空间的是从 VirtualSpaceNode 上面标记的内存地址分配的，VirtualSpaceNode 本身的空间占用只是起到描述记录作用，应该也属于元空间描述的那一类。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/virtualSpaceNode.hpp\nclass VirtualSpaceNode : public CHeapObj\u0026lt;mtClass\u0026gt; VirtualSpaceNode 是一块连续的虚拟内存空间内存的抽象。类元空间的 VirtualSpaceList 只包含一个 VirtualSpaceNode，大小是前文提到的 CompressedClassSpaceSize。\n数据元空间并不像类元空间或者堆内存那样，一下子 reserve 最大堆内存限制的内存，而是每次 reserve VirtualSpaceNode 大小。VirtualSpaceNode 大小在 64 位环境下是 64 MB：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/metaspaceSettings.hpp\nstatic const size_t _virtual_space_node_default_word_size = chunklevel::MAX_CHUNK_WORD_SIZE * NOT_LP64(2) LP64_ONLY(16); // 8MB (32-bit) / 64MB (64-bit) VirtualSpaceNode 通过两个数据结构来管理它维护的虚拟内存空间：\nCommitMask：实际是一个位图，用于维护哪些内存被 commit 了，哪些没有，位图的标记的单位就是前文提到的 commit_granule（commit 粒度）。 RootChunkAreaLUT：用于维护每个 RootMetaChunk 的内存分布。至于什么是 RootMetaChunk 在后续我们讲 MetaChunk 的时候会详细讲解。 4.3.5. MetaChunk # MetaChunk 是元空间内存分配的核心抽象，其本质就是描述一块连续的虚拟内存空间。MetaChunk 本身只是一个描述对象，它也是直接原生堆上面分配，Native Memory Tracking 中属于 Metaspace 那一类别，即元空间的抽象类占用的空间。这个描述对象是池化的，参考后面会分析的 ChunkHeaderPool。不要偷取他人的劳动成果！\n元空间的任意分配，都是在某个 MetaChunk 上进行的(不要偷取他人的劳动成果！)。MetaChunk 有级别的概念，即 ChunkLevel，每个 MetaChunk 都有自己的 ChunkLevel，这个 ChunkLevel 主要代表了 MetaChunk 描述的内存空间的大小，每一个 level 都是下一个 level 大小的 2 倍：\nChunkLevel Size ChunkLevel Size ChunkLevel Size 0 4MB 4 256KB 8 16KB 1 2MB 5 128KB 9 8KB 2 1MB 6 64KB 10 4KB 3 512KB 7 32KB 11 2KB 12 1KB 从 VirtualSpaceNode 上直接划分的 MetaChunk 是 RootMetaChunk，它的 ChunkLevel 为最高级别的 0，大小是 4MB，并且其中的内存只是 reserve 还没有 commit 的。\nMetaChunk有三个状态：\nDead：即 MetaChunk 只是对象被创建出来，但是没有关联描述实际的虚拟内存。后面我们会知道，MetaChunk 是池化可回收在利用的，MetaChunk 的池就是 ChunkHeaderPool。位于 ChunkHeaderPool 都还没有关联描述实际的虚拟内存，状态为 Dead。 Free：即 MetaChunk 关联描述了实际的虚拟内存，但是没有被实际使用。此时，这个 MetaChunk 位于 ChunkManager 管理。 InUse：即 MetaChunk 关联描述了实际的虚拟内存，也被实际使用了，此时，MetaChunkArena 管理这个 MetaChunk 上面的内存分配。 4.3.5.1. ChunkHeaderPool 池化 MetaChunk 对象 # MetaChunk 实际上只是一块连续的虚拟内存空间的描述类(不要偷取他人的劳动成果！)，即元数据类。由于类加载需要的大小不一，并且还经常会发生合并，切分等等，MetaChunk 可能有很多很多，元空间为了节省这个元数据类占用的空间，将其池化，回收再利用。这个池就是 ChunkHeaderPool。例如，从 VirtualSpaceNode 上直接划分 RootMetaChunk 的内存空间，会从 ChunkHeaderPool 申请一个 MetaChunk 用于描述。当两个 MetaChunk 的空间需要合并成一个的时候，其中一个 MetaChunk 其实就没有用了，会放回 ChunkHeaderPool，而不是直接 free 掉这个对象。\nChunkHeaderPool 本身直接原生堆上面分配，Native Memory Tracking 中属于 Metaspace 那一类别，即元空间的抽象类占用的空间。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/chunkHeaderPool.hpp\nclass ChunkHeaderPool : public CHeapObj\u0026lt;mtMetaspace\u0026gt; 其实从这里我们可以推测出，MetaChunk 本身也是直接原生堆上面分配，Native Memory Tracking 中也是属于 Metaspace 那一类别。\nChunkHeaderPool 的结构是：\n其实 ChunkHeaderPool 的机制很简单：\n申请 MetaChunk 用于描述内存： 首先查看 _freelist，是否有之前放回的 MetaChunk 可以使用，如果有，就返回那个 MetaChunk，并从 _freelist 移除这个 MetaChunk 如果没有，读取 _current_slab 指向的 Slab，Slab 核心就是一个预分配好的 MetaChunk 数组（大小是 128），_top 指的是当前使用到数组的哪一个。 如果 _top 没有到 128，返回 _top 代表的 MetaChunk，并将 _top 加 1。 如果 _top 到 128，创建新的 Slab，_current_slab 指向这个新的 Slab 回收 MetaChunk：放入 _freelist 4.3.5.2. ChunkManager 管理空闲的 MetaChunk # ChunkManager 本身直接原生堆上面分配，Native Memory Tracking 中属于 Metaspace 那一类别，即元空间的抽象类占用的空间。不要偷取他人的劳动成果！\nclass ChunkManager : public CHeapObj\u0026lt;mtMetaspace\u0026gt; https://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/metaspace/chunkManager.hpp\nChunkManager 管理已经关联内存但是还没使用（状态是 Free）的 MetaChunk。在第一次从 VirtualSpaceNode 上面分配 RootMetaChunk 的内存的时候，根据申请的内存大小，决定要将 RootMetaChunk 拆分到某个 ChunkLevel 大小之后用于当前分配，拆分出来的其他的 MetaChunk 还没有使用，先放入一个类似于之前 ChunkHeaderPool 里面的 _free_list 的结构，用于下次申请 MetaChunk 用于分配的时候，先从这个里面找，找不到之后再从 VirtualSpaceNode 上面尝试分配新的 RootMetaChunk。不要惯着cao袭的人！\nChunkManager 的整体结构是：\nChunkManager 主要维护一个 FreeChunkListVector，FreeChunkListVector 里面是一个 FreeChunkList 数组（还有xigao dog 的码）。FreeChunkList 是一个 MetaChunk 链表，链表中都是 Free 的 MetaChunk，同样 ChunkLevel 的 MetaChunk 位于同一个 FreeChunkList 中。FreeChunkList 数组以 ChunkLevel 为下标，这样的数据结构可以快速找到一个所需 ChunkLevel 的 MetaChunk。FreeChunkList这个链表其实是一个双向链表，包含头尾两个指针，如果一个 MetaChunk 管理的内存被 commit 了，就会放在链表头部，没有 commit 的放在链表尾部。\nMetaChunk 具体的分配，切分，合并流程，我们会在介绍完 MetaspaceArena 之后详细分析。但是，MetaspaceArena 和 ChunkManager 不一样，ChunkManager 是全局两个，一个属于类元空间，一个属于数据元空间，倘若没有开启压缩类指针，那么就只有一个数据元空间 ChunkManager，而 MetaspaceArena 我们后面会看到是每个 ClassLoader 独立私有的。所以，在讲 MetaspaceArena 之前，我们先要从另一个角度即 ClassLoader 加载类的角度出发，向下一层一层剖析到 MetaspaceArena。\n4.3.6. 类加载的入口 SystemDictionary 与保留所有 ClassLoaderData 的 ClassLoaderDataGraph # 类加载的入口在全局唯一的 SystemDictionary 中，这里我们只是为了看一下类加载需要哪些参数，来搞清楚对应关系，不用关心细节，入口代码是：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp\nInstanceKlass* SystemDictionary::resolve_from_stream(ClassFileStream* st, Symbol* class_name, Handle class_loader, const ClassLoadInfo\u0026amp; cl_info, TRAPS) { //隐藏类与普通类的加载方式不同，隐藏类是 JEP 371: Hidden Classes 引入的，Java 15 中发布的新特性 if (cl_info.is_hidden()) { return resolve_hidden_class_from_stream(st, class_name, class_loader, cl_info, CHECK_NULL); } else { return resolve_class_from_stream(st, class_name, class_loader, cl_info, CHECK_NULL); } } 可以看到，加载类需要以下参数：\nClassFileStream* st：类文件流 Symbol* class_name：加载的类的名称 Handle class_loader：是哪个类加载器 const ClassLoadInfo\u0026amp; cl_info：类加载器信息 在加载类的时候，SystemDictionary 会获取类加载器的 ClassLoaderData，ClassLoaderData 是每个类加载器私有的。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/systemDictionary.cpp\n//通过类加载器获取对应的 `ClassLoaderData` ClassLoaderData* SystemDictionary::register_loader(Handle class_loader, bool create_mirror_cld) { if (create_mirror_cld) { return ClassLoaderDataGraph::add(class_loader, true); } else { // 如果是 null，代表是 BootstrapClassLoader，使用全局的 BootstrapClassLoader 对应的 ClassLoaderData return (class_loader() == NULL) ? ClassLoaderData::the_null_class_loader_data() : //否则，从 ClassLoaderDataGraph 寻找或者创建 class_loader 对应的 ClassLoaderData ClassLoaderDataGraph::find_or_create(class_loader); } } ClassLoaderDataGraph 保存着所有的 ClassLoaderData，这个主要用来遍历每个类加载器，以及获取每个类加载器加载的类的信息，还有遍历类加载器加载的类，例如 jcmd 命令中的 VM.classloaders 以及 VM.classloader_stats 就是这么实现的。但是，我们就不纠结于 ClassLoaderDataGraph 的细节了，这不是咱们的重点。\n4.3.7. 每个类加载器私有的 ClassLoaderData 以及 ClassLoaderMetaspace # ClassLoaderData 本身直接原生堆上面分配，Native Memory Tracking 中属于 Class 那一类别，即元空间的加载类占用的空间。这就很合理了，不加载类就不会有 ClassLoaderData。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp\nclass ClassLoaderData : public CHeapObj\u0026lt;mtClass\u0026gt; 如前所述，ClassLoaderData 是每个类加载器私有的。ClassLoaderData 包含的元素众多，我们这里只关心它其中与元空间内存分配相关的，即 ClassLoaderMetaspace：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/classfile/classLoaderData.hpp\nClassLoaderMetaspace * volatile _metaspace; ClassLoaderMetaspace 本身直接原生堆上面分配，Native Memory Tracking 中属于 Class 那一类别，即元空间的加载类占用的空间。\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B11/src/hotspot/share/memory/classLoaderMetaspace.hpp\nclass ClassLoaderMetaspace : public CHeapObj\u0026lt;mtClass\u0026gt; ClassLoaderMetaspace 有不同的类型（MetaspaceType）：\nMetaspaceType::StandardMetaspaceType：平台类加载器（Platform ClassLoader，Java 9 之前叫做 ext ClassLoader）以及应用类加载器（Application ClassLoader）的 ClassLoaderMetaspace MetaspaceType::BootMetaspaceType：即根类加载器（Boostrap ClassLoader）的 ClassLoaderMetaspace MetaspaceType::ClassMirrorHolderMetaspaceType：加载匿名类的类加载器的 ClassLoaderMetaspace MetaspaceType::ReflectionMetaspaceType：反射调用的前几次通过 jni native 调用，超过一定次数会优化成生成字节码类调用。加载这些字节码类的类加载器是 jdk.internal.reflect.DelegatingClassLoader，这个类加载器的 ClassLoaderMetaspace 类型就是 ReflectionMetaspaceType。 ClassLoaderMetaspace 和 MetaspaceContext 类似，如果压缩类指针开启，那么 ClassLoaderMetaspace 包含一个类元空间的 MetaspaceArena 和一个数据元空间的 MetaspaceArena，否则只有一个数据元空间的 MetaspaceArena。\n4.3.8. 管理正在使用的 MetaChunk 的 MetaspaceArena # MetaspaceArena 本身直接原生堆上面分配，Native Memory Tracking 中属于 Class 那一类别，即元空间的加载类占用的空间。这也是肯定的，因为跟着类加载器存在\nclass MetaspaceArena : public CHeapObj\u0026lt;mtClass\u0026gt; MetaspaceArena 结构如下所示：\nMetaspaceArena 包含：\n一个 MetachunkList：管理在该 MetaspaceArena 分配的 MetaChunk 的列表，列表的第一个是当前分配内存的 MetaChunk。 当前 MetaspaceArena 的 ArenaGrowthPolicy：在当前分配内存的 MetaChunk 不够分配的时候，申请新的 MetaChunk 的大小。 Freeblocks： 在当前分配内存的 MetaChunk 不够分配的时候，需要分配新的 MetaChunk。当前的 MetaChunk 剩余空间放入 Freeblocks。 Freeblocks 包含一个 BinList32 和一个 BlockTree。大小大于 33 字节的进入 BlockTree，否则进入 BinList32。\nBinList32 类似于 FreeChunkListVector，是一个链表的数组，同样大小的内存在同一数组下标的链表。\nBlockTree 是一个在 Binary Search Tree（BST）的基础上，同样内存的节点在二叉树节点的后面形成链表的数据结构。\n不同的类加载器类型的类元空间的 MetaspaceArena 与数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy 不同：\n1.根类加载器（Boostrap ClassLoader）的 ClassLoaderMetaspace 类元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList每次增长都是申请大小为 256K 的 MetaChunk\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_boot_class[] = { chunklevel::CHUNK_LEVEL_256K // .. repeat last }; 2.根类加载器（Boostrap ClassLoader）的 ClassLoaderMetaspace 数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 的第一个 MetaChunk 大小为 4M，之后每个新 MetaChunk 都是 1M：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_boot_non_class[] = { chunklevel::CHUNK_LEVEL_4M, chunklevel::CHUNK_LEVEL_1M // .. repeat last }; 3.平台类加载器（Platform ClassLoader，Java 9 之前叫做 ext ClassLoader）以及应用类加载器（Application ClassLoader）的 ClassLoaderMetaspace 类元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 的第一个 MetaChunk 大小为 2K，第二个也是 2K，第三个 4K，第四个为 8K，之后每个新 MetaChunk 都是 16K（不要惯着cao袭的人！）：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_standard_class[] = { chunklevel::CHUNK_LEVEL_2K, chunklevel::CHUNK_LEVEL_2K, chunklevel::CHUNK_LEVEL_4K, chunklevel::CHUNK_LEVEL_8K, chunklevel::CHUNK_LEVEL_16K // .. repeat last }; 4.平台类加载器（Platform ClassLoader，Java 9 之前叫做 ext ClassLoader）以及应用类加载器（Application ClassLoader）的 ClassLoaderMetaspace 数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 的第一个 MetaChunk 大小为 4K，第二个也是 4K，第三个 4K，第四个为 8K，之后每个新 MetaChunk 都是 16K：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_standard_non_class[] = { chunklevel::CHUNK_LEVEL_4K, chunklevel::CHUNK_LEVEL_4K, chunklevel::CHUNK_LEVEL_4K, chunklevel::CHUNK_LEVEL_8K, chunklevel::CHUNK_LEVEL_16K // .. repeat last }; 5.加载匿名类的类加载器的 ClassLoaderMetaspace 类元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 每次增长都是申请大小为 1K 的 MetaChunk：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_anon_class[] = { chunklevel::CHUNK_LEVEL_1K, // .. repeat last }; 6.加载匿名类的类加载器的 ClassLoaderMetaspace 数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 每次增长都是申请大小为 1K 的 MetaChunk：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_anon_non_class[] = { chunklevel::CHUNK_LEVEL_1K, // .. repeat last }; 7.DelegatingClassLoader 的 ClassLoaderMetaspace 类元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 每次增长都是申请大小为 1K 的 MetaChunk：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_refl_class[] = { chunklevel::CHUNK_LEVEL_1K, // .. repeat last }; 8.DelegatingClassLoader 的 ClassLoaderMetaspace 数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 的第一个 MetaChunk 大小为 2K，之后每个新 MetaChunk 都是 1K：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArenaGrowthPolicy.cpp\nstatic const chunklevel_t g_sequ_refl_non_class[] = { chunklevel::CHUNK_LEVEL_2K, chunklevel::CHUNK_LEVEL_1K // .. repeat last }; 4.3.9. 元空间内存分配流程 # 我们过一下元空间内存分配流程，我们会忽略一些 GC 相关的还有并发安全的细节，否则涉及的概念太多，一下说不过来，这些细节，会在以后的系列中详细提到。\n4.3.9.1. 类加载器到 MetaSpaceArena 的流程 # 当类加载器加载类的时候，需要从对应的 ClassLoaderMetaspace 分配元空间进行存储。这个过程大概是：\n图中有蓝色填充的方块是我们要重点分析的流程，我们先从从 MetaChunkArena 普通分配开始分析，尝试 GC 以及扩容元空间用于分配会涉及到元空间大小限制以及 GC 界限的概念，我们后面分析。这里对应的源码是：https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace.cpp#L899\n整个流程如下：\n首先，验证要分配的内存小于最大 MetaChunk 大小，即 RootMetaChunk 大小，即 ChunkLevel = 0 的大小，即 4MB 然后，走普通分配流程，判断当前分配的数据类型是类元空间的还是数据元空间的，分别到类元空间的 MetaSpaceArena 或者数据源空间 MetaSpaceArena 进行分配。这是下一节我们要详细分析的。 如果普通分配失败，那么会触发 jdk.MetaspaceAllocationFailure 这个 JFR 事件，大家可以监控这个事件，去调整元空间大小减少由于元空间不足触发的 GC。触发之后，抄袭狗死全家 之后，尝试 GC，以及增大元空间的 GC 界限（元空间有最大大小限制，但是还有动态计算的 GC 界限，超过 GC 界限的话，第二步的普通分配也会失败）用于分配。这个流程我们后面会详细分析 最后，如果这样还是分配失败，那么就会抛出大名鼎鼎的 java.lang.OutOfMemoryError, 触发 jdk.MetaspaceOOM 这个 JFR 事件，这个我们也会详细分析。 我们先分析第二步的普通分配流程，其他的需要后续我们分析元空间大小限制的时候详细分析。\n4.3.9.2. 从 MetaChunkArena 普通分配 - 整体流程 # 从 MetaChunkArena 普通分配的流程并不太复杂：\n我们前面讲过 MetaspaceArena 的结构，如下所示：\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L222，结合流程图，我们可以整理出一个简单的分配思路：\n首先，尝试从 FreeBlocks 中分配（第一次分配，肯定 FreeBlocks 里面没有可以分配的，直接进入下一分支），如果分配成功，直接返回 然后，尝试从 current chunk 分配（第一次分配肯定 current chunk 为 NULL，直接进入下一分支）： current chunk 如果有足够的空间，并且这些空间是 committed 的或者可以 commit 成功，那么从 current chunk 分配。 current chunk 如果没有足够的空间，尝试 current chunk 扩容，如果扩容成功，并且这些空间是 committed 的或者可以 commit 成功，那么从 current chunk 分配。 如果扩容失败，或者 commit 失败，直接进入下一分支 前面分配失败，尝试从 ChunkManager 中申请一个新的 MetaChunk。 如果申请成功，则加入当前的 MetaChunkList 中，将 current chunk 指向新的 MetaChunk，老的 current chunk 的剩余空间回收到 FreeBlocks 中，然后从新的 current chunk 分配。 我们接下来分析 FreeBlocks 回收老的 current chunk 与用于后续分配的流程\n4.3.9.3. 从 MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 与用于后续分配的流程 # 首先我们前面提到过：Freeblocks 包含一个 BinList32 和一个 BlockTree。大小大于 33 字节的进入 BlockTree，否则进入 BinList32。\nBinList32 类似于 FreeChunkListVector，是一个链表的数组，同样大小的内存在同一数组下标的链表。\nBlockTree 是一个在 Binary Search Tree（BST）的基础上，同样内存的节点在二叉树节点的后面形成链表的数据结构。\n回收的流程十分简单，就是判断下 current chunk 的剩余空间，根据大小放到不同的数据结构：\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L60：\n计算 MetaChunk 剩余的 committed 的空间（很简单，直接用 committed 的位置减去已分配到的位置） 判断剩余空间是否大于 FreeBlocks 最小内存块大小（即 BinList32 最小大小即 2KB）：对待抄袭狗不必手软 从 MetaChunk 分配剩余的 committed 的空间，放入 FreeBlocks 中 内存大于 BinList32 最大内存块大小即 33KB，放入 BlockTree，否则放入 BinList32 4.3.9.4. 从 MetaChunkArena 普通分配 - 尝试从 FreeBlocks 分配 # 尝试从 FreeBlocks 分配即从其中的 BinList32 和 BlockTree 寻找是否有合适的内存，流程是：\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/freeBlocks.cpp#L42\n首先判断，要分配的内存大小是否大于 BinList32 最大内存块大小即 33KB：如果大于，就从 BlockTree 查找不小于内存大小的最接近的内存块；如果不大于，就从 BinList32 查找是否有对应大小的内存块。 如果找到了，计算 waste，waste = 内存块大小 - 要分配的内存大小。 判断 waste 大于 FreeBlocks 最小内存块大小（即 BinList32 最小大小即 2KB）。如果大于，则要回收，和前面回收 MetaChunk 的流程一样将剩余的内存放回 FreeBlocks。 4.3.9.5. 从 MetaChunkArena 普通分配 - 尝试扩容 current chunk # 对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/metaspaceArena.cpp#L171\nenlarge_chunks_in_place 是否是 true，不是的话直接结束，不过前面我们说过，目前 JVM 是代码里写死的 true 判断是否 current chunk 已经是 RootMetaChunk（代表已经不能扩容了），如果是，直接结束 current chunk 已使用大小加上要分配的内存大小是否大于 RootMetaChunk 的大小即 4MB（代表已经不能扩容了），如果是，直接结束 找到大于 current chunk 已使用大小，加上要分配的内存大小的最接近的 ChunkLevel （记为 new_level） 判断 new_level 是否小于 current chunk 的 ChunkLevel 减 1，代表要扩容到的大小大于原始大小的 2 倍以上(不允许一下子扩容两倍以上)，如果是，直接结束 current chunk 是否是 leader（这个概念后面分析到使用 ChunkManager 分配新的 MetaChunk 会提到），只有 leader 可以扩容，如果不是，直接结束（xigao 必死） 判断扩容策略中申请下一个 MetaChunk 的 ChunkLevel 是否大于 current chunk 的（代表新申请的比当前的小），如果是，也直接结束。我们这里强调下为啥扩容策略（ArenaGrowthPolicy）中申请下一个 MetaChunk 的 ChunkLevel 大于 current chunk（代表新申请的比当前的小）的话，我们就不扩容了。前面我们列出了各种类型的 ClassLoader 的不同空间的扩容策略，例如DelegatingClassLoader 的 ClassLoaderMetaspace 数据元空间的 MetaspaceArena 的 ArenaGrowthPolicy：MetachunkList 的第一个 MetaChunk 大小为 2K，之后每个新 MetaChunk 都是 1K。假设 current chunk 是第一个，这里下一个 MetaChunk 的 ChunkLevel 是 1K 对应的 ChunkLevel，大于 current chunk 当前的 ChunkLevel，所以优先申请新的，而不是扩容。之后到第二个之后，由于之后每个新的 MetaChunk 都是 1K，就会尝试扩容而不是申请新的了。 使用 ChunkManager 尝试扩容 current chunk 到 new_level。具体扩容流程，后面会分析。 4.3.9.6. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk # 回顾下 ChunkManager 结构：\n从 ChunkManager 分配新的 MetaChunk，首先会从 FreeChunkListVector 尝试搜索有没有合适的。FreeChunkListVector 如我们之前所述，是一个以 ChunkLevel 为下标的数组，每个数组都是一个 MetaChunk 的链表。commit 多的 MetaChunk 放在链表开头，完全没有 commit 的放在链表末尾。\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B12/src/hotspot/share/memory/metaspace/chunkManager.cpp#L137\n计算两个值：max_level = 大于当前申请内存大小最接近的 ChunkLevel （即新的 MetaChunk 最小多大）, preferred_level = \u0026quot;根据扩容策略（ArenaGrowthPolicy）下一个 MetaChunk 多大\u0026quot; 与 \u0026quot;max_level\u0026quot; 中小的那个值（也就是更大的 MetaChunk 大小） 优先搜索并使用 FreeChunkListVector 中那些已经 commit 足够内存的 MetaChunk 正序遍历（即 ChunkLevel 从小到大，大小从大到小） ChunkManager 的 FreeChunkListVector 里面的数组 (从 preferred_level 到 max_level 与 preferred_level + 2 中比较小的值，即最多搜索 3 个 ChunkLevel，根据前面的分析我们知道 ChunkLevel 就是数组下标)，寻找对应的 MetaChunk 链表，正序遍历每个链表（我们前面提到过，commit 多的 MetaChunk 放在开头），直到找到 commit 大小大于申请内存大小的（chaoxi 死的更惨） 逆序遍历（即 ChunkLevel 从大到小，大小从小到大） ChunkManager 的 FreeChunkListVector 里面的数组 (从 preferred_level 到最大的 ChunkLevel，即 RootMetaChunk 的大小，即 4MB)，寻找对应的 MetaChunk 链表，正序遍历每个链表（我们前面提到过，commit 多的 MetaChunk 放在开头），直到找到 commit 大小大于申请内存大小的 正序遍历（即 ChunkLevel 从小到大，大小从大到小） ChunkManager 的 FreeChunkListVector 里面的数组 (从 preferred_level 到 max_level)，寻找对应的 MetaChunk 链表，正序遍历每个链表（我们前面提到过，commit 多的 MetaChunk 放在开头），直到找到 commit 大小大于申请内存大小的 如果搜索不到已经 commit 足够内存的 MetaChunk，就退而求其次，寻找 FreeChunkListVector 存在的 MetaChunk 正序遍历（即 ChunkLevel 从小到大，大小从大到小） ChunkManager 的 FreeChunkListVector 里面的数组 (从 preferred_level 到 max_level)，寻找对应的 MetaChunk 链表，正序遍历每个链表，直到找到一个 MetaChunk 逆序遍历（即 ChunkLevel 从大到小，大小从小到大） ChunkManager 的 FreeChunkListVector 里面的数组 (从 preferred_level 到最大的 ChunkLevel，即 RootMetaChunk 的大小，即 4MB)，寻找对应的 MetaChunk 链表，正序遍历每个链表，直到找到一个 MetaChunk 如果前面没有找到合适的，从 VirtualSpaceList 申请新的 RootMetaChunk 将 RootMetahChunk 分割成需要的 ChunkLevel 大小，之后将分割剩余的放入 FreeChunkListVector，这个过程我们接下来会详细分析 判断 new_chunks_are_fully_committed 是否为 true，如果为 true 则 commit 整个 MetaChunk 的所有内存，否则 commit 要分配的大小。如果 commit 失败了（证明可能到达元空间 GC 界限或者元空间大小上限），那么将 MetaChunk 退回。 4.3.9.7. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 从 VirtualSpaceList 申请新的 RootMetaChunk # 对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21+13/src/hotspot/share/memory/metaspace/virtualSpaceList.cpp#L110\n首先判断当前 _first_node 是否有空间分配新的 RootMetaChunk，如果有则从 _first_node 上面分配新的 RootMetaChunk 如果没有，判断是否可以扩展新的 VirtualSpaceNode（类元空间不可以，数据元空间可以），如果可以则申请 Reserve 新的 VirtualSpaceNode 作为新的 _first_node，之后从 _first_node 上面分配新的 RootMetaChunk 4.3.9.8. 从 MetaChunkArena 普通分配 - 从 ChunkManager 分配新的 MetaChunk - 将 RootMetaChunk 切割成为需要的 MetaChunk # 这里的流程如果用流程图容易把人绕晕，我们这里举一个例子，比如我们想要一个 ChunkLevel 为 3 的 MetaChunk：\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B13/src/hotspot/share/memory/metaspace/chunkManager.cpp#L78\n将 RootMetaChunk 切割成 ChunkLevel 为 3 的 MetaChunk 的流程：\nRootMetaChunk 的 ChunkLevel 为 0，对半分成两个 ChunkLevel 为 1 的，第一个为 leader，第二个为 follower。 将上一步的 leader 对半成两个 ChunkLevel 为 2 的，第一个为 leader，第二个为 follower。 将上一步的 leader 对半成两个 ChunkLevel 为 3 的，第一个为 leader，第二个为 follower。 将第三步的 leader 返回，用于分配。将第一、二、三步生成的 follower 放入 FreeChunkListVector 用于前面 4.3.9.6 章节分析的 ChunkManager 先从 FreeChunkListVector 搜索合适的 MetaChunk 分配。 4.3.9.9. MetaChunk 回收 - 不同情况下， MetaChunk 如何放入 FreeChunkListVector # 我们前面主要分析的是分配，那么 MetaChunk 如何回收呢？从前面的流程我们很容易推测出来，其实就是放回 FreeChunkListVector。放回的流程如果用流程图容易把人绕晕，我们还是举例子区分不同情况。其实核心思路就是，放回的时候，尽量将 MetaChunk 向上合并之后放回：\n对应的源码是 https://github.com/openjdk/jdk/blob/jdk-21%2B13/src/hotspot/share/memory/metaspace/chunkManager.cpp#L255\n这里我们有两个例子：\n我们有一个 ChunkLevel 为 3 的 MetaChunk 要回收，但是它不是 leader，不能向上合并。只有 leader 才会尝试向上合并。这里直接放入 FreeChunkListVector。 我们又有一个 ChunkLevel 为 3 的 MetaChunk 要回收，它是 leader。它会尝试向上合并。查看它的 follower 是否是 Free 的。如果是 Free 的，他肯定首先在 ChunkManager 的 FreeChunkListVector 中， 从 FreeChunkListVector 取出，与这个 leader 合并为一个新的 ChunkLevel 为 2。之后，它还是 leader，尝试继续合并，但是它的 follower 不是空闲的，就不能继续合并了。在这里停止，放入 FreeChunkListVector。 4.3.10. ClassLoaderData 回收 # 在 GC 判断一个类加载器可以回收（该类加载器加载的类没有任何对象，该类加载器的对象也没有任何强引用指向它）的时候，不会立刻回收 ClassLoaderData，而是对应的 ClassLoaderData 的 is_alive() 就会返回 false。JVM 会定期遍历 ClassLoaderDataGraph 遍历每个 ClassLoaderData 判断 is_alive() 是否是 false，如果是的话会放入待回收的链表中。之后在不同 GC 的不同阶段，遍历这个链表将 ClassLoaderData 回收掉。\nClassLoaderData 被回收的过程如下所示：\nClassLoaderData 会记录所有加载的类与相关的数据（前文提到的 Klass 等等对象），所以它的析构函数中会将这些加载的数据的内存全部释放到它独有的 MetaSpaceArena 的 FreeBlocks 中，这些内存就是通过之前我们分析的流程分配的，由于之前的空间都是从 MetaspaceArena 的 MetaChunkList 中的 MetaChunk 分配的，这样的话这些 MetaChunk 的空间也都不再占用了。当然，也会把前面提到的 ClassLoaderData 独有的数据结构释放掉，还没有利用的 MetaWord 放回 ChunkManager 中。然后，清除掉它私有的 ClassLoadMetaSpace。根据前文分析我们知道 ClassLoaderMetaspace 在开启压缩类空间的情况下包括一个类元空间的 MetaspaceArena 和一个数据元空间的 MetaspaceArena。这两个 MetaspaceArena 分别要清理掉。MetaspaceArena 的析构函数会把 FreeBlocks 中的每个 MetaWord 都放回 ChunkManager，注意这里包含之前 ClassLoaderData 放回的加载类相关数据占用的空间，最后清理掉 FreeBlocks。（你洗稿的样子真丑。）\n4.4. 元空间分配与回收流程举例 # 我们前面介绍了元空间的组成元素，但是没有将他们完整的串联起来，我们这里举一个简单的例子，将之前的所有元素串联起来。\n通过前面的分析之后，我们知道元空间的主要抽象包括：\n全局唯一的类元空间 MetaspaceContext，它包括： 一个 VirtualSpaceList，类元空间的 VirtualSpaceList 只有一个 VirtualSpaceNode 一个 ChunkManager 全局唯一的数据元空间 MetaspaceContext，它包括： 一个 VirtualSpaceList，数据元空间的 VirtualSpaceList 才是一个真正的 VirtualSpaceNode 的链表 一个 ChunkManager 每个类加载器都有一个独有的 ClassLoaderData，它包含自己独有的 ClassLoaderMetaspace，ClassLoaderMetaspace 包含： 一个类元空间 MetaspaceArena 一个数据元空间 MetaspaceArena 假设我们全局只有一个类加载器，即类加载器 1，并且 UseCompressedClassPointers 为 true，那么我们可以假设当前元空间的初始结构为：\n接下来我们来看看详细的例子\n4.4.1. 首先类加载器 1 需要分配 1023 字节大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，但是这是第一次分配，肯定没有。\n4.尝试从 _current_chunk 分配，但是由于是第一次分配，_current_chunk 是 NULL。\n5.将要分配的内存（1023 字节）按照 8 字节对齐，即 1024 字节。大于等于它的最小 ChunkLevel 为 12，即 max_level = 12。假设这个类加载器是 Bootstrap ClassLoader，其实是啥无所谓，我们主要是想找一个对应的 ArenaGrowthPolicy，根据这个 ArenaGrowthPolicy，第一个要申请的 MeataChunk 大小是 256KB，对应的 ChunkLevel 为 4，preferred_level 是 max_level 与这个之间相比小的那个，即 4。我们从类元空间的 ChunkManager 申请这么大的 MetaChunk，对应的 ChunkLevel 是 4\n6.首先搜索 ChunkManager 的 FreeChunkListVector，看看是否有合适的。但是这是第一次分配，肯定没有。\n7.尝试从类元空间的 VirtualSpaceList 申请 RootMetaChunk 用于分配。\n8.从类元空间的 VirtualSpaceList 的唯一一个 VirtualSpaceNode 分配 RootMetaChunk，对半切分到 ChunkLevel 为 4 的 MetaChunk，返回 leader 的 ChunkLevel 为 4 的 MetaChunk 作为 _current_chunk 用于分配。分割出来剩下的 ChunkLevel 为 1， ChunkLevel 为 2， ChunkLevel 为 3， ChunkLevel 为 4 的各一个放入 FreeChunkListVector 中\n9.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n10.从 _current_chunk 分配内存，分配成功。\n4.4.2. 然后类加载器 1 还需要分配 1023 字节大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，目前还是没有。\n4.尝试从 _current_chunk 分配，将要分配的内存（1023 字节）按照 8 字节对齐，即 1024 字节，_current_chunk 空间足够。\n5.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n6.从 _current_chunk 分配内存，分配成功。\n4.4.3. 然后类加载器 1 需要分配 264 KB 大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，目前还是没有。\n4.尝试从 _current_chunk 分配，将要分配的内存（264KB）按照 8 字节对齐，即 264KB，_current_chunk 空间不足，但是如果扩容一倍就足够，所以尝试扩大 _current_chunk。\n5.查看他的兄弟 MetaChunk 是否是空闲的，当然是，从 FreeChunkListVector 移除这个 MetaChunk，将这个兄弟 MetaChunk 与 _current_chunk。_current_chunk 的大小变为原来 2 倍，_current_chunk 的 ChunkLevel 减 1 之后为 3。\n6.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n7.从 _current_chunk 分配内存，分配成功。\n4.4.4. 然后类加载器 1 需要分配 2 MB 大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，目前还是没有。\n4.尝试从 _current_chunk 分配，将要分配的内存（2MB）按照 8 字节对齐，即 2MB，_current_chunk 空间不足，扩容一倍也不够，所以就不尝试扩大 _current_chunk 了。\n5.要分配的大小是 2MB，大于等于它的最小 ChunkLevel 为 1，即 max_level = 1。根据 ArenaGrowthPolicy，下一个要申请的 MeataChunk 大小是 256KB，对应的 ChunkLevel 为 4，preferred_level 是 max_level 与这个之间相比小的那个，即 1。从 FreeChunkListVector 寻找，发现有合适的，将其作为 current_chunk 进行分配。\n6.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n7.之前的 current_chunk 的剩余空间大于 2 bytes，需要回收到 FreeBlocks 中。由于大于 33 bytes，需要放入 BlockTree。\n8.从 _current_chunk 分配内存，分配成功。\n4.4.5. 然后类加载器 1 需要分配 128KB 大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.将要分配的内存（128KB）按照 8 字节对齐，即 128KB。搜索 FreeBlocks 查看是否有可用空间，目前 FreeBlocks 有合适的可以分配。\n4.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n5.从 FreeBlocks 的 BlockTree 的节点分配内存，分配成功。为啥要打击抄袭，稿主被抄袭太多所以断更很久。\n4.4.6. 新来一个类加载器 2，需要分配 1023 Bytes 大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，但是这是第一次分配，肯定没有。\n4.尝试从 _current_chunk 分配，但是由于是第一次分配，_current_chunk 是 NULL。\n5.将要分配的内存（1023 字节）按照 8 字节对齐，即 1024 字节。大于等于它的最小 ChunkLevel 为 12，即 max_level = 12。假设这个类加载器是 Bootstrap ClassLoader，其实是啥无所谓，我们主要是想找一个对应的 ArenaGrowthPolicy。根据 ArenaGrowthPolicy，下一个要申请的 MeataChunk 大小是 256KB，对应的 ChunkLevel 为 4，preferred_level 是 max_level 与这个之间相比小的那个，即 4。\n6.首先搜索 ChunkManager 的 FreeChunkListVector，看看是否有合适的。搜索到之前放入的 ChunkLevel 为 3 的。将其取出作为 _current_chunk。\n7.commit 要分配的内存大小，如果 AlwaysPreTouch 是开启的，那么就会像之前我们分析 Java 堆内存那样进行 pre touch。\n8.从 _current_chunk 分配内存，分配成功。\n4.4.7. 然后类加载器 1 被 GC 回收掉 # 1.将类加载器 1 消耗的所有空间放回 FreeBlocks 中。前面分配了 1024 bytes, 1024 bytes, 264KB, 2MB 还有 128KB，这次放回 BlockTree，BlockTree 之前本身还有剩余一个 118KB。整体如图所示。\n2.这样一来，原来 MetaspaceArena 中 MetaChunkList 管理的 MetaChunk 的内存全都空闲了。\n将 MetaChunkList 管理的 MetaChunk 放回全局的 ChunkManager 的 FreeChunkListVector 中。并且放回的都是有 commit 过内存的，会放在每个 ChunkLevel 对应的 MetaChunk 链表的开头。 4.4.8. 然后类加载器 2 需要分配 1 MB 大小的内存，属于类空间 # 1~2.首先，类加载器 1 从它私有的 ClassLoaderData 去分配空间，由于要分配的是类元空间的，所以会从私有的类元空间的 MetaspaceArena 去分配空间。\n3.搜索 FreeBlocks 查看是否有可用空间，目前还是没有。为啥要打击抄袭，稿主被抄袭太多所以断更很久。\n4.尝试从 _current_chunk 分配，空间不足。并且 _current_chunk 不是 leader，所以就不尝试扩容了。\n5.将要分配的内存（1MB）按照 8 字节对齐，即 1MB。要分配的大小是 1MB，大于等于它的最小 ChunkLevel 为 2，即 max_level = 2。根据 ArenaGrowthPolicy，下一个要申请的 MeataChunk 大小是 256KB，对应的 ChunkLevel 为 4，preferred_level 是 max_level 与这个之间相比小的那个，即 2。从 FreeChunkListVector 寻找，发现有合适的，将其作为 current_chunk 进行分配。这个其实就是之前从类加载器 1 回收的。\n6.因为是之前回收的，里面的内存都是 committed 了，所以这里就不用 commit 了。\n7.之前的 current_chunk 的剩余空间大于 2 bytes，需要回收到 FreeBlocks 中。由于大于 33 bytes，需要放入 BlockTree。\n8.从 _current_chunk 分配内存，分配成功。\n4.5. 元空间大小限制与动态伸缩 # 前文我们没有提到，如何限制元空间的大小，其实就是限制 commit 的内存大小。元空间的限制不只是受限于我们的参数配置，并且前面我们提到了，元空间的内存回收也比较特殊，元空间的内存基本都是每个类加载器的 ClassLoaderData 申请并管理的，在类加载器被 GC 回收后，ClassLoaderData 管理的这些元空间也会被回收掉。所以，GC 是可能触发一部分元空间被回收了。所以元空间在设计的时候，还有一个动态限制 _capacity_until_GC，即触发 GC 的元空间占用大小。当要分配的空间导致元空间整体占用超过这个限制的时候，尝试触发 GC。这个动态限制也会在每次 GC 的时候动态扩大或者缩小。动态扩大以及缩小\n我们先回顾下之前提过的参数配置：\nMetaspaceSize：初始元空间大小，也是最小元空间大小。后面元空间大小伸缩的时候，不会小于这个大小。默认是 21M。 MaxMetaspaceSize：最大元空间大小，默认是无符号 int 最大值。 MinMetaspaceExpansion：每次元空间大小伸缩的时候，至少改变的大小。默认是 256K。 MaxMetaspaceExpansion：每次元空间大小伸缩的时候，最多改变的大小。默认是 4M。 MaxMetaspaceFreeRatio：最大元空间空闲比例，默认是 70，即 70%。 MinMetaspaceFreeRatio：最小元空间空闲比例，默认是 40，即 40%。 4.5.1. CommitLimiter 的限制元空间可以 commit 的内存大小以及限制元空间占用达到多少就开始尝试 GC # CommitLimiter 是一个全局单例，用来限制元空间可以 commit 的内存大小。每次分配元空间 commit 内存的时候，都会调用 CommitLimiter::possible_expansion_words 方法，这个方法会检查：\n当前元空间已经 commit 的内存大小加上要分配的大小是否超过了 MaxMetaspaceSize 当前元空间已经 commit 的内存大小加上要分配的大小是否超过了 _capacity_until_GC，超过了就尝试触发 GC 尝试 GC 的核心逻辑是：\n重新尝试分配 如果还是分配失败，检查 GCLocker 是否锁定禁止 GC，如果是的话，首先尝试提高 _capacity_until_GC 进行分配，分配成功直接返回，否则需要阻塞等待 GCLocker 释放 如果没有锁定，尝试触发 GC，之后回到第 1 步 (这里有个小参数 QueuedAllocationWarningCount，如果尝试触发 GC 的次数超过这个次数，就会打印一条警告日志，当然 QueuedAllocationWarningCount 默认是 0，不会打印，并且触发多次 GC 也无法满足的概率比较低) 4.5.2. 每次 GC 之后，也会尝试重新计算 _capacity_until_GC # 在 JVM 初始化的时候，_capacity_until_GC 先会设置为 MaxMetaspaceSize，因为 JVM 初始化的时候会加载很多类，并且这时候要避免触发 GC。在初始化之后，将 _capacity_until_GC 设置为当前元空间占用大小与 MetaspaceSize 中比较大的那个值。同时，还会初始化一个 _shrink_factor，这个 _shrink_factor 主要是如果需要缩小元空间大小，每次缩小的比例。洗稿的狗也遇到不少\n之后，在每次 GC 回收之后，需要重新计算新的 _capacity_until_GC：\n读取 crrent_shrink_factor = _shrink_factor，统计当前元空间使用的空间 used_after_gc。 首先看是否需要扩容： 先使用 MinMetaspaceFreeRatio 最小元空间空闲比例计算 minimum_free_percentage 和 maximum_used_percentage，看是否需要扩容。 计算当前元空间至少要多大 minimum_desired_capacity：使用当前元空间使用的空间 used_after_gc 除以 maximum_used_percentage，并且保证它不小于初始元空间大小 MetaspaceSize，不大于最大元空间大小 MaxMetaspaceSize。 如果当前的 _capacity_until_GC 小于计算的当前元空间至少要多大 minimum_desired_capacity，那么就查要扩容的空间是否大于等于配置 MinMetaspaceExpansion，以及小于等于 MaxMetaspaceExpansion，只有满足才会真正扩容。 扩容其实就是增加 _capacity_until_GC 然后看是否需要缩容： 使用 MaxMetaspaceFreeRatio 最大元空间空闲比例计算 minimum_free_percentage 和 maximum_used_percentage，看是否需要缩容。 计算当前元空间至少要多大 maximum_desired_capacity：使用当前元空间使用的空间 used_after_gc 除以 maximum_used_percentage，并且保证它不小于初始元空间大小 MetaspaceSize，不大于最大元空间大小 MaxMetaspaceSize。 如果当前的 _capacity_until_GC 大于计算的当前元空间至少要多大 maximum_desired_capacity，计算 shrink_bytes = _capacity_until_GC 减去 maximum_desired_capacity。 _shrink_factor 初始为 0，之后为 10%，之后每次翻 4 倍，直到 100%。扩容的大小为 shrink_bytes 乘以这个百分比 如果缩容大于等于配置 MinMetaspaceExpansion，以及小于等于 MaxMetaspaceExpansion，并且缩容后不会小于初始元空间大小 MetaspaceSize，就会缩容。 缩容其实就是减少 _capacity_until_GC 我们还可以看出，如果我们设置 MinMetaspaceFreeRatio 为 0，那么就不会扩容，如果设置 MaxMetaspaceFreeRatio 为 100，那么就不会缩容。_capacity_until_GC 就不会因为 GC 更改。\n4.6. jcmd VM.metaspace 元空间说明、元空间相关 JVM 日志以及元空间 JFR 事件详解 # 4.6.1. jcmd \u0026lt;pid\u0026gt; VM.metaspace 元空间说明 # 通过 jcmd \u0026lt;pid\u0026gt; VM.metaspace 命令可以查看对应 JVM 进程的元空间当前的详细使用情况，返回内容是：\n1.元空间从 MetaChunk 角度的使用统计信息\nTotal Usage - 1383 loaders, 33006 classes (1361 shared): Non-Class: 7964 chunks, 150.83 MB capacity, 150.77 MB (\u0026gt;99%) committed, 150.21 MB (\u0026gt;99%) used, 562.77 KB ( \u0026lt;1%) free, 6.65 KB ( \u0026lt;1%) waste , deallocated: 869 blocks with 249.52 KB Class: 2546 chunks, 21.00 MB capacity, 20.93 MB (\u0026gt;99%) committed, 20.21 MB ( 96%) used, 741.42 KB ( 3%) free, 216 bytes ( \u0026lt;1%) waste , deallocated: 1057 blocks with 264.88 KB Both: 10510 chunks, 171.83 MB capacity, 171.70 MB (\u0026gt;99%) committed, 170.42 MB (\u0026gt;99%) used, 1.27 MB ( \u0026lt;1%) free, 6.86 KB ( \u0026lt;1%) waste , deallocated: 1926 blocks with 514.41 KB 意思是：\n一共 1383 个类加载器，加载了 33006 个类（其中 1361 个是共享类）。 capacity 是指 MetaChunk 的总容量大小（Reserved 内存）；committed 是指这些 MetaChunk 中 committed 的内存大小，也就是实际占用系统物理内存是这么大（虽然可能会有点细微差异，参考本篇文章的第二章）；used 是指这些 MetaChunk 实际使用的大小，肯定比 committed 的要小；free 是指剩余的大小；committed = used + free + waste；deallocated 是指回收到 FreeBlocks 的大小，属于 free 的一部分，另一部分就是 MetaChunk 中 committed 但是还没使用的部分；waste 是指浪费的大小（前面我们提到了什么造成的浪费，主要是搜索 FreeBlocks 的空间使用的时候，可能正好剩下 1 字节，就不放回了继续使用了）洗稿的狗也遇到不少 数据元空间使用情况：一共使用了 7964 个 MetaChunk，这些 MetaChunk 相关总容量大小是 150.83 MB，目前 commit 了 150.77 MB，使用了 150.21 MB，剩余 562.77 KB 可以使用，6.65 KB 的空间被浪费了。FreeBlocks 目前回收了 869 块内存，一共 249.52 KB。 类元空间使用情况：一共使用了 2546 个 MetaChunk，总容量大小是 21.00 MB，目前 commit 了 20.93 MB，使用了 20.21 MB，剩余 741.42 KB 可以使用，216 bytes 的空间被浪费了。FreeBlocks 目前回收了 1057 块内存，一共 264.88 KB。 总的元空间使用情况（类元空间 + 数据元空间的）：一共使用了 10510 个 MetaChunk，总容量大小是 171.83 MB，目前 commit 了 171.70 MB，使用了 170.42 MB，剩余 1.27 MB 可以使用，6.86 KB 的空间被浪费了。FreeBlocks 目前回收了 1926 块内存，一共 514.41 KB。 前面的是从 MetaChunk 的角度去查看，另一个角度是从 VirtualSpaceList 去查看，接下来的信息就是：\nVirtual space: Non-class space: 152.00 MB reserved, 150.81 MB (\u0026gt;99%) committed, 19 nodes. Class space: 1.00 GB reserved, 20.94 MB ( 2%) committed, 1 nodes. Both: 1.15 GB reserved, 171.75 MB ( 15%) committed. 意思是：\n数据元空间的 VirtualSpaceList：总共 Reserve 了 152.00 MB，目前 Commit 了 150.81 MB，一共有 19 个 VirtualSpaceNode。这个与 MetaChunk 的统计信息是有差异的，VirtualSpaceList 的统计信息更体现元空间实际占用的，从 MetaChunk 角度统计的时候，将每个 MetaChunk 统计信息相加，会有精度损失。 类元空间的 VirtualSpaceList：总共 Reserve 了 1.00 GB，目前 Commit 了 20.94 MB，一共有 1 个 VirtualSpaceNode。 总的元空间的 VirtualSpaceList：总共 Reserve 了 1.15 GB，目前 Commit 了 171.75 MB。不要偷取他人的劳动成果，也不要浪费自己的时间和精力，让我们一起做一个有良知的写作者。 接下来是每个 ChunkManager 的 FreeChunkListVector 的统计信息：\nChunk freelists: Non-Class: 4m: (none) 2m: (none) 1m: 2, capacity=2.00 MB, committed=0 bytes ( 0%) 512k: (none) 256k: (none) 128k: 2, capacity=256.00 KB, committed=0 bytes ( 0%) 64k: (none) 32k: 2, capacity=64.00 KB, committed=0 bytes ( 0%) 16k: (none) 8k: 2, capacity=16.00 KB, committed=0 bytes ( 0%) 4k: 2, capacity=8.00 KB, committed=0 bytes ( 0%) 2k: (none) 1k: 2, capacity=2.00 KB, committed=0 bytes ( 0%) Total word size: 2.34 MB, committed: 0 bytes ( 0%) Class: 4m: (none) 2m: 1, capacity=2.00 MB, committed=0 bytes ( 0%) 1m: 1, capacity=1.00 MB, committed=0 bytes ( 0%) 512k: (none) 256k: (none) 128k: (none) 64k: (none) 32k: (none) 16k: (none) 8k: (none) 4k: 1, capacity=4.00 KB, committed=0 bytes ( 0%) 2k: (none) 1k: (none) Total word size: 3.00 MB, committed: 0 bytes ( 0%) Both: 4m: (none) 2m: 1, capacity=2.00 MB, committed=0 bytes ( 0%) 1m: 3, capacity=3.00 MB, committed=0 bytes ( 0%) 512k: (none) 256k: (none) 128k: 2, capacity=256.00 KB, committed=0 bytes ( 0%) 64k: (none) 32k: 2, capacity=64.00 KB, committed=0 bytes ( 0%) 16k: (none) 8k: 2, capacity=16.00 KB, committed=0 bytes ( 0%) 4k: 3, capacity=12.00 KB, committed=0 bytes ( 0%) 2k: (none) 1k: 2, capacity=2.00 KB, committed=0 bytes ( 0%) Total word size: 5.34 MB, committed: 0 bytes ( 0%) 以上的信息可能用图片更直接一些： 接下来是关于回收利用的从 MetaChunk 的角度去查看一些统计信息：\nWaste (unused committed space):(percentages refer to total committed size 171.75 MB): Waste in chunks in use: 6.86 KB ( \u0026lt;1%) Free in chunks in use: 1.27 MB ( \u0026lt;1%) In free chunks: 0 bytes ( 0%) Deallocated from chunks in use: 514.41 KB ( \u0026lt;1%) (1926 blocks) -total-: 1.78 MB ( 1%) chunk header pool: 10520 items, 748.30 KB. 包含的信息是：\n当前被使用的 MetaChunk（即存在于每个类加载器对应的 MetaspaceArena 中的 MetaChunk）中有 6.86 KB 的空间被浪费了。当前被使用的 MetaChunk（即存在于每个类加载器对应的 MetaspaceArena 中的 MetaChunk）中剩余 1.27 MB 可以使用。在 FreeChunkListVector 中没有浪费的空间，其实从前面的 FreeChunkListVector 的详细信息就能看出来。 FreeBlocks 目前回收了 1926 块内存，一共 514.41 KB。FreeBlocks 里面有 1926 个 FreeBlock，一共 514.41 KB。 ChunkHeaderPool 目前有 10520 个 ChunkHeader，一共占用 748.30 KB。 然后是一些统计信息：\nInternal statistics: num_allocs_failed_limit: 24. num_arena_births: 2768. num_arena_deaths: 2. num_vsnodes_births: 20. num_vsnodes_deaths: 0. num_space_committed: 2746. num_space_uncommitted: 0. num_chunks_returned_to_freelist: 28. num_chunks_taken_from_freelist: 10515. num_chunk_merges: 9. num_chunk_splits: 6610. num_chunks_enlarged: 4139. num_purges: 2. num_inconsistent_stats: 0. 包含的信息是：\nnum_allocs_failed_limit：元空间普通分批内存失败的次数（前文分析过详细流程），后面也有对应的 JFR 事件会分析。 num_arena_births：MetaspaceArena 的创建次数。 num_arena_deaths：MetaspaceArena 的销毁次数。发生于对应的类加载器被回收之后。 num_vsnodes_births：VirtualSpaceNode 的创建次数。（根据前面的 VirtualSpaceList 的统计信息可以知道是 19 + 1 = 20） num_vsnodes_deaths：VirtualSpaceNode 的销毁次数。 num_space_committed：Commit 内存的次数。 num_space_uncommitted：Uncommit 内存的次数。 num_chunks_returned_to_freelist：MetaChunk 被回收到 FreeChunkListVector 的次数。 num_chunks_taken_from_freelist：从 FreeChunkListVector 中获取 MetaChunk 进行分配的次数。 num_chunk_merges：MetaChunk 合并的次数。 num_chunk_splits：MetaChunk 拆分的次数。 num_chunks_enlarged：MetaChunk 扩容的次数。 num_purges：MetaspaceArena 的清理次数。一般等于销毁次数。 num_inconsistent_stats：不一致的统计次数。这个一般不用关心，主要是为了调试用的。 最后是一些参数信息：\nSettings: MaxMetaspaceSize: unlimited CompressedClassSpaceSize: 1.00 GB Initial GC threshold: 40.00 MB Current GC threshold: 210.12 MB CDS: on MetaspaceReclaimPolicy: balanced - commit_granule_bytes: 65536. - commit_granule_words: 8192. - virtual_space_node_default_size: 1048576. - enlarge_chunks_in_place: 1. - new_chunks_are_fully_committed: 0. - uncommit_free_chunks: 1. - use_allocation_guard: 0. - handle_deallocations: 1. MaxMetaspaceSize：元空间最大值。默认是无限制的。这里我们也没限制。 CompressedClassSpaceSize：压缩类空间大小。默认是 1 GB。这里我们也没指定，所以是默认的。 Initial GC threshold：初始的元空间 GC 阈值。默认是 40 MB。这里我们也没指定，所以是默认的。 Current GC threshold：当前的元空间 GC 阈值。前面我们分析过这个阈值改变的机制。 CDS：是否开启了 CDS。默认开启。这个我们不用太关心，主要和 CDS 特性相关（JEP 310: Application Class-Data Sharing 和 JEP 350: Dynamic CDS Archives），在以后的文章会详细分析。 元空间 MetaspaceReclaimPolicy 为 balanced commit 粒度（commit_granule_bytes）为 65536 字节，转化单位为字之后，是 8192 字（一 word 为 8 字节）。虚拟内存空间节点内存大小（virtual_space_node_default_size）为 1048576 字，转化单位为字之后，是 64 MB。当前 MetaChunk 不足以分配的时候，是否尝试扩容当前 MetaChunk（enlarge_chunks_in_place）为是，新分配的 MetaChunk 是否一次性全部 commit（new_chunks_are_fully_committed）为否，是否在 MetaChunk 释放的时候 uncommit（uncommit_free_chunks）为是。以上配置都在前文分析过。最后两个配置都是 debug 配置，正式版里面都是无法修改的，我们也不用太关心这两个配置的效果，并且 handle_deallocations 已经在 Java 18 中移除了（https://github.com/openjdk/jdk/commit/157e1d5073e221dab084422389f68eea53974f4c） 4.6.2. 元空间相关 JVM 日志 # 我们通过启动参数 -Xlog:metaspace*=debug::utctime,level,tags，查看元空间相关 JVM 日志。\n首先，初始化 JVM 元空间的时候，会输出元空间基本参数：\n[2023-04-11T09:07:31.994+0000][info][metaspace] Initialized with strategy: balanced reclaim. [2023-04-11T09:07:31.994+0000][info][metaspace] - commit_granule_bytes: 65536. [2023-04-11T09:07:31.994+0000][info][metaspace] - commit_granule_words: 8192. [2023-04-11T09:07:31.994+0000][info][metaspace] - virtual_space_node_default_size: 1048576. [2023-04-11T09:07:31.994+0000][info][metaspace] - enlarge_chunks_in_place: 1. [2023-04-11T09:07:31.994+0000][info][metaspace] - new_chunks_are_fully_committed: 0. [2023-04-11T09:07:31.994+0000][info][metaspace] - uncommit_free_chunks: 1. [2023-04-11T09:07:31.994+0000][info][metaspace] - use_allocation_guard: 0. [2023-04-11T09:07:31.994+0000][info][metaspace] - handle_deallocations: 1. 以上这几行日志的意思是：元空间 MetaspaceReclaimPolicy 为 balanced，commit 粒度（commit_granule_bytes）为 65536 字节，转化单位为字之后，是 8192 字（一 word 为 8 字节）。虚拟内存空间节点内存大小（virtual_space_node_default_size）为 1048576 字，转化单位为字之后，是 64 MB。当前 MetaChunk 不足以分配的时候，是否尝试扩容当前 MetaChunk（enlarge_chunks_in_place）为是，新分配的 MetaChunk 是否一次性全部 commit（new_chunks_are_fully_committed）为否，是否在 MetaChunk 释放的时候 uncommit（uncommit_free_chunks）为是。以上配置都在前文分析过。最后两个配置都是 debug 配置，正式版里面都是无法修改的，我们也不用太关心这两个配置的效果，并且 handle_deallocations 已经在 Java 18 中移除了（https://github.com/openjdk/jdk/commit/157e1d5073e221dab084422389f68eea53974f4c）\n接下来，初始化元空间的内存空间：\n[2023-04-11T09:07:32.411+0000][info ][gc,metaspace] CDS archive(s) mapped at: [0x0000000800000000-0x0000000800bde000-0x0000000800bde000), size 12443648, SharedBaseAddress: 0x0000000800000000, ArchiveRelocationMode: 0. [2023-04-11T09:07:32.411+0000][info ][gc,metaspace] Compressed class space mapped at: 0x0000000800c00000-0x0000000840c00000, reserved size: 1073741824 [2023-04-11T09:07:32.411+0000][info ][gc,metaspace] Narrow klass base: 0x0000000800000000, Narrow klass shift: 0, Narrow klass range: 0x100000000 [2023-04-11T09:07:32.417+0000][debug][metaspace ] Arena @0x0000ffff807a1cc0 (non-class sm): : born. [2023-04-11T09:07:32.417+0000][debug][metaspace ] Arena @0x0000ffff807a1dd0 (class sm): : born. [2023-04-11T09:07:32.417+0000][debug][metaspace ] CLMS @0x0000ffff807a1c80 : born (nonclass arena: 0x0000ffff807a1cc0, class arena: 0x0000ffff807a1dd0. [2023-04-11T09:07:32.411+0000][debug][metaspace ] VsListNode @0x0000ffff80784ab0 base 0x0000000800c00000 : born (word_size 134217728). [2023-04-11T09:07:32.417+0000][debug][metaspace ] VsListNode @0x0000ffff807a27b0 base 0x0000ffff52800000 : born (word_size 1048576). 这几行日志的意思是：\nCDS 元数据映射到内存的地址范围是 [0x0000000800000000-0x0000000800bde000-0x0000000800bde000)，大小为 12443648 字节，共享基地址为 0x0000000800000000，ArchiveRelocationMode 为关闭。这些信息我们不用太关心，主要和 CDS 特性相关（JEP 310: Application Class-Data Sharing 和 JEP 350: Dynamic CDS Archives），在以后的文章会详细分析。\n我们这里是默认配置，所以压缩类空间是开启的，初始化压缩类空间，映射到内存的地址范围是 [0x0000000800c00000-0x0000000840c00000)，Reserved 内存大小为 1073741824 字节（1GB），默认压缩类空间最大大小就是 1GB。加载到压缩类空间的类的基地址为 0x0000000800000000（），偏移量为 0，范围为 0x100000000，这个前面也简单分析过。\nBootstrap ClassLoader 创建了两个 MetaspaceArena，分别是前文分析的类元空间的 MetaspaceArena 和数据元空间的 MetaspaceArena，放入对应的 ClassLoadMetaSpace 中。不要偷取他人的劳动成果，也不要浪费自己的时间和精力，让我们一起做一个有良知的写作者。\n初始化类元空间的还有数据元空间的 VirtualSpaceList，并分别创建并放入各自的第一个 VirtualSpaceNode\n接下来开始加载类，从元空间申请内存进行分配：\n[2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): requested chunk: pref_level: lv12, max_level: lv12, min committed size: 0. [2023-04-11T09:07:32.411+0000][debug][metaspace] VsListNode @0x0000ffff80784ab0 base 0x0000000800c00000 : new root chunk @0x0000ffff807867f0, f, base 0x0000000800c00000, level lv00. [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): allocated new root chunk. [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): splitting chunk @0x0000ffff807867f0, f, base 0x0000000800c00000, level lv00 to lv12. [2023-04-11T09:07:32.411+0000][debug][metaspace] ChkMgr @0x0000ffff807863d0 (class-space): handing out chunk @0x0000ffff807867f0, u, base 0x0000000800c00000, level lv12. 这几行日志的意思分别是：\n加载类需要从元空间申请内存，这是第一次申请，所以各个数据结构都是空的，所以需要申请新的 MetaChunk，优先考虑的与最大的 ChunkLevel 都是 12，对应 1KB。本次申请发生在 ChunkManager @0x0000ffff807863d0 申请新的 RootMetaChunk，基址 0x0000000800c00000 将新的 RootMetaChunk 按照之前的算法拆分到 ChunkLevel 为 12，结果是 MetaChunk @0x0000ffff807867f0，将拆出来的其他 MetaChunk 放入 ChunkManager @0x0000ffff807863d0 的 FreeListVector 中 4.6.3. 元空间 JFR 事件详解 # 4.6.3.1. jdk.MetaspaceSummary 元空间定时统计事件 # 元空间定时统计事件 jdk.MetaspaceSummary，包括以下属性：\n事件开始时间：其实就是事件发生时间 GC Identifier：全局 GC 的 id 标识 When：事件发生的时机，包括 Before GC 和 After GC 两种，分别是 GC 前和 GC 后的统计数据，可以根据 GC Identifier 对比 GC 前后的数据，看看 GC 之后元空间的使用情况.plagiarism和洗稿是恶意抄袭他人劳动成果的行为，是对劳动价值的漠视和践踏！ GC Threshold：GC 阈值，即前面提的 _capacity_until_GC Class:Reserved：类元空间 Reserved 的内存空间大小 Class:Committed：类元空间 Committed 的内存空间大小 Class:Used：类元空间实际保存数据使用的内存空间大小（前面的机制分析中我们会看到，Committed 的空间会比实际使用的大，主要因为类加载器回收，以及可能 MetaChunk 分配的时候 commit 所有内存） Data:Reserved：数据元空间 Reserved 的内存空间大小 Data:Committed：数据元空间 Committed 的内存空间大小 Data:Used：数据元空间实际保存数据使用的内存空间大小 Total:Reserved：整个元空间 Reserved 的内存空间大小（其实就是类元空间 + 数据元空间） Total:Committed：整个元空间 Committed 的内存空间大小（其实就是类元空间 + 数据元空间） Total:Used：整个元空间实际保存数据使用的内存空间大小（其实就是类元空间 + 数据元空间） 4.6.3.2. jdk.MetaspaceAllocationFailure 元空间分配失败事件 # 前面提到过，如果普通分配失败，那么会触发 jdk.MetaspaceAllocationFailure 这个 JFR 事件，大家可以监控这个事件，去调整元空间大小减少由于元空间不足触发的 GC，这个事件包括以下属性：\n事件开始时间：其实就是事件发生时间 类加载器：触发 OOM 的类加载器 Hidden Class Loader：是否是隐藏类加载器 Metadata Type：元数据类型，分为属于类元空间的以及属于数据元空间的两种类型，分别是：Class 和 Metadata Metaspace Object Type：元空间对象类型，包括 Class、ConstantPool、Symbol、Method、Klass、Module、Package、Other Size：本次分配的大小 这个事件也会采集堆栈信息，用来定位分配失败的源头是哪些类的加载导致的。\n4.6.3.3. jdk.MetaspaceOOM 元空间 OOM 事件 # 前面提到过，当元空间 OOM 的时候，就会产生这个事件，这个事件包括以下属性（和 jdk.MetaspaceAllocationFailure 事件一样）：\n事件开始时间：其实就是事件发生时间 类加载器：触发 OOM 的类加载器 Hidden Class Loader：是否是隐藏类加载器 Metadata Type：元数据类型，分为属于类元空间的以及属于数据元空间的两种类型，分别是：Class 和 Metadata Metaspace Object Type：元空间对象类型，包括 Class、ConstantPool、Symbol、Method、Klass、Module、Package、Other Size：本次分配的大小 与 jdk.MetaspaceAllocationFailure 事件一样，也会采集堆栈信息，用来定位 OOM 的原因。\n4.6.3.4. jdk.MetaspaceGCThreshold 元空间 GC 阈值变化事件 # 前面我们说过，元空间的 GC 阈值（_capacity_until_GC）是动态调整的，这个事件就是用来记录元空间 GC 阈值变化的。这个事件包括以下属性：\n事件开始时间：其实就是事件发生时间 New Value：新的 GC 阈值 Old Value：旧的 GC 阈值 Updater：哪个机制触发的 GC 阈值修改，我们之前讨论过 _capacity_until_GC 有两个场景会修改： 分配过程中，达到 GC 阈值，触发 GC，但是处于 GCLocker 处于锁定禁止 GC，就尝试增大 _capacity_until_GC 进行分配。对应的 Updater 是 expand_and_allocate 每次 GC 之后，触发重新计算 _capacity_until_GC，如果有更新，就会生成这个事件，对应的 Updater 是 compute_new_size 4.6.3.5. jdk.MetaspaceChunkFreeListSummary 元空间 Chunk FreeList 统计事件 # 这个事件在 Java 16 引入 JEP 387: Elastic Metaspace 弹性元空间的设计之后，里面的统计数据就都是 0 了，还没有实现，参考：https://bugs.openjdk.org/browse/JDK-8251342，所以我们先不用关心。参考源码：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/memory/metaspaceUtils.hpp\n// (See JDK-8251342). Implement or Consolidate. static MetaspaceChunkFreeListSummary chunk_free_list_summary(Metaspace::MetadataType mdtype) { return MetaspaceChunkFreeListSummary(0,0,0,0,0,0,0,0); } 5. JVM 线程内存设计（重点研究 Java 线程） # Java 19 中 Loom 终于 Preview 了，虚拟线程（VirtualThread）是我期待已久的特性，但是这里我们说的线程内存，并不是这种 虚拟线程，还是老的线程。其实新的虚拟线程，在线程内存结构上并没有啥变化，只是存储位置的变化，实际的负载线程（CarrierThread）还是老的线程。\n同时，JVM 线程占用的内存分为两个部分：分别是线程栈占用内存，以及线程本身数据结构占用的内存。\n5.1. JVM 中有哪几种线程，对应线程栈相关的参数是什么 # JVM 中有如下几类线程：\nVM 线程：全局唯一的线程，负责执行 VM Operations，例如 JVM 的初始化，其中的操作大部分需要在安全点执行，即 Stop the world 的时候执行。所有的操作请参考：https://github.com/openjdk/jdk/blob/jdk-21+17/src/hotspot/share/runtime/vmOperation.hpp GC 线程：负责做 GC 操作的线程 Java 线程：包括 Java 应用线程（java.lang.Thread），以及 CodeCacheSweeper 线程， JVMTI 的 Agent 与 Service 线程其实也是 JAva 线程。 编译器线程： JIT 编译器的线程，有 C1 和 C2 线程(xi稿滚去shi) 定时任务时钟线程：全局唯一的线程，即 Watcher 线程，负责计时并执行定时任务，目前 JVM 中包括的定时任务可以通过查看继承 PeriodicTask 的类看到，其中两个比较重要的任务是： StatSamplerTask：定时更新采集的 JVM Performance Data（PerfData）数据， 包括 GC、类加载、运行采集等等数据，这个任务多久执行一次是通过 -XX:PerfDataSamplingInterval 参数控制的，默认为 50 毫秒（参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp）。这些数据一般通过 jstat 读取，或者通过 JMX 读取。 VMOperationTimeoutTask：由于 VM 线程是单线程，执行 VM Operations，单个任务执行不能太久，否则会阻塞其他 VM Operations。所以每次执行 VM Operations 的时候，这个定时任务都会检查当前执行了多久，如果超过 -XX:AbortVMOnVMOperationTimeoutDelay 就会报警。AbortVMOnVMOperationTimeoutDelay 默认是 1000ms（参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/share/runtime/globals.hpp）。 异步日志线程：全局唯一的线程， Java 17 引入的异步 JVM 日志特性，防止因为 JVM 日志输出阻塞影响全局安全点事件导致全局暂停过长，或者 JVM 日志输出导致线程阻塞，负责异步写日志，通过 -Xlog:async 启用 JVM 异步日志，通过 -XX:AsyncLogBufferSize= 指定异步日志缓冲大小，这个大小默认是 2097152 即 2MB JFR 采样线程：全局唯一的线程，负责采集 JFR 中的两种采样事件，一个是 jdk.ExecutionSample，另一个是 jdk.NativeMethodSample，都是采样当前正在 RUNNING 的线程，如果线程在执行 Java 代码，就属于 jdk.ExecutionSample，如果执行 native 方法，就属于 jdk.NativeMethodSample。 相关的参数有：\nThreadStackSize：每个 Java 线程的栈大小，这个参数通过 -Xss 也可以指定，各种平台的默认值为： linux 平台，x86 CPU，默认为 1024 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp linux 平台，aarch CPU，默认为 2048 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp windows 平台，x86 CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp windows 平台，aarch CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp VMThreadStackSize：VM 线程，GC 线程，定时任务时钟线程，异步日志线程，JFR 采样线程的栈大小，各种平台的默认值为： linux 平台，x86 CPU，默认为 1024 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp linux 平台，aarch CPU，默认为 2048 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp windows 平台，x86 CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp windows 平台，aarch CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp CompilerThreadStackSize：编译器线程的栈大小，各种平台的默认值为： linux 平台，x86 CPU，默认为 1024 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp linux 平台，aarch CPU，默认为 2048 KB，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/linux_aarch64/globals_linux_aarch64.hpp windows 平台，x86 CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_x86/globals_windows_x86.hpp windows 平台，aarch CPU，默认为 0，即使用操作系统默认值（64 位虚拟机为 1024KB），参考：https://github.com/openjdk/jdk/blob/jdk-21%2B17/src/hotspot/os_cpu/windows_aarch64/globals_windows_aarch64.hpp StackYellowPages：后面会提到并分析的黄色区域的页大小 linux 平台，x86 CPU，默认为 2 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp linux 平台，aarch CPU，默认为 2 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp windows 平台，x86 CPU，默认为 3 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp windows 平台，aarch CPU，默认为 2 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp StackRedPages：后面会提到并分析的红色区域的页大小 linux 平台，x86 CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp linux 平台，aarch CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp windows 平台，x86 CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp windows 平台，aarch CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp StackShadowPages：后面会提到并分析的影子区域的页大小 linux 平台，x86 CPU，默认为 20 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp linux 平台，aarch CPU，默认为 20 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp windows 平台，x86 CPU，默认为 8 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp windows 平台，aarch CPU，默认为 20 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp StackReservedPages：后面会提到并分析的保留区域的页大小 linux 平台，x86 CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp linux 平台，aarch CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp windows 平台，x86 CPU，默认为 0 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/globals_x86.hpp windows 平台，aarch CPU，默认为 1 页，参考：https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/aarch64/globals_aarch64.hpp RestrictReservedStack：默认为 true，与保留区域相关，保留区域会保护临界区代码（例如 ReentrantLock）在抛出 StackOverflow 之前先把临界区代码执行完再结束，防止临界区代码执行到一半就抛出 StackOverflow 导致状态不一致导致这个锁之后再也用不了了。标记临界区代码的注解是 @jdk.internal.vm.annotation.ReservedStackAccess。在这个配置为 true 的时候，这个注解默认只能 jdk 内部代码使用，如果你有类似于 ReentrantLock 这种带有临界区的代码也想保护起来，可以设置 -XX:-RestrictReservedStack，关闭对于 @jdk.internal.vm.annotation.ReservedStackAccess 的限制，这样你就可以在自己的代码中使用这个注解了。 我们接下来重点分析 Java 线程栈。\n5.2. Java 线程栈内存的结构 # 熟悉编译器的人应该知道激活记录（Activation Record）这个概念，它是一种数据结构，其中包含支持一次函数调用所需的所有信息。它包含该函数的所有局部变量，以及指向另一个激活记录的引用（或指针），其实你可以简单理解为，每多一次方法调用就多一个激活记录。而线程栈帧（Stack Frame），就是激活记录的实际实现。每在代码中多一次方法调用就多一个栈帧，但是这个说法并不严谨，比如，JIT 可能会内联一些方法，可能会跳过某些方法的调用等等。Java 线程的栈帧有哪几种呢，其实根据 Java 线程执行的方法有 Java 方法以及原生方法（Native）就能推测出有两种：\nJava 虚拟机栈帧（Java Virtual Machine Stack Frame）：用于保存 Java 方法的执行状态，包括局部变量表、操作数栈、方法出口等信息。 Native 方法栈帧（Native Method Stack Frame）：用于保存 Native 方法的执行状态，包括局部变量表、操作数栈、方法出口等信息。 在最早的时候，Linux 还没有线程的概念，Java 自己做了一种叫做 Green Thread 的东西即用户态线程（与现在的虚拟线程设计差异很大，不是一个概念了），但是调度有诸多问题，所以在 Linux 有线程之后，Java 也舍弃了 Green Thread。Java 线程其实底层就是通过操作系统线程实现，是一一对应的关系。不过现在，虚拟线程也快要 release 了，但是这个并不是今天的重点。并且，在最早的时候，Java 线程栈与 Native 线程栈也是分开的，虽然可能都是一个线程执行的。后来，发现这样做对于 JIT 优化，以及线程栈大小限制，以及实现高效的 StackOverflow 检查都不利，所以就把 Java 线程栈与 Native 线程栈合并了，这样就只有一个线程栈了。\nJVM 中对于线程栈可以使用的空间是限制死的。对于 Java 线程来说，这个限制是由 -Xss 或者 -XX:ThreadStackSize 来控制的，-Xss 或者 -XX:ThreadStackSize 基本等价， 一般来说，-Xss 或者 -XX:ThreadStackSize 是用来设置每个线程的栈大小的，但是更严谨的说法是，它是设置每个线程栈最大使用的内存大小，并且实际可用的大小由于保护页的存在还要小于这个值，并且设置这个值不能小于保护页需要的大小，否则没有意义。根据前面对于 JVM 其他区域的分析我们可以推测出，对于每个线程，都会先 Reserve 出 -Xss 或者 -XX:ThreadStackSize 大小的内存，之后随着线程占用内存升高而不断 Commit 内存。\n同时我们还知道，对于一段 Java 代码，分为编译器执行，C1 执行，C2 执行三种情况，因此，一个 Java 线程的栈内存结构可能如下图所示：\n这个图片我们展示了一个比较极端的情况，线程先解释执行方法 1，之后调用并解释执行方法 2，然后调用一个可能比较热点的方法 3，方法 3 已经被 C1 优化编译，这里执行的是编译后的代码，之后调用可能更热点的方法 4，方法 4 已经被 C2 优化编译，这里执行的是编译后的代码。最后方法 4 还需要调用一个 native 方法 5。\n5.3. Java 线程如何抛出的 StackOverflowError # JVM 线程内存还有一些特殊的内存区域，结构如下：\n保护区域（Guard Zone），保护区的内存没有映射物理内存，访问的话会像前面第三章提到的 NullPointerException 优化方式类似，即抛出 SIGSEGV 被 JVM 捕获，再抛出 StackOverflowError。保护区包括以下三种： 黄色区域（Yellow Zone）：大小由前面提到的 -XX:StackYellowPages 参数决定。如果栈扩展到了黄色区域，则发生 SIGSEGV，并且信号处理程序抛出 StackOverflowError 并继续执行当前线程。同时，这时候黄色页面会被映射分配内存，以提供一些额外的栈空间给异常抛出的代码使用，抛出异常结束后，黄色页面会重新去掉映射，变成保护区。 红色区域（Red Zone）：大小由前面提到的 -XX:StackRedPages 参数决定。正常的代码只会可能到黄色区域，只有 JVM 出一些 bug 的时候会到红色区域，这个相当于最后一层保证。保留这个区域是为了出这种 bug 的时候，能有空间可以将错误信息写入 hs_err_pid.log 文件用于定位。 保留区域（Reserved Zone）：大小由前面提到的 -XX:StackReservedPages 参数决定。在 Java 9 引入（JEP 270: Reserved Stack Areas for Critical Sections）（洗稿狗的区域是细狗区），主要是为了解决 JDK 内部的临界区代码（例如ReentrantLock）导致 StackOverflowError 的时候保证内部数据结构不会处于不一致的状态导致锁无法释放或者被获取。如果没有这个区域，在 ReentrantLock.lock() 方法内部调用某个内部方法的时候可能会进入黄色区域，导致 StackOverflowError，这时候可能 ReentrantLock 内部的一些数据可能已经修改，抛出异常导致这些数据无法回滚让锁处于当初设计的时候没有设计的不一致状态。为了避免这个情况，引入保留区域。在执行临界区方法的时候（被 @jdk.internal.vm.annotation.ReservedStackAccess 注解修饰的方法），如果进入保留区域，那么保留区域会被映射内存，用于执行完临界区方法，执行完临界区方法之后，再抛出 StackOverflowError，并解除保留区域的映射。另外，前面我们提到过，@jdk.internal.vm.annotation.ReservedStackAccess 这个注解默认只能 jdk 内部代码使用，如果你有类似于 ReentrantLock 这种带有临界区的代码也想保护起来，可以设置 -XX:-RestrictReservedStack，关闭对于 @jdk.internal.vm.annotation.ReservedStackAccess 的限制，这样你就可以在自己的代码中使用这个注解了。 影子区域（Shadow Zone）：这个区域的大小由前面提到的 -XX:StackShadowPages 参数决定。影子区域只是抽象概念，跟在当前栈占用的顶部栈帧后面，随着顶部栈帧变化而变化。这个区域用于保证 Native 调用不会导致 StackOverflowError。在后面的分析我们会看到，每次调用方法前需要估算方法栈帧的占用大小，但是对于 Native 调用我们无法估算，所以我们就假设 Native 大小最大不会超过影子区域大小，在发生 Native 调用前，会查看当前栈帧位置加上影子区域大小是否会达到保留区域，如果达到了保留区域，那么会抛出 StackOverflowError，如果没有达到保留区域，那么会继续执行。这里我们可以看出，JVM 假设 Native 调用占用空间不会超过影子区域大小，JDK 中自带的 native 调用也确实是这样。如果你自己实现了 Native 方法并且会占用大量栈内存，那么你需要调整 StackShadowPages。 我们看下源码中如何体现的这些区域，参考源码：https://github.com/openjdk/jdk/blob/jdk-21%2B18/src/hotspot/share/runtime/stackOverflow.hpp\nsize_t StackOverflow::_stack_red_zone_size = 0; size_t StackOverflow::_stack_yellow_zone_size = 0; size_t StackOverflow::_stack_reserved_zone_size = 0; size_t StackOverflow::_stack_shadow_zone_size = 0; void StackOverflow::initialize_stack_zone_sizes() { //读取虚拟机页大小，第二章我们分析过 size_t page_size = os::vm_page_size(); //目前各个平台最小页大小基本都是 4K size_t unit = 4*K; //使用 StackRedPages 乘以 4K 然后对虚拟机页大小进行对齐作为红色区域大小 assert(_stack_red_zone_size == 0, \u0026#34;This should be called only once.\u0026#34;); _stack_red_zone_size = align_up(StackRedPages * unit, page_size); //使用 StackYellowPages 乘以 4K 然后对虚拟机页大小进行对齐作为黄色区域大小 assert(_stack_yellow_zone_size == 0, \u0026#34;This should be called only once.\u0026#34;); _stack_yellow_zone_size = align_up(StackYellowPages * unit, page_size); //使用 StackReservedPages 乘以 4K 然后对虚拟机页大小进行对齐作为保留区域大小 assert(_stack_reserved_zone_size == 0, \u0026#34;This should be called only once.\u0026#34;); _stack_reserved_zone_size = align_up(StackReservedPages * unit, page_size); //使用 StackShadowPages 乘以 4K 然后对虚拟机页大小进行对齐作为保留区域大小 assert(_stack_shadow_zone_size == 0, \u0026#34;This should be called only once.\u0026#34;); _stack_shadow_zone_size = align_up(StackShadowPages * unit, page_size); } 5.3.1. 解释执行与编译执行时候的判断（x86为例） # 我们继续针对 Java 线程进行讨论。在前面我们已经知道，Java 线程栈的大小是有限制的，如果线程栈使用的内存超过了限制，那么就会抛出 StackOverflowError。但是，JVM 如何知道什么时候该抛出呢？\n首先，对于解释执行，一般没有任何优化，就是在调用方法前检查。不同的环境下的实现会有些差别，我们以 x86 cpu 为例：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp\nvoid TemplateInterpreterGenerator::generate_stack_overflow_check(void) { //计算栈帧的一些元数据存储的消耗 const int entry_size = frame::interpreter_frame_monitor_size() * wordSize; const int overhead_size = -(frame::interpreter_frame_initial_sp_offset * wordSize) + entry_size; //读取虚拟机页大小，第二章我们分析过 const int page_size = os::vm_page_size(); //比较当前要调用的方法的元素个数，判断与除去元数据以外一页能容纳的元素个数谁大谁小 Label after_frame_check; __ cmpl(rdx, (page_size - overhead_size) / Interpreter::stackElementSize); __ jcc(Assembler::belowEqual, after_frame_check); //大于的才会进行后续的判断，因为小于一页的话，绝对可以被黄色区域限制住，因为黄色区域要与页大小对齐，因此至少一页 //小于一页的栈帧不会导致跳过黄色区域，只有大于的须有后续仔细判断 Label after_frame_check_pop; //读取线程的 stack_overflow_limit_offset //_stack_overflow_limit = stack_end() + MAX2(stack_guard_zone_size(), stack_shadow_zone_size()); //即栈尾 加上 保护区域 或者 阴影区域 的最大值，即有效栈尾地址 //其实就是当前线程栈容量顶部减去 保护区域 或者 阴影区域 的最大值的地址，即当前线程栈只能增长到这个地址 const Address stack_limit(thread, JavaThread::stack_overflow_limit_offset()); //将前面计算的栈帧元素个数大小保存在 rax __ mov(rax, rdx); //将栈帧的元素个数转换为字节大小，然后加上栈帧的元数据消耗 __ shlptr(rax, Interpreter::logStackElementSize); __ addptr(rax, overhead_size); //加上前面计算的有效栈尾地址 __ addptr(rax, stack_limit); //与当前栈顶地址比较，如果当前栈顶地址大于 rax 当前值，证明没有溢出 __ cmpptr(rsp, rax); __ jcc(Assembler::above, after_frame_check_pop); //否则抛出 StackOverflowError 异常 __ jump(ExternalAddress(StubRoutines::throw_StackOverflowError_entry())); __ bind(after_frame_check_pop); __ bind(after_frame_check); } 代码的步骤大概是（plagiarism和洗稿是恶意抄袭他人劳动成果的行为，是对劳动价值的漠视和践踏！ ）：\n首先判断要分配的栈帧大小，是否大于一页。 如果小于等于一页，不用检查，直接结束。因为如果小于一页，那么栈帧的元素个数一定小于一页，栈增长不会导致跳过保护区域，如果达到保护区域就会触发 SIGSEGV 抛出 StackOverflowError。因为每个保护区域如前面源代码所示，都是对虚拟机页大小进行对齐的，因此至少一页。 如果大于一页，则需要检查。检查当前已经使用的空间，加上栈帧占用的空间，加上保护区域与阴影区域的最大值，占用空间是否大于栈空间限制。如果大于，则抛出 StackOverflowError 异常。为什么是保护区域与阴影区域的最大值？阴影区域其实是我们假设的最大帧大小，最后至少要有这么多空间才一定不会导致溢出栈顶污染其他内存（当然，如之前所述，如果你自己实现一个 Native 调用并且栈帧很大，则需要修改阴影区域大小）。如果本身保护区域就比阴影区域大，那么就用保护区域的大小，就也能保证这一点。 可以看出，编译执行，虽然做了一定的优化，但是还是很复杂，就算大部分栈帧应该都小于一页，但是刚开始的判断指令还是有不小的消耗。我们看看 JIT 编译后的代码，还是以 x86 cpu 为例：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/share/asm/assembler.cpp\nvoid AbstractAssembler::generate_stack_overflow_check(int frame_size_in_bytes) { //读取虚拟机页大小，第二章我们分析过 const int page_size = os::vm_page_size(); //读取影子区大小 int bang_end = (int)StackOverflow::stack_shadow_zone_size(); //如果栈帧大小大于一页，那么需要将 bang_end 加上栈帧大小，之后检查每一页是否处于保护区域 const int bang_end_safe = bang_end; if (frame_size_in_bytes \u0026gt; page_size) { bang_end += frame_size_in_bytes; } //检查每一页是否处于保护区域 int bang_offset = bang_end_safe; while (bang_offset \u0026lt;= bang_end) { bang_stack_with_offset(bang_offset); bang_offset += page_size; } } https://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/cpu/x86/macroAssembler_x86.cpp\n//检查是否处于保留区域，其实就是将 rsp - offset 的地址上的值写入 rax 上， //如果 rsp - offset 保护区域，那么就会触发 SIGSEGV void bang_stack_with_offset(int offset) { movl(Address(rsp, (-offset)), rax); } 编译后执行的指令就简单多了：\n如果栈帧大小小于一页：只需要考虑 Native 调用是否会导致 StackOverflow 即可。检查当前占用位置加上影子区域大小，之后判断是否会进入保护区域即可，不用考虑当前方法栈帧占用大小，因为肯定小于一页。验证是否进入保护区域也和之前讨论过的 NullPointeException 的处理是类似的，就是将 rsp - offset 的地址上的值写入 rax 上，如果 rsp - offset 处于保护区域，那么就会触发 SIGSEGV。 如果栈帧大小大于一页：那么需要将当前占用位置，加上栈帧大小，加上影子区域大小，之后从当前栈帧按页检查，是否处于保护区域。因为大于一页的话，直接验证最后的位置可能会溢出到其他东西占用的内存（比如其他线程占用的内存）。 5.3.2. 一个 Java 线程 Xss 最小能指定多大 # 这个和平台是相关的，我们以 linux x86 为例子，假设没有大页分配，一页就是 4K，一个线程至少要保留如下的空间：\n保护区域： 黄色区域：默认 2 页 红色区域：默认 1 页 保留区域：默认 1 页 影子区域：默认 20 页 这些加在一起是 24 页，也就是 96K。\n同时，在 JVM 代码中也限制了，除了这些空间，每种线程的最小大小：\nhttps://github.com/openjdk/jdk/blob/jdk-21%2B19/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp\nsize_t os::_compiler_thread_min_stack_allowed = 48 * K; size_t os::_java_thread_min_stack_allowed = 40 * K; size_t os::_vm_internal_thread_min_stack_allowed = 64 * K; 所以，对于 Java 线程，至少需要 40 + 96 = 136K 的空间。我们试一下：\nbash-4.2$ java -Xss1k The Java thread stack size specified is too small. Specify at least 136k Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. ","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-4-jvm-memory/","section":"文章","summary":"一次全面深入的 JVM 内存架构剖析，涵盖堆内存、元空间、线程栈以及压缩对象指针。本文从内存分配流程、Native Memory Tracking 出发，并通过 jol、jhsdb、JFR 等工具示例，帮助理解 JVM 内存管理内部机制。","title":"全网最硬核 JDK 分析 - 4. OpenJDK JVM 内存结构实现","type":"posts"},{"content":"","date":"2023年3月28日","externalUrl":null,"permalink":"/zh-cn/categories/%E6%80%A7%E8%83%BD/","section":"Categories","summary":"","title":"性能","type":"categories"},{"content":"","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/categories/engineering/","section":"Categories","summary":"","title":"Engineering","type":"categories"},{"content":"","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/tags/gc/","section":"Tags","summary":"","title":"GC","type":"tags"},{"content":"","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/tags/safepoint/","section":"Tags","summary":"","title":"Safepoint","type":"tags"},{"content":"","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/tags/troubleshooting/","section":"Tags","summary":"","title":"Troubleshooting","type":"tags"},{"content":"","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/tags/webflux/","section":"Tags","summary":"","title":"Webflux","type":"tags"},{"content":" 解决神秘的 JVM Safepoint 问题：从问题到解决方案的探索之旅 # 最近，我们在生产环境中遇到了一个特殊问题：某些高量日志应用在每小时 05 分钟触发的每小时日志同步任务时会长时间冻结。通过仔细检查我们的 safepoint 日志（通过启动参数 -Xlog:safepoint=trace:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M 启用，并利用 Java 17 的 -Xlog:async 进行异步日志记录以防止 JVM 日志输出阻塞进程），我们发现所有事件都是由以下原因引起的：\nGC 日志：-Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M JIT 编译日志：-Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M 禁用堆栈跟踪省略：这仅省略内部 JDK 异常，如 NullPointerException：-XX:-OmitStackTraceInFastThrow，我们已经优化了这一点以处理高错误量期间过多堆栈跟踪输出的性能压力 实施这些配置后，我们的应用表现出了一个奇怪的问题，有三种不同的表现，但都有一个共同特征：异常长的 safepoint 持续时间。\n1. Safepoint 日志显示所有线程到达 safepoint 的等待时间极长（到达 safepoint：超过 25 秒） 2. Safepoint 日志显示由 GC 导致的延长 safepoint 持续时间（在 safepoint：超过 37 秒） 检查 GC 日志，我们注意到 Heap before GC invocations 和堆结构输出日志之间存在显著延迟： 3. 另一个由于 GC 导致 safepoint 持续时间延长的案例，但具有不同的日志间隔模式（超过 29 秒） GC 日志显示某些堆结构输出日志存在大量延迟： 问题调查 # 当 Java 应用线程集体处于 safepoint 时，它们变得完全不活跃。这意味着依赖应用线程的监控 - 如通过 Spring Actuator 的 Prometheus 端点和 Skywalking 插桩的外部 JVM 监控 - 对实际问题变得盲目。这些监控工具只显示在 safepoint 期间调用的方法花费异常长的时间，但这并不表明这些方法本身是瓶颈。\n我们需要依赖内部 JVM 监控机制，如 JVM 日志和 JFR（Java Flight Recording）进行正确诊断。我们还使用了 async_profiler (https://github.com/jvm-profiling-tools/async-profiler/)，因为我们观察到在问题期间，进程自身的 CPU 使用率（不是机器的，而是特指这个进程）会急剧飙升：\n奇怪的是，在问题时间段通过 async_profiler 检查 CPU 使用率时，我们发现除了预期模式外：\n最值得注意的是，在 safepoint 期间，日志记录似乎被中断 - 这是一个极其罕见的情况。以下是这个观察很重要的原因：\n对于第一种现象（所有线程进入 safepoint 的等待时间延长），我们通常期望连续输出日志，指示哪些线程尚未进入 safepoint，如 JVM 源代码中引用的：\nhttps://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/safepoint.cpp 然而，在我们的第一种现象中，我们没有观察到任何日志指示哪个特定线程导致了延长的 safepoint 进入时间。\n对于第二种现象，JFR 分析没有显示任何特别耗时的 GC 阶段： 对于第三种现象，检查 JVM 源代码显示，在两个有大量间隔的日志之间，除了日志记录本身，几乎什么都没有发生。此外，所有问题时间戳都发生在每小时的 05 分钟附近。咨询我们的运维团队后，我们了解到这个时间与前一小时的日志文件删除和 EFS 同步同时发生（我们每小时生成一个日志文件），涉及大量文件 I/O 操作（由于我们使用云服务，这可能涉及 EFS 类型的 NFS 或网络对象存储，而不是传统的磁盘 I/O）。过多的文件 I/O 是否会阻塞 JVM 日志输出，从而冻结整个 JVM？\n为什么 JVM 日志输出会阻塞所有应用线程？如果 JVM 线程在 safepoint 外输出日志时卡住，它只会阻塞自己，不会阻塞所有应用线程。然而，如果这发生在 safepoint 期间，当所有应用线程已经暂停时，卡住的 JVM 线程输出日志可能会阻止所有线程进入 safepoint 或导致延长的 safepoint 持续时间。对于第一种现象，输出 JVM 日志的线程（不是应用日志 - 应用日志输出通常涉及原生文件 I/O 调用，这些调用被认为处于 safepoint 状态，不会造成问题，如我的另一篇文章中详述：JVM 相关 - SafePoint 和 Stop The World 完整指南：https://zhuanlan.zhihu.com/p/161710652）卡住，阻止该线程进入 safepoint。对于现象二和三，GC 线程在输出 JVM 日志时卡住，阻止 GC 完成。\n让我们首先检查 JVM 源代码，确认卡住的 JVM 日志输出是否确实会阻塞 JVM。\nJVM 日志输出源代码分析 # 我们使用 Java 17，它引入了异步 JVM 日志输出（在早期版本中不可用）。对于我们的源代码分析，让我们忽略异步日志代码以了解 Java 17 之前的日志输出行为：\nhttps://github.com/openjdk/jdk/blob/master/src/hotspot/share/logging/logFileStreamOutput.cpp\n这段代码清楚地表明，如果文件 I/O 输出卡住，刷新操作将阻塞。此外，会有短暂的 CPU 峰值，因为刷新等待策略可能涉及 CPU 自旋一段时间，然后进入阻塞状态。\n切换到异步日志怎么样？有哪些参数可用？JVM 异步日志在 Java 17 中引入，对应问题：https://bugs.openjdk.org/browse/JDK-8229517，具有这些关键参数： 使用 -Xlog:async 启用 JVM 异步日志，并使用 -XX:AsyncLogBufferSize= 指定异步日志缓冲区大小（默认是 2097152，即 2MB）。异步日志原理如下：\n实施异步日志：显著改善但仍有问题 # 我们通过添加启动参数修改配置以使用异步日志：-Xlog:async 和 -XX:AsyncLogBufferSize=4194304。观察后，问题显著缓解：\n然而，一个实例仍然遇到问题，但症状不同。Safepoint 日志现在显示一个线程持续运行并拒绝进入 safepoint：\n这个线程在做什么？我们使用 jstack 来识别线程：\n这是一个用于刷新微服务实例列表的定时线程，代码没有正确使用 WebFlux：\n这种对异步代码的不当使用可能导致 JIT 优化错误（正确用法被频繁调用，这种错误用法也被频繁调用，导致持续的 JIT C2 优化和去优化）。JFR 分析显示在此期间有大量 JIT 去优化：\n这可能导致缺失 safepoint，导致 I/O 操作持续自旋并等待延长时间。我们需要纠正用法：\n实施正确的方法后，线程拒绝进入 safepoint 的问题完全消失了。\n","date":"2022年6月19日","externalUrl":null,"permalink":"/zh-cn/posts/log-copy-issue/","section":"文章","summary":"深入探讨诊断和解决生产 JVM 问题，其中应用在每小时日志同步任务期间会冻结。我们探索 safepoint 分析、JVM 日志输出阻塞、异步日志实现和 WebFlux 优化以实现完整解决方案。","title":"解决神秘的 JVM Safepoint 问题：从问题到解决方案的探索之旅","type":"posts"},{"content":" 解决 JVM Safepoint 延迟：从 EFS 集成到异步日志的探索之旅 # 最近，我们升级到了 Java 17，我们的 k8s 运维团队进行了优化以集中应用日志收集。他们配置了所有 pod（可以理解为独立的 Java 微服务进程）将 JVM 日志收集到统一的 AWS EFS 服务（Elastic File System - 本质上是 NFS + S3 对象存储集群）。我们的 JVM 日志配置包括几个关键组件：\nGC 日志：-Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M JIT 编译日志：-Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M Safepoint 日志：-Xlog:safepoint=trace:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M 禁用堆栈跟踪省略：这仅影响内部 JDK 异常，如 NullPointerException：-XX:-OmitStackTraceInFastThrow（我们已经优化了应用以处理高错误量期间过多堆栈跟踪输出的性能压力） 实施这些更改后，我们遇到了一个特殊问题，有三种不同的表现，但都有一个共同症状：极其延长的 safepoint 持续时间。\n1. Safepoint 日志显示所有线程到达 safepoint 的等待时间异常长（到达 safepoint：25+ 秒） 2. Safepoint 日志显示由于 GC 在 safepoint 花费的延长时间（在 safepoint：37+ 秒） 查看 GC 日志，Heap before GC invocations 和堆结构输出之间存在显著间隙： 3. 另一个由于 GC 导致 safepoint 时间延长的实例，但在不同日志位置存在间隙（29+ 秒） GC 日志显示堆结构输出之间存在大量间隔： 问题调查 # 当 Java 应用线程集体处于 safepoint 时，它们实际上被冻结了 - 无法执行任何操作。这意味着依赖应用线程的监控（如 Spring Actuator 的 Prometheus 端点或 Skywalking 插桩等外部 JVM 监控）会变得盲目，只显示在 safepoint 期间调用的方法似乎花费了异常长的时间，而实际上这些方法并不是真正的瓶颈。\n解决方案需要内部 JVM 线程监控机制，如 JVM 日志和 JFR（Java Flight Recording）。我们还使用了 async_profiler (https://github.com/jvm-profiling-tools/async-profiler/)，因为在问题期间，我们注意到进程的 CPU 使用率（不是机器的，而是特指这个进程）会急剧飙升：\n奇怪的是，在问题时间段通过 async_profiler 检查 CPU 使用率时，我们发现除了预期活动外：\n在 safepoint 期间，日志记录似乎被中断了 - 这是一个非常不寻常的情况。这个观察很重要，原因如下：\n对于第一种现象（等待所有线程进入 safepoint 的时间延长），我们通常期望看到连续输出显示哪些线程尚未进入 safepoint，如 JVM 源代码所示：\nhttps://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/safepoint.cpp 然而，在我们的第一种场景中，我们没有看到任何日志指示哪个特定线程导致了延迟。\n对于第二种现象，JFR 没有显示任何特别耗时的 GC 阶段： 对于第三种现象，检查 JVM 源代码显示，在两个有显著间隙的日志之间，除了日志记录本身，几乎什么都没有发生。此外，所有问题事件都发生在每小时的 05 分钟标记附近。咨询我们的运维团队后，我们了解到这与每小时日志文件轮转和 EFS 同步同时发生（我们每小时生成一个日志文件），涉及大量文件 I/O（考虑到底层云服务架构，这可能涉及 EFS NFS 或网络对象存储，而不是传统的磁盘 I/O）。过多的文件 I/O 是否会阻塞 JVM 日志输出，从而冻结整个 JVM？\n为什么 JVM 日志输出会阻塞所有应用线程？如果 JVM 线程在 safepoint 外输出日志时卡住，它只会影响自己，不会影响所有应用线程。然而，在 safepoint 期间，所有应用线程已经暂停。如果此时 JVM 线程在日志输出上被阻塞，它可能会阻止线程进入 safepoint 或导致 safepoint 持续时间延长。对于第一种现象，输出 JVM 日志的线程（不是应用日志 - 应用日志输出通常涉及原生文件 I/O 调用，这些调用被认为在 safepoint 中，不会造成问题）卡住，阻止该线程进入 safepoint。对于现象二和三，GC 线程在输出 JVM 日志时卡住，阻止 GC 完成。\n让我们首先检查 JVM 源代码，确认阻塞的 JVM 日志输出是否确实会冻结 JVM。\nJVM 日志输出源代码分析 # 我们使用的是 Java 17，在此之前没有异步 JVM 日志输出。所以在以下源代码分析中，请忽略异步日志代码 - 这代表 Java 17 之前的日志输出行为：\nhttps://github.com/openjdk/jdk/blob/master/src/hotspot/share/logging/logFileStreamOutput.cpp\n这段代码清楚地表明，如果文件 I/O 输出卡住，刷新操作将阻塞。此外，会有短暂的 CPU 峰值，因为刷新等待策略可能涉及 CPU 自旋一段时间，然后进入阻塞状态。\n切换到异步日志怎么样？有哪些参数可用？JVM 异步日志在 Java 17 中引入，对应这个问题：https://bugs.openjdk.org/browse/JDK-8229517。关键在于这两个参数： 使用 -Xlog:async 启用 JVM 异步日志，并使用 -XX:AsyncLogBufferSize= 指定异步日志缓冲区大小（默认是 2097152，即 2MB）。异步日志原理如下：\n切换到异步日志：显著改善但未完全解决 # 我们通过添加启动参数将日志修改为异步模式：-Xlog:async 和 -XX:AsyncLogBufferSize=4194304。观察后，问题显著缓解：\n然而，一个实例仍然遇到问题，但症状与之前不同。Safepoint 日志显示一个线程持续运行并拒绝进入 safepoint：\n这个线程在做什么？我们使用 jstack 来识别它：\n这是一个负责定期刷新微服务实例列表的线程，代码没有正确使用 WebFlux：\n这种对异步代码的不当使用可能导致 JIT 优化错误（正确用法被频繁调用，这种错误用法也被频繁调用，导致持续的 JIT C2 优化和去优化）。JFR 显示在此期间有大量 JIT 去优化：\n这可能导致 safepoint 间隙，导致 I/O 持续自旋并等待延长时间。解决方案是实现正确的使用模式：\n进行此修正后，线程拒绝进入 safepoint 的问题完全消失了。\n","date":"2022年6月16日","externalUrl":null,"permalink":"/zh-cn/posts/async-log-issue/","section":"文章","summary":"深入调查升级到 Java 17 并实施 AWS EFS 集中式日志收集后出现的神秘 JVM safepoint 延迟问题。我们发现日志输出期间的文件 I/O 阻塞如何冻结整个 JVM 进程，并通过异步日志和正确的 WebFlux 实现解决了这个问题。","title":"解决 JVM Safepoint 延迟：从 EFS 集成到异步日志的探索之旅","type":"posts"},{"content":"","date":"2022年6月13日","externalUrl":null,"permalink":"/zh-cn/posts/","section":"文章","summary":"","title":"文章","type":"posts"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/tags/algorithms/","section":"Tags","summary":"","title":"Algorithms","type":"tags"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/categories/computer-science/","section":"Categories","summary":"","title":"Computer Science","type":"categories"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/tags/prng/","section":"Tags","summary":"","title":"Prng","type":"tags"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/categories/programming/","section":"Categories","summary":"","title":"Programming","type":"categories"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/tags/random/","section":"Tags","summary":"","title":"Random","type":"tags"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/tags/threading/","section":"Tags","summary":"","title":"Threading","type":"tags"},{"content":" 本系列将 Java 17 之前的随机数 API 以及 Java 17 之后的统一 API 都做了比较详细的说明，并且将随机数的特性以及实现思路也做了一些简单的分析，帮助大家明白为何会有这么多的随机数算法，以及他们的设计思路是什么。\n如何生成随机数 # 我们一般使用随机数生成器的时候，都认为随机数生成器（Pseudo Random Number Generator， PRNG）是一个黑盒：\n这个黑盒的产出，一般是一个数字。假设是一个 int 数字。这个结果可以转化成各种我们想要的类型，例如：如果我们想要的的其实是一个 long，那我们可以取两次，其中一次的结果作为高 32 位，另一次结果作为低 32 位，组成一个 long（boolean，byte，short，char 等等同理，取一次，取其中某几位作为结果）。如果我们想要的是一个浮点型数字，那么我们可以根据 IEEE 标准组合多次取随机 int 然后取其中某几位组合成浮点型数字的整数位以及小数位。\n如果要限制范围，最简单的方式是将结果取余 + 偏移实现。例如我们想取范围在 1 ~ 100 之间，那么我们就将结果先对 99 取余，然后取绝对值，然后 +1 即可。当然，由于取余操作是一个性能消耗比较高的操作，最简单的优化即检查这个数字 N 与 N-1 取与运算，如果等于 0 即这个书是 2 的 n 次方（2 的 n 次方 2 进制表示一定是 100000 这样的，减去 1 之后 为 011111，取与肯定是 0）；对于 2 的 n 次方取余相当于对 2 的 n 次方减一取与运算。这是一个简单的优化， 实际的优化要比这个复杂多。\n初始化这个黑盒的时候，一般采用一个 SEED 进行初始化，这个 SEED 的来源可能多种多样，这个我们先按下不表，先来看一些这个黑盒中的一些算法。\n线性同余算法 # 首先是最常见的随机数算法：线性同余（Linear Congruential Generator）。即根据当前 Seed 乘以一个系数 A，然后加上一个偏移 B，最后按照 C 进行取余（限制整体在一定范围内，这样才能选择出合适的 A 和 B，为什么要这么做后面会说），得出随机数，然后这个随机数作为下次随机的种子，即：\nX(n+1) = ( A * X(n) + B ) % C 这种算法的优势在于，实现简单，并且性能算是比较好的。 A，B 取值必须精挑细算，让在 C 范围内的所有数字都是等可能的出现的。例如一个极端的例子就是 A = 2， B = 2， C = 10，那么 1，3，5，7，9 这些奇数在后续都不可能出现。为了能计算出一个合适的 A 和 B，要限制 C 在一个比较可控的范围内。一般为了计算效率，将 C 限制为 2 的 n 次方。这样取余运算就可以优化为取与运算。不过好在，数学大师们已经将这些值（也就是魔法数）找到了，我们直接用就好了。\n这种算法生成的随机序列，是确定的，例如 X 下一个是 Y， Y 下一个是 Z，这可以理解成一个确定环（loop）。\n这个环的大小，即 Period。由于 Period 足够大，初始 SEED 一般也是每次不一样的，这样近似做到了随机。但是，假设我们需要多个随机数生成器的时候，就比较麻烦了，因为我们虽然能保证每个随机生成器的初始 SEED 不一样，但是在这种算法下，无法保证某个随机数生成器的初始 SEED 就是另一个随机数生成器初始 SEED 的下一个（或者很短步骤内的） SEED。举个例子，假设某个随机数生成器的初始 SEED 是 X，另一个是 Z，虽然 X 和 Z 可能看上去差距很大，但是他们在这个算法的随机序列中仅隔了一个 Y。这样的不同的随机数生成器，效果不好。\n那么如何能保证不同的随机数生成器之间间隔比较大呢？也就是，我们能通过简单计算（而不是计算 100w 次从而调到 100w 次之后的随机数）直接使另一个随机数生成器的初始 SEED 与当前这个的初始 SEED，间隔一个比较大的数，这种性质叫做可跳跃性。 基于线性反馈移位寄存器算法的 Xoshiro 算法给我们提供了一种可跳跃的随机数算法。\n线性反馈移位寄存器算法 # 线性反馈移位寄存器（Linear feedback shift register，LFSR）是指给定前一状态的输出，将该输出的线性函数再用作输入的移位寄存器。异或运算是最常见的单比特线性函数：对寄存器的某些位进行异或操作后作为输入，再对寄存器中的每个 bit 进行整体移位。\n但是如何选择这些 Bit，是一门学问，目前比较常见的实现是 XorShift 算法以及在此基础上进一步优化的 Xoshiro 的相关算法。Xoshiro 算法是一种比较新的优化随机数算法，计算很简单并且性能优异。同时实现了可跳跃性。\n这种算法是可跳跃的。假设我们要生成两个差距比较大的随机数生成器，我们可以使用一个随机初始 SEED 创建一个随机数生成器，然后利用算法的跳跃操作，直接生成一个间隔比较大的 SEED 作为另一个随机数生成器的初始 SEED。\n还有一点比较有意思的是，线性同余算法并不可逆，我们只能通过 X(n) 推出 X(n + 1)，而不能根据 X(n + 1) 直接推出 X(n)。这个操作对应的业务例如随机播放歌单，上一首下一首，我们不需要记录整个歌单，而是仅根据当前的随机数就能知道。线性反馈移位寄存器算法能实现可逆。\n线性反馈移位寄存器算法在生成不同的随机序列生成器也有局限性，即它们还是来自于同一个环，即使通过跳跃操作让不同的随机数生成器都间隔开了，但是如果压力不够均衡，随着时间的推移，它们还是有可能 SEED，又变成一样的了。那么有没有那种能生成不同随机序列环的随机算法呢？\nDotMix 算法 # DotMix 算法提供了另一种思路，即给定一个初始 SEED，设置一个固定步长 M，每次随机，将这个 SEED 加上步长 M，经过一个 HASH 函数，将这个值散列映射到一个 HASH 值：\nX(n+1) = HASH(X(n) + M) 这个算法对于 HASH 算法的要求比较高，重点要求 HASH 算法针对输入的一点改变则造成输出大幅度改变。基于 DotMix 算法的 SplitMix 算法使用的即 MurMurHash3 算法，这个即 Java 8 引入的 SplittableRandom 的底层原理。\n这种算法好在，我们很容易能明确两个不同参数的随机生成器他们的生成序列是不同的，例如一个生成的随机序列是 1，4，3，7，\u0026hellip; 另一个生成的是 1，5，3，2。这点正是线性同余算法无法做到的，他的序列无论怎么修改 SEED 也是确定的，而我们有不能随意更改算法中的 A、B、C 的值，因为可能会导致无法遍历到所有数字，这点之前已经说过了。Xoshiro 也是同理。而 SplitMix 算法不用担心，我们指定不同的 SEED 以及不同的步长 M 就可以保证生成的序列是不同的。这种可以生成不同序列的性质，称为可拆分性\n这也是 SplittableRandom 比 Random （Random 基于线性同余）更适合多线程的原因：\n假设多线程使用同一个 Random，保证了序列的随机性，但是有 CompareAndSet 新 seed 的性能损失。 假设每个线程使用 SEED 相同的 Random，则每个线程生成的随机序列相同。 假设每个线程使用 SEED 不相同的 Random，但是我们不能保证一个 Random 的 SEED 是否是另一个 Random SEED 的下一个结果（或者是很短步长以内的结果），这种情况下如果线程压力不均匀（线程池在比较闲的时候，其实只有一部分线程在工作，这些线程很可能他们私有的 Random 来到和其他线程同一个 SEED 的位置），某些线程也会有相同的随机序列。 使用 SplittableRandom 只要直接使用接口 split 就能给不同线程分配一个参数不同的 SplittableRandom ，并且参数不同基本就可以保证生成不了相同序列。\n思考：我们如何生成 Period 大于生成数字容量的随机序列呢？ # 最简单的做法，我们将两个 Period 等于容量的序列通过轮询合并在一起，这样就得到了 Period = 容量 + 容量 的序列：\n我们还可以直接记录两个序列的结果，然后将两个序列的结果用某种运算，例如异或或者散列操作拼到一起。这样，Period = 容量 * 容量。\n如果我们想扩展更多，都可以通过以上办法拼接。用一定的操作拼接不同算法的序列，我们可以得到每种算法的随机优势。 Java 17 引入的 LXM 算法就是一个例子。\nLXM 算法 # 这是在 Java 17 中引入的算法 LXM 算法（L 即线性同余，X 即 Xoshiro，M 即 MurMurHash）的实现较为简单，结合线性同余算法和 Xoshiro 算法，之后通过 MurMurHash 散列，例如：\nL34X64M：即使用一个 32 位的数字保存线性同余的结果，两个 32 位的数字保存 Xoshiro 算法的结果，使用 MurMurHash 散列合并这些结果到一个 64 位数字。 L128X256M：即使用两个 64 位的数字保存线性同余的结果，4 个 64 位的数字保存 Xoshiro 算法的结果，使用 MurMurHash 散列合并这些结果到一个 64 位数字。 LXM 算法通过 MurMurhash 实现了分割性，没有保留 Xoshiro 的跳跃性。\nSEED 的来源 # 由于 JDK 中所有的随机算法都是基于上一次输入的，如果我们使用固定 SEED 那么生成的随机序列也一定是一样的。这样在安全敏感的场景，不够合适，官方对于 cryptographically secure 的定义是，要求 SEED 必须是不可预知的，产生非确定性输出。\n在 Linux 中，会采集用户输入，系统中断等系统运行数据，生成随机种子放入池中，程序可以读取这个池子获取一个随机数。但是这个池子是采集一定数据后才会生成，大小有限，并且它的随机分布肯定不够好，所以我们不能直接用它来做随机数，而是用它来做我们的随机数生成器的种子。这个池子在 Linux 中被抽象为两个文件，这两个文件他们分别是：/dev/random 和 /dev/urandom。一个是必须采集一定熵的数据才放开从池子里面取否则阻塞，另一个则是不管是否采集够直接返回现有的。\n在 Linux 4.8 之前：\n在 Linux 4.8 之后：\n在熵池不够用的时候，file:/dev/random会阻塞，file:/dev/urandom不会。对于我们来说，/dev/urandom 一般就够用，所以一般通过-Djava.security.egd=file:/dev/./urandom设置 JVM 启动参数，使用 urandom 来减少阻塞。\n我们也可以通过业务中的一些特性，来定时重新设置所有 Random 的 SEED 来进一步增加被破解的难度，例如，每小时用过去一小时的活跃用户数量 * 下单数量作为新的 SEED。\n测试随机算法随机性 # 以上算法实现的都是伪随机，即当前随机数结果与上一次是强相关的关系。事实上目前基本所有快速的随机算法，都是这样的。\n并且就算我们让 SEED 足够隐秘，但是如果我们知道算法，还是可以通过当前的随机输出，推测出下一个随机输出。或者算法未知，但是能从几次随机结果反推出算法从而推出之后的结果。\n针对这种伪随机算法，需要验证算法生成的随机数满足一些特性，例如：\nperiod 尽可能长：a full cycle 或者 period 指的是随机序列将所有可能的随机结果都遍历过一遍，同时结果回到初始 seed 需要的结果个数。这个 period 要尽可能的长一些。 平均分布（equidistribution），生成的随机数的每个可能结果，在一个 Period 内要尽可能保证每种结果的出现次数是相同的。否则，会影响在某些业务的使用，例如抽奖这种业务，我们需要保证概率要准。 复杂度测试：生成的随机序列是否够复杂，不会有那种有规律的数字序列，例如等比数列，等差数列等等。 安全性测试：很难通过比较少的结果反推出这个随机算法。 目前，已经有很多框架工具用来针对某个算法生成的随机序列进行测试，评价随机序列结果，验证算法的随机性，常用的包括：\ntestU01 随机性测试：https://github.com/umontreal-simul/TestU01-2009/ NIST 随机性测试：https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-22r1a.pdf DieHarder Suite 随机性测试 Java 中内置的随机算法，基本都通过了 testU01 的大部分测试。目前，上面提到过的优化算法都或多或少的暴露出一些随机性问题。目前， Java 17 中的 LXM 算法是随机性测试中表现最好的。注意是随机性表现，而不是性能。\nJava 中涉及到的所有随机算法（不包括 SecureRandom） # Linear Congruential generator: https://doi.org/10.1093%2Fcomjnl%2F1.2.83 Linear-feedback shift register: https://www.ams.org/journals/mcom/1965-19-090/S0025-5718-1965-0184406-1/S0025-5718-1965-0184406-1.pdf XORShift: https://doi.org/10.18637%2Fjss.v008.i14 Xoroshiro128+: https://arxiv.org/abs/1805.01407 LXM: https://dl.packetstormsecurity.net/papers/general/Google_Chrome_3.0_Beta_Math.random_vulnerability.pdf SplitMix: http://gee.cs.oswego.edu/dl/papers/oopsla14.pdf 为什么我们在实际业务应用中很少考虑随机安全性问题 # 主要因为，我们一般做了负载均衡多实例部署，还有多线程。一般每个线程使用不同初始 SEED 的 Random 实例（例如 ThreadLocalRandom）。并且一个随机敏感业务，例如抽奖，单个用户一般都会限制次数，所以很难采集够足够的结果反推出算法以及下一个结果，而且你还需要和其他用户一起抽。然后，我们一般会限制随机数范围，而不是使用原始的随机数，这就更大大增加了反解的难度。最后，我们也可以定时使用业务的一些实时指标定时设置我们的 SEED，例如：，每小时用过去一小时的（活跃用户数量 * 下单数量）作为新的 SEED。\n所以，一般现实业务中，我们很少会用 SecureRandom。如果我们想初始 SEED 让编写程序的人也不能猜出来（时间戳也能猜出来），可以指定随机类的初始 SEED 源，通过 JVM 参数 -Djava.util.secureRandomSeed=true。这个对于所有 Java 中的随机数生成器都有效（例如，Random，SplittableRandom，ThreadLocalRandom 等等）\n对应源码：\nstatic { String sec = VM.getSavedProperty(\u0026#34;java.util.secureRandomSeed\u0026#34;); if (Boolean.parseBoolean(sec)) { //初始 SEED 从 SecureRandom 中取 // SecureRandom 的 SEED 源，在 Linux 中即我们前面提到的环境变量 java.security.egd 指定的 /dev/random 或者 /dev/urandom byte[] seedBytes = java.security.SecureRandom.getSeed(8); long s = (long)seedBytes[0] \u0026amp; 0xffL; for (int i = 1; i \u0026lt; 8; ++i) s = (s \u0026lt;\u0026lt; 8) | ((long)seedBytes[i] \u0026amp; 0xffL); seeder.set(s); } } 所以，针对我们的业务，我们一般只关心算法的性能以及随机性中的平均性，而通过测试的算法，一般随机性都没啥大问题，所以我们只主要关心性能即可。\n针对安全性敏感的业务，像是 SSL 加密，生成加密随机散列这种，则需要考虑更高的安全随机性。这时候才考虑使用 SecureRandom。SecureRandom 的实现中，随机算法更加复杂且涉及了一些加密思想，我们这里就不关注这些 Secure 的 Random 的算法了。\nJava 17 之前一般如何生成随机数以及对应的随机算法 # 首先放出算法与实现类的对应关系：\n使用 JDK 的 API # 1.使用 java.util.Random 和基于它的 API：\nRandom random = new Random(); random.nextInt(); Math.random() 底层也是基于 Random\njava.lang.Math：\npublic static double random() { return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); } private static final class RandomNumberGeneratorHolder { static final Random randomNumberGenerator = new Random(); } Random 本身是设计成线程安全的，因为 SEED 是 Atomic 的并且随机只是 CAS 更新这个 SEED：\njava.util.Random：\nprotected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) \u0026amp; mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed \u0026gt;\u0026gt;\u0026gt; (48 - bits)); } 同时也看出，Random 是基于线性同余算法的\n2.使用 java.util.SplittableRandom 和基于它的 API\nSplittableRandom splittableRandom = new SplittableRandom(); splittableRandom.nextInt(); 前面的分析我们提到了，SplittableRandom 基于 SplitMix 算法实现，即给定一个初始 SEED，设置一个固定步长 M，每次随机，将这个 SEED 加上步长 M，经过一个 HASH 函数（这里是 MurMurHash3），将这个值散列映射到一个 HASH 值。\nSplittableRandom 本身不是线程安全的： java.util.SplittableRandom：\npublic int nextInt() { return mix32(nextSeed()); } private long nextSeed() { //这里非线程安全 return seed += gamma; } ThreadLocalRandom 基于 SplittableRandom 实现，我们在多线程环境下使用 ThreadLocalRandom：\nThreadLocalRandom.current().nextInt(); SplittableRandom 可以通过 split 方法返回一个参数全新，随机序列特性差异很大的新的 SplittableRandom，我们可以将他们用于不同的线程生成随机数，这在 parallel Stream 中非常常见：\nIntStream.range(0, 1000) .parallel() .map(index -\u0026gt; usersService.getUsersByGood(index)) .map(users -\u0026gt; users.get(splittableRandom.split().nextInt(users.size()))) .collect(Collectors.toList()); 但是由于没有做对齐性填充以及其他一些多线程性能优化的东西，导致其多线程环境下的性能表现还是比基于 SplittableRandom 的 ThreadLocalRandom 要差。\n3. 使用 java.security.SecureRandom 生成安全性更高的随机数\nSecureRandom drbg = SecureRandom.getInstance(\u0026#34;DRBG\u0026#34;); drbg.nextInt(); 一般这种算法，基于加密算法实现，计算更加复杂，性能也比较差，只有安全性非常敏感的业务才会使用，一般业务（例如抽奖）这些是不会使用的。\n测试性能 # 单线程测试：\nBenchmark Mode Cnt Score Error Units TestRandom.testDRBGSecureRandomInt thrpt 50 940907.223 ± 11505.342 ops/s TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 992789.814 ± 71312.127 ops/s TestRandom.testRandomInt thrpt 50 106491372.544 ± 8881505.674 ops/s TestRandom.testRandomIntWithBound thrpt 50 99009878.690 ± 9411874.862 ops/s TestRandom.testSplittableRandomInt thrpt 50 295631145.320 ± 82211818.950 ops/s TestRandom.testSplittableRandomIntWithBound thrpt 50 190550282.857 ± 17108994.427 ops/s TestRandom.testThreadLocalRandomInt thrpt 50 264264886.637 ± 67311258.237 ops/s TestRandom.testThreadLocalRandomIntWithBound thrpt 50 162884175.411 ± 12127863.560 ops/s 多线程测试：\nBenchmark Mode Cnt Score Error Units TestRandom.testDRBGSecureRandomInt thrpt 50 2492896.096 ± 19410.632 ops/s TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 2478206.361 ± 111106.563 ops/s TestRandom.testRandomInt thrpt 50 345345082.968 ± 21717020.450 ops/s TestRandom.testRandomIntWithBound thrpt 50 300777199.608 ± 17577234.117 ops/s TestRandom.testSplittableRandomInt thrpt 50 465579146.155 ± 25901118.711 ops/s TestRandom.testSplittableRandomIntWithBound thrpt 50 344833166.641 ± 30676425.124 ops/s TestRandom.testThreadLocalRandomInt thrpt 50 647483039.493 ± 120906932.951 ops/s TestRandom.testThreadLocalRandomIntWithBound thrpt 50 467680021.387 ± 82625535.510 ops/s 结果和我们之前说明的预期基本一致，多线程环境下 ThreadLocalRandom 的性能最好。单线程环境下 SplittableRandom 和 ThreadLocalRandom 基本接近，性能要好于其他的。SecureRandom 和其他的相比性能差了几百倍。\n测试代码如下（注意虽然 Random 和 SecureRandom 都是线程安全的，但是为了避免 compareAndSet 带来的性能衰减过多，还是用了 ThreadLocal。）：\npackage prng; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import java.util.SplittableRandom; import java.util.concurrent.ThreadLocalRandom; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; //测试指标为吞吐量 @BenchmarkMode(Mode.Throughput) //需要预热，排除 jit 即时编译以及 JVM 采集各种指标带来的影响，由于我们单次循环很多次，所以预热一次就行 @Warmup(iterations = 1) //线程个数 @Threads(10) @Fork(1) //测试次数，我们测试50次 @Measurement(iterations = 50) //定义了一个类实例的生命周期，所有测试线程共享一个实例 @State(value = Scope.Benchmark) public class TestRandom { ThreadLocal\u0026lt;Random\u0026gt; random = ThreadLocal.withInitial(Random::new); ThreadLocal\u0026lt;SplittableRandom\u0026gt; splittableRandom = ThreadLocal.withInitial(SplittableRandom::new); ThreadLocal\u0026lt;SecureRandom\u0026gt; drbg = ThreadLocal.withInitial(() -\u0026gt; { try { return SecureRandom.getInstance(\u0026#34;DRBG\u0026#34;); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException(e); } }); @Benchmark public void testRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(random.get().nextInt()); } @Benchmark public void testRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字，因为这种数字一般不会作为实际应用的范围，但是底层针对这种数字有优化 blackhole.consume(random.get().nextInt(1, 100)); } @Benchmark public void testSplittableRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(splittableRandom.get().nextInt()); } @Benchmark public void testSplittableRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字，因为这种数字一般不会作为实际应用的范围，但是底层针对这种数字有优化 blackhole.consume(splittableRandom.get().nextInt(1, 100)); } @Benchmark public void testThreadLocalRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(ThreadLocalRandom.current().nextInt()); } @Benchmark public void testThreadLocalRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字，因为这种数字一般不会作为实际应用的范围，但是底层针对这种数字有优化 blackhole.consume(ThreadLocalRandom.current().nextInt(1, 100)); } @Benchmark public void testDRBGSecureRandomInt(Blackhole blackhole) { blackhole.consume(drbg.get().nextInt()); } @Benchmark public void testDRBGSecureRandomIntWithBound(Blackhole blackhole) { //注意不取 2^n 这种数字，因为这种数字一般不会作为实际应用的范围，但是底层针对这种数字有优化 blackhole.consume(drbg.get().nextInt(1, 100)); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(TestRandom.class.getSimpleName()).build(); new Runner(opt).run(); } } Java 17 之后的变化 # 之前的 API 的缺点 # 没有统一的接口：之前的 Random 还有 SplittableRandom 没有统一的继承类，以及统一的抽象接口，虽然 他们内部方法基本一致，互相替换的麻烦并不多，但是这样我们要想实现自己的随机算法也比较麻烦，因为没有统一的接口。 ThreadLocalRandom 与未来的 Project Loom 的虚拟线程相性比较差。虚拟线程是可以不断创建的资源，在大量虚拟线程中如果还是用 ThreadLocalRandom 一一对应的话，会有随机性减弱的问题。所以，我们需要寻找新的实现方法，并且从现在开始为 Project Loom 铺路。 新的 API 定义 # 在 Java 17 中的 JEP 356: Enhanced Pseudo-Random Number Generators 中，统一了随机数生成器的接口，即 RandomGenerator：\n其中，针对我们前面提到的可跳跃性（可以通过简单计算，跳过序列环中的很多元素）抽象了对应的接口 JumpableGenerator，如果跳跃的步长希望更大一些的话，对应的就是 LeapableGenerator。\n针对我们前面提到的可拆分性（可以通过简单计算，拆分出生成完全不同序列的随机数生成器）也抽象了接口 SplitableGenerator\n前面提到的算法，与对应的实现类是：\n统一抽象后，我们就可以这样创建不同实现类型的随机数字生成器：\nRandomGenerator random = RandomGeneratorFactory.of(\u0026#34;Random\u0026#34;).create(); RandomGenerator secureRandom = RandomGeneratorFactory.of(\u0026#34;SecureRandom\u0026#34;).create(); RandomGenerator splittableRandom = RandomGeneratorFactory.of(\u0026#34;SplittableRandom\u0026#34;).create(); RandomGenerator xoroshiro128PlusPlus = RandomGeneratorFactory.of(\u0026#34;Xoroshiro128PlusPlus\u0026#34;).create(); RandomGenerator xoshiro256PlusPlus = RandomGeneratorFactory.of(\u0026#34;Xoshiro256PlusPlus\u0026#34;).create(); RandomGenerator l64X256MixRandom = RandomGeneratorFactory.of(\u0026#34;L64X256MixRandom\u0026#34;).create(); RandomGenerator l64X128StarStarRandom = RandomGeneratorFactory.of(\u0026#34;L64X128StarStarRandom\u0026#34;).create(); RandomGenerator l64X128MixRandom = RandomGeneratorFactory.of(\u0026#34;L64X128MixRandom\u0026#34;).create(); RandomGenerator l64X1024MixRandom = RandomGeneratorFactory.of(\u0026#34;L64X1024MixRandom\u0026#34;).create(); RandomGenerator l32X64MixRandom = RandomGeneratorFactory.of(\u0026#34;L32X64MixRandom\u0026#34;).create(); RandomGenerator l128X256MixRandom = RandomGeneratorFactory.of(\u0026#34;L128X256MixRandom\u0026#34;).create(); RandomGenerator l128X128MixRandom = RandomGeneratorFactory.of(\u0026#34;L128X128MixRandom\u0026#34;).create(); RandomGenerator l128X1024MixRandom = RandomGeneratorFactory.of(\u0026#34;L128X1024MixRandom\u0026#34;).create(); 每种算法实现的随机数生成器类的属性 # 1.Random：底层是基于线性同余算法生成的是一个 48 位的数字，选择的参数保证了每个数字都能随机出来，所以 Period 为 2^48。nextInt 和 nextLong 都不能做到完全均匀随机分布，因为生成的数字是 48 位的数字，nextInt 即取其中的 32 位，nextLong 是取两次组合在一起。之前的算法分析我们提到过，这种算法不能跳跃，不能分割。\n2.SplittableRandom: 底层基于 SplitMix 算法生成的一个 64 位的数字，通过 MurMurhash 保证了区间内每个数字都会出现（所以 Period 是 2^64），并且是完全均匀分布的。对于 nextInt 是一个 Period 内每个结果都会出现两次，对于 nextLong 是一个 Period 内每个结果都会出现一次。之前的算法分析我们提到过，这种算法不能跳跃，可以分割。\n3.Xoroshiro128PlusPlus：底层基于 Xoroshiro128++ 算法，使用两个 64 位数字记录状态，这两个数字不会同时为 0，这两个数字经过计算组合成为一个 64 位随机数。由于是两个 64 位数字组合而成，那么就有 2^64 * 2^64 = 2^128 种不同组合，两个数字不会同时为 0，那么就少了一种情况，所以一共是 2^128 - 1 种情况，所以 Period 是 2^128 - 1。之前的算法分析我们提到过，这种算法可以跳跃，不能分割。\n4.Xoshiro256PlusPlus：底层基于 Xoshiro256++ 算法，使用四个 64 位数字记录状态，这四个数字不会同时为 0，这四个数字经过计算组合成为一个 64 位随机数。由于是四个 64 位数字组合而成，那么就有 2^64 * 2^64 * 2^64 * 2^64 = 2^256 种不同组合，两个数字不会同时为 0，那么就少了一种情况，所以一共是 2^256 - 1 种情况，所以 Period 是 2^256 - 1。之前的算法分析我们提到过，这种算法可以跳跃，不能分割。\n5. L64X256MixRandom：底层基于 LXM 算法，使用一个 64 位数字保存线性同余的结果，4 个 64 位数字记录 Xoshiro 组合，线性同余有 2^64 种不同组合，Xoshiro 有 2^256 - 1 种组合，一共 2^64(2^256 - 1) 种组合，所以 Period 是 2^64(2^256 - 1)。之前的算法分析我们提到过，这种算法可以分割，不能跳跃。\n其他的 LXM 实现类是类似的。\n其实可以从每个算法的实现类的 RandomGeneratorProperties 注解上，看出他们的这些属性：\n@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface RandomGeneratorProperties { /** * 算法名称 */ String name(); /** * 算法类别 */ String group() default \u0026#34;Legacy\u0026#34;; /** * period 大小，由 i, j, k 三个数字描述，即： * period = (2^i - j) * 2^k */ int i() default 0; int j() default 0; int k() default 0; /** * 均匀分布性，0 或者最大值则不是均匀分布，这个值描述在一个 Period 内每个数字出现多少次，但是不是那么精准的，会忽略一些小的偏差，例如 Xoroshiro128++ 认为每个数字出现 2^64 次而不是 2^64 - 1 次。 */ int equidistribution() default Integer.MAX_VALUE; /** * 是否是基于系统 Entropy（参考前面的 SEED 来源章节）的算法 */ boolean isStochastic() default false; /** * 是否是硬件辅助的算法 */ boolean isHardware() default false; } 我们还可以通过下面的代码，查看每种实现的属性，同样的，也可以通过这些 API 对算法进行过滤，找到适合我们业务的实现类：\nRandomGeneratorFactory.all() .map(fac -\u0026gt; fac.group()+\u0026#34;:\u0026#34;+fac.name() + \u0026#34; {\u0026#34; + (fac.isSplittable()?\u0026#34; splitable\u0026#34;:\u0026#34;\u0026#34;) + (fac.isStreamable()?\u0026#34; streamable\u0026#34;:\u0026#34;\u0026#34;) + (fac.isJumpable()?\u0026#34; jumpable\u0026#34;:\u0026#34;\u0026#34;) + (fac.isLeapable()?\u0026#34; leapable\u0026#34;:\u0026#34;\u0026#34;) + (fac.isHardware()?\u0026#34; hardware\u0026#34;:\u0026#34;\u0026#34;) + (fac.isStatistical()?\u0026#34; statistical\u0026#34;:\u0026#34;\u0026#34;) + (fac.isStochastic()?\u0026#34; stochastic\u0026#34;:\u0026#34;\u0026#34;) + \u0026#34; stateBits: \u0026#34;+fac.stateBits() + \u0026#34; }\u0026#34; ) .sorted().forEach(System.out::println); 输出是：\nLXM:L128X1024MixRandom { splitable streamable statistical stateBits: 1152 } LXM:L128X128MixRandom { splitable streamable statistical stateBits: 256 } LXM:L128X256MixRandom { splitable streamable statistical stateBits: 384 } LXM:L32X64MixRandom { splitable streamable statistical stateBits: 96 } LXM:L64X1024MixRandom { splitable streamable statistical stateBits: 1088 } LXM:L64X128MixRandom { splitable streamable statistical stateBits: 192 } LXM:L64X128StarStarRandom { splitable streamable statistical stateBits: 192 } LXM:L64X256MixRandom { splitable streamable statistical stateBits: 320 } Legacy:Random { statistical stateBits: 48 } Legacy:SecureRandom { stochastic stateBits: 2147483647 } Legacy:SplittableRandom { splitable streamable statistical stateBits: 64 } Xoroshiro:Xoroshiro128PlusPlus { streamable jumpable leapable statistical stateBits: 128 } Xoshiro:Xoshiro256PlusPlus { streamable jumpable leapable statistical stateBits: 256 } 哪种算法最快（不用测也很明显） # 这个根据之前的分析，应该还是 SplittableRandom 在单线程环境下最快，多线程环境下使用 ThreadLocalRandom 最快。新增的随机算法实现类，Period 约大需要的计算越多， LXM 的实现需要更多计算，加入这些算法是为了适应更多的随机应用，而不是为了更快。不过为了满足大家的好奇心，还是写了如下的代码进行测试，从下面的代码也可以看出，新的 RandomGenerator API 使用更加简便：\npackage prng; import java.util.random.RandomGenerator; import java.util.random.RandomGeneratorFactory; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; //测试指标为吞吐量 @BenchmarkMode(Mode.Throughput) //需要预热，排除 jit 即时编译以及 JVM 采集各种指标带来的影响，由于我们单次循环很多次，所以预热一次就行 @Warmup(iterations = 1) //线程个数 @Threads(10) @Fork(1) //测试次数，我们测试50次 @Measurement(iterations = 50) //定义了一个类实例的生命周期，所有测试线程共享一个实例 @State(value = Scope.Benchmark) public class TestRandomGenerator { @Param({ \u0026#34;Random\u0026#34;, \u0026#34;SecureRandom\u0026#34;, \u0026#34;SplittableRandom\u0026#34;, \u0026#34;Xoroshiro128PlusPlus\u0026#34;, \u0026#34;Xoshiro256PlusPlus\u0026#34;, \u0026#34;L64X256MixRandom\u0026#34;, \u0026#34;L64X128StarStarRandom\u0026#34;, \u0026#34;L64X128MixRandom\u0026#34;, \u0026#34;L64X1024MixRandom\u0026#34;, \u0026#34;L32X64MixRandom\u0026#34;, \u0026#34;L128X256MixRandom\u0026#34;, \u0026#34;L128X128MixRandom\u0026#34;, \u0026#34;L128X1024MixRandom\u0026#34; }) private String name; ThreadLocal\u0026lt;RandomGenerator\u0026gt; randomGenerator; @Setup public void setup() { final String finalName = this.name; randomGenerator = ThreadLocal.withInitial(() -\u0026gt; RandomGeneratorFactory.of(finalName).create()); } @Benchmark public void testRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(randomGenerator.get().nextInt()); } @Benchmark public void testRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字，因为这种数字一般不会作为实际应用的范围，但是底层针对这种数字有优化 blackhole.consume(randomGenerator.get().nextInt(1, 100)); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(TestRandomGenerator.class.getSimpleName()).build(); new Runner(opt).run(); } } 测试结果：\nBenchmark (name) Mode Cnt Score Error Units TestRandomGenerator.testRandomInt Random thrpt 50 276250026.985 ± 240164319.588 ops/s TestRandomGenerator.testRandomInt SecureRandom thrpt 50 2362066.269 ± 1277699.965 ops/s TestRandomGenerator.testRandomInt SplittableRandom thrpt 50 365417656.247 ± 377568150.497 ops/s TestRandomGenerator.testRandomInt Xoroshiro128PlusPlus thrpt 50 341640250.941 ± 287261684.079 ops/s TestRandomGenerator.testRandomInt Xoshiro256PlusPlus thrpt 50 343279172.542 ± 247888916.092 ops/s TestRandomGenerator.testRandomInt L64X256MixRandom thrpt 50 317749688.838 ± 245196331.079 ops/s TestRandomGenerator.testRandomInt L64X128StarStarRandom thrpt 50 294727346.284 ± 283056025.396 ops/s TestRandomGenerator.testRandomInt L64X128MixRandom thrpt 50 314790625.909 ± 257860657.824 ops/s TestRandomGenerator.testRandomInt L64X1024MixRandom thrpt 50 315040504.948 ± 101354716.147 ops/s TestRandomGenerator.testRandomInt L32X64MixRandom thrpt 50 311507435.009 ± 315893651.601 ops/s TestRandomGenerator.testRandomInt L128X256MixRandom thrpt 50 187922591.311 ± 137220695.866 ops/s TestRandomGenerator.testRandomInt L128X128MixRandom thrpt 50 218433110.870 ± 164229361.010 ops/s TestRandomGenerator.testRandomInt L128X1024MixRandom thrpt 50 220855813.894 ± 47531327.692 ops/s TestRandomGenerator.testRandomIntWithBound Random thrpt 50 248088572.243 ± 206899706.862 ops/s TestRandomGenerator.testRandomIntWithBound SecureRandom thrpt 50 1926592.946 ± 2060477.065 ops/s TestRandomGenerator.testRandomIntWithBound SplittableRandom thrpt 50 334863388.450 ± 92778213.010 ops/s TestRandomGenerator.testRandomIntWithBound Xoroshiro128PlusPlus thrpt 50 252787781.866 ± 200544008.824 ops/s TestRandomGenerator.testRandomIntWithBound Xoshiro256PlusPlus thrpt 50 247673155.126 ± 164068511.968 ops/s TestRandomGenerator.testRandomIntWithBound L64X256MixRandom thrpt 50 273735605.410 ± 87195037.181 ops/s TestRandomGenerator.testRandomIntWithBound L64X128StarStarRandom thrpt 50 291151383.164 ± 192343348.429 ops/s TestRandomGenerator.testRandomIntWithBound L64X128MixRandom thrpt 50 217051928.549 ± 177462405.951 ops/s TestRandomGenerator.testRandomIntWithBound L64X1024MixRandom thrpt 50 222495366.798 ± 180718625.063 ops/s TestRandomGenerator.testRandomIntWithBound L32X64MixRandom thrpt 50 305716905.710 ± 51030948.739 ops/s TestRandomGenerator.testRandomIntWithBound L128X256MixRandom thrpt 50 174719656.589 ± 148285151.049 ops/s TestRandomGenerator.testRandomIntWithBound L128X128MixRandom thrpt 50 176431895.622 ± 143002504.266 ops/s TestRandomGenerator.testRandomIntWithBound L128X1024MixRandom thrpt 50 198282642.786 ± 24204852.619 ops/s 在之前的结果验证中，我们已经知道了 SplittableRandom 的在单线程中的性能最好，多线程环境下表现最好的是算法与它类似但是做了多线程优化的 ThreadLocalRandom.\n如何选择随机算法 # 原则是，看你的业务场景，所有的随机组合到底有多少个，在什么范围内。然后找大于这个范围的 Period 中，性能最好的算法。例如，业务场景是一副扑克除了大小王 52 张牌，通过随机数决定发牌顺序：\n第一张牌：randomGenerator.nextInt(0, 52)，从剩余的 52 张牌选 第二张牌：randomGenerator.nextInt(0, 51)，从剩余的 51 张牌选 以此类推 那么一共有 52! 这么多结果，范围在 2^225 ~ 2^226 之间。如果我们使用的随机数生成器的 Period 小于这个结果集，那么某些牌的顺序，我们可能永远生成不了。所以，我们需要选择一个 Period \u0026gt; 54! 的随机数生成器。\n未来展望 # Project Loom 中的随机数生成器 # 如果 Project Loom 中没有针对 ThreadLocal 的优化，那么 ThreadLocalRandom 的随机性表现也会变差，因为虚拟线程是一个可以不断生成回收的类似于对象的东西，ThreadLocalRandom 并不能无限枚举下去。这时候我们可能需要使用固定的多个随机数生成器给所有的虚拟线程使用，而不是使用 ThreadLocalRandom：\nExecutorService vte = Executors.newVirtualThreadExecutor(); SplittableGenerator source = RandomGeneratorFactory.\u0026lt;SplittableGenerator\u0026gt;of(\u0026#34;L128X1024MixRandom\u0026#34;).create(); //分割出 128 个不同的随机数生成器 List\u0026lt;RandomGenerator\u0026gt; rngs = source.splits(128); AtomicInteger i = new AtomicInteger(0); vte.submit(() -\u0026gt; { long random = rngs.get(Math.abs(i.incrementAndGet() \u0026amp; 127)).nextLong(); ... }); Scoped Local 特性下的随机数生成器 # Scoped Local 是一种更通用的域变量（例如 ThreadLocal 即当前线程域本地，Scoped Local 可以支持不同的域，包括虚拟线程，线程，方法域，包域等等），目前还没公布哪个版本会计划开发，不过按现在的爆料来看，我们可以使用下面这种方式更好的给每个线程分配随机数生成器：\nprivate final static ScopeLocal\u0026lt;SplittableGenerator\u0026gt; rng_scope = ScopeLocal.inheritableForType(SplittableGenerator.class); public static void main(String[] args) throws InterruptedException { SplittableGenerator rng1 = RandomGeneratorFactory.\u0026lt;SplittableGenerator\u0026gt;of(\u0026#34;L128X1024MixRandom\u0026#34;).create(); SplittableGenerator rng2 = RandomGeneratorFactory.\u0026lt;SplittableGenerator\u0026gt;of(\u0026#34;L32X64MixRandom\u0026#34;).create(); try (ExecutorService vte = Executors.newVirtualThreadExecutor()) { for (int i = 0; i \u0026lt; 5; i++) { ScopeLocal.where(rng_scope, rng1.split(), () -\u0026gt; { vte.submit(new Task()); }); } for (int i = 0; i \u0026lt; 5; i++) { ScopeLocal.where(rng_scope, rng2.split(), () -\u0026gt; { vte.submit(new Task()); }); } } } private static class Task implements Runnable { @Override public void run() { SplittableGenerator rng = rng_scope.get(); System.out.println(rng); } } ","date":"2022年6月1日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-2-java-random/","section":"文章","summary":"全面探索 Java 中的伪随机数生成器，涵盖从基本线性同余算法到 Java 17 中高级 LXM 实现的所有内容。了解算法性能、安全考虑以及如何为你的特定用例选择合适的随机数生成器。","title":"全网最硬核 JDK 分析 - 2. Java 随机数演进","type":"posts"},{"content":"","date":"2022年5月27日","externalUrl":null,"permalink":"/zh-cn/categories/mysql/","section":"Categories","summary":"","title":"MySQL","type":"categories"},{"content":" 解决分片 MySQL 表的性能下降：理解根本原因和解决方案 # 当业务需求要求数据查询量和并发负载超过单个 MySQL 实例的限制时，数据库分片成为首选解决方案。当然，现在有许多 NewSQL 分布式数据库选项可用。如果你使用 MySQL，你可能想考虑 TiDB（它实现 MySQL 协议并与 MySQL 客户端和 SQL 语句兼容）。如果你使用 PostgreSQL，YugaByteDB 可能是你的解决方案（实现 PostgreSQL 协议，具有完整的客户端和 SQL 兼容性）。两个平台都提供自己的云部署解决方案，你可以探索：\nTiDB Cloud YugaByte Cloud 然而，传统的分片项目仍然依赖 MySQL 和 PostgreSQL 等传统关系数据库作为基础。通常，当业务开始时，团队会考虑使用特定分片键在多个表之间分区数据。以订单表为例 - 我们可能估计用户主要需要查询过去一年的订单记录。对于超过一年的订单，我们将提供替代访问点，查询 HBase 等归档数据库，而不是运营业务数据库。\n假设我们估计一年内的用户订单不会超过 10 亿条记录，更新并发（TPS，不是查询 QPS）保持在 100,000/秒以下。在这种情况下，我们可能考虑拆分为 64 个表（最好使用 2 的幂，因为与 2^n 的模运算可以优化为与 2^n - 1 的按位 AND 运算，减少分片键计算的计算开销）。我们还将实施定期归档过程，使用 delete from table 等语句删除一年前的数据以进行\u0026quot;完全删除\u0026quot;（注意删除周围的引号）。这种方法将业务表数据量保持在可管理的水平。\n然而，随着时间的推移，你会注意到某些带有分片键（在这种情况下是 user_id）的查询开始变慢，一些错误地选择了本地索引。\n为什么查询逐渐变慢 # 考虑这个 SQL 示例：\nselect * from t_pay_record WHERE (( user_id = \u0026#39;user_id1\u0026#39; AND is_del = 0 )) ORDER BY id DESC LIMIT 20 这里，分片键是 user_id。\n一个因素是数据量可能超过我们的预期，导致某些分片表增长超过最佳阈值。这导致 MySQL 索引的随机采样越来越不准确。统计不是实时更新的，而是仅在更新的行数超过一定百分比时更新。此外，这些统计使用采样而不是全表分析。当表变得非常大时，维护准确的统计变得具有挑战性。\n依赖表的自动刷新机制使参数调整变得困难（主要是 STATS_SAMPLE_PAGES 参数 - 我们通常不修改 STATS_PERSISTENT，因为内存存储需要在数据库重启后重新分析表，减慢启动时间，我们也不禁用 STATS_AUTO_RECALC，因为这会使优化器分析越来越不准确）。预测最佳值几乎不可能，业务增长与用户行为模式相结合可能导致不可预测的数据倾斜。\n当通过 ALTER TABLE 修改表的 STATS_SAMPLE_PAGES 时，它会触发与在表上运行 ANALYZE 相同的效果，获取阻止更新和事务的读锁。这使得它不适合关键在线业务表。理想情况下，我们应该从一开始就估计大表量，但这在实践中证明相当困难。\n因此，对于具有大量数据的表，我们应该通过分片主动控制各个表的大小。然而，业务增长和产品需求不断演变并变得更加复杂，很难保证我们不会遇到具有复杂索引需求的大表。在这种情况下，我们需要适当增加 STATS_SAMPLE_PAGES 并使用 force index 引导关键用户触发的查询使用正确的索引。\n但有时，即使使用了正确的索引，查询仍然有些慢。检查 SQL 扫描的行数，你会发现它并不是特别高。\n+----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | t_pay_record | NULL | index | idx_user_id,idx_user_status_pay,idx_user_id_trade_code_status_amount_create_time_is_del | idx_user_id | 32 | NULL | 16 | 0.01 | Using where | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ 你可能仍然会遇到偶尔的慢 SQL 查询，这些查询随着时间的推移变得更加频繁。这与 MySQL InnoDB 的删除机制有关。大多数业务表使用 InnoDB 引擎和默认的 Dynamic 行格式。当以这种格式插入数据时，结构大致如下：\n记录头包含删除标志：\n当发生改变记录长度的更新（例如可变长度字段变长）时，原始记录被标记为删除，并在末尾创建更新的记录。当删除记录时，只有记录头中的删除标志被标记。\n为了解决潜在的碎片，MySQL InnoDB 有期望和措施：InnoDB 引擎只存储占用每页空间 93% 的数据，留下其余部分以容纳长度变化的更新，而不会强制数据到其他页。然而，删除基本上完全浪费存储空间。\n通常，这不会导致显著的性能下降，因为删除通常针对旧数据，而更新集中在最近的数据。例如，订单更新通常影响最近的订单，旧订单很少被更新，归档删除通常针对更旧的订单。然而，随着业务逻辑变得更加复杂，归档逻辑也变得更加复杂。不同的订单类型可能有不同的保留期 - 也许一年前的预订单仍未结算，无法归档。随着时间的推移，你的数据可能看起来像这样：\n这导致原本需要扫描几页的内容逐渐需要扫描更多页，因为碎片增加，使 SQL 执行越来越慢。\n以上描述了对表数据存储本身的影响。对于二级索引，MVCC 机制意味着对索引字段的频繁更新也会在索引中创建许多间隙。参考文档：https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html\nInnoDB 多版本并发控制（MVCC）对二级索引的处理与聚集索引不同。聚集索引中的记录就地更新，它们的隐藏系统列指向撤销日志条目，可以从中重建记录的早期版本。与聚集索引记录不同，二级索引记录不包含隐藏系统列，也不就地更新。\n我们知道 MySQL InnoDB 就地更新聚集索引，但对于二级索引，当更新二级索引列时，原始记录被标记为删除，并在其他地方创建新记录。这创建了与我们之前描述的类似的存储碎片。\n总结：\nMySQL InnoDB 的 Dynamic 行格式记录更新改变记录长度，以及 DELETE 语句，实际上标记原始记录为删除。虽然 MySQL InnoDB 用保留空间优化了这一点，但随着时间的推移累积的归档删除创建了内存碎片，降低了扫描效率。 二级索引列更新的 MVCC 机制标记原始记录为删除并在其他地方创建新记录，导致二级索引扫描效率随时间下降。 解决方案 - 表重建 # 对于这种情况，我们可以通过表重建来解决。重建表实际上实现了两个目标：首先，它优化存储碎片并减少要扫描的行；其次，它重新分析数据以获得更准确的 SQL 优化器统计。\n在 MySQL 5.6.17 之前，我们需要外部工具如 pt-online-schema-change 来帮助重建表。此工具通过内部创建新表，在原始表上添加触发器以在将数据复制到新表的同时将更新同步到新表，然后获取全局锁以将新表重命名为原始名称，然后删除原始表。在 MySQL 5.6.17 之后，OPTIMIZE TABLE 命令成为 Online DDL 操作，仅在准备和最终提交阶段需要锁，而不是在执行期间 - 这意味着它不会阻止业务 DML 操作。参考文档：https://dev.mysql.com/doc/refman/5.6/en/optimize-table.html\n在 Mysql 5.6.17 之前，OPTIMIZE TABLE 不使用在线 DDL。因此，在 OPTIMIZE TABLE 运行时不允许在表上进行并发 DML（INSERT、UPDATE、DELETE），并且二级索引的创建效率不高。\n从 MySQL 5.6.17 开始，OPTIMIZE TABLE 对常规和分区 InnoDB 表使用在线 DDL，这减少了并发 DML 操作的停机时间。OPTIMIZE TABLE 触发的表重建就地完成。独占表锁仅在操作的准备阶段和提交阶段短暂获取。在准备阶段，更新元数据并创建中间表。在提交阶段，提交表元数据更改。\n在 InnoDB 表上使用 OPTIMIZE TABLE 时的关键考虑：\n对于大多数 InnoDB 表，OPTIMIZE TABLE 本质上等于表重建 + ANALYZE 命令（等同于 ALTER TABLE ... FORCE），但与 ANALYZE 不同，OPTIMIZE TABLE 是具有优化机制的在线 DDL，仅在准备和最终提交阶段获取表锁，大大减少业务 DML 阻塞时间 - 使其成为在线执行的可行优化语句（适用于 MySQL 5.6.17 及更高版本） mysql\u0026gt; OPTIMIZE TABLE foo; +----------+----------+----------+-------------------------------------------------------------------+ | Table | Op | Msg_type | Msg_text | +----------+----------+----------+-------------------------------------------------------------------+ | test.foo | optimize | note | Table does not support optimize, doing recreate + analyze instead | | test.foo | optimize | status | OK | +----------+----------+----------+-------------------------------------------------------------------+ 尽管如此，在低业务流量期间执行 OPTIMIZE TABLE。与其他 Online DDL 操作一样，它创建并记录临时文件，记录 DDL 操作期间的所有 DML 插入、更新和删除。在高峰流量期间运行可能导致日志超过 innodb_online_alter_log_max_size 限制： mysql\u0026gt; OPTIMIZE TABLE foo; +----------+----------+----------+----------------------------------------------------------------------------------------------------------------------------+ | Table | Op | Msg_type | Msg_text | +----------+----------+----------+----------------------------------------------------------------------------------------------------------------------------+ | test.foo | optimize | note | Table does not support optimize, doing recreate + analyze instead | | test.foo | optimize | error | Creating index \u0026#39;PRIMARY\u0026#39; required more than \u0026#39;innodb_online_alter_log_max_size\u0026#39; bytes of modification log. Please try again.| | test.foo | optimize | status | OK | +----------+----------+----------+----------------------------------------------------------------------------------------------------------------------------+ 如果你在低流量期间遇到此错误，可以稍微增加 innodb_online_alter_log_max_size，但不要使其太大 - 建议以 128 MB 为增量增加（默认是 128 MB）。过大的大小可能导致两个问题：（1）由于大日志，最终阶段的提交时间延长，导致长时间锁定。（2）来自业务压力的临时文件持续写入永远无法赶上，导致语句仍在高峰流量期间执行。\n在评估对在线业务的影响时，监控锁 wait/synch/sxlock/innodb/dict_sys_lock 和 wait/synch/sxlock/innodb/dict_operation_lock。如果这些与锁相关的事件变得过多，并且你注意到在线有明显的慢 SQL 查询，考虑终止操作并重新安排 OPTIMIZE TABLE 到另一个时间。\nselect thread_id,event_id,event_name,timer_wait from events_waits_history where event_name Like \u0026#34;%dict%\u0026#34; order by thread_id; SELECT event_name,COUNT_STAR FROM events_waits_summary_global_by_event_name where event_name Like \u0026#34;%dict%\u0026#34; ORDER BY COUNT_STAR DESC; ","date":"2022年5月27日","externalUrl":null,"permalink":"/zh-cn/posts/recreate_table/","section":"文章","summary":"全面指南，了解为什么 MySQL 查询在分片环境中随时间变慢，探讨存储碎片和 MVCC 相关问题的根本原因，并提供使用 OPTIMIZE TABLE 表重建技术维护最佳数据库性能的实用解决方案。","title":"解决分片 MySQL 表的性能下降：理解根本原因和解决方案","type":"posts"},{"content":"","date":"28 March 2022","externalUrl":null,"permalink":"/categories/concurrency/","section":"Categories","summary":"","title":"Concurrency","type":"categories"},{"content":"","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/memory-barriers/","section":"Tags","summary":"","title":"Memory-Barriers","type":"tags"},{"content":"","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/memory-model/","section":"Tags","summary":"","title":"Memory-Model","type":"tags"},{"content":"","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/multithreading/","section":"Tags","summary":"","title":"Multithreading","type":"tags"},{"content":"","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/tags/volatile/","section":"Tags","summary":"","title":"Volatile","type":"tags"},{"content":"","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/categories/%E5%B9%B6%E5%8F%91/","section":"Categories","summary":"","title":"并发","type":"categories"},{"content":"相信很多 Java 开发，都使用了 Java 的各种并发同步机制，例如 volatile，synchronized 以及 Lock 等等。也有很多人读过 JSR 第十七章 Threads and Locks（地址：https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html），其中包括同步、Wait/Notify、Sleep \u0026amp; Yield 以及内存模型等等做了很多规范讲解。但是也相信大多数人和我一样，第一次读的时候，感觉就是在看热闹，看完了只是知道他是这么规定的，但是为啥要这么规定，不这么规定会怎么样，并没有很清晰的认识。同时，结合 Hotspot 的实现，以及针对 Hotspot 的源码的解读，我们甚至还会发现，由于 javac 的静态代码编译优化以及 C1、C2 的 JIT 编译优化，导致最后代码的表现与我们的从规范上理解出代码可能的表现是不太一致的。并且，这种不一致，导致我们在学习 Java 内存模型（JMM，Java Memory Model），理解 Java 内存模型设计的时候，如果想通过实际的代码去试，结果是与自己本来可能正确的理解被带偏了，导致误解。 我本人也是不断地尝试理解 Java 内存模型，重读 JLS 以及各路大神的分析。这个系列，会梳理我个人在阅读这些规范以及分析还有通过 jcstress 做的一些实验而得出的一些理解，希望对于大家对 Java 9 之后的 Java 内存模型以及 API 抽象的理解有所帮助。但是，还是强调一点，内存模型的设计，出发点是让大家可以不用关心底层而抽象出来的一些设计，涉及的东西很多，我的水平有限，可能理解的也不到位，我会尽量把每一个论点的论据以及参考都摆出来，请大家不要完全相信这里的所有观点，如果有任何异议欢迎带着具体的实例反驳并留言。\n1. 理解“规范”与“实现” # 首先，我想先参考 Aleksey Shipilëv 大神的理解思路，即首先分清楚规范（Specification）与实现（Implementation）的区别。前面提到的 JLS（Java Language Specification）其实就是一种规范，它规范了 Java 语言，并且所有能编译运行 Java 语言的 JDK 实现都要实现它里面规定的功能。但是对于实际的实现，例如 Hotspot JVM 的 JDK，就是具体的实现了，从规范到实际的实现，其实是有一定的差异的。首先是下面这个代码：\nprivate int test() { int x = 1; int y = 2; int result = x + y; return result; } 实际 HotSpot 最后编译并且经过 JIT 优化与 CPU 指令优化运行的代码其实是：\nmov $3, %rax ret 即将结果 3 放入寄存器并返回，这样与原始代码其实效果是一致的，省略了无用的本地变量操作，也是合理的。那么你可能会有疑问：不会呀，我打断点运行到这里的时候，能看到本地变量 x,y,result 呀。这个其实是 JVM 运行时做的工作，如果你是以 DEBUG 模式运行 JVM，那么其实 JIT 默认就不会启用，只会简单的解释执行，所以你能看到本地变量。但是实际执行中，如果这个方法是热点方法，经过 JIT 的优化，这些本地变量其实就不存在了。\n还有一个例子是，Hotspot 会有锁膨胀机制（这个我们后面还会测试），即：\nint x, y; private void test() { synchronized(this) { x = 1; } synchronized(this) { y = 1; } } 如果按照 JLS 的描述，那么 x = 1 与 y = 1 这两个操作是不能重排序的。但是 Hotspot 实际的实现会将上面的代码优化成：\nint x, y; private void test() { synchronized(this) { x = 1; y = 1; } } 那么这样，其实 x = 1 与 y = 1 这两个操作就可以重排序了，这个我们后面也会验证。\n不同的 JVM 实现，实际的表现都会有些差异。并且就算是同一个 JVM 实现，在不同的操作系统，硬件环境等等，表现也有可能不一样。例如下面这个例子：\nlong v; Thread 1 executes: v = 0xFFFFFFFF_FFFFFFFFL; Thread 2 executes: long r1 = v; 正常情况下，r1 的值应该只有 {-1, 0} 这两个结果之一。但是在某些 32 位的 JVM 上执行会有些问题，例如在 x86_32 的环境下，可能会有 {-1, 0, -4294967296, 4294967295} 这些结果。\n所以，如果我们要全面的覆盖底层到 JMM 设计以及 Hotspot 实现和 JIT 优化等等等等，涉及的东西太多太多，一层逻辑套逻辑，面面俱到我真的做不到。并且我也没法保证我理解的百分百准确。如果我们要涉及太多的 HotSpot 实现，那么我们可能就偏离了我们这个系列的主题，我们其实主要关心的是 Java 本身内存模型的设计规范，然后从中总结出我们在实际使用中，需要知道并且注意的点的最小集合，这个也是本系列要梳理的，同时，为了保证本系列梳理出的这个最小集合准确，会加上很多实际测试的代码，大家也可以跑一下看看这里给出的结论以及对于 JMM 的理解是否正确。\n2. 什么是内存模型 # 任何需要访问内存的语言，都需要有内存模型，描述如何访问内存：即我可以用哪些方式去写内存，可以用哪些方式去读取内存，不同的写入方式以及读取方式，会有什么不同的表现。当然，如果你的程序是一个简单的串行程序，你读取到的一定是最新写入的值，这样的情况下，其实你并不需要内存模型这种东西。一般是并发的环境下，才会需要内存模型这个东西。\nJava 内存模型其实就是规定了在 Java 多线程环境下，以不同的特定方式读取或者写入内存的时候，能观察到内存的合理的值。\n也有是这么定义 Java 内存的，即 Java 指令是会重排序的，Java 内存模型规定了哪些指令是禁止重排序的，实际上这也是 JLS 第 17 章中 Java 内存模型中的主要内容。这其实也是实现观察到内存的合理的值的方式，即对于给定的源代码，可能的结果集是什么。\n我们接下来看两个简单的入门例子，作为热身。分别是原子性访问，以及字分裂。\n3. 原子性访问 # 原子性访问，对于一个字段的写入与读取，这个操作本身是原子的不可分割的。可能大家不经常关注的一点是根据 JLS 第 17 章中的说明，下面这两个操作，并不是原子性访问的：\ndouble a; long b; public void test() { a = 1.2d; } 因为大家当前的系统通常都是 64 位的，得益于此，这两个操作大多是原子性的了。但是其实根据 Java 的规范，这两个并不是原子性的，在 32 位的系统上就保证不了原子性。我这里直接引用 JLS 第 17 章的一段原话：\nFor the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write. Writes and reads of volatile long and double values are always atomic.\n翻译过来，简单来说非 volatile 的 long 或者 double 可能会按照两次单独的 32 位写更新，所以是非原子性的。volatile 的 long 或者 double 读取和写入都是原子性的。\n为了说明我们这里的原子性，我引用一个 jcstress 中的一个例子：\n@JCStressTest //If result r1 is 0, it\u0026#39;s ACCEPTABLE, description: Seeing the default value: writer had not acted yet. @Outcome(id = \u0026#34;0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seeing the default value: writer had not acted yet.\u0026#34;) //If result r1 is -1, it\u0026#39;s ACCEPTABLE, description: Seeing the full value. @Outcome(id = \u0026#34;-1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seeing the full value.\u0026#34;) //If result r1 is other values, it\u0026#39;s ACCEPTABLE_INTERESTING, description: Other cases are violating access atomicity, but allowed under JLS. @Outcome( expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Other cases are violating access atomicity, but allowed under JLS.\u0026#34;) @State public static class Longs { long v; //One thread executes this method @Actor public void writer() { v = 0xFFFFFFFF_FFFFFFFFL; } //Another thread executes this method @Actor public void reader(J_Result r) { r.r1 = v; } } 我们使用 Java 8 32bit （Java 9 之后就不再支持 32 位的机器了）的 JVM 运行这里的代码，结果是：\nRESULT SAMPLES FREQ EXPECT DESCRIPTION -1 8,818,463,884 70.12% Acceptable Seeing the full value. -4294967296 9,586,556 0.08% Interesting Other cases are violating access atomicity, but allowed u... 0 3,747,652,022 29.80% Acceptable Seeing the default value: writer had not acted yet. 4294967295 86,082 \u0026lt;0.01% Interesting Other cases are violating access atomicity, but allowed u... 可以看到，结果不止 -1 和 0 这种我们代码中的指定的值，还有一些中间结果。\n4. 字分裂（word tearing） # 字分裂（word tearing）即你更新一个字段，数组中的一个元素，会影响到另一个字段，数组中的另一个元素的值。例如处理器没有提供写单个 byte 的功能，假设最小维度是 int，在这样的处理器上更新 byte 数组，若只是简单地读取 byte 所在的整个 int，更新对应的 byte，然后将整个 int 再写回，这种做法是有问题的。Java 中没有字分裂现象，字段之间以及数组元素之间是独立的，更新一个字段或元素不能影响任何其它字段或元素的读取与更新。\n为了说明什么是字分裂，举一个不太恰当的例子，即线程不安全的 BitSet。BitSet 的抽象是比特位集合（一个一个 0，1 这样，可以理解为一个 boolean 集合），底层实现是一个 long 数组，一个 long 保存 64 个比特位，每次更新都是读取这个 long 然后通过位运算更新对应的比特位，再更新回去。接口层面是一位一位更新，但是底层却是按照 long 的维度更新的（因为是底层 long 数组），很明显，如果没有同步锁，并发访问就会并发安全问题从而造成字分裂的问题：\n@JCStressTest //Result true true is ACCEPTABLE, representing no word tearing @Outcome(id = \u0026#34;true, true\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seeing both updates intact.\u0026#34;) //Result false true represents word tearing, because the result should be true true @Outcome(id = \u0026#34;false, true\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Destroyed one update.\u0026#34;) //Result true false represents word tearing, because the result should be true true @Outcome(id = \u0026#34;true, false\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Destroyed one update.\u0026#34;) @State public static class BitSets { BitSet bs = new BitSet(); //One thread executes this method @Actor public void writer1() { bs.set(0); } //Another thread executes this method @Actor public void writer2() { bs.set(1); } //After both threads finish and memory is visible, execute this method @Arbiter public void arbiter(ZZ_Result r) { r.r1 = bs.get(0); r.r2 = bs.get(1); } } 结果是：\nThe result is:\nRESULT SAMPLES FREQ EXPECT DESCRIPTION false, true 31,106,818 2.41% Interesting Destroyed one update. true, false 31,794,680 2.46% Interesting Destroyed one update. true, true 1,226,998,534 95.12% Acceptable Seeing both updates intact. 这里用了一个不太恰当的例子来说明什么是字分裂，Java 中是可以保证没有字分裂的，对应上面的 BitSet 的例子就是我们尝试更新一个 boolean 数组，这样结果就只会是 true true：\n@JCStressTest @Outcome(id = \u0026#34;true, true\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seeing both updates intact.\u0026#34;) @Outcome( expect = FORBIDDEN, desc = \u0026#34;Other cases are forbidden.\u0026#34;) @State public static class JavaArrays { boolean[] bs = new boolean[2]; @Actor public void writer1() { bs[0] = true; } @Actor public void writer2() { bs[1] = true; } @Arbiter public void arbiter(ZZ_Result r) { r.r1 = bs[0]; r.r2 = bs[1]; } } 这个结果只会是 true true\n接下来，我们将进入一个比较痛苦的章节了，内存屏障，不过大家也不用太担心，从我个人的经验来看，内存屏障很难理解的原因是因为网上基本上不会从 Java 已经为你屏蔽的底层细节去给你讲，直接理解会很难说服自己，于是就会猜想一些东西然后造成误解，所以本文不会上来丢给你 Doug Lea 抽象的并一直沿用至今的 Java 四种内存屏障（就是 LoadLoad，StoreStore，LoadStore 和 StoreLoad 这四个，其实通过后面的分析也能看出来，这四个内存屏障的设计对于现在的 CPU 来说已经有些过时了，现在用的更多的是 acquire, release 以及 fence）希望能通过笔者看的一些关于底层细节的文章论文中提取出便于大家理解的东西供大家参考，更好地更容易的理解内存屏障。\n5. 内存屏障 # 5.1. 为何需要内存屏障 # 内存屏障(Memory Barrier)，也有叫内存栅栏(Memory Fence)，还有的资料直接为了简便，就叫 membar，这些其实意思是一样的。内存屏障主要为了解决指令乱序带来了结果与预期不一致的问题，通过加入内存屏障防止指令乱序（或者称为重排序，reordering）。\n那么为什么会有指令乱序呢？主要是因为 CPU 乱序（CPU乱序还包括 CPU 内存乱序以及 CPU 指令乱序）以及编译器乱序。内存屏障可以用于防止这些乱序。如果内存屏障对于编译器和 CPU 都生效，那么一般称为硬件内存屏障，如果只对编译器生效，那么一般被称为软件内存屏障。我们这里主要关注 CPU 带来的乱序，对于编译器的重排序我们会在最后简要介绍下。\n5.2. CPU 内存乱序相关 # 我们从 CPU 高速缓存以及缓存一致性协议出发，开始分析为何 CPU 中会有乱序。我们这里假设一种简易的 CPU 模型，请大家一定记住，实际的 CPU 要比这里列举的简易 CPU 模型复杂的多\n5.2.1. 简易 CPU 模型 - CPU 高速缓存的出发点 - 减少 CPU Stall # 我们在这里会看到，现代的 CPU 的很多设计，一切以减少 CPU Stall 出发。什么是 CPU Stall 呢？举一个简单的例子，假设 CPU 需要直接读取内存中的数据（忽略其他的结构，例如 CPU 缓存，总线与总线事件等等）：\nCPU 发出读取请求，在内存响应之前，CPU 需要一直等待，无法处理其他的事情。这一段 CPU 就是处于 Stall 状态。如果 CPU 一直直接从内存中读取，CPU 直接访问内存消耗时间很长，可能需要几百个指令周期，也就是每次访问都会有几百个指令周期内 CPU 处于 Stall 状态什么也干不了，这样效率会很低。一般需要引入若干个高速缓存（Cache）来减少 Stall：高速缓存即与处理器紧挨着的小型存储器，位于处理器和内存之间。\n我们这里不关心多级高速缓存，以及是否存在多个 CPU 共用某一缓存的情况，我们就简单认为是下面这个架构：\n当需要读取一个地址的值时，访问高速缓存看是否存在：存在代表命中（hit），直接读取。不存在被称为缺失（miss）。同样的，如果需要写一个值到一个地址，这个地址在缓存中存在也就不需要访问内存了。大部分程序都表现出较高的局部性（locality）：\n如果处理器读或写一个内存地址，那么它很可能很快还会读或写同一个地址。 如果处理器读或写一个内存地址，那么它很可能很快还会读或写附近的地址。 针对局部性，高速缓存一般会一次操作不止一个字，而是一组临近的字，称为缓存行。\n但是呢，由于告诉缓存的存在，就给更新内存带来了麻烦：当一个 CPU 需要更新一块缓存行对应内存的时候，它需要将其他 CPU 缓存中这块内存的缓存行也置为失效。为了维持每个 CPU 的缓存数据一致性，引入了缓存一致性协议（Cache Coherence Protocols）\n5.2.2. 简易 CPU 模型 - 一种简单的缓存一致性协议(实际的 CPU 用的要比这个复杂) - MESI # 现代的缓存一致性的协议以及算法非常复杂，缓存行可能会有数十种不同的状态。这里我们并不需要研究这种复杂的算法，我们这里引入一个最经典最简单的缓存一致性协议即 4 状态 MESI 协议（再次强调，实际的 CPU 用的协议要比这个复杂，MESI 其实本身有些问题解决不了），MESI 其实指的就是缓存行的四个状态：\nModified：缓存行被修改，最终一定会被写回入主存，在此之前其他处理器不能再缓存这个缓存行。 Exclusive：缓存行还未被修改，但是其他的处理器不能将这个缓存行载入缓存 Shared：缓存行未被修改，其他处理器可以加载这个缓存行到缓存 Invalid：缓存行中没有有意义的数据 根据我们前面的 CPU 缓存结构图中所示，假设所有 CPU 都共用在同一个总线上，则会有如下这些信息在总线上发送：\nRead：这个事件包含要读取的缓存行的物理地址。 Read Response：包含前面的读取事件请求的数据，数据来源可能是内存或者是其他高速缓存，例如，如果请求的数据在其他缓存处于 modified 状态的话，那么必须从这个缓存读取缓存行数据作为 Read Response Invalidate：这个事件包含要过期掉的缓存行的物理地址。其他的高速缓存必须移除这个缓存行并且响应 Invalidate Acknowledge 消息。 Invalidate Acknowledge：收到 Invalidate 消息移除掉对应的缓存行之后，回复 Invalidate Acknowledge 消息。 Read Invalidate：是 Read 消息还有 Invalidate 消息的组合，包含要读取的缓存行的物理地址。既读取这个缓存行并且需要 Read Response 消息响应，同时发给其他的高速缓存，移除这个缓存行并且响应 Invalidate Acknowledge 消息。 Writeback：这个消息包含要更新的内存地址以及数据。同时，这个消息也允许状态为 modified 的缓存行被剔除，以给其他数据腾出空间。 缓存行状态转移与事件的关系：\n这里只是列出这个图，我们不会深入去讲的，因为 MESI 是一个非常精简的协议，具体实现的时候会有很多额外的问题 MESI 无法解决，如果详细的去讲，会把读者绕进去，读者会思考在某个极限情况下这个协议要怎么做才能保证正确，但是 MESI 实际上解决不了这些。在实际的实现中，CPU 一致性协议要比 MESI 复杂的多得多，但是一般都是基于 MESI 扩展的。\n举一个简单的 MESI 的例子：\n1.CPU A 发送 Read 从地址 a 读取数据，收到 Read Response 将数据存入他的高速缓存并将对应的缓存行置为 Exclusive\n2.CPU B 发送 Read 从地址 a 读取数据，CPU A 检测到地址冲突，CPU A 响应 Read Response 返回缓存中包含 a 地址的缓存行数据，之后，地址 a 的数据对应的缓存行被 A 和 B 以 Shared 状态装入缓存\n3.CPU B 对于 a 马上要进行写操作，发送 Invalidate，等待 CPU A 的 Invalidate Acknowledge 响应之后，状态修改为 Exclusive。CPU A 收到 Invalidate 之后，将 a 所在的缓存行状态置为 Invalid 失效\n4.CPU B 修改数据存储到包含地址 a 的缓存行上，缓存行状态置为 modified\n5.这时候 CPU A 又需要 a 数据，发送 Read 从地址 a 读取数据，CPU B 检测到地址冲突，CPU B 响应 Read Response 返回缓存中包含 a 地址的缓存行数据，之后，地址 a 的数据对应的缓存行被 A 和 B 以 Shared 状态装入缓存\n我们这里可以看到，MESI 协议中，发送 Invalidate 消息需要当前 CPU 等待其他 CPU 的 Invalidate Acknowledge，也就是这里有 CPU Stall。为了避免这个 Stall，引入了 Store Buffer\n5.2.3. 简易 CPU 模型 - 避免等待 Invalidate Response 的 Stall - Store Buffer # 为了避免这种 Stall，在 CPU 与 CPU 缓存之间添加 Store Buffer，如下图所示：\n有了 Store Buffer，CPU 在发送 Invalidate 消息的时候，不用等待 Invalidate Acknowledge 的返回，将修改的数据直接放入 Store Buffer。如果收到了所有的 Invalidate Acknowledge 再从 Store Buffer 放入 CPU 的高速缓存的对应缓存行中。但是加入的这个 Store Buffer 又带来了新的问题：\n假设有两个变量 a 和 b，不会处于同一个缓存行，初始都是 0，a 现在位于 CPU A 的缓存行中，b 现在位于 CPU B 的缓存行中：\n假设 CPU B 要执行下面的代码：\na = 1; b = a + 1; assert(b == 2) 我们肯定是期望最后 b 会等于 2 的。但是真的会如我们所愿么？我们来详细看下下面这个运行步骤：\n1.CPU B 执行 a = 1：\n(1)由于 CPU B 缓存中没有 a，并且要修改，所以发布 Read Invalidate 消息（因为是要先把包含 a 的整个缓存行读取后才能更新，所以发的是 Read Invalidate，而不只是 Invalidate）。\n(2)CPU B 将 a 的修改（a=1）放入 Storage Buffer\n(3)CPU A 收到 Read Invalidate 消息，将 a 所在的缓存行标记为 Invalid 并清除出缓存，并响应 Read Response（a=0） 和 Invalidate Acknowlegde。\n2.CPU B 执行 b = a + 1:\n(1)CPU B 收到来自于 CPU A 的 Read Response，这时候这里面 a 还是等于 0。\n(2)CPU B 将 a + 1 的结果(0+1=1)存入缓存中已经包含的 b。\n3.CPU B 执行 assert(b == 2) 失败\n这个错误的原因主要是我们在加载到缓存的时候没考虑从 store buffer 最新的值，所以我们可以加上一步，在加载到缓存的时候从 store buffer 读取最新的值。这样，就能保证上面我们看到的结果 b 最后是 2：\n5.2.4. 简易 CPU 模型 - 避免 Store Buffer 带来的乱序执行 - 内存屏障 # 我们下面再来看一个示例：假设有两个变量 a 和 b，不会处于同一个缓存行，初始都是 0。假设 CPU A （缓存行里面包含 b，这个缓存行状态是 Exclusive）执行：\nvoid foo(void) { a = 1; b = 1; } 假设 CPU B 执行：\nvoid bar(void) { while (b == 0) continue; assert(a == 1); } 如果一切按照程序顺序预期执行，那么我们期望 CPU B 执行 assert(a == 1) 是成功的，但是我们来看下面这种执行流程：\n1.CPU A 执行 a = 1：\n(1)CPU A 缓存里面没有 a，并且要修改，所以发布 Read Invalidate 消息。\n(2)CPU A 将 a 的修改（a=1）放入 Storage Buffer\n2.CPU B 执行 while (b == 0) continue:\n(1)CPU B 缓存里面没有 b，发布 Read 消息。\n3.CPU A 执行 b = 1：\n(1)CPU A 缓存行里面有 b，并且状态是 Exclusive，直接更新缓存行。\n(2)之后，CPU A 收到了来自于 CPU B 的关于 b 的 Read 消息。\n(3)CPU A 响应缓存中的 b = 1，发送 Read Response 消息，并且缓存行状态修改为 Shared\n(4)CPU B 收到 Read Response 消息，将 b 放入缓存\n(5)CPU B 代码可以退出循环了，因为 CPU B 看到 b 此时为 1\n4.CPU B 执行 assert(a == 1)，但是由于 a 的更改还没更新，所以失败了。\n像这种乱序，CPU 一般是无法自动控制的，但是一般会提供内存屏障指令，告诉 CPU 防止乱序，例如：\nvoid foo(void) { a = 1; smp_mb(); b = 1; } smp_mb() 会让 CPU 将 Store Buffer 中的内容刷入缓存。加入这个内存屏障指令后，执行流程变成：\n1.CPU A 执行 a = 1：\n(1)CPU A 缓存里面没有 a，并且要修改，所以发布 Read Invalidate 消息。\n(2)CPU A 将 a 的修改（a=1）放入 Storage Buffer\n2.CPU B 执行 while (b == 0) continue:\n(1)CPU B 缓存里面没有 b，发布 Read 消息。\n3.CPU A 执行 smp_mb()：\n(1)CPU A 将当前 Store Buffer 的所有条目打上标记（目前这里只有 a，就是对 a 打上标记）\n4.CPU A 执行 b = 1：\n(1)CPU A 缓存行里面有 b，并且状态是 Exclusive，但是由于 Store Buffer 中有标记的条目 a，不直接更新缓存行，而是放入 Store Buffer（与 a 不同，没有标记）。并发出 Invalidate 消息。\n(2)之后，CPU A 收到了来自于 CPU B 的关于 b 的 Read 消息。\n(3)CPU A 响应缓存中的 b = 0，发送 Read Response 消息，并且缓存行状态修改为 Shared\n(4)CPU B 收到 Read Response 消息，将 b 放入缓存\n(5)CPU B 代码不断循环，因为 CPU B 看到 b 还是 0\n(6)CPU A 收到前面对于 a 的 \u0026ldquo;Read Invalidate\u0026rdquo; 相关的消息响应，将 Store Buffer 中打好标记的 a 条目刷入缓存，这个缓存行状态为 modified。\n(7)CPU B 收到 CPU A 发的 Invalidate b 的消息，将 b 的缓存行失效，回复 Invalidate Acknowledge\n(8)CPU A 收到 Invalidate Acknowledge，将 b 从 Store Buffer 刷入缓存。\n(9)由于 CPU B 不断读取 b，但是 b 已经不在缓存中了，所以发送 Read 消息。\n(10)CPU A 收到 CPU B 的 Read 消息，设置 b 的缓存行状态为 shared，返回缓存中 b = 1 的 Read Response\n(11)CPU B 收到 Read Response，得知 b = 1，放入缓存行，状态为 shared\n5.CPU B 得知 b = 1，退出 while (b == 0) continue 循环\n6.CPU B 执行 assert(a == 1)（这个比较简单，就不画图了）： (1)CPU B 缓存中没有 a，发出 Read 消息。 (2)CPU A 从缓存中读取 a = 1，响应 Read Response (3)CPU B 执行 assert(a == 1) 成功\nStore Buffer 一般都会比较小，如果 Store Buffer 满了，那么还是会发生 Stall 的问题。我们期望 Store Buffer 能比较快的刷入 CPU 缓存，这是在收到对应的 Invalidate Acknowledge 之后进行的。但是，其他的 CPU 可能在忙，没发很快应对收到的 Invalidate 消息并响应 Invalidate Acknowledge，这样可能造成 Store Buffer 满了导致 CPU Stall 的发生。所以，可以引入每个 CPU 的 Invalidate queue 来缓存要处理的 Invalidate 消息。\n5.2.5. 简易 CPU 模型 - 解耦 CPU 的 Invalidate 与 Store Buffer - Invalidate Queues # 加入 Invalidate Queues 之后，CPU 结构如下所示：\n有了 Invalidate Queue，CPU 可以将 Invalidate 放入这个队列之后立刻将 Store Buffer 中的对应数据刷入 CPU 缓存。同时，CPU 在想主动发某个缓存行的 Invalidate 消息之前，必须检查自己的 Invalidate Queue 中是否有相同的缓存行的 Invalidate 消息。如果有，必须等处理完自己的 Invalidate Queue 中的对应消息再发。\n同样的，Invalidate Queue 也带来了乱序执行。\n5.2.6. 简易 CPU 模型 - 由于 Invalidate Queues 带来的进一步乱序 - 需要内存屏障 # 假设有两个变量 a 和 b，不会处于同一个缓存行，初始都是 0。假设 CPU A （缓存行里面包含 a(shared), b(Exclusive)）执行：\nvoid foo(void) { a = 1; smp_mb(); b = 1; } CPU B（缓存行里面包含 a(shared)）执行：\nvoid bar(void) { while (b == 0) continue; assert(a == 1); } 1.CPU A 执行 a = 1：\n(1)CPU A 缓存里面有 a（shared），CPU A 将 a 的修改（a=1）放入 Store Buffer，发送 Invalidate 消息。\n2.CPU B 执行 while (b == 0) continue:\n(1)CPU B 缓存里面没有 b，发布 Read 消息。\n(2)CPU B 收到 CPU A 的 Invalidate 消息，放入 Invalidate Queue 之后立刻返回。\n(3)CPU A 收到 Invalidate 消息的响应，将 Store Buffer 中的缓存行刷入 CPU 缓存\n3.CPU A 执行 smp_mb()：\n(1)因为 CPU A 已经把 Store Buffer 中的缓存行刷入 CPU 缓存，所以这里直接通过\n4.CPU A 执行 b = 1：\n(1)因为 CPU A 本身包含 b 的缓存行 (Exclusive)，直接更新缓存行即可。\n(2)CPU A 收到 CPU B 之前发的 Read 消息，将 b 的缓存行状态更新为 Shared，之后发送 Read Response 包含 b 的最新值\n(3)CPU B 收到 Read Response， b 的值为 1\n5.CPU B 退出循环，开始执行 assert(a == 1)\n(1)由于目前关于 a 的 Invalidate 消息还在 Invalidate queue 中没有处理，所以 CPU B 看到的还是 a = 0，assert 失败\n所以，我们针对这种乱序，在 CPU B 执行的代码中也加入内存屏障，这里内存屏障不仅等待 CPU 刷完所有的 Store Buffer，还要等待 CPU 的 Invalidate Queue 全部处理完。加入内存屏障，CPU B 执行的代码是：\nvoid bar(void) { while (b == 0) continue; smp_mb(); assert(a == 1); } 这样，在前面的第 5 步，CPU B 退出循环，执行 assert(a == 1) 之前需要等待 Invalidate queue 处理完： (1)处理 Invalidate 消息，将 b 置为 Invalid (2)继续代码，执行 assert(a == 1)，这时候缓存内不存在 b，需要发 Read 消息，这样就能看到 b 的最新值 1 了，assert 成功。\n5.2.7. 简易 CPU 模型 - 更细粒度的内存屏障 # 我们前面提到，在我们前面提到的 CPU 模型中，smp_mb() 这个内存屏障指令，做了两件事：等待 CPU 刷完所有的 Store Buffer，等待 CPU 的 Invalidate Queue 全部处理完。但是，对于我们这里 CPU A 与 CPU B 执行的代码中的内存屏障，并不是每次都要这两个操作同时存在：\nvoid foo(void) { a = 1; smp_mb();//Only needs to wait for CPU to flush all Store Buffer b = 1; } void bar(void) { while (b == 0) continue; smp_mb();//Only needs to wait for CPU\u0026#39;s Invalidate Queue to be completely processed assert(a == 1); } 所以，一般 CPU 还会抽象出更细粒度的内存屏障指令，我们这里管等待 CPU 刷完所有的 Store Buffer 的指令叫做写内存屏障(Write Memory Buffer)，等待 CPU 的 Invalidate Queue 全部处理完的指令叫做读内存屏障(Read Memory Buffer)。\n5.2.8. 简易 CPU 模型 - 总结 # 我们这里通过一个简单的 CPU 架构出发，层层递进，讲述了一些简易的 CPU 结构以及为何会需要内存屏障，可以总结为下面这个简单思路流程图：\nCPU 每次直接访问内存太慢，会让 CPU 一直处于 Stall 等待。为了减少 CPU Stall，加入了 CPU 缓存。 CPU 缓存带来了多 CPU 间的缓存不一致性，所以通过 MESI 这种简易的 CPU 缓存一致性协议协调不同 CPU 之间的缓存一致性 对于 MESI 协议中的一些机制进行优化，进一步减少 CPU Stall： 通过将更新放入 Store Buffer，让更新发出的 Invalidate 消息不用 CPU Stall 等待 Invalidate Response。 Store Buffer 带来了指令(代码)乱序，需要内存屏障指令，强制当前 CPU Stall 等待刷完所有 Store Buffer 中的内容。这个内存屏障指令一般称为写屏障。 为了加快 Store Buffer 刷入缓存，增加 Invalidate Queue， 5.3. CPU 指令乱序相关 # CPU 指令的执行，也可能会乱序，我们这里只说一种比较常见的 - 指令并行化。\n5.3.1. 增加 CPU 执行效率 - CPU 流水线模式（CPU Pipeline） # 现代 CPU 在执行指令时，是以指令流水线的模式来运行的。因为 CPU 内部也有不同的组件，我们可以将执行一条指令分成不同阶段，不同的阶段涉及的组件不同，这样伪解耦可以让每个组件独立的执行，不用等待一个指令完全执行完再处理下一个指令。\n一般分为如下几个阶段：取指（Instrcution Fetch，IF）、译码（Instruction Decode，ID）、执行（Execute，EXE）、存取（Memory，MEM）、写回（Write-Back， WB）\n5.3.2. 进一步降低 CPU Stall - CPU 乱序流水线（Out of order execution Pipeline） # 由于指令的数据是否就绪也是不确定的，比如下面这个例子：\nb = a + 1; c = 1; 倘若数据 a 没有就绪，还没有载入到寄存器，那么我们其实没必要 Stall 等待加载 a，可以先执行 c = 1; 由此，我们可以将程序中，可以并行的指令提取出来同时安排执行，CPU 乱序流水线（Out of order execution Pipeline）就是基于这种思路：\n如图所示，CPU 的执行阶段分为：\nInstructions Fetch：批量拉取一批指令，进行指令分析，分析其中的循环以及依赖，分支预测等等 Instruction Decode：指令译码，与前面的流水线模式大同小异 Reservation stations：需要操作数输入的指令，如果输入就绪，就进入 Functoinal Unit (FU) 处理，如果没有没有就绪就监听 Bypass network，数据就绪发回信号到 Reservation stations，让指令进图 FU 处理。 Functional Unit：处理指令 Reorder Buffer：会将指令按照原有程序的顺序保存，这些指令会在被 dispatched 后添加到列表的一端，而当他们完成执行后，从列表的另一端移除。通过这种方式，指令会按他们 dispatch 的顺序完成。 这样的结构设计下，可以保证写入 Store Buffer 的顺序，与原始的指令顺序一样。但是加载数据，以及计算，是并行执行的。前面我们已经知道了在我们的简易 CPU 架构里面，有着多 CPU 缓存 MESI， Store Buffer 以及 Invalidate Queue 导致读取不到最新的值，这里的乱序并行加载以及处理更加剧了这一点。并且，结构设计下，仅能保证检测出同一个线程下的指令之间的互相依赖，保证这样的互相依赖之间的指令执行顺序是对的，但是多线程程序之间的指令依赖，CPU 批量取指令以及分支预测是无法感知的。所以还是会有乱序。这种乱序，同样可以通过前面的内存屏障避免。\n5.4. 实际的 CPU # 实际的 CPU 多种多样，有着不同的 CPU 结构设计以及不同的 CPU 缓存一致性协议，就会有不同种类的乱序，如果每种单独来看，就太复杂了。所以，大家通过一种标准来抽象描述不同的 CPU 的乱序现象（即第一个操作为 M，第二个操作为 N，这两个操作是否会乱序，是不是很像 Doug Lea 对于 JMM 的描述，其实 Java 内存模型也是参考这个设计的），参考下面这个表格：\n我们先来说一下每一列的意思：\nLoads Reordered After Loads：第一个操作是读取，第二个也是读取，是否会乱序。 Loads Reordered After Stores：第一个操作是读取，第二个是写入，是否会乱序。 Stores Reordered After Stores：第一个操作是写入，第二个也是写入，是否会乱序。 Stores Reordered After Loads：第一个操作是写入，第二个是读取，是否会乱序。 Atomic Instructions Reordered With Loads：两个操作是原子操作（一组操作，同时发生，例如同时修改两个字这种指令）与读取，这两个互相是否会乱序。 Atomic Instructions Reordered With Stores：两个操作是原子操作（一组操作，同时发生，例如同时修改两个字这种指令）与写入，这两个互相是否会乱序。 Dependent Loads Reordered：如果一个读取依赖另一个读取的结果，是否会乱序。 Incoherent Instruction Cache/Pipeline：是否会有指令乱序执行。 举一个例子来看即我们自己的 PC 上面常用的 x86 结构，在这种结构下，仅仅会发生 Stores Reordered After Loads 以及 Incoherent Instruction Cache/Pipeline。其实后面要提到的 LoadLoad，LoadStore，StoreLoad，StoreStore 这四个 Java 中的内存屏障，为啥在 x86 的环境下其实只需要实现 StoreLoad，其实就是这个原因。\n5.5. 编译器乱序 # 除了 CPU 乱序以外，在软件层面还有编译器优化重排序导致的，其实编译器优化的一些思路与上面说的 CPU 的指令流水线优化其实有些类似。比如编译器也会分析你的代码，对相互不依赖的语句进行优化。对于相互没有依赖的语句，就可以随意的进行重排了。但是同样的，编译器也是只能从单线程的角度去考虑以及分析，并不知道你程序在多线程环境下的依赖以及联系。再举一个简单的例子，假设没有任何 CPU 乱序的环境下，有两个变量 x = 0，y = 0，线程 1 执行：\nx = 1; y = 1; 线程 2 执行：\nwhile(y == 1); assert(x == 1); 那么线程 2 是可能 assert 失败的，因为编译器可能会让 x = 1 与 y = 1 之间乱序。\n编译器乱序，可以通过增加不同操作系统上的编译器屏障语句进行避免。例如线程一执行：\nx = 1; compiler_barrier(); y = 1; 这样就不会出现 x = 1 与 y = 1 之间乱序的情况。\n同时，我们在实际使用的时候，一般内存屏障指的是硬件内存屏障，即通过硬件 CPU 指令实现的内存屏障，这种硬件内存屏障一般也会隐式地带上编译器屏障。编译器屏障一般被称为软件内存屏障，仅仅是控制编译器软件层面的屏障，举一个例子即 C++ 中的 volaile，它与 Java 中的 volatile 不一样， C++ 中的 volatile 仅仅是禁止编译器重排即有编译器屏障，但是无法避免 CPU 乱序。\n以上，我们就基本搞清楚了乱序的来源，以及内存屏障的作用。接下来，我们即将步入正题，开始我们的 Java 9+ 内存模型之旅。在这之前，再说一件需要注意的事情：为什么最好不要自己写代码验证 JMM 的一些结论，而是使用专业的框架去测试\n6. 为什么最好不要自己写代码验证 JMM 的一些结论 # 通过前面的一系列分析我们知道，程序乱序的问题错综复杂，假设一段代码，没有任何限制所有可能的输出结果是如下图所示这个全集：\n在 Java 内存模型的限制下，可能的结果被限制到了所有乱序结果中的一个子集：\n在 Java 内存模型的限制下，在不同的 CPU 架构上，CPU 乱序情况不同，有的场景有的 CPU 会乱序，有的则不会，但是都在 JMM 的范围内所以是合理的，这样所有可能的结果集又被限制到 JMM 的一个个不同子集：\n在 Java 内存模型的限制下，在不同的操作系统的编译器编译出来的 JVM 的代码执行顺序不同，底层系统调用定义不同，在不同操作系统执行的 Java 代码又有可能会有些微小的差异，但是由于都在 JMM 的限制范围内，所以也是合理的：\n最后呢，在不同的执行方式以及 JIT 编译下，底层执行的代码还是有差异的，进一步导致了结果集的分化。\n所以，如果你自己编写代码在自己的唯一一台电脑唯一一种操作系统上面去试，那么你所能试出来的结果集只是 JMM 的一个子集，很可能有些乱序结果你是看不到的。并且，有些乱序执行次数少或者没走到 JIT 优化，还看不到，所以，真的不建议你自己写代码去实验。\n那么应该怎么做呢？使用较为官方的用来测试并发可见性的框架 - jcstress，这个框架虽然不能模拟不同的 CPU 架构和不同操作系统，但是能让你排除不同执行（解释执行，C1执行，C2执行）以及测试压力不足次数少的原因，后面的所有讲解都会附上对应的 jcstress 代码实例供大家使用。\n7. 层层递进可见性与 Java 9+ 内存模型的对应 API # 这里主要参考了 Aleksey 大神的思路，去总结出不同层次，层层递进的 Java 中的一些内存可见性限制性质以及对应的 API。Java 9+ 中，将原来的普通变量（非 volatile，final 变量）的普通访问，定义为了 Plain。普通访问，没有对这个访问的地址做任何屏障（不同 GC 的那些屏障，比如分代 GC 需要的指针屏障，不是这里要考虑的，那些屏障只是 GC 层面的，对于这里的可见性没啥影响），会有前面提到的各种乱序。那么 Java 9+ 内存模型中究竟提出了那些限制以及对应这些限制的 API 是啥，我们接下层层递进讲述。\n7.1. Coherence（相干性，连贯性）与 Opaque # Java 9+ VarHandle API Compiler Barrier Memory Barrier Coherence Causality Consensus Plain (ordinary field access) None None Not guaranteed Not guaranteed Not guaranteed Opaque (similar to C++ volatile) Yes None Guaranteed Not guaranteed Not guaranteed 这里的标题我不太清楚究竟应该翻译成什么，因为我看网上很多地方把 CPU Cache Coherence Protocol 翻译成了 CPU 缓存一致性协议，即 Coherence 在那种语境下代表一致性，但是我们这里的 Coherence 如果翻译成一致性就不太合适。所以，之后的一些名词我也直接沿用 Doug Lea 大神的以及 Aleksey 大神的定义。\n那么这里什么是 coherence 呢？举一个简单的例子：假设某个对象字段 int x 初始为 0，一个线程执行：\nx = 1; 另一个线程执行(r1, r2 为本地变量)：\nint r1 = x; int r2 = x; 那么在 Java 内存模型下，可能的结果是包括：\nr1 = 1, r2 = 1 r1 = 0, r2 = 1 r1 = 1, r2 = 0 r1 = 0, r2 = 0 其中第三个结果很有意思，从程序上理解即我们先看到了 x = 1，之后又看到了 x 变成了 0.当然，通过前面的分析，我们知道实际上是因为编译器乱序。如果我们不想看到这个第三种结果，我们所需要的特性即 coherence。\ncoherence 的定义，我引用下原文：\nThe writes to the single memory location appear to be in a total order consistent with program order.\n即对单个内存位置的写看上去是按照与程序顺序一致的总顺序进行的。看上去有点难以理解，结合上面的例子，可以这样理解：在全局，x 由 0 变成了 1，那么每个线程中看到的 x 只能从 0 变成 1，而不会可能看到从 1 变成 0.\n正如前面所说，Java 内存模型定义中的 Plain 读写，是不能保证 coherence 的。但是如果大家跑一下针对上面的测试代码，会发现跑不出来第三种结果。这是因为 Hotspot 虚拟机中的语义分析会认为这两个对于 x 的读取（load）是互相依赖的，进而限制了这种乱序：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCoherence { int x; @Actor public void actor1(){ x = 1; } @Actor public void actor2(II_Result result){ result.r1 = x; result.r2 = x; } } 这就是我在前面一章中提到的，为什么最好不要自己写代码验证 JMM 的一些结论。虽然在 Java 内存模型的限制中，是允许第三种结果 1, 0 的，但是这里通过这个例子是试不出来的。\n我们这里通过一个别扭的例子来骗过 Java 编译器造成这种乱序：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCoherence { //h1,h2 are actually the same object private final Holder h1 = new Holder(); private final Holder h2 = h1; private static class Holder { //This is the field we want to access int a; //This field is mainly added to trick the compiler int trap; } @Actor public void actor1(){ //One thread updates the field h1.a = 1; } @Actor public void actor2(II_Result result){ //Another thread reads final Holder h1 = this.h1; final Holder h2 = this.h2; //Initialize, so subsequent loads of h1,h2 will be non-mutually dependent loads h1.trap = 1; h2.trap = 1; result.r1 = h1.a; result.r2 = h2.a; } } 我们不用太深究其原理，直接看结果：\nScheduling class: actor1: package group free, core group free actor2: package group free, core group free CPU allocation: unspecified Compilation: split actor1: C2 actor2: C2 JVM args: [-Dfile.encoding=GBK] Fork: #1 RESULT SAMPLES FREQ EXPECT DESCRIPTION 0, 0 12,077,605 19.67% Acceptable ordered 0, 1 54 \u0026lt;0.01% Acceptable ordered 1, 0 50,481 0.08% Interesting reordered 1, 1 49,262,708 80.24% Acceptable ordered 发现出现了乱序的结果，并且，如果你自己跑一下这个例子，会发现这个乱序是发生在执行 JIT C2 编译后的 actor2 方法才会出现。\n那么如何避免这种乱序呢？使用 volatile 肯定是可以避免的，但是这里我们并不用劳烦 volatile 这种重操作出马，就用 Opaque 访问即可。Opaque 其实就是禁止 Java 编译器优化，但是没有涉及任何的内存屏障，和 C++ 中的 volatile 非常类似。测试下：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCoherence { static final VarHandle VH; static { try { VH = MethodHandles.lookup().findVarHandle(Holder.class, \u0026#34;a\u0026#34;, int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalStateException(e); } } private final Holder h1 = new Holder(); private final Holder h2 = h1; private static class Holder { int a; int trap; } @Actor public void actor1(){ h1.a = 1; } @Actor public void actor2(II_Result result){ final Holder h1 = this.h1; final Holder h2 = this.h2; h1.trap = 1; h2.trap = 1; //This way, there won\u0026#39;t be reordering result.r1 = (int) VH.getOpaque(h1); result.r2 = (int) VH.getOpaque(h2); } } 运行下，可以发现，这个就没有乱序了(命令行如果没有 ACCEPTABLE_INTERESTING，FORBIDDEN，UNKNOWN 的 结果就不会输出了，只能最后看输出的 html)：\n7.2. Causality（因果性）与 Acquire/Release # Java 9+ VarHandle API Compiler Barrier Memory Barrier Coherence Causality Consensus Plain (ordinary field access) None None Not guaranteed Not guaranteed Not guaranteed Opaque (similar to C++ volatile) Yes None Guaranteed Not guaranteed Not guaranteed Release/Acquire Yes Release: LoadStore + StoreStore before; Acquire: LoadLoad + LoadStore after Guaranteed Guaranteed Not guaranteed 在 Coherence 的基础上，我们一般在某些场景还会需要 Causality\n一般到这里，大家会接触到两个很常见的词，即 happens-before 以及 synchronized-with order，我们这里先不从这两个比较晦涩的概念开始介绍（具体概念介绍不会在这一章节解释），而是通过一个例子，即假设某个对象字段 int x 初始为 0，int y 也初始为 0，这两个字段不在同一个缓存行中（后面的 jcstress 框架会自动帮我们进行缓存行填充），一个线程执行：\nx = 1; y = 1; 另一个线程执行(r1, r2 为本地变量)：\nint r1 = y; int r2 = x; 这个例子与我们前面的 CPU 缓存那里的乱序分析举得例子很像，在 Java 内存模型中，可能的结果有：\nr1 = 1, r2 = 1 r1 = 0, r2 = 1 r1 = 1, r2 = 0 r1 = 0, r2 = 0 同样的，第三个结果也是很有趣的，第二个线程先看到 y 更新，但是没有看到 x 的更新。这个在前面的 CPU 缓存乱序那里我们详细分析，在前面的分析中，我们需要像这样加内存屏障才能避免第三种情况的出现，即：\nx = 1; write_barrier(); y = 1; 以及\nint r1 = y; read_barrier(); int r2 = x; 简单回顾下，线程 1 执行 x = 1 之后，在 y = 1 之前执行了写屏障，保证 store buffer 的更新都更新到了缓存，y = 1 之前的更新都保证了不会因为存在 store buffer 中导致不可见。线程 2 执行 int r1 = y 之后执行了读屏障，保证 invalidate queue 中的需要失效的数据全部被失效，保证当前缓存中不会有脏数据。这样，如果线程 2 看到了 y 的更新，就一定能看到 x 的更新。\n我们进一步更形象的描述一下：我们把写屏障以及后面的一个 Store（即 y = 1）理解为将前面的更新打包，然后将这个包在这点发射出去，读屏障与前面一个 Load（即 int r1 = y）理解成一个接收点，如果接收到发出的包，就在这里将包打开并读取进来。所以，如下图所示：\n在发射点，会将发射点之前（包括发射点本身的信息）的所有结果打包，如果在执行接收点的代码的时候接收到了这个包，那么在这个接收点之后的所有指令就能看到包里面的所有内容，即发射点之前以及发射点的内容。Causality（因果性），有的地方也叫做 Casual Consistency（因果一致性），它在不同的语境下有不同的含义，我们这里仅特指：可以定义一系列写入操作，如果读取看到了最后一个写入，那么这个读取之后的所有读取操作，都能看到这个写入以及之前的所有写入操作。这是一种 Partial Order（半顺序），而不是 Total Order（全顺序），关于这个定义将在后面的章节详细说明。\n在 Java 中，Plain 访问与 Opaque 访问都不能保证 Causality，因为 Plain 没有任何的内存屏障，Opaque 只是有编译器屏障，我们可以通过如下代码测试出来：\n首先是 Plain：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCausality { int x; int y; @Actor public void actor1() { x = 1; y = 1; } @Actor public void actor2(II_Result result) { result.r1 = y; result.r2 = x; } } 结果是：\nRESULT SAMPLES FREQ EXPECT DESCRIPTION 0, 0 1,004,331,860 32.89% Acceptable ordered 0, 1 9,094,601 0.30% Acceptable ordered 1, 0 1,713,825 0.06% Interesting reordered 1, 1 2,038,722,626 66.76% Acceptable ordered 然后是 Opaque：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCausality { static final VarHandle VH_X; static final VarHandle VH_Y; static { try { VH_X = MethodHandles.lookup().findVarHandle(TestCausality.class, \u0026#34;x\u0026#34;, int.class); VH_Y = MethodHandles.lookup().findVarHandle(TestCausality.class, \u0026#34;y\u0026#34;, int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalStateException(e); } } int x; int y; @Actor public void actor1() { VH_X.setOpaque(this, 1); VH_Y.setOpaque(this, 1); } @Actor public void actor2(II_Result result) { result.r1 = (int) VH_Y.getOpaque(this); result.r2 = (int) VH_X.getOpaque(this); } } 这里我们需要注意：由于前面我们看到， x86 CPU 是天然保证一些指令不乱序的，稍后我们就能看到是哪些不乱序保证了这里的 Causality，所以 x86 的 CPU 都看不到乱序，Opaque 访问就能看到因果一致性的结果，如下图所示（AMD64 是一种 x86 的实现）：\n但是，如果我们换成其他稍微弱一致一些的 CPU，就能看到 Opaque 访问保证不了因果一致性，下面的结果是我在 aarch64 （是一种 arm 的实现）：\n并且，还有一个比较有意思的点，即乱序都是 C2 编译执行的时候发生的。\n那么，我们如何保证 Causality 呢？同样的，我们同样不必劳烦 volatile 这么重的操作，采用 release/acquire 模式即可。release/acquire 可以保证 Coherence + Causality。release/acquire 必须成对出现（一个 acquire 对应一个 release），可以将 release 视为前面提到的发射点，acquire 视为前面提到的接收点，那么我们就可以像下图这样实现代码：\n@JCStressTest @State @Outcome(id = \u0026#34;1, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;0, 1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) @Outcome(id = \u0026#34;1, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;reordered\u0026#34;) @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;ordered\u0026#34;) public class TestCausality { static final VarHandle VH_X; static final VarHandle VH_Y; static { try { VH_X = MethodHandles.lookup().findVarHandle(TestCausality.class, \u0026#34;x\u0026#34;, int.class); VH_Y = MethodHandles.lookup().findVarHandle(TestCausality.class, \u0026#34;y\u0026#34;, int.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalStateException(e); } } int x; int y; @Actor public void actor1() { x = 1; VH_Y.setRelease(this, 1); } @Actor public void actor2(II_Result result) { result.r1 = (int) VH_Y.getAcquire(this); result.r2 = x; } } 然后，继续在刚刚的 aarch64 的机器上面执行，结果是：\n可以看出，Causuality 由于使用了 Release/Acquire 保证了 Causality。注意，对于发射点和接收点的选取一定要选好，例如这里我们如果换个位置，那么就不对了：\n示例一：发射点只会打包之前的所有更新，对于 x = 1 的更新在发射点之后，相当于没有打包进去，所以还是会出现 1,0 的结果。\n@Actor public void actor1() { VH_Y.setRelease(this, 1); x = 1; } @Actor public void actor2(II_Result result) { result.r1 = (int) VH_Y.getAcquire(this); result.r2 = x; } 示例二：在接收点会解包，从而让后面的读取看到包里面的结果，对于 x 的读取在接收点之前，相当于没有看到包里面的更新，所以还是会出现 1,0 的结果。\n@Actor public void actor1() { x = 1; VH_Y.setRelease(this, 1); } @Actor public void actor2(II_Result result) { result.r2 = x; result.r1 = (int) VH_Y.getAcquire(this); } 由此，我们类比下 Doug Lea 的 Java 内存屏障设计，来看看这里究竟用了哪些 Java 中设计的内存屏障。在 Doug Lea 的很早也是很经典的一篇文章中，介绍了 Java 内存模型以及其中的内存屏障设计，提出了四种屏障：\n1.LoadLoad\n如果有两个完全不相干的互不依赖（即可以乱序执行的）的读取（Load），可以通过 LoadLoad 屏障避免它们的乱序执行（即在 Load(x) 执行之前不会执行 Load(y)）：\nLoad(x); LoadLoad Load(y); 2.LoadStore\n如果有一个读取（Load）以及一个完全不相干的（即可以乱序执行的）的写入（Store），可以通过 LoadStore 屏障避免它们的乱序执行（即在 Load(x) 执行之前不会执行 Store(y)）：\nLoad(x); LoadStore Store(y); 3.StoreStore\n如果有两个完全不相干的互不依赖（即可以乱序执行的）的写入（Store），可以通过 StoreStore 屏障避免它们的乱序执行（即在 Store(x) 执行之前不会执行 Store(y)）：\nStore(x); StoreStore Store(y); 4.StoreLoad\n如果有一个写入（Store）以及一个完全不相干的（即可以乱序执行的）的读取（Load），可以通过 LoadStore 屏障避免它们的乱序执行（即在 Store(x) 执行之前不会执行 Load(y)）：\nStore(x); StoreLoad Load(y); 那么如何通过这些内存屏障实现的 Release/Acquire 呢？我们可以通过前面我们的抽象推出来，首先是发射点。发射点首先是一个 Store，并且保证打包前面的所有，那么不论是 Load 还是 Store 都要打包，都不能跑到后面去，所以需要在 Release 的前面加上 LoadStore，StoreStore 两种内存屏障来实现。同理，接收点是一个 Load，并且保证后面的都能看到包里面的值，那么无论 Load 还是 Store 都不能跑到前面去，所以需要在 Acquire 的后面加上 LoadLoad，LoadStore 两种内存屏障来实现。\n但是呢我们可以在下一章中看到，其实目前来看这四个内存屏障的设计有些过时了（由于 CPU 的发展以及 C++ 语言的发展） ，JVM 内部用的更多的是 acquire，release，fence 这三个。这里的 acquire 以及 release 其实就是我们这里提到的 Release/Acquire。这三个与传统的四屏障的设计的关系是：\n我们这里知道了 Release/Acquire 的内存屏障，x86 为何没有设置这个内存屏障就没有这种乱序呢？参考前面的 CPU 乱序图：\n通过这里我们知道，x86 对于 Store 与 Store，Load 与 Load，Load 与 Store 都不会乱序，所以天然就能保证 Casuality\n7.3. Consensus（共识性）与 Volatile # Java 9+ VarHandle API Compiler Barrier Memory Barrier Coherence Causality Consensus Plain (ordinary field access) None None Not guaranteed Not guaranteed Not guaranteed Opaque (similar to C++ volatile) Yes None Guaranteed Not guaranteed Not guaranteed Release/Acquire Yes Release: LoadStore + StoreStore before; Acquire: LoadLoad + LoadStore after Guaranteed Guaranteed Not guaranteed Volatile Yes Volatile write: LoadStore + StoreStore before; StoreLoad after; Volatile read: LoadLoad + LoadStore after Guaranteed Guaranteed Guaranteed 最后终于来到我们所熟悉的 Volatile 了，Volatile 其实就是在 Release/Acquire 的基础上，进一步保证了 Consensus；Consensus 即所有线程看到的内存更新顺序是一致的，即所有线程看到的内存顺序全局一致，举个例子：假设某个对象字段 int x 初始为 0，int y 也初始为 0，这两个字段不在同一个缓存行中（后面的 jcstress 框架会自动帮我们进行缓存行填充），一个线程执行：\nx = 1; int r1 = y; 另一个执行：\ny = 1; int r2 = x; 在 Java 内存模型下，同样可能有4种结果：\nr1 = 1, r2 = 1 r1 = 0, r2 = 1 r1 = 1, r2 = 0 r1 = 0, r2 = 0 第四个结果比较有意思，他是不符合 Consensus 的，因为两个线程看到的更新顺序不一样（第一个线程看到 0 代表他认为 x 的更新是在 y 的更新之前执行的，第二个线程看到 0 代表他认为 y 的更新是在 x 的更新之前执行的）。如果没有乱序，那么肯定不会看到 x, y 都是 0，因为线程 1 和线程 2 都是先更新后读取的。但是也正如前面所有的讲述一样，各种乱序造成了我们可以看大第三个这样的结果。那么 Release/Acquire 能否保证不会出现这样的结果呢？我们来简单分析下，如果对于 x，y 的访问都是 Release/Acquire 模式的，那么线程 1 实际执行的就是：\nLoadStore StoreStore x = 1; int r1 = y; LoadLoad LoadStore 这里我们就可以看出来，x = 1 与 int r1 = y 之间没有任何内存屏障，所以实际可能执行的是：\nLoadStore StoreStore int r1 = y; x = 1; LoadLoad LoadStore 同理，线程 2 可能执行的是：\nLoadStore StoreStore y = 1; int r2 = x; LoadLoad LoadStore 或者：\nLoadStore StoreStore int r2 = x; y = 1; LoadLoad LoadStore 这样，就会造成我们可能看到第四种结果。我们通过代码测试下：\n@JCStressTest @State @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;not consensus\u0026#34;) //Other results are acceptable but not what we care about @Outcome(expect = ACCEPTABLE, desc = \u0026#34;boring\u0026#34;) public class TestConsensus { int x; int y; private static final VarHandle X; private static final VarHandle Y; static { try { //Initialize handles X = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;x\u0026#34;, int.class); Y = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;y\u0026#34;, int.class); } catch (Exception e) { throw new Error(e); } } @Actor public void actor1(II_Result result) { X.setRelease(this, 1); result.r1 = (int) Y.getAcquire(this); } @Actor public void actor2(II_Result result) { Y.setRelease(this, 1); result.r2 = (int) X.getAcquire(this); } } 测试结果是：\n如果要保证 Consensus，我们只要保证线程 1 的代码与线程 2 的代码不乱序即可，即在原本的内存屏障的基础上，添加 StoreLoad 内存屏障，即线程 1 执行：\nLoadStore StoreStore x = 1; StoreLoad int r1 = y; LoadLoad LoadStore 线程 2 执行：\nLoadStore StoreStore y = 1; StoreLoad int r2 = x; LoadLoad LoadStore 这样就能保证不会乱序，这其实就是 volatile 访问了。Volatile 访问即在 Release/Acquire 的基础上增加 StoreLoad 屏障，我们来测试下：\n@JCStressTest @State @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;not consensus\u0026#34;) @Outcome(expect = ACCEPTABLE, desc = \u0026#34;boring\u0026#34;) public class TestConsensus { int x; int y; private static final VarHandle X; private static final VarHandle Y; static { try { X = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;x\u0026#34;, int.class); Y = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;y\u0026#34;, int.class); } catch (Exception e) { throw new Error(e); } } @Actor public void actor1(II_Result result) { X.setVolatile(this, 1); result.r1 = (int) Y.getVolatile(this); } @Actor public void actor2(II_Result result) { Y.setVolatile(this, 1); result.r2 = (int) X.getVolatile(this); } } 结果是：\n那么引出另一个问题，这个 StoreLoad 屏障是 Volatile Store 之后添加，还是 Volatile Load 之前添加呢？我们来做下这个实验：\n首先保留 Volatile Store，将 Volatile Load 改成 Plain Load，即：\n@JCStressTest @State @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;not consensus\u0026#34;) @Outcome(expect = ACCEPTABLE, desc = \u0026#34;boring\u0026#34;) public class TestConsensus { int x; int y; private static final VarHandle X; private static final VarHandle Y; static { try { X = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;x\u0026#34;, int.class); Y = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;y\u0026#34;, int.class); } catch (Exception e) { throw new Error(e); } } @Actor public void actor1(II_Result result) { X.setVolatile(this, 1); result.r1 = (int) Y.get(this); } @Actor public void actor2(II_Result result) { Y.setVolatile(this, 1); result.r2 = (int) X.get(this); } } 测试结果：\n从结果中可以看出，仍然保持了 Consensus。再来看保留 Volatile Load，将 Volatile Store 改成 Plain Store：\n@JCStressTest @State @Outcome(id = \u0026#34;0, 0\u0026#34;, expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;not consensus\u0026#34;) @Outcome(expect = ACCEPTABLE, desc = \u0026#34;boring\u0026#34;) public class TestConsensus { int x; int y; private static final VarHandle X; private static final VarHandle Y; static { try { X = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;x\u0026#34;, int.class); Y = MethodHandles.lookup().findVarHandle(TestConsensus.class, \u0026#34;y\u0026#34;, int.class); } catch (Exception e) { throw new Error(e); } } @Actor public void actor1(II_Result result) { X.set(this, 1); result.r1 = (int) Y.getVolatile(this); } @Actor public void actor2(II_Result result) { Y.set(this, 1); result.r2 = (int) X.getVolatile(this); } } 测试结果：\n发现又乱序了。\n所以，可以得出结论，这个 StoreLoad 是加在 Volatile 写之后的，在后面的 JVM 底层源码分析我们也能看出来。\n7.4 Final 的作用 # Java 中，创建对象通过调用类的构造函数实现，我们还可能在构造函数中放一些初始化一些字段的值，例如：\npublic class Holder { int x; int y; int z; public Holder(int i) { this.x = i; this.y = x + 1; this.z = y + 1; } } 我们可以这样调用构造器创建一个对象：\nHolder holder = new Holder(i); 我们合并这些步骤，用伪代码表示底层实际执行的是：\nnewOb = new Holder(); //1 newOb.x = 1; //2 newOb.y = x + 1; //3 newOb.z = y + 1; //4 Holder holder = newOb; //5 他们之间，没有任何内存屏障，同时根据语义分析，1 和 5 之间有依赖关系，所以 1 和 5 的前后顺序不能变。1，2，3，4 之间有依赖，所以 1，2，3，4 的前后顺序也不能变。2，3，4 与 5 之间，没有任何关系，他们之间的执行顺序是可能乱序的。如果 5 在 2，3，4 中的任一一步之前执行，那么就会造成我们可能看到构造器还未执行完，x,y,z 还是初始值的情况。测试下：\n@JCStressTest @Outcome(id = \u0026#34;-1, -1, -1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Object is not seen yet.\u0026#34;) @Outcome(id = \u0026#34;1, 2, 3\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seen the complete object.\u0026#34;) @Outcome( expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Seeing partially constructed object.\u0026#34;) @State public class TestFinal { public static class MyObject { int x, y, z; public MyObject(int i) { x = i; y = x + 1; z = y + 1; } } MyObject o; @Actor public void actor1() { o = new MyObject(1); } @Actor public void actor2(III_Result r) { MyObject o = this.o; if (o != null) { r.r1 = o.x; r.r2 = o.y; r.r3 = o.z; } else { r.r1 = -1; r.r2 = -1; r.r3 = -1; } } } 在 x86 平台的测试结果，你只会看到两个结果，即 -1, -1, -1（代表没看到对象初始化）和 1, 2, 3（看到对象初始化，并且没有乱序），结果如下图所示（AMD64 是一种 x86 的实现）：\n这是因为，前文我们也提到过类似的， x86 CPU 是比较强一致性的 CPU，这里不会乱序。至于由于 x86 哪种不乱序性质这里才不乱序，我们后面会看到。\n还是和前文一样，我们换到不那么强一致性的 CPU （ARM）上执行，这里看到的结果就比较热闹了，如下图所示（aarch64 是一种 ARM 实现）：\n那我们如何保证看到构造器执行完的结果呢？ 用前面的内存屏障设计，我们可以把伪代码的第五步改成 setRelease，即：\nnewOb = new Holder(); //1 newOb.x = 1; //2 newOb.y = x + 1; //3 newOb.z = y + 1; //4 Holder holder.setRelease(newOb); //5 前面我们提到过 setRelease 会在前面加上 LoadStore 和 StoreStore 屏障，StoreStore 屏障会防止 2，3，4 与 5 乱序，所以可以避免这个问题，我们来试试看：\n@JCStressTest @Outcome(id = \u0026#34;-1, -1, -1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Object is not seen yet.\u0026#34;) @Outcome(id = \u0026#34;1, 2, 3\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seen the complete object.\u0026#34;) @Outcome( expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Seeing partially constructed object.\u0026#34;) @State public class TestFinal { public static class MyObject { int x, y, z; public MyObject(int i) { x = i; y = x + 1; z = y + 1; } } MyObject o; private static final VarHandle O; static { try { O = MethodHandles.lookup().findVarHandle(TestFinal.class, \u0026#34;o\u0026#34;, MyObject.class); } catch (Exception e) { throw new Error(e); } } @Actor public void actor1() { O.setRelease(this, new MyObject(1)); } @Actor public void actor2(III_Result r) { MyObject o = this.o; if (o != null) { r.r1 = o.x; r.r2 = o.y; r.r3 = o.z; } else { r.r1 = -1; r.r2 = -1; r.r3 = -1; } } } 再到前面的 aarch64 机器上试一下，结果是：\n从结果可以看出，只能看到要么没初始化，要么完整的构造器执行后的结果了。\n我们再进一步，其实我们这里只需要 StoreStore 屏障就够了，由此引出了 Java 的 final 关键字：final 其实就是在更新后面紧接着加入 StoreStore 屏障，这样也相当于在构造器结束之前加入 StoreStore 屏障，保证了只要我们能看到对象，对象的构造器一定是执行完了的。测试代码：\n@JCStressTest @Outcome(id = \u0026#34;-1, -1, -1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Object is not seen yet.\u0026#34;) @Outcome(id = \u0026#34;1, 2, 3\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seen the complete object.\u0026#34;) @Outcome(expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Seeing partially constructed object.\u0026#34;) @State public class TestFinal { public static class MyObject { final int x, y, z; public MyObject(int i) { x = i; y = x + 1; z = y + 1; } } MyObject o; @Actor public void actor1() { this.o = new MyObject(1); } @Actor public void actor2(III_Result r) { MyObject o = this.o; if (o != null) { r.r1 = o.x; r.r2 = o.y; r.r3 = o.z; } else { r.r1 = -1; r.r2 = -1; r.r3 = -1; } } } 我们再进一步，由于伪代码中 2，3，4 是互相依赖的，所以这里我们只要保证 4 先于 5 执行，那么2，3，一定先于 5 执行，也就是我们只需要对 z 设置为 final，从而加 StoreStore 内存屏障，而不是每个都声明为 final，从而多加内存屏障：\n@JCStressTest @Outcome(id = \u0026#34;-1, -1, -1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Object is not seen yet.\u0026#34;) @Outcome(id = \u0026#34;1, 2, 3\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seen the complete object.\u0026#34;) @Outcome(expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Seeing partially constructed object.\u0026#34;) @State public class TestFinal { public static class MyObject { int x, y; final int z; public MyObject(int i) { x = i; y = x + 1; z = y + 1; } } MyObject o; @Actor public void actor1() { this.o = new MyObject(1); } @Actor public void actor2(III_Result r) { MyObject o = this.o; if (o != null) { r.r1 = o.x; r.r2 = o.y; r.r3 = o.z; } else { r.r1 = -1; r.r2 = -1; r.r3 = -1; } } } 然后，我们继续用 aarch64 测试，测试结果依然是对的：\n最后我们需要注意，final 仅仅是在更新后面加上 StoreStore 屏障，如果你在构造器过程中，将 this 暴露了出去，那么还是会看到 final 的值没有初始化，我们测试下：\n@JCStressTest @Outcome(id = \u0026#34;-1, -1, -1\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Object is not seen yet.\u0026#34;) @Outcome(id = \u0026#34;1, 2, 3\u0026#34;, expect = ACCEPTABLE, desc = \u0026#34;Seen the complete object.\u0026#34;) @Outcome(expect = ACCEPTABLE_INTERESTING, desc = \u0026#34;Seeing partially constructed object.\u0026#34;) @State public class TestFinal { public class MyObject { int x, y; final int z; public MyObject(int i) { o = this; x = i; y = x + 1; z = y + 1; } } MyObject o; @Actor public void actor1() { MyObject myObject = new MyObject(1); } @Actor public void actor2(III_Result r) { MyObject o = this.o; if (o != null) { r.r1 = o.x; r.r2 = o.y; r.r3 = o.z; } else { r.r1 = -1; r.r2 = -1; r.r3 = -1; } } } 这次我们在 x86 的机器上就能看到 final 没有初始化：\n最后，为何这里的示例中 x86 不需要内存屏障就能实现，参考前面的 CPU 图：\nx86 本身 Store 与 Store 之间就不会乱序，天然就有保证。\n最后给上表格：\nJava 9+ VarHandle API Compiler Barrier Memory Barrier Coherence Causality Consensus Plain (ordinary field access) None None Not guaranteed Not guaranteed Not guaranteed Opaque (similar to C++ volatile) Yes None Guaranteed Not guaranteed Not guaranteed Release/Acquire Yes Release: LoadStore + StoreStore before; Acquire: LoadLoad + LoadStore after Guaranteed Guaranteed Not guaranteed Volatile Yes Volatile write: LoadStore + StoreStore before; StoreLoad after; Volatile read: LoadLoad + LoadStore after Guaranteed Guaranteed Guaranteed Final Yes Only adds StoreStore barrier after final Store Not guaranteed Not guaranteed Not guaranteed 8. 底层 JVM 实现分析 # 8.1. JVM 中的 OrderAccess 定义 # JVM 中有各种用到内存屏障的地方：\n实现 Java 的各种语法元素（volatile，final，synchronized，等等） 实现 JDK 的各种 API（VarHandle，Unsafe，Thread，等等） GC 需要的内存屏障：因为要考虑 GC 多线程与应用线程（在 GC 算法中叫做 Mutator）的工作方式，究竟是停止世界（Stop-the-world， STW）的方式，还是并发的方式 对象引用屏障：例如分代 GC，复制算法，年轻代 GC 的时候我们一般是从一个 S 区复制存活对象到另一个 S 区，如果复制的过程，我们不想停止世界（Stop-the-world， STW），而是和应用线程同时进行，那么我们就需要内存屏障，例如； 维护屏障：例如分区 GC 算法，我们需要维护每个区的跨区引用表以及使用情况表，例如 Card Table。这个如果我们想要应用线程与 GC 线程并发修改访问，而不是停止世界，那么也需要内存屏障。 JIT 也需要内存屏障：同样地，应用线程究竟是解释执行代码还是执行 JIT 优化后的代码，这里也是需要内存屏障的。 这些内存屏障，不同的 CPU，不同的操作系统，底层需要不同的代码实现，统一的接口设计是：\n源代码地址：orderAccess.hpp\nclass OrderAccess : public AllStatic { public: //LoadLoad barrier static void loadload(); //StoreStore barrier static void storestore(); //LoadStore barrier static void loadstore(); //StoreLoad barrier static void storeload(); //acquire barrier static void acquire(); //release barrier static void release(); //fence barrier static void fence(); } 不同的 CPU，不同的操作系统实现是不一样的，结合前面 CPU 乱序表格：\n我们来看下 linux + x86 的实现：\n源代码地址：orderAccess_linux_x86.hpp\n//Compiler barrier, actually just c++ volatile, no memory barriers static inline void compiler_barrier() { __asm__ volatile (\u0026#34;\u0026#34; : : : \u0026#34;memory\u0026#34;); } //fence barrier: x86 only has StoreLoad reordering, mfence can also implement full barrier, but this instruction is considered slower than lock + addl here //so lock + addl instruction is used here, also needs compiler_barrier to form complete fence inline void OrderAccess::fence() { #ifdef AMD64 __asm__ volatile (\u0026#34;lock; addl $0,0(%%rsp)\u0026#34; : : : \u0026#34;cc\u0026#34;, \u0026#34;memory\u0026#34;); #else __asm__ volatile (\u0026#34;lock; addl $0,0(%%esp)\u0026#34; : : : \u0026#34;cc\u0026#34;, \u0026#34;memory\u0026#34;); #endif compiler_barrier(); } //x86 CPU itself doesn\u0026#39;t reorder Load with Load, so only compiler barrier needed inline void OrderAccess::loadload() { compiler_barrier(); } //x86 CPU itself doesn\u0026#39;t reorder Store with Store, so only compiler barrier needed inline void OrderAccess::storestore() { compiler_barrier(); } //x86 CPU itself doesn\u0026#39;t reorder Load with Store, so only compiler barrier needed inline void OrderAccess::loadstore() { compiler_barrier(); } //x86 CPU can\u0026#39;t guarantee Store with Load won\u0026#39;t reorder, use fence to implement storeload inline void OrderAccess::storeload() { fence(); } //acquire equals LoadLoad + LoadStore, so only compiler barrier needed here inline void OrderAccess::acquire() { compiler_barrier(); } //release equals StoreStore + LoadStore, so only compiler barrier needed here inline void OrderAccess::release() { compiler_barrier(); } 对于 x86，由于 Load 与 Load，Load 与 Store，Store 与 Store 本来有一致性保证，所以只要没有编译器乱序，那么就天生有 StoreStore，LoadLoad，LoadStore 屏障，所以这里我们看到 StoreStore，LoadLoad，LoadStore 屏障的实现都只是加了编译器屏障。同时，前文中我们分析过，acquire 其实就是相当于在 Load 后面加上 LoadLoad，LoadStore 屏障，对于 x86 还是需要编译器屏障就够了。release 我们前文中也分析过，其实相当于在 Store 前面加上 LoadStore 和 StoreStore，对于 x86 还是需要编译器屏障就够了。于是，我们有如下表格：\n我们再看下前面我们经常使用的 Linux aarch64 下的实现：\n源代码地址：orderAccess_linux_aarch64.hpp\n//These don\u0026#39;t use specific CPU instructions but C++ standard library functions for memory barriers //Full barrier #define FULL_MEM_BARRIER __sync_synchronize() //Read barrier #define READ_MEM_BARRIER __atomic_thread_fence(__ATOMIC_ACQUIRE); //Write barrier #define WRITE_MEM_BARRIER __atomic_thread_fence(__ATOMIC_RELEASE); //acquire implemented through read barrier inline void OrderAccess::acquire() { READ_MEM_BARRIER; } //release implemented through write barrier inline void OrderAccess::release() { WRITE_MEM_BARRIER; } //fence implemented through full barrier inline void OrderAccess::fence() { FULL_MEM_BARRIER; } //LoadLoad implemented through read barrier inline void OrderAccess::loadload() { acquire(); } //StoreStore implemented through write barrier inline void OrderAccess::storestore() { release(); } //LoadStore implemented through read barrier inline void OrderAccess::loadstore() { acquire(); } //StoreLoad implemented through full barrier inline void OrderAccess::storeload() { fence(); } 如前面表格里面说，ARM 的 CPU Load 与 Load，Load 与 Store，Store 与 Store，Store 与 Load 都会乱序。JVM 针对 aarch64 没有直接使用 CPU 指令，而是使用了 C++ 封装好的内存屏障实现。C++ 封装好的很像我们前面讲的简易 CPU 模型的内存屏障，即读内存屏障（__atomic_thread_fence(__ATOMIC_ACQUIRE)），写内存屏障（__atomic_thread_fence(__ATOMIC_RELEASE)），读写内存屏障（全内存屏障，__sync_synchronize()）。acquire 的作用是作为接收点解包让后面的都看到包里面的内容，类比简易 CPU 模型，其实就是阻塞等待 invalidate queue 完全处理完保证 CPU 缓存没有脏数据。release 的作用是作为发射点将前面的更新打包发出去，类比简易 CPU 模型，其实就是阻塞等待 store buffer 完全刷入 CPU 缓存。所以，acquire，release 分别使用读内存屏障和写内存屏障实现。\nLoadLoad 保证第一个 Load 先于第二个，那么其实就是在第一个 Load 后面加入读内存屏障，阻塞等待 invalidate queue 完全处理完；LoadStore 同理，保证第一个 Load 先于第二个 Store，只要 invalidate queue 处理完，那么当前 CPU 中就没有对应的脏数据了，就不需要等待当前的 CPU 的 store buffer 也清空。\nStoreStore 保证第一个 Store 先于第二个，那么其实就是在第一个写入后面放读内存屏障，阻塞等待 store buffer 完全刷入 CPU 缓存；对于 StoreLoad，比较特殊，由于第二个 Load 需要看到 Store 的最新值，也就是更新不能只到 store buffer，同时过期不能存在于 invalidate queue 未处理，所以需要读写内存屏障，即全屏障。\n8.2. volatile 与 final 的内存屏障源码 # 我们接下来看一下 volatile 的内存屏障插入的相关代码，以 arm 为例子. 我们其实通过跟踪 iload 这个字节码就可以看出来如果 load 的是 volatile 关键字或者 final 关键字修饰的字段会怎么样，以及 istore就可以看出来如果 store的是 volatile 关键字或者 final 关键字修饰的字段会怎么样\n对于字段访问，JVM 中也有快速路径和慢速路径，我们这里只看快速路径的代码：\n对应源码：\n源代码地址：templateTable_arm.cpp\nvoid TemplateTable::fast_accessfield(TosState state) { //Omit code for actual Load execution //Check if volatile modified Label notVolatile; __ tbz(Rflags, ConstantPoolCacheEntry::is_volatile_shift, notVolatile); //If so, add LoadLoad + LoadStore memory barriers volatile_barrier(MacroAssembler::Membar_mask_bits(MacroAssembler::LoadLoad | MacroAssembler::LoadStore), Rtemp); __ bind(notVolatile); } void TemplateTable::fast_storefield(TosState state) { //Omit irrelevant code //Check if volatile modified Label notVolatile; __ tbz(Rflags, ConstantPoolCacheEntry::is_volatile_shift, notVolatile); //If so, add StoreStore + LoadStore memory barriers volatile_barrier(MacroAssembler::Membar_mask_bits(MacroAssembler::StoreStore | MacroAssembler::LoadStore), Rtemp); __ bind(notVolatile); //Omit code for actual Store execution Label notVolatile2; Label skipMembar; //Check if volatile modified or final modified __ tst(Rflags, 1 \u0026lt;\u0026lt; ConstantPoolCacheEntry::is_volatile_shift | 1 \u0026lt;\u0026lt; ConstantPoolCacheEntry::is_final_shift); __ b(skipMembar, eq); __ tbz(Rflags, ConstantPoolCacheEntry::is_volatile_shift, notVolatile2); //If volatile, add StoreLoad memory barrier volatile_barrier(MacroAssembler::StoreLoad, Rtemp); __ b(skipMembar); //If final, add StoreStore memory barrier __ bind(notVolatile2); volatile_barrier(MacroAssembler::StoreStore, Rtemp); __ bind(skipMembar); } 9. 一些 QA # 9.1. 为什么看到某些地方在方法本地变量使用 final # 对于本地变量中的 final（和前面提到的修饰字段的 final 不是一回事），这个单纯从语义上讲，其实并没有什么性能方面的考虑，仅仅是作为一种标记。即：你可能在方法本地声明很多变量，但是为了语义清晰，就将肯定不会改的声明为 final。\n9.2. 解密 DCL（Double Check Locking） # 我发现多年来对于 Java 内存模型有很多误解，并且我发现很多很多人都存在这样的误解，所以这次通过不断优化一个经典的 DCL （Double Check Locking）程序实例来帮助大家消除这个误解。\n首先有这样一个程序, 我们想实现一个单例值，只有第一次调用的时候初始化，并且有多线程会访问这个单例值，那么我们会有：\ngetValue 的实现就是经典的 DCL 写法。\n在 Java 内存模型的限制下，这个 ValueHolder 有两个潜在的问题：\n如果根据 Java 内存模型的定义，不考虑实际 JVM 的实现，那么 getValue 是有可能返回 null 的。 可能读取到没有初始化完成的 Value 的字段值。 下面我们就这两个问题进行进一步分析并优化。\n9.2.1. 根据 Java 内存模型的定义，不考虑实际 JVM 的实现，getValue 有可能返回 null 的原因 # 在7.1. Coherence（相干性，连贯性）与 Opaque中我们提到过：假设某个对象字段 int x 初始为 0，一个线程执行：\nx = 1 另一个线程执行(r1, r2 为本地变量)：\nint r1 = x; int r2 = x; 那么这个实际上是两次对于字段的读取（对应字节码 getfield），在 Java 内存模型下，可能的结果是包括：\nr1 = 1, r2 = 1 r1 = 0, r2 = 1 r1 = 1, r2 = 0 r1 = 0, r2 = 0 其中第三个结果很有意思，从程序上理解即我们先看到了 x = 1，之后又看到了 x 变成了 0.实际上这是因为编译器乱序。如果我们不想看到这个第三种结果，我们所需要的特性即 coherence。这里由于private Value value是普通的字段，所以根据 Java 内存模型来看并不保证 coherence。\n回到我们的程序，我们有三次对字段读取（对应字节码 getfield），分别位于:\n由于 1，2 之间有明显的分支关系（2 根据 1 的结果而执行或者不执行），所以无论在什么编译器看来，都要先执行 1 然后执行 2。但是对于 1 和 3，他们之间并没有这种依赖关系，在一些简单的编译器看来，他们是可以乱序执行的。在 Java 内存模型下，也没有限制 1 与 3 之间是否必须不能乱序。所以，可能你的程序先执行 3 的读取，然后执行 1 的读取以及其他逻辑，最后方法返回 3 读取的结果。\n但是，在 OpenJDK Hotspot 的相关编译器环境下，这个是被避免了的。OpenJDK Hotspot 编译器是比较严谨的编译器，它产生的 1 和 3 的两次读取（针对同一个字段的两次读取）也是两次互相依赖的读取，在编译器维度是不会有乱序的（注意这里说的是编译器维度哈，不是说这里会有内存屏障连可能的 CPU 乱序也避免了，不过这里针对同一个字段读取，前面已经说了仅和编译器乱序有关，和 CPU 乱序无关）\n不过，这个仅仅是针对一般程序的写法，我们可以通过一些奇怪的写法骗过编译器，让他任务两次读取没有关系，例如在7.1. Coherence（相干性，连贯性）与 Opaque中的实验环节，OpenJDK Hotspot 对于下面的程序是没有编译器乱序的：\n但是如果你换成下面这种写法，就骗过了编译器：\n我们不用太深究其原理，直接看其中一个结果：\n对于 DCL 这种写法，我们也是可以骗过编译器的，但是一般我们不会这么写，这里就不赘述了。\n9.2.2. 可能读取到没有初始化完成的 Value 的字段值 # 这个就不只是编译器乱序了，还涉及了 CPU 指令乱序以及 CPU 缓存乱序，需要内存屏障解决可见性问题。\n我们从 Value 类的构造器入手：\n对于 value = new Value(10); 这一步，将代码分解为更详细易于理解的伪代码则是：\n这中间没有任何内存屏障，根据语义分析，1 与 5 之间有依赖关系，因为 5 依赖于 1 的结果，必须先执行 1 再执行 5。 2 与 3 之间也是有依赖关系的，因为 3 依赖 2 的结果。但是，2和3，与 4，以及 5 这三个之间没有依赖关系，是可以乱序的。我们使用使用代码测试下这个乱序：\n虽然在注释中写出了这么编写代码的原因，但是这里还是想强调下这么写的原因：\njcstress 的 @Actor 是使用一个线程执行这个方法中的代码，在测试中，每次会用不同的 JVM 启动参数让这段代码解释执行，C1编译执行，C2编译执行，同时对于 JIT 编译还会修改编译参数让它的编译代码效果不一样。这样我们就可以看到在不同的执行方式下是否会有不同的编译器乱序效果。 jcstress 的 @Actor 是使用一个线程执行这个方法中的代码，在每次使用不同的 JVM 测试启动时，会将这个 @Actor 绑定到一个 CPU 执行，这样保证在测试的过程中，这个方法只会在这个 CPU 上执行， CPU 缓存由这个方法的代码独占，这样才能更容易的测试出 CPU 缓存不一致导致的乱序。所以，我们的 @Actor 注解方法的数量需要小于 CPU 个数。 我们测试机这里只有两个 CPU，那么只能有两个线程，如果都执行原始代码的话，那么很可能都执行到 synchronized 同步块等待，synchronized 本身有内存屏障的作用（后面会提到）。为了更容易测试出没有走 synchronized 同步块的情况，我们第二个 @Actor 注解的方法直接去掉同步块逻辑，并且如果 value 为 null，我们就设置结果都是 -1 用来区分 我分别在 x86 和 arm CPU 上测试了这个程序，结果分别是：\nx86 - AMD64：\narm - aarch64:\n我们可以看到，在比较强一致性的 CPU 如 x86 中，是没有看到未初始化的字段值的，但是在 arm 这种弱一致性的 CPU 上面，我们就看到了未初始化的值。我们也多次提到了这个 CPU 乱序表格：\n在这里，我们需要的内存屏障是 StoreStore（同时我们也从上面的表格看出，x86 天生不需要 StoreStore，只要没有编译器乱序的话，CPU 层面是不会乱序的，而 arm 需要内存屏障保证 Store 与 Store 不会乱序），只要这个内存屏障保证我们前面伪代码中第 2,3 步在第 5 步前，第 4 步在第 5 步之前即可，那么我们可以怎么做呢？我们可以有如下做法，每种做法我们都会对比其内存屏障消耗：\n9.2.2.1. 使用 final # final 是在赋值语句末尾添加 StoreStore 内存屏障，所以我们只需要在第 2,3 步以及第 4 步末尾添加 StoreStore 内存屏障即把 a2 和 b 设置成 final 即可，如下所示：\n对应伪代码：\n我们测试下：\n这次在 arm 上的结果是：\n如你所见，这次 arm CPU 上也没有看到未初始化的值了。\n这里 a1 不需要设置成 final，因为前面我们说过，2 与 3 之间是有依赖的，可以把他们看成一个整体，只需要整体后面添加好内存屏障即可。但是这个并不可靠！！！！因为在某些 JDK 中可能会把这个代码：\n优化成这样：\n这样 a1, a2 之间就没有依赖了！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！所以最好还是所有的变量都设置为 final\n但是，这在我们不能将字段设置为 final 的时候，就不好使了。\n9.2.2.2. 使用 volatile，这是大家常用以及官方推荐的做法 # 将 value 设置为 volatile 的，我们知道对于 volatile 写入，我们通过在写入之前加入 LoadStore + StoreStore 内存屏障，在写入之后加入 StoreLoad 内存屏障实现的，如果把 value 设置为 volatile 的，那么前面的伪代码就变成了：\n我们通过下面的代码测试下：\n依旧在 arm 机器上面测试，结果是：\n没有看到未初始化值了\n9.2.2.3. 对于 Java 9+ 可以使用 Varhandle 的 acquire/release # 前面分析，我们其实只需要保证在伪代码第五步之前保证有 StoreStore 内存屏障即可，所以 volatile 其实有点重，我们可以通过使用 Varhandle 的 acquire/release 这一级别的可见性 api 实现，这样伪代码就变成了：\n我们的测试代码变成了：\n测试结果是：\n也是没有看到未初始化值了。这种方式是用内存屏障最少，同时不用限制目标类型里面不必使用 final 字段的方式。\n9.2.2.4. 一种有趣但是没啥用的思路 - 如果是静态方法，可以通过类加载器机制实现很简便的写法 # 如果我们，ValueHolder 里面的方法以及字段可以是 static 的，例如：\n将 ValueHolder 作为一个单独的类，或者一个内部类，这样也是能保证 Value 里面字段的可见性的，这是通过类加载器机制实现的，在加载同一个类的时候(类加载的过程中会初始化 static 字段并且运行 static 块代码)，是通过 synchronized 关键字同步块保护的，参考其中类加载器(ClassLoader.java)的源码：\nClassLoader.java\n对于 syncrhonized 底层对应的 monitorenter 和 monitorexit，monitorenter 与 volatile 读有一样的内存屏障，即在操作之后加入 LoadLoad 和 LoadStore，monitorexit 与 volatile 写有一样的内存屏障，在操作之前加入 LoadStore + StoreStore 内存屏障，在操作之后加入 StoreLoad 内存屏障。所以，也是能保证可见性的。但是这样虽然写起来貌似很简便，效率上更加低（低了很多，类加载需要更多事情）并且不够灵活，只是作为一种扩展知识知道就好。\n","date":"2022年3月28日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-3-jmm/","section":"文章","summary":"从规范到实现深入探讨 Java 内存模型（JMM），涵盖内存屏障、CPU 重排序和 Java 9+ VarHandle API。了解一致性、因果性、共识性，以及 volatile、final 和其他同步机制在底层的工作原理，并提供实用的 jcstress 示例。","title":"全网最硬核 JDK 分析 - 3. Java 新内存模型解析与实验","type":"posts"},{"content":"","date":"2022年3月2日","externalUrl":null,"permalink":"/zh-cn/tags/log4j2/","section":"Tags","summary":"","title":"Log4j2","type":"tags"},{"content":"","date":"2022年3月2日","externalUrl":null,"permalink":"/zh-cn/categories/microservices/","section":"Categories","summary":"","title":"Microservices","type":"categories"},{"content":" 隐藏的性能杀手：为什么日志中的代码位置会摧毁你的微服务性能 # 当我们首次启动服务时，我们将生产日志级别配置为 INFO，并在日志输出中包含代码位置信息。格式看起来像这样：\n2022-03-02 19:57:59.425 INFO [service-apiGateway,,] [35800] [main][org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator:88]: Loaded RoutePredicateFactory [Query] 我们使用 Log4j2 作为日志框架，并启用了异步日志。对于 Log4j2 的 Disruptor WaitStrategy，我们通过设置 AsyncLoggerConfig.WaitStrategy=Sleep 选择了对 CPU 友好的 Sleep 选项。随着业务增长，我们注意到一些实例经历了极高的 CPU 使用率，特别是在大量日志记录期间。为了彻底解决这个问题，我们捕获了 JFR 转储以进行进一步调查。\n让我们从检查垃圾收集模式开始。我们使用 G1GC，所以我们专注于 G1 垃圾收集事件：\n分析显示所有 GC 活动都包括正常执行时间和没有异常频率模式的 Young GC。\n接下来，我们深入 CPU 使用率分析。查看 Thread CPU Load 事件向我们显示了每个线程的 CPU 消耗。我们发现的情况相当令人担忧 - reactor-http-epoll 线程池线程总共消耗了近 100% 的 CPU。\n这些线程处理 reactor-netty 中的业务逻辑。当我们将其与其他健康实例进行比较时，我们发现正常操作不应该产生如此高的 CPU 负载。那么是什么导致了这种过度的负载？是时候进行一些线程转储分析了！\n在检查多个线程堆栈转储后，我们发现这些线程一致处于 Runnable 状态，执行与 StackWalker 相关的原生方法。这与我们 JFR 数据中的 Method Runnable 事件完美对齐，确认 CPU 主要由这些特定方法调用消耗：\n罪魁祸首是两个原生方法：\njava.lang.StackStreamFactory$AbstractStackWalker.callStackWalk java.lang.StackStreamFactory$AbstractStackWalker.fetchStackFrames 这里有一个关键的理解：微服务线程堆栈可能非常深（大约 150 层），响应式代码使这变得更糟（可能达到 300 层）。这是因为 servlet 和过滤器使用责任链模式，每个过滤器都会添加到堆栈中。响应式代码通过其分层方法和各种观察点放大了这一点。上面的堆栈跟踪是响应式堆栈深度的完美示例。\n这些原生方法正在做一件特定的事情：确定调用日志记录方法的代码位置。这包括类名、方法名和行号。在我们的堆栈跟踪示例中，实际的日志记录调用位置是：at com.xxx.apigateway.filter.AccessCheckFilter.filter(AccessCheckFilter.java:144)，我们使用 log.info() 输出一些日志。\n显然，Log4j2 通过遍历当前线程的堆栈来获取调用代码位置。它不仅仅是堆栈的顶部 - 它需要找到 log4j2 框架元素之后的第一个堆栈元素，以识别实际的日志记录调用位置。\nLog4j2 如何检索堆栈信息 # 让我们思考如何自己实现这个。在 Java 9 之前，获取当前线程的堆栈（我们这里不处理其他线程）可以通过以下方式完成：\n注意 Thread.currentThread().getStackTrace(); 本质上在底层是 new Exception().getStackTrace();，所以它们在功能上是等价的。\nJava 9 引入了新的 StackWalker 接口，它提供了一种更优雅的方式来使用 Stream 接口读取堆栈：\n让我们检查 new Exception().getStackTrace(); 如何在内部工作：\njavaClasses.cpp 这是 StackWalker 的核心实现： 两种方法基本上都填充堆栈详细信息，但一种填充所有内容，而另一种减少填充的堆栈信息量。填充堆栈信息主要涉及访问 SymbolTable 和 StringTable，因为我们需要实际的类和方法名而不是内存地址。显然：基于异常的堆栈检索比 StackWalker 更频繁地访问 Symbol Table 和 String Table，因为它需要填充更多堆栈帧。\n让我们通过模拟检索代码执行位置如何影响我们原始代码的性能在不同堆栈深度下测试这一点。\n性能影响比较：代码位置检索 vs 无位置 # 以下代码基于 Log4j2 的官方单元测试。首先，让我们模拟各种深度的堆栈调用：\n然后我们将编写测试代码来比较纯执行和执行与堆栈检索之间的性能差异：\n运行测试给我们这些结果：\n结果清楚地表明检索代码执行位置（即获取堆栈）会导致显著的性能下降。此外，这种性能损失与堆栈填充相关 - 更多填充的帧意味着更大的损失。我们可以从 StackWalker 相比基于异常的堆栈检索的优越性能中看到这一点，随着堆栈深度增加，性能差距扩大。\n为什么它很慢？测试 String::intern 性能影响 # 这种性能下降源于 StringTable 和 SymbolTable 访问，正如我们在 JVM 源代码分析中看到的。我们可以模拟这种访问，因为底层 StringTable 操作使用 String 的 intern 方法。这是我们使用 String::intern 的模拟测试：\n测试结果： 比较 StackWalkBenchmark.baseline 与 StackWalkBenchmark.toString 显示 bh.consume(time); 本身的性能影响可以忽略不计。然而，将这些与 StackWalkBenchmark.intern 和 StackWalkBenchmark.intern3 进行比较，揭示了严重的性能下降，随着访问频率增加（类似于堆栈检索）而恶化。\n结论和建议 # 从这个分析中，我们可以得出一些明确的结论：\n日志中的代码位置输出在 Java 9 之前使用基于异常的堆栈检索，之后使用 StackWalker 两种方法都需要访问 SymbolTable 和 StringTable，StackWalker 通过限制填充的堆栈帧来减少访问 两种方法都会导致严重的性能下降。 因此，我强烈建议：对于微服务环境，特别是具有非常深堆栈的响应式微服务环境，避免在高量日志中包含代码位置，因为它会导致严重的性能下降。\n在禁用日志中的代码位置输出后，我们在相同负载条件下观察到显著降低的 CPU 使用率，以及整体吞吐量的明显改善。\n","date":"2022年3月2日","externalUrl":null,"permalink":"/zh-cn/posts/log-with-position/","section":"文章","summary":"了解在日志中启用代码位置如何导致微服务中的严重 CPU 性能问题，特别是响应式应用。这个深入分析揭示了 Log4j2 中堆栈遍历的隐藏成本，并为高吞吐量系统提供了可行的解决方案。","title":"隐藏的性能杀手：为什么日志中的代码位置会摧毁你的微服务性能","type":"posts"},{"content":"","date":"2022年2月28日","externalUrl":null,"permalink":"/zh-cn/tags/jmap/","section":"Tags","summary":"","title":"Jmap","type":"tags"},{"content":"","date":"2022年2月28日","externalUrl":null,"permalink":"/zh-cn/categories/performance-tuning/","section":"Categories","summary":"","title":"Performance Tuning","type":"categories"},{"content":"","date":"2022年2月28日","externalUrl":null,"permalink":"/zh-cn/categories/spring-boot/","section":"Categories","summary":"","title":"Spring Boot","type":"categories"},{"content":" 问题背景 # 然而，在升级到 Spring Boot 2.4.6 + Spring Cloud 2020.0.x 后，我们注意到 YoungGC 频率和对象分配率显著增加，而对象提升到老年代保持不变。这表明新创建的对象正在被快速垃圾收集。让我们检查我们处理大约每秒 100 个 HTTP 请求的其中一个进程的监控数据：\n这令人困惑。请求率并不是特别高，但监控显示每秒分配近 2GB 内存。在升级之前，在类似的请求负载下，此分配率约为 100-200MB。所有这些额外的内存消耗来自哪里？\n问题调查 # 我们需要使用 jmap 命令检查内存中各种对象的统计数据。然而，我们不能只查看仅存活对象的统计，因为监控表明问题不是过多的老年代对象 - 提升率没有增加。理想情况下，我们希望从分析中排除当前存活的对象。此外，由于 GC 发生得相当频繁（大约每秒一次），我们不能期望在单次尝试中捕获所需的 jmap 数据。由于 jmap 导致所有线程进入 safepoint 并触发 STW（Stop-The-World），影响生产，我们不能太频繁地运行它。因此，我们采用了以下策略：\n通过添加一个实例进行扩展，然后使用服务注册表和速率限制器将一半流量从特定实例重定向； 在此实例上，连续执行 jmap -histo（所有对象的统计）和 jmap -histo:live（仅存活对象的统计）； 以 100ms、300ms、500ms 和 700ms 的间隔重复步骤 2 五次； 移除此实例上的速率限制并关闭新扩展的实例。 通过比较这些 jmap 结果，我们发现在 jmap 统计中排名靠前的对象类型中，有一个 Spring 框架对象：\nnum #instances #bytes class name (module) ------------------------------------------------------- 1: 7993252 601860528 [B (java.base@11.0.8) 2: 360025 296261160 [C (java.base@11.0.8) 3: 10338806 246557984 [Ljava.lang.Object; (java.base@11.0.8) 4: 6314471 151547304 java.lang.String (java.base@11.0.8) 5: 48170 135607088 [J (java.base@11.0.8) 6: 314420 126487344 [I (java.base@11.0.8) 7: 4591109 110100264 [Ljava.lang.Class; (java.base@11.0.8) 8: 245542 55001408 org.springframework.core.ResolvableType 9: 205234 29042280 [Ljava.util.HashMap$Node; (java.base@11.0.8) 10: 386252 24720128 [org.springframework.core.ResolvableType; 11: 699929 22397728 java.sql.Timestamp (java.sql@11.0.8) 12: 89150 21281256 [Ljava.beans.PropertyDescriptor; (java.desktop@11.0.8) 13: 519029 16608928 java.util.HashMap$Node (java.base@11.0.8) 14: 598728 14369472 java.util.ArrayList (java.base@11.0.8) 这些对象是如何创建的？如何追踪频繁创建但不再存活的对象，特别是当对象类型是框架内部的时候？\n首先，MAT（Eclipse Memory Analyzer）+ jmap dump 用于完整堆分析不太适合，原因如下：\n对象不再存活。MAT 更适合内存泄漏分析，而我们的问题涉及创建许多意外对象，这些对象消耗大量内存但很快变得不可达。 对于不再存活的对象，MAT 无法准确分析它们的创建者，主要是因为不确定转储是否捕获了我们所需的信息，或者数据中可能有太多噪音。 虽然这种方法不适合我们的问题，但我仍会展示我从收集的 jmap 转储中获得的 MAT 分析结果：\n那么我们如何进行分析？这让我们回到了我们的老朋友：JFR + JMC。经常阅读的读者知道我经常使用 JFR 来排查生产问题。我们如何在这里使用它？虽然没有直接统计哪些对象被频繁创建的 JFR 事件，但有间接事件可以揭示谁在创建这么多对象。我通常按以下方式处理：\n使用 Thread Allocation Statistics 事件来识别哪些线程正在分配过多对象。 使用 Method Profiling Samples 来分析哪些热代码路径可能正在生成这些对象。对于大量创建的对象，捕获 Runnable 代码有很高的概率被采样，并将在事件中显示高比例。 首先，检查 Thread Allocation Statistics 事件，我们发现基本上所有 servlet 线程（处理 HTTP 请求的线程 - 我们使用 Undertow，所以线程名称以 XNIO 开头）都在分配许多对象。这无助于定位问题：\n接下来，我们通过单击 Method Profiling Sample 事件并查看堆栈跟踪统计来检查热代码统计，以查看哪些具有高比例。\n我们发现排名靠前的方法似乎都与 ResolvableType 相关。为了进一步调查，我们双击第一个方法以查看调用堆栈统计：\n我们发现调用者是 BeanUtils.copyProperties。检查其他与 ResolvableType 相关的调用，它们都与 BeanUtils.copyProperties 相关。此方法在我们的项目中经常用于相同或不同类型对象之间的属性复制。为什么此方法创建这么多 ResolvableType 对象？\n问题分析 # 通过检查源代码，我们发现从 Spring 5.3.x 开始，BeanUtils 开始使用 ResolvableType 作为属性复制的统一类信息包装器：\n/** * * \u0026lt;p\u0026gt;As of Spring Framework 5.3, this method honors generic type information */ private static void copyProperties(Object source, Object target, @Nullable Class\u0026lt;?\u0026gt; editable, @Nullable String... ignoreProperties) throws BeansException { } 内部源代码在每次复制操作期间为源和目标对象类型的每个属性方法创建新的 ResolvableType 实例，并且没有实现缓存。这导致单个复制操作创建大量 ResolvableType 实例。让我们进行一个实验：\npublic class Test { public static void main(String[] args) { TestBean testBean1 = new TestBean(\u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;2\u0026#34;, \u0026#34;3\u0026#34;, \u0026#34;4\u0026#34;, \u0026#34;5\u0026#34;, \u0026#34;6\u0026#34;, \u0026#34;7\u0026#34;, \u0026#34;8\u0026#34;); TestBean testBean2 = new TestBean(); for (int i = 0; i \u0026gt; -1; i++) { BeanUtils.copyProperties(testBean1, testBean2); System.out.println(i); } } } 我们使用两个不同的依赖项执行此代码：spring-beans 5.2.16.RELEASE 和 spring-beans 5.3.9，使用 JVM 参数 -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx512m。这些参数使用 EpsilonGC，当堆内存满时不执行 GC 而抛出 OutOfMemory 异常并终止程序，最大堆大小为 512MB。此设置基本上测试不同版本的 BeanUtils.copyProperties 在内存耗尽之前可以执行多少次。\n实验结果显示：spring-beans 5.2.16.RELEASE 执行了 444,489 次，而 spring-beans 5.3.9 仅执行了 27,456 次。这是一个显著差异。\n因此，我向 spring-framework GitHub 存储库提交了一个Issue。\n对于我们项目中经常使用 BeanUtils.copyProperties 的区域，我们用 BeanCopier 替换它们并创建了一个简单的包装类：\npublic class BeanUtils { private static final Cache\u0026lt;String, BeanCopier\u0026gt; CACHE = Caffeine.newBuilder().build(); public static void copyProperties(Object source, Object target) { Class\u0026lt;?\u0026gt; sourceClass = source.getClass(); Class\u0026lt;?\u0026gt; targetClass = target.getClass(); BeanCopier beanCopier = CACHE.get(sourceClass.getName() + \u0026#34; to \u0026#34; + targetClass.getName(), k -\u0026gt; { return BeanCopier.create(sourceClass, targetClass, false); }); beanCopier.copy(source, target, null); } } 然而，重要的是要注意，用 BeanCopier 替换 BeanUtils.copyProperties 时最直接的问题是具有不同类型但相同名称的属性无法复制。例如，在 int 和 Integer 之间复制将不起作用。深度复制行为也存在差异，需要彻底的单元测试。\n实施这些更改后，问题得到解决。\n问题后续 # Spring 此后修复了此问题（在版本 v6.0.14 中）：\n发布说明：https://github.com/spring-projects/spring-framework/releases/tag/v6.0.14 相应提交：https://github.com/spring-projects/spring-framework/commit/09aa59f9e79e19a2f09e66002c665b6a5a03ae20 主要修复方法包括：\n通过避免对简单类型检查使用 ResolvableType 来减少内存使用 通过使用 HashSet 更高效地查找忽略的属性来提高性能 ","date":"2022年2月28日","externalUrl":null,"permalink":"/zh-cn/posts/spring-5-regression/","section":"文章","summary":"调查升级到 Spring Boot 2.4.6 + Spring Cloud 2020.0.x 后过度内存分配和 YoungGC 频率增加的问题，揭示 BeanUtils.copyProperties 如何在 Spring 5.3.x 版本中创建大量 ResolvableType 对象而不进行缓存。","title":"Spring Boot 升级后内存问题排查：深入探讨 ResolvableType 对象创建","type":"posts"},{"content":"最近，我遇到了另一个引起我注意的慢查询问题。经过快速调查，结果又是 MySQL 优化器在查询规划中做出不准确估计的情况。这是有问题的 SQL：\nselect * from t_pay_record WHERE (( user_id = \u0026#39;user_id1\u0026#39; AND is_del = 0 )) ORDER BY id DESC LIMIT 20 这个查询花了整整 20 分钟才返回结果！然而，当我们切换到不同的 user_id 时，执行速度非常快。从我们的生产环境观察，大多数用户都经历了正常性能。我们甚至测试了具有相似数据分布模式的用户，这些查询也运行顺利。\n让我们先检查原始有问题的 SQL 的 EXPLAIN 输出：\n+----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | t_pay_record | NULL | index | idx_user_id,idx_user_status_pay,idx_user_id_trade_code_status_amount_create_time_is_del | PRIMARY | 8 | NULL | 22593 | 0.01 | Using where | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------+---------+------+-------+----------+-------------+ 现在，当我们测试具有相似数据分布但响应时间正常的用户时，我们得到了不同的 EXPLAIN 结果。一些显示：\n+----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | t_pay_record | NULL | index | idx_user_id,idx_user_status_pay,idx_user_id_trade_code_status_amount_create_time_is_del | idx_user_id_trade_code_status_amount_create_time_is_del | 195 | NULL | 107561| 10.00| Using where | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------+-------+----------+-------------+ 而其他显示：\n+----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | t_pay_record | NULL | index | idx_user_id,idx_user_status_pay,idx_user_id_trade_code_status_amount_create_time_is_del | idx_user_id | 195 | NULL | 87514| 10.00| Using where | +----+-------------+--------------+------------+-------+-----------------------------------------------------------------------------------------+-------------+---------+------+-------+----------+-------------+ 基于这些观察，很明显查询使用了错误的索引。但为什么 MySQL 会选择不合适的索引？ 这是由于多个相互关联的因素造成的，本文将深入分析这些原因，同时提供实用解决方案。\n分析 MySQL 慢查询 # 在我之前的文章中，我提到 SQL 优化通常依赖于三个基本工具：\nEXPLAIN：这提供了表面级别的分析，而不实际执行 SQL。虽然它可能并不总是完全准确或详细，但它可以揭示关键问题。\nPROFILING：使用 set profiling = 1 启用，此工具对 SQL 执行进行采样，并将查询分解为不同阶段及其各自的时间。它需要实际的 SQL 执行和成功，尽管阶段分解并不总是足够细粒度。它主要用于识别和避免某些阶段（如防止内存排序）。\nOPTIMIZER TRACE：此工具提供优化器采取的每个步骤的详细视图，需要实际的 SQL 执行和成功。MySQL 的优化器通过多次迭代考虑众多因素，使其配置相当复杂。虽然默认设置在大多数场景中工作正常，但特殊情况需要手动干预。\n值得注意的是，在不同的 MySQL 版本中，由于 MySQL 的固有设计限制，EXPLAIN 和 OPTIMIZER TRACE 结果可能不同。EXPLAIN 往往更接近实际执行结果，而 OPTIMIZER TRACE 就像整个过程中的检查点采样。在 MySQL 的持续开发迭代中，一些不一致是不可避免的。\n对于我们特定的 SQL 案例，EXPLAIN 已经显示它使用了错误的索引。然而，要理解为什么会发生这种情况，我们需要使用 OPTIMIZER TRACE 进行更深入的分析。在我们继续该分析之前，让我解释 MySQL 的 InnoDB 查询优化器统计配置。\nMySQL InnoDB 优化器统计配置 # 官方文档：https://dev.mysql.com/doc/refman/8.0/en/innodb-persistent-stats.html\n为了优化用户 SQL 查询，MySQL 执行 SQL 解析、重写和查询计划优化。对于 InnoDB 引擎，创建查询计划涉及分析：\n全表扫描的成本 哪些索引可用于 WHERE 和 ORDER BY 条件 每个潜在索引的查询成本 选择并执行成本最低的计划 每个索引的查询成本通过 InnoDB 优化器统计确定。这些数据通过对表和索引数据进行采样收集 - 不是通过完整收集，而是通过统计采样。几个配置参数控制此过程：\ninnodb_stats_persistent：此全局变量控制统计是否默认持久化（默认 ON）。我们通常不能接受内存存储，因为数据库重启需要重新分析表，减慢启动时间。单个表控制使用 STATS_PERSISTENT（在 CREATE TABLE 和 ALTER TABLE 语句中）。\ninnodb_stats_auto_recalc：此全局变量控制默认自动更新（默认 ON），当超过 10% 的表行被修改时触发后台异步更新。单个表控制使用 STATS_AUTO_RECALC（在 CREATE TABLE 和 ALTER TABLE 语句中）。\ninnodb_stats_persistent_sample_pages：此全局变量控制默认采样的页数（默认 20）。每次更新从表和每个索引中随机采样 20 页，以估计每个索引和全表扫描的查询成本。单个表控制使用 STATS_SAMPLE_PAGES（在 CREATE TABLE 和 ALTER TABLE 语句中）。\n最慢 SQL 执行的根本原因分析 # 从我们之前的 EXPLAIN 结果，我们知道最终查询使用了 PRIMARY 键索引。这意味着整个 SQL 执行过程涉及：以反向主键顺序遍历表中的每一行，直到找到 20 条匹配记录。考虑到执行时间，我们知道这个过程在收集 20 条匹配之前检查了许多记录，使其效率极低。但为什么会发生这种情况？\n查看我们的 SQL 语句，在前面提到的第二步中，考虑的索引包括与 WHERE 条件中的 user_id 和 is_del 相关的索引（如 EXPLAIN 所示：idx_user_id,idx_user_status_pay,idx_user_id_trade_code_status_amount_create_time_is_del），以及来自 ORDER BY 条件的 id 索引（主键索引）。假设随机采样的页看起来像这样：\n蓝色部分表示采样的页，表中的每个索引默认采样 20 页。假设我们的采样结果与图表匹配，其他索引被相对均匀地采样，导致优化器估计使用其他索引需要扫描数万行。然而，从主键采样的最后一页恰好包含此特定用户在末尾的所有记录。由于语句包含 LIMIT 20，如果末尾恰好有 20 条记录（都满足 WHERE 条件），优化器会得出结论，通过主键向后扫描 20 条记录将是最有效的。这导致优化器相信主键扫描具有最低成本。实际上，这不是真的，因为采样数据不代表全貌 - 之后可能有很多很多不属于此用户的记录，特别是在大表中。\n如果我们移除 LIMIT 子句，EXPLAIN 显示选择了正确的索引，因为在不限制结果的情况下，主键索引需要扫描整个表，使其不可能比与 user_id 相关的索引成本更低。\n为什么具有正常执行时间的不同 user_id 显示不同的索引选择 # 类似地，由于所有索引优化器统计都是随机采样的，随着表变得更大和索引扩展，加上可能添加更复杂的索引，这放大了使用不同参数（在我们的案例中是不同的 user_id）分析索引成本的方差。\n这提出了你可能遇到的另一个问题：在现有索引之上添加复合索引时（例如，最初只有 idx_user_id，然后添加 idx_user_status_pay），以前仅按 user_id 搜索的 SQL 查询可能有时使用 idx_user_id，有时使用 idx_user_status_pay。使用 idx_user_status_pay 可能比使用 idx_user_id 慢。因此，添加新的复合索引可能会减慢其他业务 SQL 查询，这些查询本不应该由复合索引优化，所以应该谨慎进行。\n随着数据量增长和表变得更复杂，此设计产生的问题 # 由于统计不是实时更新的，而是仅在修改的行超过一定百分比时更新，并且统计是采样的而不是全面的，当表数据量很大时，这些统计可能非常不准确。\n由于统计本身不准确，具有不同数据类型、许多字段，特别是各种复合索引的复杂表设计使统计更加不准确。\n作为旁注：MySQL 表不应该太大，需要适当的水平分区，而字段不应该太多，需要良好的垂直分区。索引不应该随意添加 - 添加太多会加剧统计不准确性，导致错误的索引选择。\n手动 ANALYZE TABLE 向表添加读锁，阻止更新和事务。这不能用于关键在线业务表。考虑在低流量期间为关键业务表安排 ANALYZE。\n依赖自动表刷新机制使参数难以调整（主要是 STATS_SAMPLE_PAGES 参数 - 我们通常不会更改 STATS_PERSISTENT，因为我们不能接受由于重启延迟而导致的内存存储，我们也不会禁用 STATS_AUTO_RECALC，因为它会使优化器分析越来越不准确）。很难预测最佳值。业务增长和用户行为导致的数据倾斜也是不可预测的。使用 ALTER TABLE 修改特定表的 STATS_SAMPLE_PAGES 与 ANALYZE TABLE 具有相同的效果 - 添加读锁并阻止更新和事务。这不能用于关键在线业务表，所以最好从一开始就估计大表规模，尽管这很困难。\n结论和建议 # 总之，对于具有大数据量的生产表，我建议通过数据库和表分区主动控制每个表的数据量。然而，业务增长和产品需求不断迭代并变得更加复杂，很难保证我们不会最终得到具有复杂索引的大表。在这种情况下，我们需要适当增加 STATS_SAMPLE_PAGES，同时使用 FORCE INDEX 引导关键用户触发的查询使用正确的索引。这防止了本文描述的问题，即不准确的 MySQL 优化器统计导致某些用户 ID 使用错误的索引。\n","date":"2022年2月24日","externalUrl":null,"permalink":"/zh-cn/posts/sql-index/","section":"文章","summary":"深入探讨 MySQL 的 InnoDB 优化器统计以及采样不准确如何导致索引选择不当，造成相似查询之间的显著性能差异。学习防止由优化器误判导致的慢 SQL 查询的实用解决方案。","title":"MySQL 优化器统计：为什么你的查询选择了错误的索引","type":"posts"},{"content":"","date":"2022年2月24日","externalUrl":null,"permalink":"/zh-cn/tags/sql-optimization/","section":"Tags","summary":"","title":"Sql-Optimization","type":"tags"},{"content":"","date":"2022年2月24日","externalUrl":null,"permalink":"/zh-cn/categories/troubleshooting/","section":"Categories","summary":"","title":"Troubleshooting","type":"categories"},{"content":"","date":"2022年1月5日","externalUrl":null,"permalink":"/zh-cn/tags/lettuce/","section":"Tags","summary":"","title":"Lettuce","type":"tags"},{"content":"","date":"2022年1月5日","externalUrl":null,"permalink":"/zh-cn/categories/redis/","section":"Categories","summary":"","title":"Redis","type":"categories"},{"content":"","date":"2022年1月5日","externalUrl":null,"permalink":"/zh-cn/tags/redis/","section":"Tags","summary":"","title":"Redis","type":"tags"},{"content":"","date":"2022年1月5日","externalUrl":null,"permalink":"/zh-cn/tags/spring-data-redis/","section":"Tags","summary":"","title":"Spring-Data-Redis","type":"tags"},{"content":"最近，我收到了用户的消息和评论，询问一个有趣的问题：当使用 spring-data-redis 与 lettuce 时，他们通过数据包捕获注意到 pipeline 操作实际上没有按预期工作。那么我们如何正确配置这个以使其工作？\n首先，让我们回顾一下我们在之前的文章中讨论的关于 Spring-data-redis + Lettuce 的基本原理。在此设置中，RedisTemplate 使用的连接在内部包括：\nasyncSharedConn：这可以为 null，但如果启用了连接共享（默认情况下是启用的），它不会为空。这是所有 LettuceConnection 实例使用的共享 Redis 连接 - 它们实际上都在底层使用相同的连接。它用于执行简单命令，由于 Netty 的客户端架构和 Redis 的单线程处理特性，共享一个连接仍然相当快。如果禁用连接共享，此字段保持为空，并使用 asyncDedicatedConn 代替。 asyncDedicatedConn：这是一个私有连接，当你需要维护会话状态、执行事务或使用固定连接运行 Pipeline 命令时必须使用。 execute(RedisCallback) 的流程如下：\n对于 executePipelined(RedisCallback)，当正确使用时，它应该利用 asyncDedicatedConn 私有连接。但\u0026quot;正确使用\u0026quot;是什么意思？\n你必须使用回调的连接进行 Redis 调用 - 你不能直接使用 redisTemplate 调用，否则 pipeline 不会工作：\nPipeline 正确工作：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { connection.get(\u0026#34;test\u0026#34;.getBytes()); connection.get(\u0026#34;test2\u0026#34;.getBytes()); return null; } }); Pipeline 不工作：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { redisTemplate.opsForValue().get(\u0026#34;test\u0026#34;); redisTemplate.opsForValue().get(\u0026#34;test2\u0026#34;); return null; } }); 这确保了我们在应用级别正确使用 pipeline API，但在默认配置下，底层 pipeline 仍然不执行。这里发生了什么？\nRedis Pipeline vs Lettuce 的 AutoFlushCommands # Redis Pipeline 是 Redis 的批量操作功能。它允许你将一组 Redis 命令打包在一起，一次性发送到 Redis，并返回结果集。这大大减少了如果单独发送命令所需的 RTT（往返时间） - 包括 Redis 客户端和服务器切换系统调用以发送/接收数据的时间，以及网络传输时间。\n如果命令原本是这样发送的：\nClient -\u0026gt; Server: INCR X\\r\\n Server -\u0026gt; Client: 1 Client -\u0026gt; Server: INCR X\\r\\n Server -\u0026gt; Client: 2 Client -\u0026gt; Server: INCR X\\r\\n Server -\u0026gt; Client: 3 Client -\u0026gt; Server: INCR X\\r\\n Server -\u0026gt; Client: 4 使用 PIPELINE，命令会这样发送：\nClient -\u0026gt; Server: INCR X\\r\\nINCR X\\r\\nINCR X\\r\\nINCR X\\r\\n Server -\u0026gt; Client: 1\\r\\n2\\r\\n3\\r\\n4 如你所见，原理是客户端首先将所有命令连接在一起并在本地缓存它们，然后一次性将它们全部发送到服务器。服务器执行所有命令并一起响应所有结果。\nLettuce 连接有一个 AutoFlushCommands 配置，它确定在此连接上执行的命令如何发送到服务器。默认情况下，它是 true，意味着每个命令在收到后立即发送到服务器。如果设置为 false，所有命令都被缓存，只有在手动调用 flushCommands 时才发送到服务器 - 这基本上实现了 Pipeline 功能。\n配置 Spring-data-redis + Lettuce 用于 Pipeline # 从版本 2.3.0 开始，Spring-data-redis 为 Lettuce 添加了 Pipeline 配置支持。参考：\nDATAREDIS-1011 - Allow configuration of Lettuce pipelining flush behavior https://github.com/spring-projects/spring-data-redis/issues/1581 我们可以这样配置：\n@Bean public BeanPostProcessor lettuceConnectionFactoryBeanProcessor() { return new BeanPostProcessor() { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { //在 LettuceConnectionFactory bean 初始化后，将 PipeliningFlushPolicy 设置为 flushOnClose if (bean instanceof LettuceConnectionFactory) { LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) bean; lettuceConnectionFactory.setPipeliningFlushPolicy(LettuceConnection.PipeliningFlushPolicy.flushOnClose()); //感谢评论者 [孤胆枪手](https://juejin.cn/user/2084329775180605) 的纠正，我之前忘记了这个配置 lettuceConnectionFactory.setShareNativeConnection(false); } return bean; } }; } 注意我们在这里将 shareNativeConnection 设置为 false。通常，基于 Lettuce 的 RedisTemplate 中的大多数请求可以通过连接共享共享相同的连接。禁用这意味着我们每次都获得专用连接。在这种情况下，我们需要小心使用连接池（以防止每次都创建新连接），并确保池大小大于可能的并发线程数，以防止在等待连接时阻塞。\n为什么我们需要禁用连接共享？让我们看看源代码：\nRedisClusterAsyncCommands\u0026lt;byte[], byte[]\u0026gt; getAsyncConnection() { //仅对 Redis 事务为 true if (this.isQueueing()) { return this.getAsyncDedicatedConnection(); } else { //如果有共享连接，返回它；否则返回专用连接。只有专用连接使 PipeliningFlushPolicy 生效 - PipeliningFlushPolicy 不会修改共享连接 return (RedisClusterAsyncCommands)(this.asyncSharedConn != null \u0026amp;\u0026amp; this.asyncSharedConn instanceof StatefulRedisConnection ? ((StatefulRedisConnection)this.asyncSharedConn).async() : this.getAsyncDedicatedConnection()); } } 由于我们想使用 PipeliningFlushPolicy，我们需要这返回专用连接，这意味着我们不能启用连接共享。\n让我们看看 PipeliningFlushPolicy 源代码以了解 flushOnClose 的含义：\npublic interface PipeliningFlushPolicy { //这是默认值 - 每个命令直接发送到 Redis Server static PipeliningFlushPolicy flushEachCommand() { return FlushEachCommand.INSTANCE; } //当连接关闭时，一起发送所有命令到 Redis static PipeliningFlushPolicy flushOnClose() { return FlushOnClose.INSTANCE; } //手动设置缓冲多少个命令后再发送到 Redis，但连接关闭也会触发发送 static PipeliningFlushPolicy buffered(int bufferSize) { return () -\u0026gt; new BufferedFlushing(bufferSize); } } 所有三个类都实现 PipeliningFlushState 接口：\npublic interface PipeliningFlushState { //对于 executePipelined，这通过 connection.openPipeline() 在开始时调用 void onOpen(StatefulConnection\u0026lt;?, ?\u0026gt; connection); //为 executePipelined 中的每个命令调用 void onCommand(StatefulConnection\u0026lt;?, ?\u0026gt; connection); //在 executePipelined 结束时通过 connection.closePipeline() 调用 void onClose(StatefulConnection\u0026lt;?, ?\u0026gt; connection); } 直接向 Redis Server 发送每个命令的默认实现基本上在其方法中什么都不做：\nprivate enum FlushEachCommand implements PipeliningFlushPolicy, PipeliningFlushState { INSTANCE; @Override public PipeliningFlushState newPipeline() { return INSTANCE; } @Override public void onOpen(StatefulConnection\u0026lt;?, ?\u0026gt; connection) {} @Override public void onCommand(StatefulConnection\u0026lt;?, ?\u0026gt; connection) {} @Override public void onClose(StatefulConnection\u0026lt;?, ?\u0026gt; connection) {} } 对于 flushOnClose：\nprivate enum FlushOnClose implements PipeliningFlushPolicy, PipeliningFlushState { INSTANCE; @Override public PipeliningFlushState newPipeline() { return INSTANCE; } @Override public void onOpen(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //首先，将连接的 AutoFlushCommands 设置为 false，以便命令不会立即发送到 Redis connection.setAutoFlushCommands(false); } @Override public void onCommand(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //接收命令时什么都不做 } @Override public void onClose(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //当 pipeline 关闭时发送所有命令 connection.flushCommands(); //恢复默认配置，以便连接在返回到池时不影响未来使用 connection.setAutoFlushCommands(true); } } 对于 buffered：\nprivate static class BufferedFlushing implements PipeliningFlushState { private final AtomicLong commands = new AtomicLong(); private final int flushAfter; public BufferedFlushing(int flushAfter) { this.flushAfter = flushAfter; } @Override public void onOpen(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //首先，将连接的 AutoFlushCommands 设置为 false，以便命令不会立即发送到 Redis connection.setAutoFlushCommands(false); } @Override public void onCommand(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //如果命令计数达到指定数量，发送到 Redis if (commands.incrementAndGet() % flushAfter == 0) { connection.flushCommands(); } } @Override public void onClose(StatefulConnection\u0026lt;?, ?\u0026gt; connection) { //当 pipeline 关闭时发送所有命令 connection.flushCommands(); //恢复默认配置，以便连接在返回到池时不影响未来使用 connection.setAutoFlushCommands(true); } } ","date":"2022年1月5日","externalUrl":null,"permalink":"/zh-cn/posts/spring-data-redis-pipeline/","section":"文章","summary":"关于如何正确配置 Spring Data Redis 与 Lettuce 以启用 pipeline 功能的综合指南。了解连接共享、AutoFlushCommands 和 PipeliningFlushPolicy 配置，以优化你的 Redis 批量操作并减少网络往返时间。","title":"配置 Spring Data Redis 与 Lettuce 以实现有效的 Pipeline 操作","type":"posts"},{"content":"","date":"2021年10月14日","externalUrl":null,"permalink":"/zh-cn/categories/debugging/","section":"Categories","summary":"","title":"Debugging","type":"categories"},{"content":" 本文基于 Spring Data Redis 2.4.9\n我们最近又遇到了一个生产事件！一个新的微服务系统刚刚上线，部署后立即，我们开始收到发送到此系统的所有请求的超时错误。这里发生了什么？\n排查方法 # 我们再次转向我们值得信赖的 JFR 进行调查（你可以查看我的其他系列文章，其中 JFR 经常拯救局面）。对于历史慢请求响应，我通常遵循以下诊断流程：\n检查 STW（Stop-the-world）： 是否有由 GC 引起的长时间 STW？ 是否有其他原因导致所有进程线程进入 safepoint，触发 STW？ I/O 是否耗时过长？比如调用其他微服务、访问各种存储系统（磁盘、数据库、缓存等） 线程是否在某个锁上阻塞时间过长？ CPU 使用率是否过高？哪些线程导致的？ 通过 JFR 分析，我们发现许多 HTTP 线程在单个锁上被阻塞 - 从 Redis 连接池获取连接的锁。我们的项目使用 spring-data-redis，底层客户端是 lettuce。为什么会在这里阻塞？经过调查，我发现 spring-data-redis 存在连接泄漏问题。\nSpring Data Redis Lettuce 深入探讨 # 让我们从 Lettuce 的快速介绍开始。简单来说，Lettuce 是一个使用 Project Reactor + Netty 实现的非阻塞响应式 Redis 客户端。Spring-data-redis 为 Redis 操作提供统一封装。我们的项目使用 spring-data-redis + Lettuce 组合。\n为了帮助大家理解根本原因，让我首先简要解释 spring-data-redis + lettuce API 结构。\n首先，官方 Lettuce 团队不推荐使用连接池，但他们没有解释在什么情况下这个决定适用。这里是提前的结论：\n如果你的项目使用 spring-data-redis + lettuce 仅用于简单 Redis 命令（没有 Redis 事务、管道等），那么不使用连接池是最优的（假设你没有禁用 Lettuce 连接共享，默认启用）。 如果你的项目大量使用 Redis 事务，则建议使用连接池 更准确地说，如果你经常使用触发 execute(SessionCallback) 的命令，建议使用连接池。如果你主要使用 execute(RedisCallback) 命令，则不需要连接池。对于重度管道使用，仍然建议使用连接池。 现在让我们深入了解 spring-data-redis API 原理。在我们的项目中，我们主要使用 spring-data-redis 的两个核心 API：同步 RedisTemplate 和异步 ReactiveRedisTemplate。我们将专注于同步 RedisTemplate 作为示例。ReactiveRedisTemplate 本质上是一个异步包装器 - 由于 Lettuce 本质上是异步的，ReactiveRedisTemplate 实际上更简单实现。\nRedisTemplate 中的所有 Redis 操作最终都包装成两种类型的操作对象。首先是 RedisCallback\u0026lt;T\u0026gt;：\npublic interface RedisCallback\u0026lt;T\u0026gt; { @Nullable T doInRedis(RedisConnection connection) throws DataAccessException; } 这是一个以 RedisConnection 作为输入参数的功能接口，允许通过 RedisConnection 进行 Redis 操作。它可以包含多个 Redis 操作。RedisTemplate 中的大多数简单 Redis 操作都是这样实现的。例如，Get 请求源代码实现：\n//在 RedisCallback 之上添加统一反序列化操作 abstract class ValueDeserializingRedisCallback implements RedisCallback\u0026lt;V\u0026gt; { private Object key; public ValueDeserializingRedisCallback(Object key) { this.key = key; } public final V doInRedis(RedisConnection connection) { byte[] result = inRedis(rawKey(key), connection); return deserializeValue(result); } @Nullable protected abstract byte[] inRedis(byte[] rawKey, RedisConnection connection); } //Redis Get 命令实现 public V get(Object key) { return execute(new ValueDeserializingRedisCallback(key) { @Override protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { //使用连接执行 get 命令 return connection.get(rawKey); } }, true); } 另一种类型是 SessionCallback\u0026lt;T\u0026gt;：\npublic interface SessionCallback\u0026lt;T\u0026gt; { @Nullable \u0026lt;K, V\u0026gt; T execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException; } SessionCallback 也是一个功能接口，可以在其方法体中包含多个命令。顾名思义，此方法内的所有命令共享同一个会话 - 使用不能共享的相同 Redis 连接。这通常用于 Redis 事务。\nRedisTemplate 中的主要 API 是这几个方法，所有命令都使用这些底层 API 实现：\nexecute(RedisCallback\u0026lt;?\u0026gt; action) 和 executePipelined(final SessionCallback\u0026lt;?\u0026gt; session)：执行一系列 Redis 命令，作为所有方法的基础。执行后自动释放连接资源。 executePipelined(RedisCallback\u0026lt;?\u0026gt; action) 和 executePipelined(final SessionCallback\u0026lt;?\u0026gt; session)：使用 Pipeline 执行一系列命令。执行后自动释放连接资源。 executeWithStickyConnection(RedisCallback\u0026lt;T\u0026gt; callback)：执行一系列 Redis 命令。连接资源不会自动释放。各种 Scan 命令通过此方法实现，因为 Scan 命令返回需要维护连接（会话）的 Cursor，由用户决定何时关闭。 连接获取机制 # 通过源代码分析，我们可以看到 RedisTemplate 中的三个 API 在实际应用中经常涉及嵌套递归调用。\n例如，像这样的情况：\nredisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { orders.forEach(order -\u0026gt; { connection.hashCommands().hSet(orderKey.getBytes(), order.getId().getBytes(), JSON.toJSONBytes(order)); }); return null; } }); 和\nredisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { orders.forEach(order -\u0026gt; { redisTemplate.opsForHash().put(orderKey, order.getId(), JSON.toJSONString(order)); }); return null; } }); 是等价的。redisTemplate.opsForHash().put() 实际上调用 execute(RedisCallback) 方法，创建了 executePipelined 与 execute(RedisCallback) 的嵌套场景。这允许我们组合各种复杂情况，但连接在内部是如何维护的？\n这些方法都使用 RedisConnectionUtils.doGetConnection 方法来获取连接并执行命令。对于 Lettuce 客户端，这返回一个 org.springframework.data.redis.connection.lettuce.LettuceConnection。此连接包装器包含两个实际的 Lettuce Redis 连接：\nprivate final @Nullable StatefulConnection\u0026lt;byte[], byte[]\u0026gt; asyncSharedConn; private @Nullable StatefulConnection\u0026lt;byte[], byte[]\u0026gt; asyncDedicatedConn; asyncSharedConn：可以为 null。如果启用连接共享（默认），这不为 null。这是所有 LettuceConnections 共享的 Redis 连接 - 本质上每个 LettuceConnection 使用相同的连接。用于执行简单命令。由于 Netty 客户端和 Redis 单线程处理特性，共享一个连接仍然非常快。如果禁用连接共享，此字段为 null，命令使用 asyncDedicatedConn。 asyncDedicatedConn：私有连接。如果需要会话维护、事务执行、管道命令或固定连接，必须使用此 asyncDedicatedConn 进行 Redis 命令执行。 让我们通过一个简单示例查看执行流程。首先，一个简单命令：redisTemplate.opsForValue().get(\u0026quot;test\u0026quot;)。根据我们之前的源代码分析，我们知道这本质上是底层的 execute(RedisCallback)。流程是：\n如我们所见，如果使用 RedisCallback，不需要连接绑定，也不涉及事务。Redis 连接在回调内返回。注意，当调用 executePipelined(RedisCallback) 时，你必须使用回调的连接进行 Redis 调用，不能直接使用 redisTemplate 调用，否则 pipeline 不会生效：\nPipeline 有效：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { connection.get(\u0026#34;test\u0026#34;.getBytes()); connection.get(\u0026#34;test2\u0026#34;.getBytes()); return null; } }); Pipeline 无效：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { redisTemplate.opsForValue().get(\u0026#34;test\u0026#34;); redisTemplate.opsForValue().get(\u0026#34;test2\u0026#34;); return null; } }); 接下来，让我们尝试将其添加到事务中。由于我们的目标实际上不是测试事务，而是演示问题，我们将简单地用 SessionCallback 包装 GET 命令：\nredisTemplate.execute(new SessionCallback\u0026lt;Object\u0026gt;() { @Override public \u0026lt;K, V\u0026gt; Object execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException { return operations.opsForValue().get(\u0026#34;test\u0026#34;); } }); 这里最大的区别是，当外层获取连接时，这次 bind = true，意味着连接绑定到当前线程以维护会话连接。外层流程是：\n内部 SessionCallback 本质上是 redisTemplate.opsForValue().get(\u0026quot;test\u0026quot;)，使用共享连接，而不是专用连接，因为我们还没有启动事务（即执行 multi 命令）。如果启动了事务，将使用专用连接。流程是：\n由于 SessionCallback 需要维护连接，流程发生了显著变化。首先，需要连接绑定 - 本质上是获取连接并将其放在 ThreadLocal 中。此外，LettuceConnection 用引用计数变量包装。每个嵌套 execute 将此计数加 1，执行后减 1。每次 execute 结束时，它检查此引用计数，如果引用计数达到零，它调用 LettuceConnection.close()。\n现在让我们看看 executePipelined(SessionCallback) 会发生什么：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new SessionCallback\u0026lt;Object\u0026gt;() { @Override public \u0026lt;K, V\u0026gt; Object execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException { operations.opsForValue().get(\u0026#34;test\u0026#34;); return null; } }); 与第二个示例在流程上的主要区别是使用的连接不是共享连接，而是直接使用专用连接。\n最后，让我们看一个在 execute(RedisCallback) 内基于 executeWithStickyConnection(RedisCallback\u0026lt;T\u0026gt; callback) 执行命令的示例。各种 SCAN 操作基于 executeWithStickyConnection(RedisCallback\u0026lt;T\u0026gt; callback)，例如：\nredisTemplate.execute(new SessionCallback\u0026lt;Object\u0026gt;() { @Override public \u0026lt;K, V\u0026gt; Object execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException { Cursor\u0026lt;Map.Entry\u0026lt;Object, Object\u0026gt;\u0026gt; scan = operations.opsForHash().scan((K) \u0026#34;key\u0026#34;.getBytes(), ScanOptions.scanOptions().match(\u0026#34;*\u0026#34;).count(1000).build()); //scan 必须关闭，这里使用 try-with-resource try (scan) { } catch (IOException e) { e.printStackTrace(); } return null; } }); Session 回调流程如下所示。因为它在 SessionCallback 内，executeWithStickyConnection 检测到当前绑定了连接，所以它将标记加 1，但不减 1，因为 executeWithStickyConnection 可以向外暴露资源（如此处的 Cursor），需要手动外部关闭。\n连接泄漏的根本原因 # 在这个示例中，发生连接泄漏。首先，执行：\nredisTemplate.execute(new SessionCallback\u0026lt;Object\u0026gt;() { @Override public \u0026lt;K, V\u0026gt; Object execute(RedisOperations\u0026lt;K, V\u0026gt; operations) throws DataAccessException { Cursor\u0026lt;Map.Entry\u0026lt;Object, Object\u0026gt;\u0026gt; scan = operations.opsForHash().scan((K) \u0026#34;key\u0026#34;.getBytes(), ScanOptions.scanOptions().match(\u0026#34;*\u0026#34;).count(1000).build()); //scan 必须关闭，这里使用 try-with-resource try (scan) { } catch (IOException e) { e.printStackTrace(); } return null; } }); 这样，LettuceConnection 绑定到当前线程，最后，引用计数不是零，而是 1。当游标关闭时，它调用 LettuceConnection 的 close 方法。然而，LettuceConnection 的 close 实现只标记状态并关闭专用连接 asyncDedicatedConn。由于当前没有使用专用连接，它是 null，不需要关闭，如下面的源代码所示：\nLettuceConnection：\n@Override public void close() throws DataAccessException { super.close(); if (isClosed) { return; } isClosed = true; if (asyncDedicatedConn != null) { try { if (customizedDatabaseIndex()) { potentiallySelectDatabase(defaultDbIndex); } connectionProvider.release(asyncDedicatedConn); } catch (RuntimeException ex) { throw convertLettuceAccessException(ex); } } if (subscription != null) { if (subscription.isAlive()) { subscription.doClose(); } subscription = null; } this.dbIndex = defaultDbIndex; } 然后我们继续执行 Pipeline 命令：\nList\u0026lt;Object\u0026gt; objects = redisTemplate.executePipelined(new RedisCallback\u0026lt;Object\u0026gt;() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { connection.get(\u0026#34;test\u0026#34;.getBytes()); redisTemplate.opsForValue().get(\u0026#34;test\u0026#34;); return null; } }); 此时，由于连接已经绑定到当前线程，并且如前一节分析，第一步应该释放此绑定，但 LettuceConnection 的 close 已被调用。执行此代码会创建专用连接，并且由于计数无法达到零，连接仍然绑定到当前线程。因此，此专用连接永远不会关闭（如果有连接池，它永远不会返回到池）。\n即使我们稍后手动关闭此连接，根据源代码，由于 isClosed 状态已经为 true，专用连接仍然无法关闭。这导致连接泄漏。\n我已经向 spring-data-redis 提交了关于此 bug 的问题：Lettuce Connection Leak while using execute(SessionCallback) and executeWithStickyConnection in same thread by random turn\n解决方案 # 尽可能避免使用 SessionCallback；仅在真正需要 Redis 事务时使用 SessionCallback。 单独封装使用 SessionCallback 的函数，将事务相关命令保持在一起，避免在外层嵌套额外的 RedisTemplate execute 相关函数。 ","date":"2021年10月14日","externalUrl":null,"permalink":"/zh-cn/posts/spring-data-redis-connection-leak/","section":"文章","summary":"生产事件调查，揭示 Spring Data Redis + Lettuce 在混合使用 SessionCallback 和 executeWithStickyConnection 操作时如何泄漏连接。深入探讨连接管理机制、JFR 分析技术和实用解决方案，防止你的 Redis 连接池成为黑洞。","title":"Spring Data Redis 连接泄漏之谜：当你的微服务失控时","type":"posts"},{"content":"","date":"2021年9月1日","externalUrl":null,"permalink":"/zh-cn/categories/incident-response/","section":"Categories","summary":"","title":"Incident Response","type":"categories"},{"content":"","date":"2021年9月1日","externalUrl":null,"permalink":"/zh-cn/tags/reactive-programming/","section":"Tags","summary":"","title":"Reactive-Programming","type":"tags"},{"content":"","date":"2021年9月1日","externalUrl":null,"permalink":"/zh-cn/tags/spring-cloud-gateway/","section":"Tags","summary":"","title":"Spring-Cloud-Gateway","type":"tags"},{"content":" 问题描述和背景 # 昨晚，我们的网关在一段时间内经历了雪崩。症状如下：\n1. 各种微服务不断报告异常：在写入 HTTP 响应时连接关闭：\nreactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response 2. 同时，存在请求尚未完成读取但连接已经关闭的异常：\norg.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: UT000128: Remote peer closed connection before all data could be read 3. 前端不断触发请求超时警报：504 Gateway Time-out\n4. 网关进程不断失败健康检查并被重启\n5. 重启后，网关进程立即经历激增的请求量 - 每个实例峰值 2000 qps，安静期间 500 qps，由于自动扩展，繁忙时通常每个实例保持在 1000 qps 以下。然而，健康检查端点响应时间极长，导致实例不断重启\n问题 1 和 2 可能是由网关的持续重启和由于某种原因失败的优雅关闭导致的，导致强制关闭突然终止连接，从而产生相关异常。\n我们的网关基于 Spring Cloud Gateway 构建，具有基于 CPU 负载的自动扩展。奇怪的是，当请求量激增时，CPU 使用率并没有显著增加，保持在 60% 左右。由于 CPU 负载没有达到扩展阈值，自动扩展从未触发。为了快速解决问题，我们手动扩展了几个网关实例，将每个实例的负载控制在 1000 以下，这暂时解决了问题。\n问题分析 # 为了彻底解决这个问题，我们使用了 JFR 分析。首先，我们基于已知线索进行分析：\nSpring Cloud Gateway 是基于 Spring-WebFlux 的异步响应式网关，HTTP 业务线程有限（默认是 2 * 可用 CPU 核心数，在我们的情况下是 4）。 网关进程不断失败健康检查，这调用了不断超时的 /actuator/health 端点。 健康检查端点超时通常有两个原因：\n健康检查接口在检查某些组件时被阻塞。例如，如果数据库卡住，数据库健康检查可能永远不会返回。 HTTP 线程池在健康检查请求超时之前无法处理它们。 我们首先检查了 JFR 中的超时堆栈跟踪，看看 HTTP 线程是否卡在健康检查上。查看问题发生后的线程堆栈，专注于那 4 个 HTTP 线程，我们发现它们都有基本相同的堆栈，都在执行 Redis 命令：\n\u0026#34;reactor-http-nio-1\u0026#34; #68 daemon prio=5 os_prio=0 cpu=70832.99ms elapsed=199.98s tid=0x0000ffffb2f8a740 nid=0x69 waiting on condition [0x0000fffe8adfc000] java.lang.Thread.State: TIMED_WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base@11.0.8/Native Method) - parking to wait for \u0026lt;0x00000007d50eddf8\u0026gt; (a java.util.concurrent.CompletableFuture$Signaller) at java.util.concurrent.locks.LockSupport.parkNanos(java.base@11.0.8/LockSupport.java:234) at java.util.concurrent.CompletableFuture$Signaller.block(java.base@11.0.8/CompletableFuture.java:1798) at java.util.concurrent.ForkJoinPool.managedBlock(java.base@11.0.8/ForkJoinPool.java:3128) at java.util.concurrent.CompletableFuture.timedGet(java.base@11.0.8/CompletableFuture.java:1868) at java.util.concurrent.CompletableFuture.get(java.base@11.0.8/CompletableFuture.java:2021) at io.lettuce.core.protocol.AsyncCommand.await(AsyncCommand.java:83) at io.lettuce.core.internal.Futures.awaitOrCancel(Futures.java:244) at io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:75) at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80) at com.sun.proxy.$Proxy245.get(Unknown Source) at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.get(LettuceStringCommands.java:68) at org.springframework.data.redis.connection.DefaultedRedisConnection.get(DefaultedRedisConnection.java:267) at org.springframework.data.redis.connection.DefaultStringRedisConnection.get(DefaultStringRedisConnection.java:406) at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperations.java:57) at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:60) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:222) at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:189) at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:96) at org.springframework.data.redis.core.DefaultValueOperations.get(DefaultValueOperations.java:53) at com.jojotech.apigateway.filter.AccessCheckFilter.traced(AccessCheckFilter.java:196) at com.jojotech.apigateway.filter.AbstractTracedFilter.filter(AbstractTracedFilter.java:39) at org.springframework.cloud.gateway.handler.FilteringWebHandler$GatewayFilterAdapter.filter(FilteringWebHandler.java:137) at org.springframework.cloud.gateway.filter.OrderedGatewayFilter.filter(OrderedGatewayFilter.java:44) at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain.lambda$filter$0(FilteringWebHandler.java:117) at org.springframework.cloud.gateway.handler.FilteringWebHandler$DefaultGatewayFilterChain$$Lambda$1478/0x0000000800b84c40.get(Unknown Source) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at com.jojotech.apigateway.common.TracedMono.subscribe(TracedMono.java:24) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:120) at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onNext(MonoFilterWhen.java:149) at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) at reactor.core.publisher.MonoFilterWhen$MonoFilterWhenMain.onSubscribe(MonoFilterWhen.java:112) at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onNext(FluxConcatMap.java:250) at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:98) at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onNext(FluxDematerialize.java:44) at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:270) at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:228) at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.request(FluxDematerialize.java:127) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:235) at reactor.core.publisher.FluxDematerialize$DematerializeSubscriber.onSubscribe(FluxDematerialize.java:77) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62) at reactor.core.publisher.FluxDefer.subscribe(FluxDefer.java:54) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at org.springframework.cloud.sleuth.instrument.web.TraceWebFilter$MonoWebFilterTrace.subscribe(TraceWebFilter.java:184) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) at reactor.core.publisher.Mono.subscribe(Mono.java:4150) at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) at reactor.netty.http.server.HttpServer$HttpServerHandle.onStateChange(HttpServer.java:915) at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:654) at reactor.netty.transport.ServerTransport$ChildObserver.onStateChange(ServerTransport.java:478) at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:526) at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:209) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at reactor.netty.http.server.logging.AccessLogHandlerH1.channelRead(AccessLogHandlerH1.java:59) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324) at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.lang.Thread.run(java.base@11.0.8/Thread.java:834) 我们发现 HTTP 线程没有卡在健康检查上，也没有其他线程有任何与健康检查相关的堆栈（在异步环境中，健康检查也是异步的，一些进程可能被移交给其他线程）。因此，健康检查请求应该在执行之前超时。\n为什么会发生这种情况？同时，我注意到这里使用了 RedisTemplate - spring-data-redis 的同步 Redis API。我突然想起，之前编写此代码时，我走了一个捷径，没有使用异步 API，因为我只是验证键是否存在并修改键过期时间。这会不会通过用同步 API 阻塞 HTTP 线程而导致雪崩？\n让我们验证这个假设：我们的项目通过 spring-data-redis + Lettuce 连接池使用 Redis 操作，启用了增强的 JFR 监控用于 Lettuce 命令。你可以参考我的文章：\u0026ldquo;这个新的 Redis 连接池监控方法很棒 - 让我添加一些额外的调味料\u0026rdquo;。到目前为止，我的 pull request 已被合并，此功能将在 6.2.x 版本中发布。让我们看看问题发生时的 Redis 命令收集：\n让我们计算执行 Redis 命令导致的阻塞时间（我们的收集是每 10 秒，count 是命令数，时间单位是微秒）：使用这里的命令数乘以 50% 中位数，除以 10（因为它是 10 秒），我们得到执行 Redis 命令每秒导致的阻塞时间：\n32*152=4864 1*860=860 5*163=815 32*176=5632 1*178=178 16959*168=2849112 774*176=136224 3144*166=521904 17343*179=3104397 702*166=116532 Total: 6740518 6740518 / 10 = 674051.8 us = 0.67s 这只是使用中位数计算的阻塞时间。从图中的分布，我们可以看到实际值应该更大。这样，每秒在 Redis 同步接口上阻塞所需的时间很容易超过 1 秒。随着不减少的连续请求，请求不断累积，最终导致雪崩。\n此外，由于这些是阻塞接口，线程花费大量时间等待 I/O，所以 CPU 使用率不会增加，阻止自动扩展。在业务高峰期间，由于预先配置的扩展，网关实例没有达到有问题的压力水平，所以没有问题。\n问题解决 # 让我们重写原始代码。使用同步 spring-data-redis API 的原始代码是（本质上是 spring-cloud-gateway Filter 接口中核心方法 public Mono\u0026lt;Void\u0026gt; traced(ServerWebExchange exchange, GatewayFilterChain chain) 的方法体）：\nif (StringUtils.isBlank(token)) { //如果 token 不存在，根据路径决定是继续请求还是返回未授权状态 return continueOrUnauthorized(path, exchange, chain, headers); } else { try { String accessTokenValue = redisTemplate.opsForValue().get(token); if (StringUtils.isNotBlank(accessTokenValue)) { //如果 accessTokenValue 不为空，延长 4 小时以确保登录用户只要有活动，令牌就不会过期 Long expire = redisTemplate.getExpire(token); log.info(\u0026#34;accessTokenValue = {}, expire = {}\u0026#34;, accessTokenValue, expire); if (expire != null \u0026amp;\u0026amp; expire \u0026lt; 4 * 60 * 60) { redisTemplate.expire(token, 4, TimeUnit.HOURS); } //解析以获取 userId JSONObject accessToken = JSON.parseObject(accessTokenValue); String userId = accessToken.getString(\u0026#34;userId\u0026#34;); //仅当 userId 不为空时有效 if (StringUtils.isNotBlank(userId)) { //解析 Token HttpHeaders newHeaders = parse(accessToken); //继续请求 return FilterUtil.changeRequestHeader(exchange, chain, newHeaders); } } } catch (Exception e) { log.error(\u0026#34;read accessToken error: {}\u0026#34;, e.getMessage(), e); } //如果 token 无效，根据路径决定是继续请求还是返回未授权状态 return continueOrUnauthorized(path, exchange, chain, headers); } 转换为使用异步：\nif (StringUtils.isBlank(token)) { return continueOrUnauthorized(path, exchange, chain, headers); } else { HttpHeaders finalHeaders = headers; //必须用 tracedPublisherFactory 包装，否则跟踪信息会丢失。参考我的另一篇文章：Spring Cloud Gateway 没有跟踪信息，我完全困惑 return tracedPublisherFactory.getTracedMono( redisTemplate.opsForValue().get(token) //必须切换线程，否则后续线程仍将使用 Redisson 的线程。如果耗时，它会影响其他使用 Redis 的业务，此时间消耗也会计入 Redis 连接命令超时 .publishOn(Schedulers.parallel()), exchange ).doOnSuccess(accessTokenValue -\u0026gt; { if (accessTokenValue != null) { //AccessToken 续期，4 小时 tracedPublisherFactory.getTracedMono(redisTemplate.getExpire(token).publishOn(Schedulers.parallel()), exchange).doOnSuccess(expire -\u0026gt; { log.info(\u0026#34;accessTokenValue = {}, expire = {}\u0026#34;, accessTokenValue, expire); if (expire != null \u0026amp;\u0026amp; expire.toHours() \u0026lt; 4) { redisTemplate.expire(token, Duration.ofHours(4)).subscribe(); } }).subscribe(); } }) //必须转换为非空，否则 flatmap 不会执行；也不能在最后使用 switchIfEmpty，因为整体返回是 Mono\u0026lt;Void\u0026gt;，无论如何都携带空内容，导致每个请求被发送两次。 .defaultIfEmpty(\u0026#34;\u0026#34;) .flatMap(accessTokenValue -\u0026gt; { try { if (StringUtils.isNotBlank(accessTokenValue)) { JSONObject accessToken = JSON.parseObject(accessTokenValue); String userId = accessToken.getString(\u0026#34;userId\u0026#34;); if (StringUtils.isNotBlank(userId)) { //解析 Token HttpHeaders newHeaders = parse(accessToken); //继续请求 return FilterUtil.changeRequestHeader(exchange, chain, newHeaders); } } return continueOrUnauthorized(path, exchange, chain, finalHeaders); } catch (Exception e) { log.error(\u0026#34;read accessToken error: {}\u0026#34;, e.getMessage(), e); return continueOrUnauthorized(path, exchange, chain, finalHeaders); } }); } 以下是几个关键点需要注意：\nSpring-Cloud-Sleuth 优先在 Spring-WebFlux 中进行跟踪。如果我们在 Filters 中创建新的 Flux 或 Mono，内部没有跟踪信息，需要手动添加。这可以参考我的另一篇文章：Spring Cloud Gateway 没有跟踪信息，我完全困惑 对于 spring-data-redis + Lettuce 连接池组合，对于异步接口，我们应该在获得响应后切换到不同的线程池。否则，后续线程仍将使用 Redisson 的线程，如果耗时，它会影响其他使用 Redis 的业务，此时间消耗也会计入 Redis 连接命令超时 Project Reactor 如果中间结果有 null 值，不会执行后续的 flatmap、map 和其他流操作。如果在这里终止，前端会收到有问题的响应。所以我们需要在中间结果的每一步考虑 null 问题。 spring-cloud-gateway 中的核心 GatewayFilter 接口从其核心方法返回 Mono\u0026lt;Void\u0026gt;。Mono 本质上携带空内容，阻止我们在最后使用 switchIfEmpty 来简化中间步骤中的 null 处理。使用它会导致每个请求被发送两次。 经过此修改后，对网关进行压力测试显示，即使每个单实例 20k qps 也没有重现此问题。\n","date":"2021年9月1日","externalUrl":null,"permalink":"/zh-cn/posts/spring-cloud-gateway-avalanche/","section":"文章","summary":"深入探讨生产事件，其中我们的 Spring Cloud Gateway 由于阻塞的 Redis 操作而经历了级联故障。了解响应式环境中的同步 API 调用如何导致线程饥饿，导致健康检查失败和系统范围的雪崩，以及使用异步模式的完整解决方案。","title":"网关雪崩危机：同步 Redis 调用如何几乎摧毁我们的系统","type":"posts"},{"content":"","date":"2021年8月9日","externalUrl":null,"permalink":"/zh-cn/tags/arthas/","section":"Tags","summary":"","title":"Arthas","type":"tags"},{"content":"","date":"2021年8月9日","externalUrl":null,"permalink":"/zh-cn/categories/production-issues/","section":"Categories","summary":"","title":"Production Issues","type":"categories"},{"content":" 一个奇怪的 Bug 追踪：当异常失去声音时 # 最近，我们的团队一直在使用第三方 SDK 进行一些开发任务。一切运行顺利，直到生产环境突然开始抛出错误，我们遇到了一些真正奇怪的事情。我的一个队友来找我说，\u0026ldquo;代码只是执行到一半就停止了，跳过了整个部分！\u0026rdquo;\n有问题的代码出人意料地简单：\ntry { log.info(\u0026#34;initiate client with conf: {}\u0026#34;, conf); SDKClient client = new SDKClient(conf); client.init(); log.info(\u0026#34;client initiated\u0026#34;); } catch (Exception e) { log.error(\u0026#34;initiate client failed\u0026#34;, e); } log.info(\u0026#34;start to manipulate...\u0026#34;); 我们发现客户端实际上没有成功初始化，导致所有后续业务逻辑失败。当我们检查日志时，这是我们发现的：\ninitiate client with conf: xxxxx start to manipulate... 这正是我队友所说的\u0026quot;代码跳来跳去\u0026quot;的意思。我们在日志中从未看到 client initiated 或 initiate client failed - 它直接跳到了 start to manipulate...\n使用 Arthas 进行侦探工作 # 对于熟悉我们设置的人来说，我们在 k8s + Docker 上运行，每个镜像都内置了 Arthas，使用 Java 16 并启用 JFR。我们的日志包括跟踪信息，并通过 ELK Agent 拉取到集中式日志服务器。\n由于 SDK 需要 IP 白名单才能远程访问，我们无法在本地直接测试他们的生产环境。我们对他们的测试环境的本地测试工作得很好。所以我们不得不依赖 Arthas 进行调查。\n首先，让我们使用 jad 命令验证生产代码是否与我们本地看到的匹配：\njad fully.qualified.class.name 反编译后，我们确认代码与我们的源代码相同。\n接下来，让我们观察实际的执行流程：\ntrace fully.qualified.class.name method 重新执行方法并检查跟踪后，我们发现初始化期间确实抛出了异常：\n# 省略无关细节 +---[min=0.010174ms,max=0.01184ms,total=0.022014ms,count=2] org.apache.logging.log4j.Logger:info() #130 +---[min=599.388978ms,max=630.23967ms,total=1229.628648ms,count=2] com.dasha13.sdk.SDKClient:\u0026lt;init\u0026gt;() #131 +---[min=203.617545ms,max=221.785512ms,total=425.403057ms,count=2] com.dasha13.sdk.SDKClient:init() #132 [throws Exception,2] +---[min=0.034798ms,max=0.084505ms,total=0.119303ms,count=2] org.apache.logging.log4j.Logger:error() #136 +---[min=0.010174ms,max=0.01184ms,total=0.022014ms,count=2] org.apache.logging.log4j.Logger:info() #138 但这里有个谜：为什么这个异常没有被记录？ 让我们使用深度为 2 的 watch 命令进行更深入的研究，以捕获堆栈跟踪和消息：\nwatch com.dasha13.sdk.SDKClient init {throwExp} -x 2 然而，我们只得到了看起来像消息的输出：\nmethod=com.dasha13.sdk.SDKClient init location=AtExceptionExit ts=2021-08-10 02:58:15; [cost=131.20209ms] result=ERROR DATA!!! object class: class java.util.ArrayList, exception class: class com.google.common.util.concurrent.UncheckedExecutionException, exception message: java.lang.IllegalArgumentException 这很奇怪。通常，深度设置为 2 时，如果抛出异常，输出应该包括异常消息和堆栈跟踪。这里发生了什么？让我们尝试分别获取堆栈跟踪和消息。\n首先，堆栈跟踪：\nwatch com.dasha13.sdk.SDKClient init {throwExp.getStackTrace()} -x 2 重新执行有问题的方法，堆栈跟踪正常输出。查看它，问题似乎与 Google 的依赖注入框架 Guice（类似于 Spring）加载 bean 失败有关：\nts=2021-08-10 03:03:37; [cost=146.644563ms] result=@ArrayList[ @StackTraceElement[][ @StackTraceElement[com.google.inject.internal.InjectorImpl$2.get(InjectorImpl.java:1025)], @StackTraceElement[com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1051)], @StackTraceElement[com.dasha13.sdk.SDKClient.init(SDKClient.java:482)], # 其他堆栈跟踪省略 现在让我们检查异常消息：\nwatch com.dasha13.sdk.SDKClient init {throwExp.getMessage()} -x 2 重新执行方法，我们发现 watch 命令失败了：\nwatch failed, condition is: null, express is: {throwExp.getMessage()}, com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException, visit /app/arthas/arthas.log for more details. 按照建议，我们检查了 arthas 日志，发现了这个异常堆栈跟踪：\n2021-08-10 03:07:11 [XNIO-2 task-3] ERROR c.t.a.c.command.express.OgnlExpress -Error during evaluating the expression: com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203) at com.google.common.cache.LocalCache.get(LocalCache.java:3937) at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941) at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824) at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4830) at com.google.inject.internal.util.StackTraceElements.forMember(StackTraceElements.java:66) at com.google.inject.internal.Errors.formatSource(Errors.java:806) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.formatInjectionPoint(Errors.java:839) at com.google.inject.internal.Errors.formatSource(Errors.java:800) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.format(Errors.java:584) at com.google.inject.ProvisionException.getMessage(ProvisionException.java:60) cause by: MethodNotFoundException: Method not found: class com.google.common.xxxxxxxxx 啊哈！我们发现 ProvisionException 的 getMessage() 方法正在抛出异常 - 异常的 getMessage() 导致了异常！ 查看根本原因，我们确定了 Guava 和 Guice 之间的版本不兼容。根本问题是第三方接口超时，导致初始化失败。这些异常被包装在 ProvisionException 中，但由于 ProvisionException 的 getMessage 依赖于 Guava Cache 来缓存异常信息，而我们项目的 Guava 版本与 Guice 版本不兼容，某些方法不存在。这导致 ProvisionException 的 getMessage 也抛出异常。以前这工作正常，因为第三方接口在初始化期间没有遇到超时。\n为什么异常信息没有被记录 # 我们使用 log4j2 的异步日志配置，将异常作为最后一个参数传递给日志方法。在正常情况下，这将输出异常的消息和堆栈跟踪。但正如我们上面发现的，获取消息本身正在抛出异常。Log4j 使用生产者-消费者架构处理日志事件。在这里，消费者尝试获取异常的消息和堆栈跟踪，但在获取消息时遇到异常。对于 Log4j2 异步日志，当发生异常时，原始日志事件被完全丢弃，异常被输出到 StatusLogger（这基本上进入标准错误输出）。这对应于 log4j 源代码：\nAppenderControl\nprivate void tryCallAppender(final LogEvent event) { try { //调用 appender 输出日志 appender.append(event); } catch (final RuntimeException error) { //处理 RuntimeException handleAppenderError(event, error); } catch (final Exception error) { //处理其他 Exceptions handleAppenderError(event, new AppenderLoggingException(error)); } } private void handleAppenderError(final LogEvent event, final RuntimeException ex) { appender.getHandler().error(createErrorMsg(\u0026#34;An exception occurred processing Appender \u0026#34;), event, ex); if (!appender.ignoreExceptions()) { throw ex; } } ErrorHandler 通常是默认实现 DefaultErrorHandler，它输出到 StatusLogger：\nDefaultErrorHandler\nprivate static final Logger LOGGER = StatusLogger.getLogger(); public void error(final String msg, final LogEvent event, final Throwable t) { final long current = System.nanoTime(); if (current - lastException \u0026gt; EXCEPTION_INTERVAL || exceptionCount++ \u0026lt; MAX_EXCEPTIONS) { LOGGER.error(msg, t); } lastException = current; if (!appender.ignoreExceptions() \u0026amp;\u0026amp; t != null \u0026amp;\u0026amp; !(t instanceof AppenderLoggingException)) { throw new AppenderLoggingException(msg, t); } } StatusLogger 基本上输出到标准错误 System.err：\nStatusLogger\nthis.logger = new SimpleLogger(\u0026#34;StatusLogger\u0026#34;, Level.ERROR, false, true, showDateTime, false, dateFormat, messageFactory, PROPS, //标准错误输出 System.err); 在我们的部署架构中，标准错误输出到一个相当隐蔽的位置，基本上没有人检查，所以我们错过了它。当我们最终查看标准错误输出时，我们确实找到了异常：\n2021-08-10 03:30:29,810 Log4j2-TF-10-AsyncLoggerConfig-3 ERROR An exception occurred processing Appender file com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203) at com.google.common.cache.LocalCache.get(LocalCache.java:3937) at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941) at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824) at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4830) at com.google.inject.internal.util.StackTraceElements.forMember(StackTraceElements.java:66) at com.google.inject.internal.Errors.formatSource(Errors.java:806) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.formatInjectionPoint(Errors.java:839) at com.google.inject.internal.Errors.formatSource(Errors.java:800) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.format(Errors.java:584) at com.google.inject.ProvisionException.getMessage(ProvisionException.java:60) at org.apache.logging.log4j.core.impl.ThrowableProxy.\u0026lt;init\u0026gt;(ThrowableProxy.java:105) at org.apache.logging.log4j.core.impl.ThrowableProxy.\u0026lt;init\u0026gt;(ThrowableProxy.java:93) at org.apache.logging.log4j.core.impl.Log4jLogEvent.getThrownProxy(Log4jLogEvent.java:629) at org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter.format(ExtendedThrowablePatternConverter.java:63) at org.springframework.boot.logging.log4j2.ExtendedWhitespaceThrowablePatternConverter.format(ExtendedWhitespaceThrowablePatternConverter.java:50) at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38) at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:345) at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244) at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229) at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181) at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:312) at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156) at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129) at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120) at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84) at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:543) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.callAppenders(AsyncLoggerConfig.java:127) at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:502) at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:485) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.log(AsyncLoggerConfig.java:121) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logToAsyncLoggerConfigsOnCurrentThread(AsyncLoggerConfig.java:169) at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:111) at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:97) at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:168) at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125) at java.base/java.lang.Thread.run(Thread.java:834) Caused by: java.lang.IllegalArgumentException at com.google.inject.internal.asm.$ClassReader.\u0026lt;init\u0026gt;(Unknown Source) at com.google.inject.internal.asm.$ClassReader.\u0026lt;init\u0026gt;(Unknown Source) at com.google.inject.internal.asm.$ClassReader.\u0026lt;init\u0026gt;(Unknown Source) at com.google.inject.internal.util.LineNumbers.\u0026lt;init\u0026gt;(LineNumbers.java:65) at com.google.inject.internal.util.StackTraceElements$1.load(StackTraceElements.java:43) at com.google.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3527) at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2319) at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2282) at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2197) ... 41 more 此外，基于 Appender 的 ignoreExceptions 配置（默认为 true），它确定调用位置是否应该抛出异常。然而，这是针对同步日志。异步日志将异常抛出到 Disruptor 的异常处理器，Log4j2 的 Disruptor 异常处理也将异常输出到 System.err（标准错误输出）。默认情况下，不抛出异常，因为对于同步日志，没有人希望业务逻辑仅仅因为日志问题而失败。对于异步日志，由于之前的处理已经输出到标准错误，不需要冗余。\n","date":"2021年8月9日","externalUrl":null,"permalink":"/zh-cn/posts/log-exception/","section":"文章","summary":"深入探讨一个神秘的生产问题，其中异常日志神秘消失，引导我们通过 Arthas 调试、Log4j2 内部机制，以及发现异常的 getMessage() 方法本身由于 Guava-Guice 版本不兼容而抛出异常。","title":"一个奇怪的 Bug 追踪：当异常失去声音时","type":"posts"},{"content":"今天，我们的运维团队联系了我们，说有一个复杂的 SQL 查询让他们很困扰。这个查询如此复杂，甚至运行 EXPLAIN 都需要很长时间才能执行！我们的后端团队介入帮助解决这个 SQL 性能问题，在这个过程中，我们偶然发现了一个深深隐藏在我们系统中的生产问题。\n有问题的 SQL 查询 # 这是引发一切的查询：\nselect a.share_code, a.generated_time, a.share_user_id, b.user_count, b.order_count, a.share_order_id, b.rewarded_amount from t_risk_share_code a, (select count(distinct r.user_id) user_count, count(distinct r.order_id) order_count, s.rewarded_amount, r.share_code from t_order s,t_order_rel r where r.order_id = s.id and r.type = 1 and r.share_code = \u0026#39;recently_shared_order_code\u0026#39; group by r.share_code) b where a.share_code = b.share_code and a.type = 1 第一个危险信号是，即使在这个查询上运行 EXPLAIN 也非常慢，这表明一些子查询实际上在规划阶段被执行。所以我们的第一步是分解子查询并逐段分析：\nselect count(distinct r.user_id) user_count, count(distinct r.order_id) order_count, max(s.rewarded_amount), r.share_code from t_order s,t_order_rel r where r.order_id = s.id and r.type = 1 and r.share_code = \u0026#39;recently_shared_order_code\u0026#39; group by r.share_code 当我们在这个隔离的子查询上运行 EXPLAIN 时，它执行得很快，但结果令人困惑：\n等等，什么？为什么 t_order 表在进行全表扫描？这个表有适当的索引，id 作为主键！\n根本原因分析 # 根据官方 MySQL 文档，MySQL 可能选择全表扫描的原因有几个：\n表太小 - 使用索引不值得。但这不是我们的情况；两个表都包含数千万条记录。\n没有适合 WHERE 或 ON 条件的索引 - 也不是我们的情况。两个表都有适合 WHERE 和 ON 条件的索引（尽管这里所有条件都在 WHERE 子句中，MySQL 会在执行期间将其优化为 JOIN ON + WHERE）。\n索引分析显示大多数表值会被命中 - 当 MySQL 通过索引分析确定无论如何都需要将表的大多数页加载到内存中以进行最终数据检索时，直接扫描整个表实际上比首先将索引加载到内存以获取匹配行，然后无论如何加载大多数表页更快。从性能角度来看，这是有道理的。然而，在我们的 SQL 中，t_order_rel 表根据 WHERE 条件只会返回几十条记录，而 t_order 与 t_order_rel 具有一对多关系，所以这不应该命中太多记录。\n列的低基数（唯一性） - 基数是不同值的数量除以总行数，最大值为 1。对于 InnoDB，此值不是实时计算的，可能不准确，特别是当列值更新导致页内行位置更改时。然而，对于 DISTINCT 或主键列，这不需要计算，因为它总是 1。低基数类似于情况 #3 - 会命中太多行。由于我们的 SQL 使用主键，这不适用于这里。\n虽然这些情况都不符合我们的情况，但以下是我们用来避免全表扫描的一些优化策略：\n定期统计更新：为了使 SQL 执行计划分析器更准确（解决情况 #4），我们在低流量期间定期运行 ANALYZE TABLE 以确保分析器统计数据的准确性。\n强制索引使用：考虑到数据库分片以及数据库 SQL 执行计划并不总是完美的，可能选择错误的索引，我们通常在 OLTP 查询中添加 FORCE INDEX 以强制使用特定索引。这是使用基于中间件的分片解决方案（如 sharding-jdbc）或原生分布式数据库（如 TiDB）时的常见陷阱。\nMySQL 配置：我们设置 --max-seeks-for-key = 10000（默认值非常大）。这限制了 SQL 执行计划分析器认为在使用索引时可能扫描的行数。原理很简单，如源代码所示：\nsql_planner.cc\ndouble find_cost_for_ref(const THD *thd, TABLE *table, unsigned keyno, double num_rows, double worst_seeks) { // 比较分析的扫描行数与 max_seeks_for_key，取较小值 // 这意味着 SQL 分析器的结论对于索引扫描不会超过 max_seeks_for_key num_rows = std::min(num_rows, double(thd-\u0026gt;variables.max_seeks_for_key)); if (table-\u0026gt;covering_keys.is_set(keyno)) { // 我们可以只使用索引树 const Cost_estimate index_read_cost = table-\u0026gt;file-\u0026gt;index_scan_cost(keyno, 1, num_rows); return index_read_cost.total_cost(); } else if (keyno == table-\u0026gt;s-\u0026gt;primary_key \u0026amp;\u0026amp; table-\u0026gt;file-\u0026gt;primary_key_is_clustered()) { const Cost_estimate table_read_cost = table-\u0026gt;file-\u0026gt;read_cost(keyno, 1, num_rows); return table_read_cost.total_cost(); } else return min(table-\u0026gt;cost_model()-\u0026gt;page_read_cost(num_rows), worst_seeks); } 此值不应设置得太低，因为它可能导致系统在多个索引可用时选择扫描最多行的索引。\n使用 Optimizer Trace # 当 EXPLAIN 不足以进行分析时，我们不得不转向 optimizer_trace。我们不将 optimizer_trace 作为首选，因为它需要 SQL 完全执行后才能提供所有有用信息。\n## 启用 optimizer_trace set session optimizer_trace=\u0026#34;enabled=on\u0026#34;; ## 执行 SQL select ..... ## 查询跟踪结果 SELECT trace FROM information_schema.OPTIMIZER_TRACE; 通过跟踪结果，我们发现实际执行的 SQL 是：\nSELECT various_fields FROM `t_order_rel` `r` JOIN `t_order` `s` WHERE ( ( `r`.`order_id` = CONVERT ( `s`.`id` USING utf8mb4 ) ) AND ( `r`.`type` = 1 ) AND ( `r`.`share_code` = \u0026#39;B2MTB6C\u0026#39; ) ) 啊哈！两个表的字段具有不同的字符编码！这导致 JOIN ON 条件包装了字符编码转换：CONVERT ( s.id USING utf8mb4 )。我们知道，当字段在条件匹配中被函数包装时，无法使用索引。例如：date(create_time) \u0026lt; \u0026quot;2021-8-1\u0026quot; 不能使用索引，但 create_time \u0026lt; \u0026quot;2021-8-1\u0026quot; 可以。不同列类型之间的比较也不能使用索引，因为 MySQL 会自动用类型转换函数包装它们。这是由 MySQL 的语法糖引起的常见误用。\nt_order_rel 表与其他表具有不同的默认编码。由于某些字段使用表情符号表达式，整个表默认使用 utf8mb4 编码创建。此外，此表仅用于日志记录目的，没有 OLTP 业务操作 - 只有运维团队成员使用的一些 OLAP 场景。这就是为什么这个问题被忽视这么长时间。\n修复字段编码后，SQL 终于停止进行全表扫描。展望未来，我们将更加注意：\n在数据库级别指定默认编码，不为表指定默认编码，只为需要特殊编码的单个字段指定编码 注意 JOIN 和 WHERE 比较两侧的类型一致性，以避免阻止索引使用 这是一个很好的提醒，有时最有影响力的问题就是那些隐藏在显而易见的地方的问题！\n","date":"2021年8月7日","externalUrl":null,"permalink":"/zh-cn/posts/big-table-index-invalid/","section":"文章","summary":"当我们的运维团队带来一个执行时间极长的复杂 SQL 查询时，我们以为这只是一个性能问题。我们不知道，这次调查会发现一个深深隐藏的字符编码不匹配问题，它一直在我们的生产数据库中默默地导致全表扫描。","title":"通过 SQL 优化发现的隐藏生产问题","type":"posts"},{"content":"","date":"2021年3月27日","externalUrl":null,"permalink":"/zh-cn/categories/technical-analysis/","section":"Categories","summary":"","title":"Technical Analysis","type":"categories"},{"content":" 使用 JFR 排查 SSL 性能瓶颈 # 在某个时间点，我们的微服务的一个实例突然经历了 CPU 使用率的急剧飙升：\n同时，我们注意到建立了异常数量的数据库连接： 有趣的是，其他实例根本没有显示这种行为。\n根本原因调查 # 考虑到大量的数据库连接，我们最初的假设是数据库可能表现不佳。然而，当我们检查这个时间段内的 SQL 统计时，数据库性能看起来完全正常：\n在此期间，我们微服务的热点 SQL 查询执行时没有任何明显的延迟。那么是什么可能导致这个问题？我们考虑了几种可能性：垃圾收集开销、safepoint 操作或长时间获取锁（更多详细信息，请查看：使用 JFR 进行 Java 监控完整指南）。为了彻底解决这个问题，我们决定捕获 JFR 转储并分析 safepoint 事件、GC 活动和 Monitor Blocked 事件。\n我们的第一站是检查 GC 行为。我们发现所有垃圾收集都是 Young GC 事件，暂停时间可接受：\n接下来，我们查看了 safepoint 操作。虽然我们确实捕获了一些 safepoint 事件，但它们的暂停持续时间并不特别令人担忧：\n最后，我们调查了 Java Monitor Blocks，发现了一些有趣的事情 - 大量长时间锁等待的实例：\n堆栈跟踪显示线程在以下位置被阻塞：void sun.security.provider.SecureRandom.engineNextBytes(byte[])。这指向了一个与随机数生成相关的经典 Java 性能问题。查看 NativePRNG 中的相关代码：\n// name of the *System* property, takes precedence over PROP_RNDSOURCE private static final String PROP_EGD = \u0026#34;java.security.egd\u0026#34;; // name of the *Security* property private static final String PROP_RNDSOURCE = \u0026#34;securerandom.source\u0026#34;; private static final boolean useLegacyDSA = Boolean.parseBoolean(GetPropertyAction.privilegedGetProperty (\u0026#34;jdk.security.legacyDSAKeyPairGenerator\u0026#34;)); static final String URL_DEV_RANDOM = \u0026#34;file:/dev/random\u0026#34;; static final String URL_DEV_URANDOM = \u0026#34;file:/dev/urandom\u0026#34;; 这涉及两种不同的生成随机数种子的方法：\u0026ldquo;file:/dev/random\u0026rdquo; 和 \u0026ldquo;file:/dev/urandom\u0026rdquo;。你可以通过设置系统属性 java.security.egd 来指定使用哪一个，默认选择是 \u0026ldquo;file:/dev/random\u0026rdquo;。\n理解随机数生成和解决方案 # 在 Linux 4.8 之前：\n从 Linux 4.8 开始：\n关键洞察是：当熵池不足时，默认的 \u0026ldquo;file:/dev/random\u0026rdquo; 将阻塞并等待，而 \u0026ldquo;file:/dev/urandom\u0026rdquo; 继续运行而不阻塞。对于我们的用例，\u0026ldquo;file:/dev/urandom\u0026rdquo; 提供了足够的随机性，因此我们可以通过设置系统属性 -Djava.security.egd=file:/dev/./urandom 来使用 urandom 并消除阻塞行为来解决这个问题。\n","date":"2021年3月27日","externalUrl":null,"permalink":"/zh-cn/posts/jfr-ssl/","section":"文章","summary":"深入分析微服务性能问题，包括 CPU 峰值和数据库连接异常。通过 JFR 分析，我们发现根本原因是 Java SecureRandom 在 /dev/random 上阻塞，并提供使用 /dev/urandom 的解决方案。","title":"使用 JFR 排查 SSL 性能瓶颈","type":"posts"},{"content":"","date":"2021年2月3日","externalUrl":null,"permalink":"/zh-cn/categories/memory-management/","section":"Categories","summary":"","title":"Memory Management","type":"categories"},{"content":"","date":"2021年2月3日","externalUrl":null,"permalink":"/zh-cn/tags/tlab/","section":"Tags","summary":"","title":"TLAB","type":"tags"},{"content":" 1. 观前提醒 # 本期内容比较硬核，非常全面，涉及到了设计思想到实现原理以及源码，并且还给出了相应的日志以及监控方式，如果有不清楚或者有疑问的地方，欢迎留言。\n其中涉及到的设计思想主要为个人理解，实现原理以及源码解析也是个人整理，如果有不准确的地方，非常欢迎指正！提前感谢~~\n2. 分配内存实现思路 # 我们经常会 new 一个对象，这个对象是需要占用空间的，第一次 new 一个对象占用的空间如 图00 所示，\n我们这里先只关心堆内部的存储，元空间中的存储，我们会在另一个系列详细讨论。堆内部的存储包括对象头，对象体以及内存对齐填充，那么这块空间是如何分配的呢？\n首先，对象所需的内存，在对象的类被解析加载进入元空间之后，就可以在分配内存创建前计算出来。假设现在我们自己来设计堆内存分配，一种最简单的实现方式就是线性分配，也被称为撞针分配（bump-the-pointer）。\n每次需要分配内存时，先计算出需要的内存大小，然后 CAS 更新如 图01 中所示的内存分配指针，标记分配的内存。但是内存一般不是这么整齐的，可能有些内存在分配有些内存就被释放回收了。所以一般不会只靠撞针分配。一种思路是在撞针分配的基础上，加上一个 FreeList。\n简单的实现是将释放的对象内存加入 FreeList，下次分配对象的时候，优先从 FreeList 中寻找合适的内存大小进行分配，之后再在主内存中撞针分配。\n这样虽然一定程度上解决了问题，但是目前大多数应用是多线程的，所以内存分配是多线程的，都从主内存中分配，CAS 更新重试过于频繁导致效率低下。目前的应用，一般根据不同业务区分了不同的线程池，在这种情况下，一般每个线程分配内存的特性是比较稳定的。这里的比较稳定指的是，每次分配对象的大小，每轮 GC 分配区间内的分配对象的个数以及总大小。所以，我们可以考虑每个线程分配内存后，就将这块内存保留起来，用于下次分配，这样就不用每次从主内存中分配了。如果能估算每轮 GC 内每个线程使用的内存大小，则可以提前分配好内存给线程，这样就更能提高分配效率。这种内存分配的实现方式，在 JVM 中就是 TLAB （Thread Local Allocate Buffer）。\n3. JVM 对象堆内存分配流程简述 # 我们这里不考虑栈上分配，这些会在 JIT 的章节详细分析，我们这里考虑的是无法栈上分配需要共享的对象。\n对于 HotSpot JVM 实现，所有的 GC 算法的实现都是一种对于堆内存的管理，也就是都实现了一种堆的抽象，它们都实现了接口 CollectedHeap。当分配一个对象堆内存空间时，在 CollectedHeap 上首先都会检查是否启用了 TLAB，如果启用了，则会尝试 TLAB 分配；如果当前线程的 TLAB 大小足够，那么从线程当前的 TLAB 中分配；如果不够，但是当前 TLAB 剩余空间小于最大浪费空间限制（这是一个动态的值，我们后面会详细分析），则从堆上（一般是 Eden 区） 重新申请一个新的 TLAB 进行分配。否则，直接在 TLAB 外进行分配。TLAB 外的分配策略，不同的 GC 算法不同。例如G1：\n如果是 Humongous 对象（对象在超过 Region 一半大小的时候），直接在 Humongous 区域分配（老年代的连续区域）。 根据 Mutator 状况在当前分配下标的 Region 内分配 4. TLAB 的生命周期 # TLAB 是线程私有的，线程初始化的时候，会创建并初始化 TLAB。同时，在 GC 扫描对象发生之后，线程第一次尝试分配对象的时候，也会创建并初始化 TLAB。 TLAB 生命周期停止（TLAB 生命周期停止不代表内存被回收，只是代表这个 TLAB 不再被这个线程私有管理）在：\n当前 TLAB 不够分配，并且剩余空间小于最大浪费空间限制，那么这个 TLAB 会被退回 Eden，重新申请一个新的 发生 GC 的时候，TLAB 被回收。 5. TLAB 要解决的问题以及带来的问题与解决方案的思考 # TLAB 要解决的问题很明显，尽量避免从堆上直接分配内存从而避免频繁的锁争用。\n引入 TLAB 之后，TLAB 的设计上，也有很多值得考虑的问题。\n5.1. 引入 TLAB 后，会有内存孔隙问题，还可能影响 GC 扫描性能 # 出现孔隙的情况：\n当前 TLAB 不够分配时，如果剩余空间小于最大浪费空间限制，那么这个 TLAB 会被退回 Eden，重新申请一个新的。这个剩余空间就会成为孔隙。 当发生 GC 的时候，TLAB 没有用完，没有分配的内存也会成为孔隙。 如果不管这些孔隙，由于 TLAB 仅线程内知道哪些被分配了，在 GC 扫描发生时返回 Eden 区，如果不填充的话，外部并不知道哪一部分被使用哪一部分没有，需要做额外的检查，那么会影响 GC 扫描效率。所以 TLAB 回归 Eden 的时候，会将剩余可用的空间用一个 dummy object 填充满。如果填充已经确认会被回收的对象，也就是 dummy object， GC 会直接标记之后跳过这块内存，增加扫描效率。但是同时，由于需要填充这个 dummy object，所以需要预留出这个对象的对象头的空间。\n5.2. 某个线程在一轮 GC 内分配的内存并不稳定 # 如果我们能提前知道在这一轮内每个线程会分配多少内存，那么我们可以直接提前分配好。但是，这简直是痴人说梦。每个线程在每一轮 GC 的分配情况可能都是不一样的：\n不同的线程业务场景不同导致分配对象大小不同。我们一般会按照业务区分不同的线程池，做好线程池隔离。对于用户请求，每次分配的对象可能比较小。对于后台分析请求，每次分配的对象相对大一些。 不同时间段内线程压力并不均匀。业务是有高峰有低谷的，高峰时间段内肯定分配对象更多。 同一时间段同一线程池内的线程的业务压力也不一定不能做到很均匀。很可能只有几个线程很忙，其他线程很闲。 所以，综合考虑以上情况，我们应该这么实现 TLAB：\n不能一下子就给一个线程申请一个比较大的 TLAB，而是考虑这个线程 TLAB 分配满之后再申请新的，这样更加灵活。 每次申请 TLAB 的大小是变化的，并不是固定的。 每次申请 TLAB 的大小需要考虑当前 GC 轮次内会分配对象的线程的个数期望 每次申请 TLAB 的大小需要考虑所有线程期望 TLAB 分配满重新申请新的 TLAB 次数 6. JVM 中的期望计算 EMA # 在上面提到的 TLAB 大小设计的时候，我们经常提到期望。这个期望是根据历史数据计算得出的，也就是每次输入采样值，根据历史采样值得出最新的期望值。不仅 TLAB 用到了这种期望计算，GC 和 JIT 等等 JVM 机制中都用到了。这里我们来看一种 TLAB 中经常用到的 EMA（Exponential Moving Average 指数平均数） 算法：\nEMA 算法的核心在于设置合适的最小权重，我们假设一个场景：首先采样100个 100（算法中的前 100 个是为了排除不稳定的干扰，我们这里直接忽略前 100 个采样），之后采样 50 个 2，最后采样 50 个 200，对于不同的最小权重，来看一下变化曲线。\n可以看出，最小权重越大，变化得越快，受历史数据影响越小。根据应用设置合适的最小权重，可以让你的期望更加理想。\n这块对应的源代码：gcUtil.hpp 的 AdaptiveWeightedAverage 类。\n7. TLAB 相关的 JVM 参数 # 这里仅仅是列出来，并附上简介，看不懂没关系，之后会有详细分析，帮助你理解每一个参数。等你理解后，这个小章节就是你的工具书啦~~ 以下参数以及默认值基于 OpenJDK 17\n7.1. TLABStats（已过期） # 从 Java 12 开始已过期，目前已经没有相关的逻辑了。之前是用于 TLAB 统计数据从而更好地伸缩 TLAB 但是性能消耗相对较大，但是现在主要通过 EMA 计算了。\n7.2. UseTLAB # 说明：是否启用 TLAB，默认是启用的。\n默认：true\n举例：如果想关闭：-XX:-UseTLAB\n7.3. ZeroTLAB # 说明：是否将新创建的 TLAB 内的所有字节归零。我们创建一个类的时候，类的 field 是有默认值的，例如 boolean 是 false，int 是 0 等等，实现的方式就是对分配好的内存空间赋 0。设置 ZeroTLAB 为 true 代表在 TLAB 申请好的时候就赋 0，否则会在分配对象并初始化的时候赋 0.讲道理，由于 TLAB 分配的时候会涉及到 Allocation Prefetch 优化 CPU 缓存，在 TLAB 分配好之后立刻更新赋 0 对于 CPU 缓存应该是更友好的，并且，如果 TLAB 没有用满，填充的 dummy object 其实依然是 0 数组，相当于大部分不用改。这么看来，开启应该更好。但是ZeroTLAB 默认还是不开启的。\n默认：false\n举例：-XX:+ZeroTLAB\n7.4. ResizeTLAB # 说明：TLAB 是否是可变的，默认为是，也就是会根据线程历史分配数据相关 EMA 计算出每次期望 TLAB 大小并以这个大小为准申请 TLAB。\n默认：true\n举例：如果想关闭：-XX:-ResizeTLAB\n7.5. TLABSize # 说明：初始 TLAB 大小。单位是字节\n默认：0， 0 就是不主动设置 TLAB 初始大小，而是通过 JVM 自己计算每一个线程的初始大小\n举例：-XX:TLABSize=65536\n7.6. MinTLABSize # 说明：最小 TLAB 大小。单位是字节\n默认：2048\n举例：-XX:TLABSize=4096\n7.7. TLABAllocationWeight # 说明： TLAB 初始大小计算和线程数量有关，但是线程是动态创建销毁的。所以需要基于历史线程个数推测接下来的线程个数来计算 TLAB 大小。一般 JVM 内像这种预测函数都采用了 EMA 。这个参数就是 图06 中的最小权重，权重越高，最近的数据占比影响越大。TLAB 重新计算大小是根据分配比例，分配比例也是采用了 EMA 算法，最小权重也是 TLABAllocationWeight\n默认：35\n举例：-XX:TLABAllocationWeight=70\n7.8. TLABWasteTargetPercent # 说明：TLAB 的大小计算涉及到了 Eden 区的大小以及可以浪费的比率。TLAB 浪费指的是上面提到的重新申请新的 TLAB 的时候老的 TLAB 没有分配的空间。这个参数其实就是 TLAB 浪费占用 Eden 的百分比，这个参数的作用会在接下来的原理说明内详细说明\n默认：1\n举例：-XX:TLABWasteTargetPercent=10\n7.9. TLABRefillWasteFraction # 说明： 初始最大浪费空间限制计算参数，初始最大浪费空间限制 = 当前期望 TLAB 大小 / TLABRefillWasteFraction\n默认：64\n举例：-XX:TLABRefillWasteFraction=32\n7.10. TLABWasteIncrement # 说明： 最大浪费空间限制并不是不变的，在发生 TLAB 缓慢分配的时候（也就是当前 TLAB 空间不足以分配的时候），会增加最大浪费空间限制。这个参数就是 TLAB 缓慢分配时允许的 TLAB 浪费增量。单位不是字节，而是 MarkWord 个数，也就是 Java 堆的内存最小单元，64 位虚拟机的情况下，MarkWord 大小为 8 字节。\n默认：4\n举例：-XX:TLABWasteIncrement=4\n8.TLAB 基本流程 # 8.0. 如何设计每个线程的 TLAB 大小 # 之前我们提到了引入 TLAB 要面临的问题以及解决方式，根据这些我们可以这么设计 TLAB。\n首先，TLAB 的初始大小，应该和每个 GC 内需要对象分配的线程个数相关。但是，要分配的线程个数并不一定是稳定的，可能这个时间段线程数多，下个阶段线程数就不那么多了，所以，需要用 EMA 的算法采集每个 GC 内需要对象分配的线程个数来计算这个个数期望。\n接着，我们最理想的情况下，是每个 GC 内，所有用来分配对象的内存都处于对应线程的 TLAB 中。每个 GC 内用来分配对象的内存从 JVM 设计上来讲，其实就是 Eden 区大小。在 最理想的情况下，最好只有Eden 区满了的时候才会 GC，不会有其他原因导致的 GC，这样是最高效的情况。Eden 区被用光，如果全都是 TLAB 内分配，也就是 Eden 区被所有线程的 TLAB 占满了，这样分配是最快的。\n然后，每轮 GC 分配内存的线程个数以及大小是不一定的，如果一下子分配一大块会造成浪费，如果太小则会频繁从 Eden 申请 TLAB，降低效率。这个大小比较难以控制，但是我们可以限制每个线程究竟在一轮 GC 内，最多从 Eden 申请多少次 TLAB，这样对于用户来说更好控制。\n最后，每个线程分配的内存大小，在每轮 GC 并不一定稳定，只用初始大小来指导之后的 TLAB 大小，显然不够。我们换个思路，每个线程分配的内存和历史有一定关系因此我们可以从历史分配中推测，所以每个线程也需要采用 EMA 的算法采集这个线程每次 GC 分配的内存，用于指导下次期望的 TLAB 的大小。\n综上所述，我们可以得出这样一个近似的 TLAB 计算公式：\n每个线程 TLAB 初始大小 = Eden区大小 / (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB * 当前 GC 分配线程个数 EMA)\nGC 后，重新计算 TLAB 大小 = Eden区大小 / (线程单个 GC 轮次内最多从 Eden 申请多少次 TLAB * 当前 GC 分配线程个数 EMA)\n接下来，我们来详细分析 TLAB 的整个生命周期的每个流程。\n8.1. TLAB 初始化 # 线程初始化的时候，如果 JVM 启用了 TLAB（默认是启用的， 可以通过 -XX:-UseTLAB 关闭），则会初始化 TLAB，在发生对象分配时，会根据期望大小申请 TLAB 内存。同时，在 GC 扫描对象发生之后，线程第一次尝试分配对象的时候，也会重新申请 TLAB 内存。我们先只关心初始化，初始化的流程图如 图08 所示：\n初始化时候会计算 TLAB 初始期望大小。这涉及到了 TLAB 大小的限制：\nTLAB 的最小大小：通过MinTLABSize指定 TLAB 的最大大小：不同的 GC 中不同，G1 GC 中为大对象（humongous object）大小，也就是 G1 region 大小的一半。因为开头提到过，在 G1 GC 中，大对象不能在 TLAB 分配，而是老年代。ZGC 中为页大小的 8 分之一，类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度。对于其他的 GC，则是 int 数组的最大大小，这个和之前提到的填充 dummy object 有关，后面会提到详细流程。 之后的流程里面，无论何时，TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内，为了避免啰嗦，我们不会再强调这个限制~~~！！！ 之后的流程里面，无论何时，TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内，为了避免啰嗦，我们不会再强调这个限制~~~！！！ 之后的流程里面，无论何时，TLAB 的大小都会在这个 TLAB 的最小大小 到 TLAB 的最大大小 的范围内，为了避免啰嗦，我们不会再强调这个限制~~~！！！ 重要的事情说三遍~\nTLAB 期望大小（desired size） 在初始化的时候会计算 TLAB 期望大小，之后再 GC 等操作回收掉 TLAB 需要重计算这个期望大小。根据这个期望大小，TLAB 在申请空间的时候每次申请都会以这个期望大小作为基准的空间作为 TLAB 分配空间。\n8.1.1. TLAB 初始期望大小计算 # 如 图08 所示，如果指定了 TLABSize，就用这个大小作为初始期望大小。如果没有指定，则按照如下的公式进行计算：\n堆给TLAB的空间总大小/(当前有效分配线程个数期望*重填次数配置)\n堆给 TLAB 的空间总大小：堆上能有多少空间分配给 TLAB，不同的 GC 算法不一样，但是大多数 GC 算法的实现都是 Eden 区大小，例如： 传统的已经弃用的 Parallel Scanvage 中，就是 Eden 区大小。参考：parallelScavengeHeap.cpp 默认的G1 GC 中是 （YoungList 区域个数减去 Survivor 区域个数） * 区域大小，其实就是 Eden 区大小。参考：g1CollectedHeap.cpp ZGC 中是 Page 剩余空间大小，Page 类似于 Eden 区，是大部分对象分配的区域。参考：zHeap.cpp Shenandoah GC 中是 FreeSet 的大小，也是类似于 Eden 的概念。参考：shenandoahHeap.cpp 当前有效分配线程个数期望：这是一个全局 EMA，EMA 是什么之前已经说明了，是一种计算期望的方式。有效分配线程个数 EMA 的最小权重是 TLABAllocationWeight。有效分配线程个数 EMA 在有线程进行第一次有效对象分配的时候进行采集，在 TLAB 初始化的时候读取这个值计算 TLAB 期望大小。 TLAB 重填次数配置（refills time）：根据 TLABWasteTargetPercent 计算的次数，公式为。TLABWasteTargetPercent 的意义其实是限制最大浪费空间限制，为何重填次数与之相关后面会详细分析。 8.1.2. TLAB 初始分配比例计算 # 如 图08 所示，接下来会计算TLAB 初始分配比例。\n线程私有分配比例 EMA：与有效分配线程个数 EMA对应，有效分配线程个数 EMA是对于全局来说，每个线程应该占用多大的 TLAB 的描述，而分配比例 EMA 相当于对于当前线程应该占用的总 TLAB 空间的大小的一种动态控制。\n初始化的时候，分配比例其实就是等于 1/当前有效分配线程个数。图08 的公式，代入之前的计算 TLAB 期望大小的公式，消参简化之后就是1/当前有效分配线程个数。这个值作为初始值，采集如线程私有的分配比例 EMA。\n8.1.3. 清零线程私有统计数据 # 这些采集数据会用于之后的当前线程的分配比例的计算与采集，从而影响之后的当前线程 TLAB 期望大小。\n8.2. TLAB 分配 # TLAB 分配流程如 图09 所示。\n8.2.1. 从线程当前 TLAB 分配 # 如果启用了 TLAB（默认是启用的， 可以通过 -XX:-UseTLAB 关闭），则首先从线程当前 TLAB 分配内存，如果分配成功则返回，否则根据当前 TLAB 剩余空间与当前最大浪费空间限制大小进行不同的分配策略。在下一个流程，就会提到这个限制究竟是什么。\n8.2.2. 重新申请 TLAB 分配 # 如果当前 TLAB 剩余空间大于当前最大浪费空间限制(根据 图08 的流程，我们知道这个初始值为 期望大小/TLABRefillWasteFraction)，直接在堆上分配。否则，重新申请一个 TLAB 分配。 为什么需要最大浪费空间呢？\n当重新分配一个 TLAB 的时候，原有的 TLAB 可能还有空间剩余。原有的 TLAB 被退回堆之前，需要填充好 dummy object。由于 TLAB 仅线程内知道哪些被分配了，在 GC 扫描发生时返回 Eden 区，如果不填充的话，外部并不知道哪一部分被使用哪一部分没有，需要做额外的检查，如果填充已经确认会被回收的对象，也就是 dummy object， GC 会直接标记之后跳过这块内存，增加扫描效率。反正这块内存已经属于 TLAB，其他线程在下次扫描结束前是无法使用的。这个 dummy object 就是 int 数组。为了一定能有填充 dummy object 的空间，一般 TLAB 大小都会预留一个 dummy object 的 header 的空间，也是一个 int[] 的 header，所以 TLAB 的大小不能超过int 数组的最大大小，否则无法用 dummy object 填满未使用的空间。\n但是，填充 dummy 也造成了空间的浪费，这种浪费不能太多，所以通过最大浪费空间限制来限制这种浪费。\n新的 TLAB 大小，取如下两个值中较小的那个：\n当前堆剩余给 TLAB 可分配的空间，大部分 GC 的实现其实就是对应的 Eden 区剩余大小： 传统的已经弃用的 Parallel Scanvage 中，就是 Eden 区剩余大小。参考：parallelScavengeHeap.cpp 默认的G1 GC 中是当前 Region 中剩余大小，其实就是将 Eden 分区了。参考：g1CollectedHeap.cpp ZGC 中是 Page 剩余空间大小，Page 类似于 Eden 区，是大部分对象分配的区域。参考：zHeap.cpp Shenandoah GC 中是 FreeSet 的剩余大小，也是类似于 Eden 的概念。参考：shenandoahHeap.cpp TLAB 期望大小 + 当前需要分配的空间大小 当分配出来 TLAB 之后，根据 ZeroTLAB 配置，决定是否将每个字节赋 0。在创建对象的时候，本来也要对每个字段赋初始值，大部分字段初始值都是 0，并且，在 TLAB 返还到堆时，剩余空间填充的也是 int[] 数组，里面都是 0。所以其实可以提前填充好。并且，TLAB 刚分配出来的时候，赋 0 也能利用好 Allocation prefetch 的机制适应 CPU 缓存行（Allocation prefetch 的机制会在另一个系列说明），所以可以通过打开 ZeroTLAB 来在分配 TLAB 空间之后立刻赋 0。\n8.2.3. 直接从堆上分配 # 直接从堆上分配是最慢的分配方式。一种情况就是，如果当前 TLAB 剩余空间大于当前最大浪费空间限制，直接在堆上分配。并且，还会增加当前最大浪费空间限制，每次有这样的分配就会增加 TLABWasteIncrement 的大小，这样在一定次数的直接堆上分配之后，当前最大浪费空间限制一直增大会导致当前 TLAB 剩余空间小于当前最大浪费空间限制，从而申请新的 TLAB 进行分配。\n8.3. GC 时 TLAB 回收与重计算期望大小 # 相关流程如 图10 所示，在 GC 前与 GC 后，都会对 TLAB 做一些操作。\n8.3.1. GC 前的操作 # 在 GC 前，如果启用了 TLAB（默认是启用的， 可以通过 -XX:-UseTLAB 关闭），则需要将所有线程的 TLAB 填充 dummy Object 退还给堆，并计算并采样一些东西用于以后的 TLAB 大小计算。\n首先为了保证本次计算具有参考意义，需要先判断是否堆上 TLAB 空间被用了一半以上，假设不足，那么认为本轮 GC 的数据没有参考意义。如果被用了一半以上，那么计算新的分配比例，新的分配比例 = 线程本轮 GC 分配空间的大小 / 堆上所有线程 TLAB 使用的空间，这么计算主要因为分配比例描述的是当前线程占用堆上所有给 TLAB 的空间的比例，每个线程不一样，通过这个比例动态控制不同业务线程的 TLAB 大小。\n线程本轮 GC 分配空间的大小包含 TLAB 中分配的和 TLAB 外分配的，从 图8、图9、图10 流程图中对于线程记录中的线程分配空间大小的记录就能看出，读取出线程分配空间大小减去上一轮 GC 结束时线程分配空间大小就是线程本轮 GC 分配空间的大小。\n最后，将当前 TLAB 填充好 dummy object 之后，返还给堆。\n8.3.2. GC 后的操作 # 如果启用了 TLAB（默认是启用的， 可以通过 -XX:-UseTLAB 关闭），以及 TLAB 大小可变（默认是启用的， 可以通过 -XX:-ResizeTLAB 关闭），那么在 GC 后会重新计算每个线程 TLAB 的期望大小，新的期望大小 = 堆给TLAB的空间总大小 * 当前分配比例 EMA / 重填次数配置。然后会重置最大浪费空间限制，为当前 期望大小 / TLABRefillWasteFraction。\n9. OpenJDK HotSpot TLAB 相关源代码分析 # 如果这里看的比较吃力，可以直接看第 10 章，热门 Q\u0026amp;A，里面有很多大家常问的问题\n9.1. TLAB 类构成 # 线程初始化的时候，如果 JVM 启用了 TLAB（默认是启用的， 可以通过 -XX:-UseTLAB 关闭），则会初始化 TLAB。\nTLAB 包括如下几个 field （HeapWord* 可以理解为堆中的内存地址）： src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\n//静态全局变量 static size_t _max_size; // 所有 TLAB 的最大大小 static int _reserve_for_allocation_prefetch; // CPU 缓存优化 Allocation Prefetch 的保留空间，这里先不用关心 static unsigned _target_refills; //每个 GC 周期内期望的重填次数 //以下是 TLAB 的主要构成 field HeapWord* _start; // TLAB 起始地址，表示堆内存地址都用 HeapWord* HeapWord* _top; // 上次分配的内存地址 HeapWord* _end; // TLAB 结束地址 size_t _desired_size; // TLAB 大小 包括保留空间，表示内存大小都需要通过 size_t 类型，也就是实际字节数除以 HeapWordSize 的值 size_t _refill_waste_limit; // TLAB最大浪费空间，剩余空间不足分配浪费空间限制。在TLAB剩余空间不足的时候，根据这个值决定分配策略，如果浪费空间大于这个值则直接在 Eden 区分配，如果小于这个值则将当前 TLAB 放回 Eden 区管理并从 Eden 申请新的 TLAB 进行分配。 AdaptiveWeightedAverage _allocation_fraction; // 当前 TLAB 分配比例 EMA //以下是我们这里不用太关心的 field HeapWord* _allocation_end; // TLAB 真正可以用来分配内存的结束地址，这个是 _end 结束地址排除保留空间（预留给 dummy object 的对象头空间） HeapWord* _pf_top; // Allocation Prefetch CPU 缓存优化机制相关需要的参数，这里先不用考虑 size_t _allocated_before_last_gc; // 这个用于计算 图10 中的线程本轮 GC 分配空间的大小，记录上次 GC 时，线程分配的空间大小 unsigned _number_of_refills; // 线程分配内存数据采集相关，TLAB 剩余空间不足分配次数 unsigned _fast_refill_waste; // 线程分配内存数据采集相关，TLAB 快速分配浪费，快速分配就是直接在 TLAB 分配，这个在现在 JVM 中已经用不到了 unsigned _slow_refill_waste; // 线程分配内存数据采集相关，TLAB 慢速分配浪费，慢速分配就是重填一个 TLAB 分配 unsigned _gc_waste; // 线程分配内存数据采集相关，gc浪费 unsigned _slow_allocations; // 线程分配内存数据采集相关，TLAB 慢速分配计数 size_t _allocated_size; // 分配的内存大小 size_t _bytes_since_last_sample_point; // JVM TI 采集指标相关 field，这里不用关心 9.2. TLAB 初始化 # 首先是 JVM 启动的时候，全局 TLAB 需要初始化： src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\nvoid ThreadLocalAllocBuffer::startup_initialization() { //初始化，也就是归零统计数据 ThreadLocalAllocStats::initialize(); // 假设平均下来，GC 扫描的时候，每个线程当前的 TLAB 都有一半的内存被浪费，这个每个线程使用内存的浪费的百分比率（也就是 TLABWasteTargetPercent），也就是等于（注意，仅最新的那个 TLAB 有浪费，之前 refill 退回的假设是没有浪费的）：1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100 //那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent， 默认也就是 50 次。 _target_refills = 100 / (2 * TLABWasteTargetPercent); // 但是初始的 _target_refills 需要设置最多不超过 2 次来减少 VM 初始化时候 GC 的可能性 _target_refills = MAX2(_target_refills, 2U); //如果 C2 JIT 编译存在并启用，则保留 CPU 缓存优化 Allocation Prefetch 空间，这个这里先不用关心，会在别的章节讲述 #ifdef COMPILER2 if (is_server_compilation_mode_vm()) { int lines = MAX2(AllocatePrefetchLines, AllocateInstancePrefetchLines) + 2; _reserve_for_allocation_prefetch = (AllocatePrefetchDistance + AllocatePrefetchStepSize * lines) / (int)HeapWordSize; } #endif // 初始化 main 线程的 TLAB guarantee(Thread::current()-\u0026gt;is_Java_thread(), \u0026quot;tlab initialization thread not Java thread\u0026quot;); Thread::current()-\u0026gt;tlab().initialize(); log_develop_trace(gc, tlab)(\u0026quot;TLAB min: \u0026quot; SIZE_FORMAT \u0026quot; initial: \u0026quot; SIZE_FORMAT \u0026quot; max: \u0026quot; SIZE_FORMAT, min_size(), Thread::current()-\u0026gt;tlab().initial_desired_size(), max_size()); } 每个线程维护自己的 TLAB，同时每个线程的 TLAB 大小不一。TLAB 的大小主要由 Eden 的大小，线程数量，还有线程的对象分配速率决定。 在 Java 线程开始运行时，会先分配 TLAB： src/hotspot/share/runtime/thread.cpp\nvoid JavaThread::run() { // initialize thread-local alloc buffer related fields this-\u0026gt;initialize_tlab(); //剩余代码忽略 } 分配 TLAB 其实就是调用 ThreadLocalAllocBuffer 的 initialize 方法。 src/hotspot/share/runtime/thread.hpp\nvoid initialize_tlab() { //如果没有通过 -XX:-UseTLAB 禁用 TLAB，则初始化TLAB if (UseTLAB) { tlab().initialize(); } } // Thread-Local Allocation Buffer (TLAB) support ThreadLocalAllocBuffer\u0026amp; tlab() { return _tlab; } ThreadLocalAllocBuffer _tlab; ThreadLocalAllocBuffer 的 initialize 方法初始化 TLAB 的上面提到的我们要关心的各种 field： src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\nvoid ThreadLocalAllocBuffer::initialize() { //设置初始指针，由于还没有从 Eden 分配内存，所以这里都设置为 NULL initialize(NULL, // start NULL, // top NULL); // end //计算初始期望大小，并设置 set_desired_size(initial_desired_size()); //所有 TLAB 总大小，不同的 GC 实现有不同的 TLAB 容量， 一般是 Eden 区大小 //例如 G1 GC，就是等于 (_policy-\u0026gt;young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes，可以理解为年轻代减去Survivor区，也就是Eden区 size_t capacity = Universe::heap()-\u0026gt;tlab_capacity(thread()) / HeapWordSize; //计算这个线程的 TLAB 期望占用所有 TLAB 总体大小比例 //TLAB 期望占用大小也就是这个 TLAB 大小乘以期望 refill 的次数 float alloc_frac = desired_size() * target_refills() / (float) capacity; //记录下来，用于计算 EMA _allocation_fraction.sample(alloc_frac); //计算初始 refill 最大浪费空间，并设置 //如前面原理部分所述，初始大小就是 TLAB 的大小（_desired_size） / TLABRefillWasteFraction set_refill_waste_limit(initial_refill_waste_limit()); //重置统计 reset_statistics(); } 9.2.1. 初始期望大小是如何计算的呢？ # src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\n//计算初始大小 size_t ThreadLocalAllocBuffer::initial_desired_size() { size_t init_sz = 0; //如果通过 -XX:TLABSize 设置了 TLAB 大小，则用这个值作为初始期望大小 //表示堆内存占用大小都需要用占用几个 HeapWord 表示，所以用TLABSize / HeapWordSize if (TLABSize \u0026gt; 0) { init_sz = TLABSize / HeapWordSize; } else { //获取当前epoch内线程数量期望，这个如之前所述通过 EMA 预测 unsigned int nof_threads = ThreadLocalAllocStats::allocating_threads_avg(); //不同的 GC 实现有不同的 TLAB 容量，Universe::heap()-\u0026gt;tlab_capacity(thread()) 一般是 Eden 区大小 //例如 G1 GC，就是等于 (_policy-\u0026gt;young_list_target_length() - _survivor.length()) * HeapRegion::GrainBytes，可以理解为年轻代减去Survivor区，也就是Eden区 //整体大小等于 Eden区大小/(当前 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置) //target_refills已经在 JVM 初始化所有 TLAB 全局配置的时候初始化好了 init_sz = (Universe::heap()-\u0026gt;tlab_capacity(thread()) / HeapWordSize) / (nof_threads * target_refills()); //考虑对象对齐，得出最后的大小 init_sz = align_object_size(init_sz); } //保持大小在 min_size() 还有 max_size() 之间 //min_size主要由 MinTLABSize 决定 init_sz = MIN2(MAX2(init_sz, min_size()), max_size()); return init_sz; } //最小大小由 MinTLABSize 决定，需要表示为 HeapWordSize，并且考虑对象对齐，最后的 alignment_reserve 是 dummy object 填充的对象头大小（这里先不考虑 JVM 的 CPU 缓存 prematch，我们会在其他章节详细分析）。 static size_t min_size() { return align_object_size(MinTLABSize / HeapWordSize) + alignment_reserve(); } 9.2.2. TLAB 最大大小是怎样决定的呢？ # 不同的 GC 方式，有不同的方式：\nG1 GC 中为大对象（humongous object）大小，也就是 G1 region 大小的一半：src/hotspot/share/gc/g1/g1CollectedHeap.cpp\n// For G1 TLABs should not contain humongous objects, so the maximum TLAB size // must be equal to the humongous object limit. size_t G1CollectedHeap::max_tlab_size() const { return align_down(_humongous_object_threshold_in_words, MinObjAlignment); } ZGC 中为页大小的 8 分之一，类似的在大部分情况下 Shenandoah GC 也是每个 Region 大小的 8 分之一。他们都是期望至少有 8 分之 7 的区域是不用退回的减少选择 Cset 的时候的扫描复杂度: src/hotspot/share/gc/shenandoah/shenandoahHeap.cpp\nMaxTLABSizeWords = MIN2(ShenandoahElasticTLAB ? RegionSizeWords : (RegionSizeWords / 8), HumongousThresholdWords); src/hotspot/share/gc/z/zHeap.cpp\nconst size_t ZObjectSizeLimitSmall = ZPageSizeSmall / 8; 对于其他的 GC，则是 int 数组的最大大小，这个和为了填充 dummy object 表示 TLAB 的空区域有关。这个原因之前已经说明了。\n9.3. TLAB 分配内存 # 当 new 一个对象时，需要调用instanceOop InstanceKlass::allocate_instance(TRAPS) src/hotspot/share/oops/instanceKlass.cpp\ninstanceOop InstanceKlass::allocate_instance(TRAPS) { bool has_finalizer_flag = has_finalizer(); // Query before possible GC int size = size_helper(); // Query before forming handle. instanceOop i; i = (instanceOop)Universe::heap()-\u0026gt;obj_allocate(this, size, CHECK_NULL); if (has_finalizer_flag \u0026amp;\u0026amp; !RegisterFinalizersAtInit) { i = register_finalizer(i, CHECK_NULL); } return i; } 其核心就是heap()-\u0026gt;obj_allocate(this, size, CHECK_NULL)从堆上面分配内存： src/hotspot/share/gc/shared/collectedHeap.inline.hpp\ninline oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) { ObjAllocator allocator(klass, size, THREAD); return allocator.allocate(); } 使用全局的 ObjAllocator 实现进行对象内存分配： src/hotspot/share/gc/shared/memAllocator.cpp\noop MemAllocator::allocate() const { oop obj = NULL; { Allocation allocation(*this, \u0026amp;obj); //分配堆内存，继续看下面一个方法 HeapWord* mem = mem_allocate(allocation); if (mem != NULL) { obj = initialize(mem); } else { // The unhandled oop detector will poison local variable obj, // so reset it to NULL if mem is NULL. obj = NULL; } } return obj; } HeapWord* MemAllocator::mem_allocate(Allocation\u0026amp; allocation) const { //如果使用了 TLAB，则从 TLAB 分配，分配代码继续看下面一个方法 if (UseTLAB) { HeapWord* result = allocate_inside_tlab(allocation); if (result != NULL) { return result; } } //否则直接从 tlab 外分配 return allocate_outside_tlab(allocation); } HeapWord* MemAllocator::allocate_inside_tlab(Allocation\u0026amp; allocation) const { assert(UseTLAB, \u0026quot;should use UseTLAB\u0026quot;); //从当前线程的 TLAB 分配内存，TLAB 快分配 HeapWord* mem = _thread-\u0026gt;tlab().allocate(_word_size); //如果没有分配失败则返回 if (mem != NULL) { return mem; } //如果分配失败则走 TLAB 慢分配，需要 refill 或者直接从 Eden 分配 return allocate_inside_tlab_slow(allocation); } 9.3.1. TLAB 快分配 # src/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp\ninline HeapWord* ThreadLocalAllocBuffer::allocate(size_t size) { //验证各个内存指针有效，也就是 _top 在 _start 和 _end 范围内 invariants(); HeapWord* obj = top(); //如果空间足够，则分配内存 if (pointer_delta(end(), obj) \u0026gt;= size) { set_top(obj + size); invariants(); return obj; } return NULL; } 9.3.2. TLAB 慢分配 # src/hotspot/share/gc/shared/memAllocator.cpp\nHeapWord* MemAllocator::allocate_inside_tlab_slow(Allocation\u0026amp; allocation) const { HeapWord* mem = NULL; ThreadLocalAllocBuffer\u0026amp; tlab = _thread-\u0026gt;tlab(); // 如果 TLAB 剩余空间大于 最大浪费空间，则记录并让最大浪费空间递增 if (tlab.free() \u0026gt; tlab.refill_waste_limit()) { tlab.record_slow_allocation(_word_size); return NULL; } //重新计算 TLAB 大小 size_t new_tlab_size = tlab.compute_size(_word_size); //TLAB 放回 Eden 区 tlab.retire_before_allocation(); if (new_tlab_size == 0) { return NULL; } // 计算最小大小 size_t min_tlab_size = ThreadLocalAllocBuffer::compute_min_size(_word_size); //分配新的 TLAB 空间，并在里面分配对象 mem = Universe::heap()-\u0026gt;allocate_new_tlab(min_tlab_size, new_tlab_size, \u0026amp;allocation._allocated_tlab_size); if (mem == NULL) { assert(allocation._allocated_tlab_size == 0, \u0026quot;Allocation failed, but actual size was updated. min: \u0026quot; SIZE_FORMAT \u0026quot;, desired: \u0026quot; SIZE_FORMAT \u0026quot;, actual: \u0026quot; SIZE_FORMAT, min_tlab_size, new_tlab_size, allocation._allocated_tlab_size); return NULL; } assert(allocation._allocated_tlab_size != 0, \u0026quot;Allocation succeeded but actual size not updated. mem at: \u0026quot; PTR_FORMAT \u0026quot; min: \u0026quot; SIZE_FORMAT \u0026quot;, desired: \u0026quot; SIZE_FORMAT, p2i(mem), min_tlab_size, new_tlab_size); //如果启用了 ZeroTLAB 这个 JVM 参数，则将对象所有字段置零值 if (ZeroTLAB) { // ..and clear it. Copy::zero_to_words(mem, allocation._allocated_tlab_size); } else { // ...and zap just allocated object. } //设置新的 TLAB 空间为当前线程的 TLAB tlab.fill(mem, mem + _word_size, allocation._allocated_tlab_size); //返回分配的对象内存地址 return mem; } 9.3.2.1 TLAB最大浪费空间 # TLAB最大浪费空间 _refill_waste_limit 初始值为 TLAB 大小除以 TLABRefillWasteFraction： src/hotspot/share/gc/shared/threadLocalAllocBuffer.hpp\nsize_t initial_refill_waste_limit() { return desired_size() / TLABRefillWasteFraction; } 每次慢分配，调用record_slow_allocation(size_t obj_size)记录慢分配的同时，增加 TLAB 最大浪费空间的大小：\nsrc/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\nvoid ThreadLocalAllocBuffer::record_slow_allocation(size_t obj_size) { //每次慢分配，_refill_waste_limit 增加 refill_waste_limit_increment，也就是 TLABWasteIncrement set_refill_waste_limit(refill_waste_limit() + refill_waste_limit_increment()); _slow_allocations++; log_develop_trace(gc, tlab)(\u0026quot;TLAB: %s thread: \u0026quot; INTPTR_FORMAT \u0026quot; [id: %2d]\u0026quot; \u0026quot; obj: \u0026quot; SIZE_FORMAT \u0026quot; free: \u0026quot; SIZE_FORMAT \u0026quot; waste: \u0026quot; SIZE_FORMAT, \u0026quot;slow\u0026quot;, p2i(thread()), thread()-\u0026gt;osthread()-\u0026gt;thread_id(), obj_size, free(), refill_waste_limit()); } //refill_waste_limit_increment 就是 JVM 参数 TLABWasteIncrement static size_t refill_waste_limit_increment() { return TLABWasteIncrement; } 9.3.2.2. 重新计算 TLAB 大小 # 重新计算会取 当前堆剩余给 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 中的小的那个：\nsrc/hotspot/share/gc/shared/threadLocalAllocBuffer.inline.hpp\ninline size_t ThreadLocalAllocBuffer::compute_size(size_t obj_size) { //获取当前堆剩余给 TLAB 可分配的空间 const size_t available_size = Universe::heap()-\u0026gt;unsafe_max_tlab_alloc(thread()) / HeapWordSize; //取 TLAB 可分配的空间 和 TLAB 期望大小 + 当前需要分配的空间大小 以及 TLAB 最大大小中的小的那个 size_t new_tlab_size = MIN3(available_size, desired_size() + align_object_size(obj_size), max_size()); // 确保大小大于 dummy obj 对象头 if (new_tlab_size \u0026lt; compute_min_size(obj_size)) { log_trace(gc, tlab)(\u0026quot;ThreadLocalAllocBuffer::compute_size(\u0026quot; SIZE_FORMAT \u0026quot;) returns failure\u0026quot;, obj_size); return 0; } log_trace(gc, tlab)(\u0026quot;ThreadLocalAllocBuffer::compute_size(\u0026quot; SIZE_FORMAT \u0026quot;) returns \u0026quot; SIZE_FORMAT, obj_size, new_tlab_size); return new_tlab_size; } 9.3.2.3. 当前 TLAB 放回堆 # src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\n//在TLAB慢分配被调用，当前 TLAB 放回堆 void ThreadLocalAllocBuffer::retire_before_allocation() { //将当前 TLAB 剩余空间大小加入慢分配浪费空间大小 _slow_refill_waste += (unsigned int)remaining(); //执行 TLAB 退还给堆，这个在后面 GC 的时候还会被调用用于将所有的线程的 TLAB 退回堆 retire(); } //对于 TLAB 慢分配，stats 为空 //对于 GC 的时候调用，stats 用于记录每个线程的数据 void ThreadLocalAllocBuffer::retire(ThreadLocalAllocStats* stats) { if (stats != NULL) { accumulate_and_reset_statistics(stats); } //如果当前 TLAB 有效 if (end() != NULL) { invariants(); //将用了的空间记录如线程分配对象大小记录 thread()-\u0026gt;incr_allocated_bytes(used_bytes()); //填充dummy object insert_filler(); //清空当前 TLAB 指针 initialize(NULL, NULL, NULL); } } 9.4. GC 相关 TLAB 操作 # 9.4.1. GC 前 # 不同的 GC 可能实现不一样，但是 TLAB 操作的时机是基本一样的，这里以 G1 GC 为例，在真正 GC 前：\nsrc/hotspot/share/gc/g1/g1CollectedHeap.cpp\nvoid G1CollectedHeap::gc_prologue(bool full) { //省略其他代码 // Fill TLAB\u0026#39;s and such { Ticks start = Ticks::now(); //确保堆内存是可以解析的 ensure_parsability(true); Tickspan dt = Ticks::now() - start; phase_times()-\u0026gt;record_prepare_tlab_time_ms(dt.seconds() * MILLIUNITS); } //省略其他代码 } 为何要确保堆内存是可以解析的呢？这样有利于更快速的扫描堆上对象。确保内存可以解析里面做了什么呢？其实主要就是退还每个线程的 TLAB 以及填充 dummy object。\nsrc/hotspot/share/gc/g1/g1CollectedHeap.cpp\nvoid CollectedHeap::ensure_parsability(bool retire_tlabs) { //真正的 GC 肯定发生在安全点上，这个在后面安全点章节会详细说明 assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(), \u0026quot;Should only be called at a safepoint or at start-up\u0026quot;); ThreadLocalAllocStats stats; for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next();) { BarrierSet::barrier_set()-\u0026gt;make_parsable(thread); //如果全局启用了 TLAB if (UseTLAB) { //如果指定要回收，则回收 TLAB if (retire_tlabs) { //回收 TLAB，调用 9.3.2.3. 当前 TLAB 放回堆 提到的 retire 方法 thread-\u0026gt;tlab().retire(\u0026amp;stats); } else { //当前如果不回收，则将 TLAB 填充 Dummy Object 利于解析 thread-\u0026gt;tlab().make_parsable(); } } } stats.publish(); } 9.4.2. GC 后 # 不同的 GC 可能实现不一样，但是 TLAB 操作的时机是基本一样的，这里以 G1 GC 为例，在 GC 后：\nsrc/hotspot/share/gc/g1/g1CollectedHeap.cpp _desired_size是什么时候变得呢？怎么变得呢？\nvoid G1CollectedHeap::gc_epilogue(bool full) { //省略其他代码 resize_all_tlabs(); } src/hotspot/share/gc/shared/collectedHeap.cpp\nvoid CollectedHeap::resize_all_tlabs() { //需要在安全点，GC 会处于安全点的 assert(SafepointSynchronize::is_at_safepoint() || !is_init_completed(), \u0026quot;Should only resize tlabs at safepoint\u0026quot;); //如果 UseTLAB 和 ResizeTLAB 都是打开的（默认就是打开的） if (UseTLAB \u0026amp;\u0026amp; ResizeTLAB) { for (JavaThreadIteratorWithHandle jtiwh; JavaThread *thread = jtiwh.next(); ) { //重新计算每个线程 TLAB 期望大小 thread-\u0026gt;tlab().resize(); } } } 重新计算每个线程 TLAB 期望大小： src/hotspot/share/gc/shared/threadLocalAllocBuffer.cpp\nvoid ThreadLocalAllocBuffer::resize() { assert(ResizeTLAB, \u0026quot;Should not call this otherwise\u0026quot;); //根据 _allocation_fraction 这个 EMA 采集得出平均数乘以Eden区大小，得出 TLAB 当前预测占用内存比例 size_t alloc = (size_t)(_allocation_fraction.average() * (Universe::heap()-\u0026gt;tlab_capacity(thread()) / HeapWordSize)); //除以目标 refill 次数就是新的 TLAB 大小，和初始化时候的计算方法差不多 size_t new_size = alloc / _target_refills; //保证在 min_size 还有 max_size 之间 new_size = clamp(new_size, min_size(), max_size()); size_t aligned_new_size = align_object_size(new_size); log_trace(gc, tlab)(\u0026quot;TLAB new size: thread: \u0026quot; INTPTR_FORMAT \u0026quot; [id: %2d]\u0026quot; \u0026quot; refills %d alloc: %8.6f desired_size: \u0026quot; SIZE_FORMAT \u0026quot; -\u0026gt; \u0026quot; SIZE_FORMAT, p2i(thread()), thread()-\u0026gt;osthread()-\u0026gt;thread_id(), _target_refills, _allocation_fraction.average(), desired_size(), aligned_new_size); //设置新的 TLAB 大小 set_desired_size(aligned_new_size); //重置 TLAB 最大浪费空间 set_refill_waste_limit(initial_refill_waste_limit()); } 10. TLAB 流程常见问题 Q\u0026amp;A # 这里我会持续更新的，解决大家的各种疑问\n10.1. 为何 TLAB 在退还给堆的时候需要填充 dummy object # 主要保证 GC 的时候扫描高效。由于 TLAB 仅线程内知道哪些被分配了，在 GC 扫描发生时返回 Eden 区，如果不填充的话，外部并不知道哪一部分被使用哪一部分没有，需要做额外的检查，如果填充已经确认会被回收的对象，也就是 dummy object， GC 会直接标记之后跳过这块内存，增加扫描效率。反正这块内存已经属于 TLAB，其他线程在下次扫描结束前是无法使用的。这个 dummy object 就是 int 数组。为了一定能有填充 dummy object 的空间，一般 TLAB 大小都会预留一个 dummy object 的 header 的空间，也是一个 int[] 的 header，所以 TLAB 的大小不能超过int 数组的最大大小，否则无法用 dummy object 填满未使用的空间。\n10.2. 为何 TLAB 需要最大浪费空间限制 # 当重新分配一个 TLAB 的时候，原有的 TLAB 可能还有空间剩余。原有的 TLAB 被退回堆之前，需要填充好 dummy object。这样导致这块内存无法分配对象，所示被称为“浪费”。如果不限制，遇到 TLAB 剩余空间不足的情况就会重新申请，导致分配效率降低，大部分空间被 dummy object 占满了，导致 GC 更加频繁。\n10.3. 为何 TLAB 重填次数配置 等于 100 / (2 * TLABWasteTargetPercent) # TLABWasteTargetPercent 描述了初始最大浪费空间配置占 TLAB 的比例\n首先，最理想的情况就是尽量让所有对象在 TLAB 内分配，也就是 TLAB 可能要占满 Eden。 在下次 GC 扫描前，退回 Eden 的内存别的线程是不能用的，因为剩余空间已经填满了 dummy object。所以所有线程使用内存大小就是 下个 epcoh 内会分配对象期望线程个数 * 每个 epoch 内每个线程 refill 次数配置，对象一般都在 Eden 区由某个线程分配，也就所有线程使用内存大小就最好是整个 Eden。但是这种情况太过于理想，总会有内存被填充了 dummy object而造成了浪费，因为 GC 扫描随时可能发生。假设平均下来，GC 扫描的时候，每个线程当前的 TLAB 都有一半的内存被浪费，这个每个线程使用内存的浪费的百分比率（也就是 TLABWasteTargetPercent），也就是等于（注意，仅最新的那个 TLAB 有浪费，之前 refill 退回的假设是没有浪费的）：\n1/2 * (每个 epoch 内每个线程期望 refill 次数) * 100\n那么每个 epoch 内每个线程 refill 次数配置就等于 50 / TLABWasteTargetPercent， 默认也就是 50 次。\n10.4. 为何考虑 ZeroTLAB # 当分配出来 TLAB 之后，根据 ZeroTLAB 配置，决定是否将每个字节赋 0。在 TLAB 申请时，由于申请 TLAB 都发生在对象分配的时候，也就是这块内存会立刻被使用，并修改赋值。操作内存，涉及到 CPU 缓存行，如果是多核环境，还会涉及到 CPU 缓存行 false sharing，为了优化，JVM 在这里做了 Allocation Prefetch，简单理解就是分配 TLAB 的时候，会尽量加载这块内存到 CPU 缓存，也就是在分配 TLAB 内存的时候，修改内存是最高效的。\n在创建对象的时候，本来也要对每个字段赋初始值，大部分字段初始值都是 0，并且，在 TLAB 返还到堆时，剩余空间填充的也是 int[] 数组，里面都是 0。\n所以，TLAB 刚分配出来的时候，赋 0 避免了后续再赋 0。也能利用好 Allocation prefetch 的机制适应 CPU 缓存行（Allocation prefetch 的机制详情会在另一个系列说明）\n10.5. 为何 JVM 需要预热，为什么 Java 代码越执行越快（这里只提 TLAB 相关的，JIT，MetaSpace，GC等等其他系列会说） # 根据之前的分析，每个线程的 TLAB 的大小，会根据线程分配的特性，不断变化并趋于稳定，大小主要是由分配比例 EMA 决定，但是这个采集是需要一定运行次数的。并且 EMA 的前 100 次采集默认是不够稳定的，所以 TLAB 大小也在程序一开始的时候变化频繁。当程序线程趋于稳定，运行一段时间后， 每个线程 TLAB 大小也会趋于稳定并且调整到最适合这个线程对象分配特性的大小。这样，就更接近最理想的只有 Eden 区满了才会 GC，所有 Eden 区的对象都是通过 TLAB 分配的高效分配情况。这就是 Java 代码越执行越快在 TLAB 方面的原因。\n11. TLAB 相关 JVM 日志解析 # 11.1. 准备 Java WhiteBox API # 首先需要准备好Java WhiteBox API\n11.1.1. 什么是 WhiteBox API # WhiteBox API 是 HotSpot VM 自带的白盒测试工具，将内部的很多核心机制的 API 暴露出来，用于白盒测试 JVM，压测 JVM 特性，以及辅助学习理解 JVM 并调优参数。WhiteBox API 是 Java 7 引入的，目前 Java 8 LTS 以及 Java 11 LTS（其实是 Java 9+ 以后的所有版本，这里只关心 LTS 版本，Java 9 引入了模块化所以 WhiteBox API 有所变化）都是有的。但是默认这个 API 并没有编译在 JDK 之中，但是他的实现是编译在了 JDK 里面了。所以如果想用这个 API，需要用户自己编译需要的 API，并加入 Java 的 BootClassPath 并启用 WhiteBox API。\n11.1.2. WhiteBox API 如何实现的 # WhiteBox API 是一个 Java 类，位于 JDK 的测试包中，默认没有编译进标准发行版的 JDK 中。\ntest/lib/sun/hotspot/WhiteBox.java\npackage sun.hotspot; public class WhiteBox { //仅举两个例子，省略其他 api 以及代码 // Force Young GC public native void youngGC(); // Force Full GC public native void fullGC(); } 可以看出，其实里面的所有 API 都是 JNI 调用，具体实现是：\nsrc/hotspot/share/prims/whitebox.cpp\nWB_ENTRY(void, WB_FullGC(JNIEnv* env, jobject o)) Universe::heap()-\u0026gt;soft_ref_policy()-\u0026gt;set_should_clear_all_soft_refs(true); Universe::heap()-\u0026gt;collect(GCCause::_wb_full_gc); #if INCLUDE_G1GC if (UseG1GC) { // Needs to be cleared explicitly for G1 Universe::heap()-\u0026gt;soft_ref_policy()-\u0026gt;set_should_clear_all_soft_refs(false); } #endif // INCLUDE_G1GC WB_END WB_ENTRY(void, WB_YoungGC(JNIEnv* env, jobject o)) Universe::heap()-\u0026gt;collect(GCCause::_wb_young_gc); WB_END {CC\u0026#34;youngGC\u0026#34;, CC\u0026#34;()V\u0026#34;, (void*)\u0026amp;WB_YoungGC }, {CC\u0026#34;fullGC\u0026#34;, CC\u0026#34;()V\u0026#34;, (void*)\u0026amp;WB_FullGC }, //省略其他代码 可以看出，JNI 调用实现直接调用了底层 JVM 的相关接口，相当于把 JVM 的一些关键机制暴露出来，用于白盒测试。但是如之前所说，JDK 发行版没有包括 test 下的测试代码，也就是 WhiteBox API 所在的 jar 包并没有打进默认的 JDK 中。这就需要我们自己编译一下这个代码。\n11.1.3. 什么是 BootClassPath # Java 内有三种不同的类加载器：应用类加载器（application classloader），扩展类加载器（extension classloader）还有根类加载器（bootstrap classloader）\n应用类加载器，加载我们classpath目录下的所有类文件 扩展类加载器，加载标准 Java 类库扩展的类，就是你的jre目录下的/lib/ext目录下的所有类 根类加载器（bootstrap classloader），扫描 BootClassPath 下的 标准 Java 类库的类加载器。标准 Java 类库限制了一些包路径的类，必须通过根类加载器加载。 对于 WhiteBox API，由于是他的包为sun.hotspot，普通的类加载器是不能加载这个包路径的类的，需要通过根类加载器加载。\n11.1.4. 怎么指定 BootClassPath # 在 Java 8，通过 -Xbootclasspath: 或者 -Xbootclasspath/p:指定，例如：\n-Xbootclasspath:/home/project/whitebox.jar -Xbootclasspath/p:/home/project/whitebox.jar 在 Java 9 之后的版本，这两个参数已经过期了，需要改成-Xbootclasspath/a:，例如：\n-Xbootclasspath/a:/home/project/whitebox.jar 否则会报错-Xbootclasspath is no longer a supported option.\n这里对应的 JDK 源码是： src/hotspot/share/runtime/arguments.cpp\n// -bootclasspath: } else if (match_option(option, \u0026#34;-Xbootclasspath:\u0026#34;, \u0026amp;tail)) { jio_fprintf(defaultStream::output_stream(), \u0026#34;-Xbootclasspath is no longer a supported option.\\n\u0026#34;); return JNI_EINVAL; // -bootclasspath/a: } else if (match_option(option, \u0026#34;-Xbootclasspath/a:\u0026#34;, \u0026amp;tail)) { //将参数添加到 bootclasspath 中 Arguments::append_sysclasspath(tail); // -bootclasspath/p: } else if (match_option(option, \u0026#34;-Xbootclasspath/p:\u0026#34;, \u0026amp;tail)) { jio_fprintf(defaultStream::output_stream(), \u0026#34;-Xbootclasspath/p is no longer a supported option.\\n\u0026#34;); return JNI_EINVAL; } 11.1.5. 使用 WhiteBox API # 1. 编译 WhiteBox API\n将https://github.com/openjdk/jdk/tree/master/test/lib路径下的sun目录取出，编译成一个 jar 包，名字假设是 whitebox.jar\n2. 编写测试程序\n将 whitebox.jar 添加到你的项目依赖，之后写代码\npublic static void main(String[] args) throws Exception { WhiteBox whiteBox = WhiteBox.getWhiteBox(); //获取 ReservedCodeCacheSize 这个 JVM flag 的值 Long reservedCodeCacheSize = whiteBox.getUintxVMFlag(\u0026#34;ReservedCodeCacheSize\u0026#34;); System.out.println(reservedCodeCacheSize); //打印堆内存各项指标 whiteBox.printHeapSizes(); //执行full GC whiteBox.fullGC(); //保持进程不退出，保证日志打印完整 Thread.currentThread().join(); } 3. 启动程序查看效果\n使用启动参数 -Xbootclasspath/a:/home/project/whitebox.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xlog:gc 启动程序。其中前三个 Flag 表示启用 WhiteBox API，最后一个表示打印 GC info 级别的日志到控制台。\n我的输出：\n[0.025s][info][gc] Using G1 251658240 Minimum heap 8388608 Initial heap 268435456 Maximum heap 4276092928 Space alignment 2097152 Heap alignment 2097152 [0.899s][info][gc] GC(0) Pause Full (WhiteBox Initiated Full GC) 5M-\u0026gt;0M(20M) 45.183ms 至此，我们就准备好了 WhiteBox 调试环境\n11.2. 测试 TLAB 查看日志 # 编写测试代码：\n//对于字节数组对象头占用16字节 private static final int BYTE_ARRAY_OVERHEAD = 16; //我们要测试的对象大小是100kb private static final int OBJECT_SIZE = 100 * 1024; //需要使用静态field，而不是方法内本地变量，否则编译后循环内的new byte[]全部会被省略，只剩最后一次的 public static byte[] tmp; public static void main(String[] args) throws Exception { WhiteBox whiteBox = WhiteBox.getWhiteBox(); //强制 fullGC 防止接下来程序发生 GC //同时可以区分出初始化带来的其他线程的TLAB相关的日志 whiteBox.fullGC(); //分配对象，大小1KB for (int i = 1; i \u0026lt; 512; ++i) { tmp = new byte[OBJECT_SIZE - BYTE_ARRAY_OVERHEAD]; } //强制 fullGC，回收所有 TLAB whiteBox.fullGC(); //分配对象，大小100KB for (int i = 1; i \u0026lt; 500; ++i) { tmp = new byte[OBJECT_SIZE * 100 - BYTE_ARRAY_OVERHEAD]; } whiteBox.fullGC(); //阻塞程序，保证所有日志输出完 Thread.currentThread().join(); } 之后，我们以如下的启动参数（前三个启动参数是我们前面章节提到的启用 WhiteBox API 需要的参数）启动这个程序，查看日志（关于日志配置，请参考之前的章节）。\n-Xbootclasspath/a:./jdk-white-box-17.0-SNAPSHOT.jar -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xms512m -Xmx512m -XX:+UseTLAB -Xlog:gc+tlab=trace -Xlog:gc 可以看到下面类似的日志，我们来根据代码分析下，首先是运行到第一个 fullGC 结束之前的所有日志，首先是 JVM 启动的时候会输出用的是什么 GC 的日志，这里是默认的 G1：\n[0.022s][info][gc] Using G1 还会输出 TLAB 的通用配置：\n[0.030s][trace][gc,tlab] TLAB min: 328 initial: 60293 max: 65536 也就是这里 TLAB 最小为 328 MarkWordSize，初始为 60293 MarkWordSize，最大为 65536 MarkWordSize。默认的 64位 JVM 的 MarkWordSize 为 8 字节，也就是堆内存 8 字节对齐。\n然后，由于 JVM 启动时，默认会初始化很多线程，包括：\nmain 线程：执行 main 方法的线程 Attach listener 线程：Attach Listener 线程是负责接收到外部的命令，而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息，如：java -version、jmap、jstack等等。 如果该线程在jvm启动的时候没有初始化，那么，则会在用户第一次执行jvm命令时，得到启动。 Signal Dispatcher线程：Attach Listener线程的职责是接收外部jvm命令，当命令接收成功后，会交给signal dispather 线程去进行分发到各个不同的模块处理命令，并且返回处理结果。 signal dispather线程也是在第一次接收外部jvm命令时，进行初始化工作。 Reference Handler 线程：JVM在创建main线程后就创建Reference Handler线程，它主要用于处理引用对象本身（软引用、弱引用、虚引用）的垃圾回收问题 。 Finalizer线程：这个线程也是在main线程之后创建的，主要用于在垃圾收集前，调用对象的finalize()方法。 DestroyJavaVM线程：执行main()的线程在main执行完后调用JNI中的 jni_DestroyJavaVM() 方法唤起DestroyJavaVM 线程，它将在虚拟机中所有其它非守护线程全部结束后销毁虚拟机。 在运行过程中，根据你的JIT编译配置，GC参数，还会有：\nCompilerThread 线程：JIT编译相关线程，主要是负责 C1 C2 即时编译以及 OSR（On stack Replacement） 替换等任务 GC 相关线程：执行GC任务的线程 除了这些之外，Java 8 之后 ForkJoinPool 还会创建一个默认大小为 cpu 核数 -1 的线程池：CommonForkJoinPool，是用来处理 ParallelStream 的默认线程池还有 Future 框架 CompletableFuture 的默认线程池。\n这些线程中的一部分会在 JVM 初始化的时候创建一些对象使用，那么就肯定会涉及到 TLAB，所以会有如下日志：\n[0.042s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(2) returns 65536 [0.042s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 1 waste 0.0% gc: 0B slow: 0B fast: 0B [0.155s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(25) returns 65536 [0.155s][trace][gc,tlab] TLAB: fill thread: 0x000002a60028e900 [id: 15380] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 1 waste 0.0% gc: 0B slow: 0B fast: 0B [0.340s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(2) returns 256 [0.340s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 2048KB refills: 2 waste 0.1% gc: 0B slow: 576B fast: 0B //省略其他线程的 TLAB 日志，这里 23480 是 Main 线程。读者可以通过程序输出日志中执行循环分配对象的线程 TLAB 日志判断哪一个是 Main 线程 其中，[0.042s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(2) returns 65536的对应的就是调用了compute_size计算初始 TLAB 大小，传入的 2 就是当前这个线程分配的对象所需的大小（MarkWordSize），计算出初始大小为 65536，因为 MarkWordSize = 8 所以 就是 65536*8=524288 字节，也就是 512 KB。下一行日志，代表这个线程初始化申请一块内存作为 TLAB 了，[0.042s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 1 waste 0.0% gc: 0B slow: 0B fast: 0B，这个 TLAB 的信息包括：\n线程号 0x000002a66a471710 [id: 12916] 期望大小，就是刚刚计算出来的 512KB：desired_size: 512KB 慢分配次数，就是不在当前 TLAB 直接分配的分配次数：slow allocs: 0 当前浪费空间限制，也就是重新申请 TLAB 造成的浪费限制大小，refill waste: 8192B，也就是最多能浪费 8192 字节 当前 _allocation_fraction 相关信息，alloc: 1.00000 1024KB，代表当前 _allocation_fraction 是 1.00000，TLAB 一共用了 1024 KB 发生 refills 重新申请 TLAB 的次数：refills: 1 浪费比例：waste 0.0% GC 回收造成的浪费大小：gc: 0B 慢refill造成的浪费：slow: 0B 快refill造成的浪费：fast: 0B 我们这里来计算下为何当前浪费空间为 8192 字节，也就是 8KB。TLABRefillWasteFraction 我们并没有修改，也就是默认的 64，那么初始的最大浪费空间 = TLAB 大小 / TLABRefillWasteFraction，也就是 512KB / 64 = 8KB\n第一次强制 FullGC 之后，看到如下相关日志：\n//首先输出了每一个线程的当前 TLAB 的信息 [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 15 waste 7.1% gc: 360616B slow: 13880B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a60028d180 [id: 24604] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a60028e900 [id: 15380] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 99.9% gc: 524008B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a6002dc380 [id: 10316] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600319040 [id: 3856] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a60031a1f0 [id: 16808] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600326970 [id: 292] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600328620 [id: 10932] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a60032ac90 [id: 14528] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 99.8% gc: 521328B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600343ec0 [id: 20040] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600ca03f0 [id: 14304] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600e157e0 [id: 24148] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 60.9% gc: 1248B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600f17090 [id: 13736] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 99.9% gc: 523976B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a600f0e850 [id: 19208] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 99.9% gc: 521688B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a601381710 [id: 9804] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a6013aef00 [id: 23640] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a6013f7650 [id: 1860] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a601ad77b0 [id: 17292] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 1 waste 99.9% gc: 521752B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a601971200 [id: 17448] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a601972220 [id: 11844] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B [0.915s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000002a601705560 [id: 7832] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 8192KB refills: 0 waste 0.0% gc: 0B slow: 0B fast: 0B //GC TLAB 统计 [0.915s][debug][gc,tlab] GC(0) TLAB totals: thrds: 7 refills: 21 max: 15 slow allocs: 0 max 0 waste: 38.0% gc: 2974616B max: 524008B slow: 13880B max: 13880B fast: 0B max: 0B //每个线程 TLAB 期望大小的变化 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a66a471710 [id: 12916] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a60028d180 [id: 24604] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a60028e900 [id: 15380] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a6002dc380 [id: 10316] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600319040 [id: 3856] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a60031a1f0 [id: 16808] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600326970 [id: 292] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600328620 [id: 10932] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a60032ac90 [id: 14528] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600343ec0 [id: 20040] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600ca03f0 [id: 14304] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600e157e0 [id: 24148] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600f17090 [id: 13736] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a600f0e850 [id: 19208] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a601381710 [id: 9804] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a6013aef00 [id: 23640] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a6013f7650 [id: 1860] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a601ad77b0 [id: 17292] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a601971200 [id: 17448] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a601972220 [id: 11844] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 [0.980s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a601705560 [id: 7832] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536 //GC 信息 [0.980s][info ][gc ] GC(0) Pause Full (WhiteBox Initiated Full GC) 7M-\u0026gt;0M(512M) 65.162ms 首先是输出了每一个线程的当前 TLAB 的信息。与前面发生 refill 分配 TLAB 时相似。只不过多了 GC 全局序号，从 0 开始， GC(0) 代表的就是第一次 GC 相关的日志 然后是 GC TLAB 统计：[0.915s][debug][gc,tlab] GC(0) TLAB totals: thrds: 7 refills: 21 max: 15 slow allocs: 0 max 0 waste: 38.0% gc: 2974616B max: 524008B slow: 13880B max: 13880B fast: 0B max: 0B：\n一共有7个线程用了 TLAB：thrds: 7，也就是前面带 GC(0) 的 TLAB 信息日志中，只有 7 个线程的 refills 是大于 0 的。 本次 GC 所有线程 refills 的次数 refills: 21 历史最大的某次 GC 内 refills 的次数 max: 15 本次 GC 所有线程慢分配的次数 slow allocs: 0 历史最大的某次 GC 内慢分配的次数 max: 0 本次 GC 所有线程 TLAB 内存浪费比例 waste: 38.0% 各种浪费内存大小：`gc: 2974616B max: 524008B slow: 13880B max: 13880B fast: 0B max: 0B`` 接着打印了每个线程 TLAB 期望大小的变化：[0.979s][trace][gc,tlab] GC(0) TLAB new size: thread: 0x000002a66a471710 [id: 12916] refills 50 alloc: 1.000000 desired_size: 65536 -\u0026gt; 65536，这里还是 MarkWordSize 而不是实际字节大小。 最后是本次 GC 信息：[0.980s][info ][gc ] GC(0) Pause Full (WhiteBox Initiated Full GC) 7M-\u0026gt;0M(512M) 65.162ms，代表是 FullGC，并且是 WhiteBox 触发的，堆内存使用从 7M 回收到了 0M，堆内存总大小是 512M，一共停顿时间是 65.162 ms。\n之后我们的程序申请了 512 个大小为 1KB 的对象。为何new byte[OBJECT_SIZE - BYTE_ARRAY_OVERHEAD]大小是 1KB 呢？因为数组对象头默认是 16 字节，所以再加上 1012 个 byte 就是 1KB。循环结束后，输出了下面两行日志：\n[0.989s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(128) returns 65536 [0.989s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 1 waste 0.0% gc: 0B slow: 0B fast: 0B [0.989s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(128) returns 65536 [0.989s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 2 waste 0.1% gc: 0B slow: 1024B fast: 0B 可以看出是发生了两次 refill，第一次是线程第一次创建对象时申请的，第二次是申请到第 512 个对象，TLAB 大小是 512 KB，之前的 511KB 已经被占用了，根据前一篇的 TLAB 原理分析，我们知道由于需要填充 dummy objects 所以要保留一个数组对象头的大小，所以剩下可分配的空间其实不足 1KB，所以需要 refill。并且，浪费的空间（1KB）小于当前浪费空间限制（8KB），所以可以重新申请新的 TLAB 进行分配。\n然后我们的程序在 FullGC 之后，继续申请了 200 个大小为 100KB 的大对象。这里我们忽略 GC 相关日志，只看分配对象的时候产生的日志。\n[3036.734s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(12800) returns 65536 [3036.734s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 0 refill waste: 8192B alloc: 1.00000 1024KB refills: 1 waste 0.0% gc: 0B slow: 0B fast: 0B [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1028 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1032 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1036 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1040 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1044 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1048 //省略中间分配日志。。。 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1452 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1456 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1460 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1464 [3047.279s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(12800) returns 65536 [3047.279s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 110 refill waste: 11712B alloc: 1.00000 13312KB refills: 2 waste 1.2% gc: 0B slow: 12288B fast: 0B [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1028 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1032 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1036 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1040 //省略中间分配日志。。。 [3047.281s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1340 100KB 的对象，换算成 MarkWordSize 就是 12800，对应日志：[3036.734s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(12800) returns 65536，本次计算 TLAB 大小依然是 65536 MarkWordSize，也就是 512KB。在分配第五个对象开始， TLAB 的剩余内存就不够了。但是初始最大浪费空间是 8KB，所以只能直接在 Eden 区分配，并根据 TLABWasteIncrement（默认为 4） 设置的值递增最大浪费空间，也就是每次递增 4 * MarkWordSize 也就是 32 字节。体现在了日志:\n[3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1028 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1032 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1036 [3047.276s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1040 可以看出，每次 TLAB 外分配都让最大浪费空间限制加 4。当剩余空间小于最大浪费空间限制时，线程 refill 申请了一块新的 TLAB 进行分配：\n[3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1456 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1460 [3047.279s][trace][gc,tlab] TLAB: slow thread: 0x000002a66a471710 [id: 12916] obj: 12800 free: 1464 waste: 1464 [3047.279s][trace][gc,tlab] ThreadLocalAllocBuffer::compute_size(12800) returns 65536 [3047.279s][trace][gc,tlab] TLAB: fill thread: 0x000002a66a471710 [id: 12916] desired_size: 512KB slow allocs: 110 refill waste: 11712B alloc: 1.00000 13312KB refills: 2 waste 1.2% gc: 0B slow: 12288B fast: 0B 至此，我们就分析了基本所有 TLAB 相关的日志。\n12. 监控 TLAB 慢分配与 TLAB 外分配 - JFR 相关事件解析 # 我们可以通过 JFR 来监控 TLAB 慢分配或者 TLAB 外分配事件。也就是jdk.ObjectAllocationOutsideTLAB与jdk.ObjectAllocationInNewTLAB这两个事件。\njdk.ObjectAllocationOutsideTLAB 和 jdk.ObjectAllocationInNewTLAB 这两个事件在default.jfc中( JFR 默认事件采集配置)是没有开启采集的:\n\u0026lt;event name=\u0026#34;jdk.ObjectAllocationInNewTLAB\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; \u0026lt;event name=\u0026#34;jdk.ObjectAllocationOutsideTLAB\u0026#34;\u0026gt; \u0026lt;setting name=\u0026#34;enabled\u0026#34;\u0026gt;false\u0026lt;/setting\u0026gt; \u0026lt;setting name=\u0026#34;stackTrace\u0026#34;\u0026gt;true\u0026lt;/setting\u0026gt; \u0026lt;/event\u0026gt; 一般的，采集这两个事件，是需要连着堆栈一起采集，但是无法通过持续时间（因为这个事件没有持续时间这一概念）限制采集哪些，也就是只要开启就是全部采集，所以不建议长期开启这个采集。而是通过一些其他的监控项，按照需要，动态开启这个采集一段时间，之后关闭并 dump 出 JFR 文件用于分析。\n那么一般根据什么指标判断呢？一般的，当 Young GC 过于频繁时，我们就要考虑是不是由于 TLAB 造成很多空间被浪费导致 GC 频繁了。至于如果采集 Young GC 频率从而动态开启，这个会在后面的动态监控章节详细说明。\n我们还用上面的程序，根据之前的日志，对于 1KB 的对象，应该有两次 jdk.ObjectAllocationInNewTLAB 事件，第一次是线程第一次申请 TLAB，第二次是在分配第 512 个对象的时候，TLAB 剩余空间不足，同时剩余空间小于最大浪费空间限制，所以申请新的 TLAB 分配。对于 1KB 的分配，没有发生 jdk.ObjectAllocationOutsideTLAB。对于 100KB 的对象分配，在第五次分配时，TLAB 剩余空间不足，但是剩余空间大于最大浪费空间限制，直接在 Eden 区分配，同时将最大浪费空间限制增加 4。在第 114 次对象分配时，最大浪费空间限制达到了剩余空间，所以申请新的 TLAB 分配。所以对于 100KB 对象的 200 次分配里面，jdk.ObjectAllocationInNewTLAB也只有两次。\n同时由于开启了 JFR，导致 TLAB 可能会被占用一部分，所以上面说的这些次数可能不太准确，不过没关系，大体上应该是对的。\n//对于字节数组对象头占用16字节 private static final int BYTE_ARRAY_OVERHEAD = 16; //我们要测试的对象大小是100kb private static final int OBJECT_SIZE = 100 * 1024; //需要使用静态field，而不是方法内本地变量，否则编译后循环内的new byte[]全部会被省略，只剩最后一次的 public static byte[] tmp; public static void main(String[] args) throws Exception { WhiteBox whiteBox = WhiteBox.getWhiteBox(); //初始化 JFR 记录 Recording recording = new Recording(); //启用 jdk.ObjectAllocationOutsideTLAB 事件监控 recording.enable(\u0026#34;jdk.ObjectAllocationOutsideTLAB\u0026#34;); recording.enable(\u0026#34;jdk.ObjectAllocationInNewTLAB\u0026#34;); // JFR 记录启动 recording.start(); //强制 fullGC 防止接下来程序发生 GC //同时可以区分出初始化带来的其他线程的TLAB相关的日志 whiteBox.fullGC(); //分配对象，大小1KB for (int i = 0; i \u0026lt; 512; ++i) { tmp = new byte[OBJECT_SIZE - BYTE_ARRAY_OVERHEAD]; } //强制 fullGC，回收所有 TLAB whiteBox.fullGC(); //分配对象，大小100KB for (int i = 0; i \u0026lt; 200; ++i) { tmp = new byte[OBJECT_SIZE * 100 - BYTE_ARRAY_OVERHEAD]; } whiteBox.fullGC(); //将 JFR 记录 dump 到一个文件 Path path = new File(new File(\u0026#34;.\u0026#34;).getAbsolutePath(), \u0026#34;recording-\u0026#34; + recording.getId() + \u0026#34;-pid\u0026#34; + ProcessHandle.current().pid() + \u0026#34;.jfr\u0026#34;).toPath(); recording.dump(path); int countOf1KBObjectAllocationInNewTLAB = 0; int countOf100KBObjectAllocationInNewTLAB = 0; int countOf1KBObjectAllocationOutsideTLAB = 0; int countOf100KBObjectAllocationOutsideTLAB = 0; //读取文件中的所有 JFR 事件 for (RecordedEvent event : RecordingFile.readAllEvents(path)) { //获取分配的对象的类型 String className = event.getString(\u0026#34;objectClass.name\u0026#34;); if ( //确保分配类型是 byte[] BYTE_ARRAY_CLASS_NAME.equalsIgnoreCase(className) ) { RecordedFrame recordedFrame = event.getStackTrace().getFrames().get(0); //同时必须是咱们这里的main方法分配的对象，并且是Java堆栈中的main方法 if (recordedFrame.isJavaFrame() \u0026amp;\u0026amp; \u0026#34;main\u0026#34;.equalsIgnoreCase(recordedFrame.getMethod().getName()) ) { //获取分配对象大小 long allocationSize = event.getLong(\u0026#34;allocationSize\u0026#34;); //统计各种事件个数 if (\u0026#34;jdk.ObjectAllocationOutsideTLAB\u0026#34;.equalsIgnoreCase(event.getEventType().getName())) { if (allocationSize == 102400) { countOf100KBObjectAllocationOutsideTLAB++; } else if (allocationSize == 1024) { countOf1KBObjectAllocationOutsideTLAB++; } } else if (\u0026#34;jdk.ObjectAllocationInNewTLAB\u0026#34;.equalsIgnoreCase(event.getEventType().getName())) { if (allocationSize == 102400) { countOf100KBObjectAllocationInNewTLAB++; } else if (allocationSize == 1024) { countOf1KBObjectAllocationInNewTLAB++; } } else { throw new Exception(\u0026#34;unexpected size of TLAB event\u0026#34;); } System.out.println(event); } } } System.out.println(\u0026#34;countOf1KBObjectAllocationInNewTLAB: \u0026#34; + countOf1KBObjectAllocationInNewTLAB); System.out.println(\u0026#34;countOf100KBObjectAllocationInNewTLAB: \u0026#34; + countOf100KBObjectAllocationInNewTLAB); System.out.println(\u0026#34;countOf1KBObjectAllocationOutsideTLAB: \u0026#34; + countOf1KBObjectAllocationOutsideTLAB); System.out.println(\u0026#34;countOf100KBObjectAllocationOutsideTLAB: \u0026#34; + countOf100KBObjectAllocationOutsideTLAB); //阻塞程序，保证所有日志输出完 Thread.currentThread().join(); } 输出应该近似于：\n//省略其他事件的详细信息，这里每种挑一个展示 jdk.ObjectAllocationInNewTLAB { startTime = 13:07:51.681 objectClass = byte[] (classLoader = bootstrap) allocationSize = 1.0 kB tlabSize = 478.2 kB eventThread = \u0026#34;main\u0026#34; (javaThreadId = 1) stackTrace = [ com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 96 ] } jdk.ObjectAllocationInNewTLAB { startTime = 13:07:51.777 objectClass = byte[] (classLoader = bootstrap) allocationSize = 100.0 kB tlabSize = 512.0 kB eventThread = \u0026#34;main\u0026#34; (javaThreadId = 1) stackTrace = [ com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 102 ] } jdk.ObjectAllocationOutsideTLAB { startTime = 13:07:51.784 objectClass = byte[] (classLoader = bootstrap) allocationSize = 100.0 kB eventThread = \u0026#34;main\u0026#34; (javaThreadId = 1) stackTrace = [ com.github.hashjang.jfr.test.TestAllocOutsideTLAB.main(String[]) line: 102 ] } //省略其他事件的详细信息，这里每种挑一个展示 countOf1KBObjectAllocationInNewTLAB: 2 countOf100KBObjectAllocationInNewTLAB: 2 countOf1KBObjectAllocationOutsideTLAB: 0 countOf100KBObjectAllocationOutsideTLAB: 190 可以看出jdk.ObjectAllocationInNewTLAB包含：\n开始时间：startTime = 13:07:51.784 分配对象类型：objectClass = byte[] (classLoader = bootstrap) 分配大小：allocationSize = 100.0 kB 新的 TLAB 大小：tlabSize = 512.0 kB 线程：eventThread = \u0026quot;main\u0026quot; (javaThreadId = 1) 堆栈 jdk.ObjectAllocationOutsideTLAB包含：\n开始时间：startTime = 13:07:51.784 分配对象类型：objectClass = byte[] (classLoader = bootstrap) 分配大小：allocationSize = 100.0 kB 线程：eventThread = \u0026quot;main\u0026quot; (javaThreadId = 1) 堆栈 ","date":"2021年2月3日","externalUrl":null,"permalink":"/zh-cn/posts/tough-jdk-1-tlab/","section":"文章","summary":"深入探讨 JVM 的线程本地分配缓冲区（TLAB）机制，涵盖设计原理、实现细节、性能优化和源代码分析。了解 TLAB 如何提高多线程环境中的内存分配效率，并掌握 TLAB 调优技术。","title":"全网最硬核 JDK 分析 - 1. TLAB 全面解析","type":"posts"},{"content":"","date":"2020年2月6日","externalUrl":null,"permalink":"/zh-cn/tags/logging/","section":"Tags","summary":"","title":"Logging","type":"tags"},{"content":"","date":"2020年2月6日","externalUrl":null,"permalink":"/zh-cn/tags/openjdk/","section":"Tags","summary":"","title":"OpenJDK","type":"tags"},{"content":" OpenJDK 11 JVM 日志：新统一配置完整指南 # 你好，Java 爱好者们！🚀 让我们深入了解 OpenJDK 11 带来的最令人兴奋的改进之一 - 完全重新设计的 JVM 日志系统。如果你一直在与 OpenJDK 8 中分散且令人困惑的日志选项作斗争，那么你会喜欢的！\nOpenJDK 11 标志着 OpenJDK 8 之后的第一个长期支持版本，天哪，它确实为 JVM 日志带来了一些惊人的变化！团队终于决定解决多年来让开发者头疼的混乱日志标志。告别处理数十个不同参数的日子 - 欢迎进入统一、标准化日志配置的时代！\n理解 JVM 日志标签：你的新朋友 # 将 JVM 日志视为与 Java 代码中的应用日志非常相似。当你编写应用日志时，通常这样做：\nLogger logger = LogFactory.getLogger(\u0026quot;core-logger\u0026quot;); logger.info(\u0026quot;this is core logger log\u0026quot;); 产生如下输出：\n2020-02-05 10:50:52.670 INFO [core-logger] [22] [pool-13-thread-1]: this is core logger log JVM 日志遵循相同的原则！看看这个例子：\n[0.182s][debug][jit,compilation] 1 3 java.lang.StringLatin1::hashCode (42 bytes) [0.183s][debug][jit,compilation] 2 3 java.lang.Object::\u0026lt;init\u0026gt; (1 bytes) [0.183s][debug][jit,compilation] 3 3 java.lang.String::hashCode (49 bytes) 默认的 JVM 日志格式包括：\n[startup-time][log-level][log-tags-comma-separated] log-content 每个日志条目可以包含多个标签，所有 JVM 日志配置都围绕这些标签。虽然许多标签是为 JVM 开发者设计的，但有几个对性能调优和调试非常有用。让我按类别分解最重要的：\n1. 垃圾收集相关标签 # GC 日志提供大量标签组合，大多数以 gc 标签与其他标签混合开始。以下是重要的：\n标签 gc\n这提供高级 GC 信息。将其设置为 info 级别以查看 GC 时间、持续时间和内存使用。例如：Pause Young (Normal) (g1 Evacuation Pause) 3480M-\u0026gt;1565M(5120M) 15.968ms 显示 GC 类型、原因、内存收集大小和持续时间。\n标签 gc,age\n显示 GC 期间与年龄相关的信息。年龄较高的对象移动到老年代。Trace 级别输出每个年龄的详细大小信息，而 debug 级别仅显示最高年龄和预期大小：\n[2020-02-26T08:34:12.823+0000][debug][gc,age ] gc(1661) Desired survivor size 167772160 bytes, new threshold 6 (max threshold 6) [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 1: 16125960 bytes, 16125960 total [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 2: 16259512 bytes, 32385472 total [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 3: 2435240 bytes, 34820712 total [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 4: 17179320 bytes, 52000032 total [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 5: 43986952 bytes, 95986984 total [2020-02-26T08:34:12.823+0000][trace][gc,age ] age 6: 20858328 bytes, 116845312 total 标签 gc,alloc 和 gc,alloc,region\n这些是 G1 GC 特定的！gc,alloc 在 trace 级别记录哪个线程触发了 GC 和返回的地址 - 对 GC 调试非常有用。gc,alloc,region 在 debug 级别提供区域统计：\n[2020-02-28T02:14:02.694+0000][trace][gc,alloc ] sdk-27692-2-amqp-t-4: Successfully scheduled collection returning 0x00000007ffc00000 [2020-02-28T02:16:00.372+0000][debug][gc,alloc,region ] gc(7848) Mutator Allocation stats, regions: 677, wasted size: 63832B ( 0.0%) 标签 gc,cpu\n这是你进行 GC 性能分析的首选！Info 级别显示实际 GC 时间消耗：\n[2020-02-28T01:59:46.406+0000][info ][gc,cpu ] gc(7841) User=0.10s Sys=0.00s Real=0.04s [2020-02-28T02:01:20.148+0000][info ][gc,cpu ] gc(7842) User=0.04s Sys=0.06s Real=0.04s 注意：此时间可能与 JFR 统计不同，因为 JFR 从 GC 调度开始计数，而这是从实际标记开始测量。\n标签 gc,ergo, gc,ergo,cset, gc,ergo,ihop, gc,ergo,refine\n这些涵盖自适应大小策略详细信息。使用 trace 级别进行深入算法研究，debug 级别用于一般理解：\n[2020-02-28T01:59:46.367+0000][trace][gc,ergo,cset ] gc(7841) Start choosing CSet. pending cards: 26996 predicted base time: 13.34ms remaining time: 186.66ms target pause time: 200.00ms [2020-02-28T01:59:46.367+0000][trace][gc,ergo,cset ] gc(7841) Add young regions to CSet. eden: 676 regions, survivors: 6 regions, predicted young region time: 19.02ms, target pause time: 200.00ms [2020-02-28T01:59:46.367+0000][debug][gc,ergo,cset ] gc(7841) Finish choosing CSet. old: 0 regions, predicted old region time: 0.00ms, time remaining: 167.64 标签 gc,heap 和 gc,heap,region\ngc,heap 在 debug 级别显示 GC 期间的堆概览。对于 G1 GC，gc,heap,region 在 trace 级别打印详细区域信息（主要用于调试）：\n[2020-02-28T06:01:20.787+0000][debug][gc,heap ] gc(7922) Heap before gc invocations=7922 (full 0): garbage-first heap total 8388608K, used 4076387K [0x0000000600000000, 0x0000000800000000) [2020-02-28T06:01:20.787+0000][debug][gc,heap ] gc(7922) region size 4096K, 682 young (2793472K), 5 survivors (20480K) [2020-02-28T06:01:20.787+0000][debug][gc,heap ] gc(7922) Metaspace used 163068K, capacity 166731K, committed 169728K, reserved 1198080K 标签 gc,humongous\n非常适合遇到疏散失败或巨大分配问题的 G1 GC 用户：\n[2020-02-28T06:01:20.831+0000][debug][gc,humongous ] gc(7922) Live humongous region 219 object size 2160888 start 0x0000000636c00000 with remset 1 code roots 0 is marked 0 reclaim candidate 0 type array 0 标签 gc,metaspace, gc,metaspace,freelist, gc,metaspace,freelist,blocks\n监控元空间 GC 活动。gc,metaspace 在 info 级别显示内存变化，而其他提供详细的 trace 级别信息：\n[2020-02-28T04:32:13.123+0000][info ][gc,metaspace ] gc(7896) Metaspace: 163062K-\u0026gt;163062K(1198080K) 标签 gc,phases, gc,phases,ref, gc,phases,task, gc,ref, gc,start, gc,ref,start\n这些对于详细理解 GC 算法和阶段非常有用。\n标签 safepoint\n由于 GC 只在 safepoint 发生，debug 级别的 safepoint 日志对 GC 分析非常有见地。\n2. 类加载和运行时编译标签 # 标签 class,preorder, class,init, class,load, class,unload\n不言自明！Info 级别为大多数用例提供足够的详细信息。使用 trace 级别深入 JVM 类加载：\n[8.931s][debug][class,preorder ] com.fasterxml.jackson.core.PrettyPrinter source: file:/D:/Repositories/maven/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0.jar [8.931s][info][class,init ] 2740 Initializing 'com/fasterxml/jackson/core/PrettyPrinter' (0x0000000801399220) [8.934s][info][class,load ] com.fasterxml.jackson.core.PrettyPrinter source: file:/D:/Repositories/maven/com/fasterxml/jackson/core/jackson-core/2.10.0/jackson-core-2.10.0.jar 标签 jit,compilation\n对于 JIT 编译优化，debug 级别的 jit,compilation 日志是你最好的朋友：\n[2020-02-28T03:01:51.619+0000][debug][jit,compilation] 153756 ! 4 jdk.internal.reflect.GeneratedConstructorAccessor161::newInstance (49 bytes) made zombie 3. 其他运行时相关标签 # 标签 monitorinflation\nDebug 级别的同步锁日志 - 非常适合死锁调试：\n[5.033s][debug][monitorinflation] Deflating object 0x0000000708310378 , mark 0x0000021cef446002 , type java.lang.ref.ReferenceQueue$Lock 标签 biasedlocking\n偏向锁定信息。Info 级别用于一般监控，trace 级别用于实现详细信息：\n[7.273s][info ][biasedlocking] Revoking bias by walking my own stack: [7.273s][info ][biasedlocking] Revoking bias of object 0x0000000711b1ca40, mark 0x000001c6d0acc905, type sun.net.www.protocol.jar.URLJarFile JVM 日志配置：让一切协同工作 # 配置格式遵循此模式：\n-Xlog[:[what][:[output][:[decorators][:output-options[,...]]]]] 没有任何配置时，默认是：\n-Xlog:all=warning:stdout:uptime,level,tags 这个冒号分隔的配置分解为：what:output:decorators:output-options。任何缺失的部分使用上面的默认值。这些配置是等价的：\n-Xlog:all=warning ≡ -Xlog::stdout ≡ -Xlog::::uptime,level,tags ≡ -Xlog:all=warning:stdout:uptime,level,tags -Xlog:gc*=info ≡ -Xlog:gc*=info:stdout:uptime,level,tags 1. \u0026ldquo;What\u0026rdquo; 参数 # 这结合了标签和日志级别：\n-Xlog:gc=info - 仅在 info 级别记录恰好带有 gc 标签的日志 -Xlog:gc*=info - 在 info 级别记录所有包含 gc 标签的日志 -Xlog:gc+age=debug - 在 debug 级别记录恰好带有 gc 和 age 标签的日志 -Xlog:gc*=info,gc+heap=debug,gc+heap+region=debug - 多个配置组合 由于 age 只与 gc 配对，这些是等价的：\n-Xlog:gc*=info,age*=debug ≡ -Xlog:gc*=info,gc+age=debug 日志级别包括：\noff: 禁用 trace: 所有级别（trace, debug, info, warning, error） debug: debug, info, warning, error info: info, warning, error warning: warning, error error: 仅 error 未指定级别默认为 info！ 所以 -Xlog:gc* ≡ -Xlog:gc*=info\n2. 输出选项 # 三种输出类型可用：\nstdout: 标准输出 stderr: 标准错误 file=filename: 文件输出，带有 filecount=50,filesize=100M 等选项 3. 装饰器：让日志更美观 # 装饰器 含义 time, t 当前时间（ISO-8601） utctime, utc UTC 时间 uptime, u 自启动以来的时间（毫秒） timemillis, tm 毫秒时间戳 uptimemillis, um 自启动以来的毫秒数 timenanos, tn 纳秒时间戳 uptimenanos, un 自启动以来的纳秒数 hostname, hn 主机名 pid, p 进程 ID tid, ti 线程 ID level, l 日志级别 tags, tg 日志标签 4. 转换遗留参数 # GC 相关转换：\n遗留参数 新等价参数 g1PrintHeapRegions -Xlog:gc+region=trace PrintTenuringDistribution -Xlog:gc+age*=level PrintAdaptiveSizePolicy -Xlog:gc+ergo*=level Printgc -Xlog:gc=info PrintgcDetails -Xlog:gc*=info PrintgcApplicationConcurrentTime -Xlog:safepoint PrintHeapAtgc -Xlog:gc+heap=trace 其他转换：\n遗留参数 新等价参数 TraceClassLoading -Xlog:class+load=info TraceClassUnloading -Xlog:class+unload=info TraceSafepoint -Xlog:safepoint=debug TraceMonitorInflation -Xlog:monitorinflation=debug TraceBiasedLocking -Xlog:biasedlocking=level 动态 JVM 日志配置：实时调整 # 这就是真正酷的地方！你可以使用 jcmd 和 VM.log 命令实时修改 JVM 日志。对于 PID 为 22 的进程：\njcmd 22 VM.log 查看当前配置 # jcmd 22 VM.log list 示例输出：\nLog output configuration: #0: stdout all=warning uptime,level,tags #1: stderr all=off uptime,level,tags #2: file=/project/log/gc.log all=off,gc*=debug utctime,level,tags filecount=50,filesize=100M #3: file=/project/log/jit_compile.log all=off,jit+compilation=debug utctime,level,tags filecount=10,filesize=100M 轮转日志文件 # jcmd 22 VM.log rotate 添加新日志配置 # jcmd 22 VM.log output=/project/core/log/gc.log output_options=\u0026quot;filecount=50,filesize=100M\u0026quot; decorators=\u0026quot;utctime,level,tags\u0026quot; what=\u0026quot;gc*=debug\u0026quot; 修改现有配置 # 配置由其 output 参数标识。相同的输出 = 修改：\njcmd 22 VM.log output=/project/core/log/gc.log what=\u0026quot;gc*=info\u0026quot; 重要：每个 what 设置是累加的，不是替换！系统智能地合并相关标签。\n禁用所有日志 # jcmd 22 VM.log disable 这完全清除所有日志配置，包括启动参数！\n就是这样！OpenJDK 11 中的新统一日志系统起初可能看起来很复杂，但一旦你掌握了它，它就非常强大和灵活。不再需要处理数十个不同的标志 - 只需一个干净、一致的接口来满足你所有的 JVM 日志需求！🎉\n祝你日志记录愉快，愿你的性能调优冒险永远成功！🚀\n","date":"2020年2月6日","externalUrl":null,"permalink":"/zh-cn/posts/jvm-log/","section":"文章","summary":"了解 OpenJDK 11 如何通过其统一配置系统彻底改变 JVM 日志记录。学习用于 GC 分析、类加载和运行时编译的基本日志标签，以及使用 jcmd 进行实时性能调优的动态日志配置。","title":"OpenJDK 11 JVM 日志：新统一配置完整指南","type":"posts"},{"content":" 👋 你好世界！NeatGuy 正在编程~ # 💫 关于我 # 🔭 作为技术领导者推动创新项目向前发展 🌱 通过新兴技术不断扩展专业知识，拥抱新挑战 💬 欢迎讨论所有技术相关话题——随时联系 🛠️ 技术栈 # 类别 技术 编程语言 行式数据库 列式数据库 图数据库 搜索引擎 缓存 \u0026 NoSQL 数据湖 \u0026 数据仓库 向量数据库 消息队列 DevOps 云服务 Java 框架 Python 框架 Go 框架 前端框架 👨‍💻 仓库贡献 # 项目 描述 技术 Stars Forks 我的贡献 Dify Dify 是一个开源 LLM 应用开发平台。Dify 的直观界面结合了 AI 工作流、RAG 管道、代理能力、模型管理、可观测性功能等，让你快速从原型到生产。 我的贡献 Dify-plugin-daemon Dify Plugin Daemon 是一个管理插件生命周期的服务。 我的贡献 Mycat-Server 分片数据库数据的综合解决方案 我的贡献 JFR-Unit 用于断言 JDK Flight Recorder 事件的 JUnit 扩展 我的贡献 OpenJDK JDK 主线开发 https://openjdk.org/projects/jdk 我的贡献 bookkeeper Apache BookKeeper - 一个可扩展、容错且低延迟的存储服务，针对仅追加工作负载进行了优化 我的贡献 RocketMQ Apache RocketMQ 是一个云原生消息和流平台，使构建事件驱动应用程序变得简单。 我的贡献 Spring Cloud Gateway 基于 Spring Framework 和 Spring Boot 构建的 API 网关，提供路由等功能。 我的贡献 Lettuce 用于线程安全同步、异步和响应式使用的高级 Java Redis 客户端。支持集群、哨兵、管道和编解码器。 我的贡献 Spring Framework https://spring.io/projects/spring-framework 我的贡献 Spring Cloud Commons 在不同 Spring Cloud 实现中使用的通用类 我的贡献 Netty Socketio 在 Java 上实现的 Socket.IO 服务器。实时 Java 框架 我的贡献 langchain4j LangChain 的 Java 版本 我的贡献 📊 GitHub 统计 # 🏆 GitHub 奖杯 # 编程快乐！ 😊 ","externalUrl":null,"permalink":"/zh-cn/about/","section":"Neat Guy Coding","summary":"\u003ch1 class=\"relative group\"\u003e👋 你好世界！NeatGuy 正在编程~\n    \u003cdiv id=\"-你好世界neatguy-正在编程\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-%e4%bd%a0%e5%a5%bd%e4%b8%96%e7%95%8cneatguy-%e6%ad%a3%e5%9c%a8%e7%bc%96%e7%a8%8b\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h1\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://readme-typing-svg.herokuapp.com?font=Fira+Code\u0026pause=1000\u0026color=36BCF7\u0026center=true\u0026vCenter=true\u0026width=635\u0026lines=Passionate+Technology+Leader+and+Developer;Always+Learning+and+Challenging\" alt=\"Typing SVG\" /\u003e\n\u003c/div\u003e\n\n\u003ch2 class=\"relative group\"\u003e💫 关于我\n    \u003cdiv id=\"-关于我\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-%e5%85%b3%e4%ba%8e%e6%88%91\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e🔭 作为技术领导者推动创新项目向前发展\u003c/li\u003e\n\u003cli\u003e🌱 通过新兴技术不断扩展专业知识，拥抱新挑战\u003c/li\u003e\n\u003cli\u003e💬 欢迎讨论所有技术相关话题——随时联系\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003ch2 class=\"relative group\"\u003e🛠️ 技术栈\n    \u003cdiv id=\"-技术栈\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-%e6%8a%80%e6%9c%af%e6%a0%88\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003ctable class=\"tech-table\"\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003e类别\u003c/th\u003e\n      \u003cth\u003e技术\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e编程语言\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Java-007396?style=flat-square\u0026logo=java\u0026logoColor=white\" alt=\"Java\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-C++-00599C?style=flat-square\u0026logo=cplusplus\u0026logoColor=white\" alt=\"C++\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Python-3776AB?style=flat-square\u0026logo=python\u0026logoColor=white\" alt=\"Python\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Go-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Go\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Rust-000000?style=flat-square\u0026logo=rust\u0026logoColor=white\" alt=\"Rust\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat-square\u0026logo=javascript\u0026logoColor=black\" alt=\"JavaScript\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square\u0026logo=typescript\u0026logoColor=white\" alt=\"TypeScript\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Ruby-CC342D?style=flat-square\u0026logo=ruby\u0026logoColor=white\" alt=\"Ruby\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-PHP-777BB4?style=flat-square\u0026logo=php\u0026logoColor=white\" alt=\"PHP\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Kotlin-0095D5?style=flat-square\u0026logo=kotlin\u0026logoColor=white\" alt=\"Kotlin\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Swift-FA7343?style=flat-square\u0026logo=swift\u0026logoColor=white\" alt=\"Swift\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Scala-DC322F?style=flat-square\u0026logo=scala\u0026logoColor=white\" alt=\"Scala\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e行式数据库\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-MySQL-4479A1?style=flat-square\u0026logo=mysql\u0026logoColor=white\" alt=\"MySQL\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Oracle-F80000?style=flat-square\u0026logo=oracle\u0026logoColor=white\" alt=\"Oracle\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-PostgreSQL-336791?style=flat-square\u0026logo=postgresql\u0026logoColor=white\" alt=\"PostgreSQL\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-DynamoDB-4053D6?style=flat-square\u0026logo=amazon-dynamodb\u0026logoColor=white\" alt=\"DynamoDB\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-MariaDB-003545?style=flat-square\u0026logo=mariadb\u0026logoColor=white\" alt=\"MariaDB\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-SQLite-003B57?style=flat-square\u0026logo=sqlite\u0026logoColor=white\" alt=\"SQLite\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e列式数据库\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-HBase-D22128?style=flat-square\u0026logo=apache\u0026logoColor=white\" alt=\"HBase\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ClickHouse-FFCC01?style=flat-square\u0026logo=clickhouse\u0026logoColor=black\" alt=\"ClickHouse\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Bigtable-4285F4?style=flat-square\u0026logo=google-cloud\u0026logoColor=white\" alt=\"Bigtable\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e图数据库\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Neo4J-008CC1?style=flat-square\u0026logo=neo4j\u0026logoColor=white\" alt=\"Neo4J\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ArangoDB-DDE072?style=flat-square\u0026logo=arangodb\u0026logoColor=black\" alt=\"ArangoDB\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-JanusGraph-2F4F4F?style=flat-square\u0026logo=apache\u0026logoColor=white\" alt=\"JanusGraph\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e搜索引擎\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ElasticSearch-005571?style=flat-square\u0026logo=elasticsearch\u0026logoColor=white\" alt=\"ElasticSearch\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Solr-D9411E?style=flat-square\u0026logo=apache-solr\u0026logoColor=white\" alt=\"Solr\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e缓存 \u0026 NoSQL\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Redis-DC382D?style=flat-square\u0026logo=redis\u0026logoColor=white\" alt=\"Redis\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Cassandra-1287B1?style=flat-square\u0026logo=apache-cassandra\u0026logoColor=white\" alt=\"Cassandra\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Memcached-005571?style=flat-square\u0026logo=memcached\u0026logoColor=white\" alt=\"Memcached\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e数据湖 \u0026 数据仓库\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Hive-FDEE21?style=flat-square\u0026logo=apache-hive\u0026logoColor=black\" alt=\"Hive\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Doris-00BFFF?style=flat-square\u0026logo=apache\u0026logoColor=white\" alt=\"Doris\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Starrocks-0078D4?style=flat-square\u0026logo=starrocks\u0026logoColor=white\" alt=\"Starrocks\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Druid-29F1FB?style=flat-square\u0026logo=apache-druid\u0026logoColor=black\" alt=\"Druid\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Pinot-E95420?style=flat-square\u0026logo=apache\u0026logoColor=white\" alt=\"Pinot\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Snowflake-29B5E8?style=flat-square\u0026logo=snowflake\u0026logoColor=white\" alt=\"Snowflake\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-BigQuery-4285F4?style=flat-square\u0026logo=google-cloud\u0026logoColor=white\" alt=\"BigQuery\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e向量数据库\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Pinecone-000000?style=flat-square\u0026logo=pinecone\u0026logoColor=white\" alt=\"Pinecone\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Qdrant-5A29E4?style=flat-square\u0026logo=qdrant\u0026logoColor=white\" alt=\"Qdrant\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Weaviate-3F51B5?style=flat-square\u0026logo=weaviate\u0026logoColor=white\" alt=\"Weaviate\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Milvus-45B8AC?style=flat-square\u0026logo=milvus\u0026logoColor=white\" alt=\"Milvus\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e消息队列\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-RocketMQ-D77310?style=flat-square\u0026logo=apache-rocketmq\u0026logoColor=white\" alt=\"RocketMQ\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Kafka-231F20?style=flat-square\u0026logo=apache-kafka\u0026logoColor=white\" alt=\"Kafka\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Pulsar-188FFF?style=flat-square\u0026logo=apache-pulsar\u0026logoColor=white\" alt=\"Pulsar\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-RabbitMQ-FF6600?style=flat-square\u0026logo=rabbitmq\u0026logoColor=white\" alt=\"RabbitMQ\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ActiveMQ-EF2D56?style=flat-square\u0026logo=apache\u0026logoColor=white\" alt=\"ActiveMQ\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003eDevOps\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Kubernetes-326CE5?style=flat-square\u0026logo=kubernetes\u0026logoColor=white\" alt=\"Kubernetes\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Helm-0F1689?style=flat-square\u0026logo=helm\u0026logoColor=white\" alt=\"Helm\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ArgoCD-EF7B4D?style=flat-square\u0026logo=argo\u0026logoColor=white\" alt=\"ArgoCD\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Jenkins-D24939?style=flat-square\u0026logo=jenkins\u0026logoColor=white\" alt=\"Jenkins\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-GitLab-FCA121?style=flat-square\u0026logo=gitlab\u0026logoColor=white\" alt=\"GitLab\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-GitHub-181717?style=flat-square\u0026logo=github\u0026logoColor=white\" alt=\"GitHub\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Terraform-623CE4?style=flat-square\u0026logo=terraform\u0026logoColor=white\" alt=\"Terraform\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Ansible-EE0000?style=flat-square\u0026logo=ansible\u0026logoColor=white\" alt=\"Ansible\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e云服务\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-AWS-232F3E?style=flat-square\u0026logo=amazon-aws\u0026logoColor=white\" alt=\"AWS\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Route53-8C4FFF?style=flat-square\u0026logo=amazon-aws\u0026logoColor=white\" alt=\"Route53\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-ALB/ELB-FF9900?style=flat-square\u0026logo=amazon-aws\u0026logoColor=white\" alt=\"ALB/ELB\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-S3-569A31?style=flat-square\u0026logo=amazon-s3\u0026logoColor=white\" alt=\"S3\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-EC2-FF9900?style=flat-square\u0026logo=amazon-ec2\u0026logoColor=white\" alt=\"EC2\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-RDS-527FFF?style=flat-square\u0026logo=amazon-aws\u0026logoColor=white\" alt=\"RDS\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-SageMaker-FF9900?style=flat-square\u0026logo=amazon-aws\u0026logoColor=white\" alt=\"SageMaker\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-GCP-4285F4?style=flat-square\u0026logo=google-cloud\u0026logoColor=white\" alt=\"GCP\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Azure-0078D4?style=flat-square\u0026logo=microsoft-azure\u0026logoColor=white\" alt=\"Azure\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003eJava 框架\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Spring-6DB33F?style=flat-square\u0026logo=spring\u0026logoColor=white\" alt=\"Spring\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Spring%20Boot-6DB33F?style=flat-square\u0026logo=spring-boot\u0026logoColor=white\" alt=\"Spring Boot\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Spring%20Cloud-6DB33F?style=flat-square\u0026logo=spring\u0026logoColor=white\" alt=\"Spring Cloud\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Quarkus-4695EB?style=flat-square\u0026logo=quarkus\u0026logoColor=white\" alt=\"Quarkus\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-MyBatis-000000?style=flat-square\u0026logo=mybatis\u0026logoColor=white\" alt=\"MyBatis\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Netty-2C2D72?style=flat-square\u0026logo=netty\u0026logoColor=white\" alt=\"Netty\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Hibernate-59666C?style=flat-square\u0026logo=hibernate\u0026logoColor=white\" alt=\"Hibernate\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003ePython 框架\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Celery-37814A?style=flat-square\u0026logo=celery\u0026logoColor=white\" alt=\"Celery\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-SQLAlchemy-D71F00?style=flat-square\u0026logo=sqlalchemy\u0026logoColor=white\" alt=\"SQLAlchemy\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Pydantic-E92063?style=flat-square\u0026logo=pydantic\u0026logoColor=white\" alt=\"Pydantic\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-NumPy-013243?style=flat-square\u0026logo=numpy\u0026logoColor=white\" alt=\"NumPy\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-SciPy-8CAAE6?style=flat-square\u0026logo=scipy\u0026logoColor=white\" alt=\"SciPy\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Keras-D00000?style=flat-square\u0026logo=keras\u0026logoColor=white\" alt=\"Keras\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Flask-F12345?style=flat-square\u0026logo=flask\u0026logoColor=white\" alt=\"Flask\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Django-AC1289?style=flat-square\u0026logo=django\u0026logoColor=white\" alt=\"Django\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-FastAPI-009688?style=flat-square\u0026logo=fastapi\u0026logoColor=white\" alt=\"FastAPI\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003eGo 框架\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Gin-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Gin\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Kratos-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Kratos\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-SQLX-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"SQLX\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Cobra-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Cobra\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Testify-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Testify\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Echo-00ADD8?style=flat-square\u0026logo=go\u0026logoColor=white\" alt=\"Echo\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd class=\"category-column\"\u003e前端框架\u003c/td\u003e\n      \u003ctd class=\"technologies-column\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Next.js-000000?style=flat-square\u0026logo=next.js\u0026logoColor=white\" alt=\"Next.js\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-React-61DAFB?style=flat-square\u0026logo=react\u0026logoColor=black\" alt=\"React\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square\u0026logo=tailwind-css\u0026logoColor=white\" alt=\"TailwindCSS\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-HeadlessUI-66E3FF?style=flat-square\u0026logo=headlessui\u0026logoColor=black\" alt=\"HeadlessUI\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Lexical-61DAFB?style=flat-square\u0026logo=react\u0026logoColor=black\" alt=\"Lexical\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Vue.js-4FC08D?style=flat-square\u0026logo=vue.js\u0026logoColor=white\" alt=\"Vue.js\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Angular-DD0031?style=flat-square\u0026logo=angular\u0026logoColor=white\" alt=\"Angular\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/-Svelte-FF3E00?style=flat-square\u0026logo=svelte\u0026logoColor=white\" alt=\"Svelte\"\u003e\n      \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch2 class=\"relative group\"\u003e👨‍💻 仓库贡献\n    \u003cdiv id=\"-仓库贡献\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-%e4%bb%93%e5%ba%93%e8%b4%a1%e7%8c%ae\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e项目\u003c/th\u003e\n          \u003cth\u003e描述\u003c/th\u003e\n          \u003cth\u003e技术\u003c/th\u003e\n          \u003cth\u003eStars\u003c/th\u003e\n          \u003cth\u003eForks\u003c/th\u003e\n          \u003cth\u003e我的贡献\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langgenius/dify\"\n    target=\"_blank\"\n  \u003eDify\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eDify 是一个开源 LLM 应用开发平台。Dify 的直观界面结合了 AI 工作流、RAG 管道、代理能力、模型管理、可观测性功能等，让你快速从原型到生产。\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Python\"\n    src=\"https://img.shields.io/badge/-Python-3776AB?style=flat-square\u0026amp;logo=python\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n \u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"JavaScript\"\n    src=\"https://img.shields.io/badge/-JavaScript-F7DF1E?style=flat-square\u0026amp;logo=javascript\u0026amp;logoColor=black\"\n    \u003e\u003c/figure\u003e\n \u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"TypeScript\"\n    src=\"https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square\u0026amp;logo=typescript\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/langgenius/dify?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/langgenius/dify?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langgenius/dify/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langgenius/dify-plugin-daemon\"\n    target=\"_blank\"\n  \u003eDify-plugin-daemon\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eDify Plugin Daemon 是一个管理插件生命周期的服务。\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Go\"\n    src=\"https://img.shields.io/badge/-Go-00ADD8?style=flat-square\u0026amp;logo=go\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/langgenius/dify-plugin-daemon?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/langgenius/dify-plugin-daemon?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langgenius/dify-plugin-daemon/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/MyCATApache/Mycat-Server\"\n    target=\"_blank\"\n  \u003eMycat-Server\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e分片数据库数据的综合解决方案\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/MyCATApache/Mycat-Server?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/MyCATApache/Mycat-Server?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/MyCATApache/Mycat-Server/issues?q=author%3ANeatGuyCoding\u0026#43;\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/moditect/jfrunit\"\n    target=\"_blank\"\n  \u003eJFR-Unit\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e用于断言 JDK Flight Recorder 事件的 JUnit 扩展\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/moditect/jfrunit?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/moditect/jfrunit?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/moditect/jfrunit/commits/main/?author=NeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/openjdk/jdk\"\n    target=\"_blank\"\n  \u003eOpenJDK\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eJDK 主线开发 \u003ca\n  href=\"https://openjdk.org/projects/jdk\"\n    target=\"_blank\"\n  \u003ehttps://openjdk.org/projects/jdk\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"C\u0026#43;\u0026#43;\"\n    src=\"https://img.shields.io/badge/-C\u0026#43;\u0026#43;-00599C?style=flat-square\u0026amp;logo=cplusplus\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/openjdk/jdk?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/openjdk/jdk?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/openjdk/jdk/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/apache/bookkeeper\"\n    target=\"_blank\"\n  \u003ebookkeeper\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eApache BookKeeper - 一个可扩展、容错且低延迟的存储服务，针对仅追加工作负载进行了优化\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/apache/bookkeeper?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/apache/bookkeeper?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/apache/bookkeeper/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/apache/rocketmq\"\n    target=\"_blank\"\n  \u003eRocketMQ\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eApache RocketMQ 是一个云原生消息和流平台，使构建事件驱动应用程序变得简单。\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/apache/rocketmq?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/apache/rocketmq?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/apache/rocketmq/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-cloud/spring-cloud-gateway\"\n    target=\"_blank\"\n  \u003eSpring Cloud Gateway\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e基于 Spring Framework 和 Spring Boot 构建的 API 网关，提供路由等功能。\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/spring-cloud/spring-cloud-gateway?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/spring-cloud/spring-cloud-gateway?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-cloud/spring-cloud-gateway/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/redis/lettuce\"\n    target=\"_blank\"\n  \u003eLettuce\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e用于线程安全同步、异步和响应式使用的高级 Java Redis 客户端。支持集群、哨兵、管道和编解码器。\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/redis/lettuce?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/redis/lettuce?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/redis/lettuce/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-projects/spring-framework\"\n    target=\"_blank\"\n  \u003eSpring Framework\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://spring.io/projects/spring-framework\"\n    target=\"_blank\"\n  \u003ehttps://spring.io/projects/spring-framework\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/spring-projects/spring-framework?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/spring-projects/spring-framework?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-projects/spring-framework/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-cloud/spring-cloud-commons\"\n    target=\"_blank\"\n  \u003eSpring Cloud Commons\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e在不同 Spring Cloud 实现中使用的通用类\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/spring-cloud/spring-cloud-commons?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/spring-cloud/spring-cloud-commons?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/spring-cloud/spring-cloud-commons/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/mrniko/netty-socketio\"\n    target=\"_blank\"\n  \u003eNetty Socketio\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003e在 Java 上实现的 Socket.IO 服务器。实时 Java 框架\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/mrniko/netty-socketio?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/mrniko/netty-socketio?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/mrniko/netty-socketio/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langchain4j/langchain4j\"\n    target=\"_blank\"\n  \u003elangchain4j\u003c/a\u003e\u003c/td\u003e\n          \u003ctd\u003eLangChain 的 Java 版本\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Java\"\n    src=\"https://img.shields.io/badge/-Java-ED8B00?style=flat-square\u0026amp;logo=openjdk\u0026amp;logoColor=white\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Stars\"\n    src=\"https://img.shields.io/github/stars/langchain4j/langchain4j?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003cfigure\u003e\u003cimg\n    class=\"my-0 rounded-md\"\n    loading=\"lazy\"\n    decoding=\"async\"\n    fetchpriority=\"low\"\n    alt=\"Forks\"\n    src=\"https://img.shields.io/github/forks/langchain4j/langchain4j?style=flat-square\u0026amp;labelColor=343b41\"\n    \u003e\u003c/figure\u003e\n\u003c/td\u003e\n          \u003ctd\u003e\u003ca\n  href=\"https://github.com/langchain4j/langchain4j/issues?q=author%3ANeatGuyCoding\"\n    target=\"_blank\"\n  \u003e我的贡献\u003c/a\u003e\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ch2 class=\"relative group\"\u003e📊 GitHub 统计\n    \u003cdiv id=\"-github-统计\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-github-%e7%bb%9f%e8%ae%a1\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github-readme-stats.vercel.app/api/top-langs/?username=NeatGuyCoding\u0026layout=compact\u0026theme=tokyonight\u0026hide_border=true\" alt=\"Top Languages\" height=\"170\"/\u003e\n  \u003cimg src=\"https://github-readme-streak-stats.herokuapp.com/?user=NeatGuyCoding\u0026theme=tokyonight\u0026hide_border=true\" alt=\"GitHub Streak\" /\u003e\n\u003c/div\u003e\n\n\u003ch2 class=\"relative group\"\u003e🏆 GitHub 奖杯\n    \u003cdiv id=\"-github-奖杯\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none\"\u003e\n        \u003ca class=\"text-primary-300 dark:text-neutral-700 !no-underline\" href=\"#-github-%e5%a5%96%e6%9d%af\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e\n    \n\u003c/h2\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github-profile-trophy.vercel.app/?username=NeatGuyCoding\u0026theme=nord\u0026column=7\u0026no-frame=true\" alt=\"GitHub Trophies\" /\u003e\n\u003c/div\u003e\n\u003chr\u003e\n\u003cdiv align=\"center\"\u003e\n  \u003ci\u003e编程快乐！\u003c/i\u003e 😊\n\u003c/div\u003e","title":"","type":"page"},{"content":"","externalUrl":null,"permalink":"/zh-cn/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/zh-cn/series/","section":"Series","summary":"","title":"Series","type":"series"}]