<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>OOM on My Learning Notes</title><link>https://eleanora-lyh.github.io/MyLearningNotes/tags/oom/</link><description>Recent content in OOM on My Learning Notes</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 20 Oct 2026 10:11:30 +0000</lastBuildDate><atom:link href="https://eleanora-lyh.github.io/MyLearningNotes/tags/oom/index.xml" rel="self" type="application/rss+xml"/><item><title>【工作杂谈_20261012】数据平台与工程治理: Spark OOM 排查：Java Heap、Executor 与代码内存模型</title><link>https://eleanora-lyh.github.io/MyLearningNotes/posts/work/dataplatformengineering/20261012_%E6%95%B0%E6%8D%AE%E5%B9%B3%E5%8F%B0%E4%B8%8E%E5%B7%A5%E7%A8%8B%E6%B2%BB%E7%90%86__sparkoom/</link><pubDate>Sun, 11 Oct 2026 21:35:07 +0800</pubDate><guid>https://eleanora-lyh.github.io/MyLearningNotes/posts/work/dataplatformengineering/20261012_%E6%95%B0%E6%8D%AE%E5%B9%B3%E5%8F%B0%E4%B8%8E%E5%B7%A5%E7%A8%8B%E6%B2%BB%E7%90%86__sparkoom/</guid><description>&lt;h1 id="1文档目的"&gt;1、文档目的
&lt;/h1&gt;&lt;p&gt;本文整理 2026-08-01 Spark ingestion 发生 Java Heap OOM 的完整调查过程，覆盖以下方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Spark 报错与 JVM 指标解读。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;问题类型判断：数据倾斜、总量增长还是代码内存模型问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;相关 Spark 参数的真实作用及常见误区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当前临时处理、短期稳态处理和长期根治方案。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="2日志分析"&gt;2、日志分析
&lt;/h1&gt;&lt;p&gt;打开spark的日志（敏感部分已打码）从第一句话就可以看到 log 显示：&lt;code&gt;java.lang.OutOfMemoryError: Java heap space&lt;/code&gt;&lt;strong&gt;当前 JVM 的 Java Heap 无法继续分配内存&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;错误发生在 Spark Executor 的 task worker，而不是 Driver。&lt;/li&gt;
&lt;li&gt;失败资源是 Executor JVM 的 Java Heap，不是磁盘空间，也不是单纯的 YARN memory overhead。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-log" data-lang="log"&gt;26/08/01 18:16:00 ERROR Executor [Executor task launch worker for task 3.2 in stage 11.0 (TID 180)]: Exception in task 3.2 in stage 11.0 (TID 180)
java.lang.OutOfMemoryError: Java heap space
  at java.util.Arrays.copyOf(Arrays.java:3181) ~[?:1.8.0_492]
  at java.util.ArrayList.grow(ArrayList.java:267) ~[?:1.8.0_492]
  at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241) ~[?:1.8.0_492]
  at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233) ~[?:1.8.0_492]
  at java.util.ArrayList.add(ArrayList.java:464) ~[?:1.8.0_492]
 ....
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.init, value=1054867456
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.max, value=11453595648
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.usage, value=0.716910699866886
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.heap.used, value=8224523704
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.capacity, value=0
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.count, value=0
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.mapped.used, value=0
...
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.committed, value=8589934592
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.init, value=703594496
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.max, value=8589934592
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.usage, value=0.9457348454743624
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.used, value=8123800464
26/08/01 18:16:00 INFO metrics [shutdown-hook-0]: type=GAUGE, name=application_1783999226249_22616.8.jvm.pools.PS-Old-Gen.used-after-gc, value=8123800464
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;总结日志证据链&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;日志&lt;/th&gt;
 &lt;th&gt;能得出的结论&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ERROR Executor [Executor task launch worker ...]&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;异常发生在 Executor 执行 Task 时&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;java.lang.OutOfMemoryError: Java heap space&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Executor JVM 的 Java Heap 耗尽&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;ArrayList.grow&lt;/code&gt; / &lt;code&gt;Arrays.copyOf&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;扩容 &lt;code&gt;ArrayList&lt;/code&gt; 时无法申请新的数组&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;jvm.heap.max, value=11453595648&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Executor 实际最大 Heap 约为$11453595648KB/1024^3 ≈ 10.67 GB          $&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PS-Old-Gen.usage, value=0.945734845...&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Old Gen 已使用约 94.57%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PS-Old-Gen.used-after-gc = 8123800464&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;已知：内存设置的为8GiB&lt;br/&gt;Full GC 后仍保留约 7.57 GiB，说明大部分对象无法释放&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PS-MarkSweep.count=19, time=174557&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;已执行 19 次 Full GC，累计约 174 秒，仍无法腾出足够空间&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这表明大量对象仍被业务集合引用，Full GC 无法回收。GC 不是没有执行，而是没有可回收的对象（全都被占用）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;定位异常发生的代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;ContentInsightsUniqueUserRecord&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; records &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ArrayList&lt;span style="color:#f92672"&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (Entries.&lt;span style="color:#a6e22e"&gt;hasNext&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Entries.&lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;_2&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;forEach&lt;/span&gt;(records::add);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里把整个 Spark partition 中所有 key 的记录持续加入同一个 &lt;code&gt;ArrayList&lt;/code&gt;。&lt;code&gt;ArrayList&lt;/code&gt; 扩容时需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个更大的新数组&lt;code&gt;Object[]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;Arrays.copyOf&lt;/code&gt; 将旧数组复制到新数组&lt;/li&gt;
&lt;li&gt;复制完成后旧数组在没有引用后才可被 GC 回收&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此扩容瞬间需要额外的连续 Heap 空间同时容纳新旧两个数组。Old Gen 已经达到 94.57% 时，&lt;code&gt;Arrays.copyOf&lt;/code&gt; 无法申请连续的新数组，于是抛出 &lt;code&gt;OutOfMemoryError: Java heap space&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一次运行还出现 Executor heartbeat 或 RPC timeout，是次生现象：&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Heap 持续增长
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; Young GC / Full GC 频繁发生
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; task 和心跳线程长时间得不到有效运行时间
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; heartbeat/RPC timeout
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; task 重试
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; ArrayList 继续增长
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -&amp;gt; Java heap space OOM
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;因此仅增大 &lt;code&gt;spark.executor.heartbeatInterval&lt;/code&gt; 或 &lt;code&gt;spark.network.timeout&lt;/code&gt; 可能延迟报错，但不能修复 Heap 根因。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;总结一下上面的因果关系：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;此次运行的数据较以往偏大，数据膨胀&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;executor heap 紧张&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;频繁/长时间 Full GC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;心跳超时&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ArrayList 继续增长&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Java heap space OOM&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="3数据膨胀的猜测和验证"&gt;3、数据膨胀的猜测和验证
&lt;/h1&gt;&lt;h2 id="31-猜测"&gt;3.1 猜测
&lt;/h2&gt;&lt;p&gt;由于之前从未发生过这类问题，所以有两个大方向的假设：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spark 偶发性执行问题&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;同一份输入重跑后仍在相同代码路径发生 OOM，因此可以基本排除一次性的节点抖动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;输入的内存工作集确实变大&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一次ingestion的过程中会拿到所有的key及对应的记录，其行数可以按照下面的公式计算：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
TotalRows = KeyCount \times AverageRowsPerKey
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KeyCount&lt;/code&gt;：有多少个不可聚合的 &lt;code&gt;Key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AverageRowsPerKey&lt;/code&gt;：每个 key平均有多少条维度记录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：8月的总量增长可能来自：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;KeyCount&lt;/code&gt; 数增加&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;AverageRowsPerKey&lt;/code&gt; 增加&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;两者同时增加&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="32-验证"&gt;3.2 验证
&lt;/h2&gt;&lt;p&gt;计算7月和8月的&lt;code&gt;KeyCount&lt;/code&gt;和&lt;code&gt;AverageRowsPerKey&lt;/code&gt;变化，其中KeyCount分为两个输入源，这里简称A和B&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;数据源&lt;/th&gt;
 &lt;th&gt;KeyCount 增长&lt;/th&gt;
 &lt;th&gt;AverageRowsPerKey 增长&lt;/th&gt;
 &lt;th&gt;分类&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;A&lt;/td&gt;
 &lt;td&gt;317,908→723,315（+127.5%）&lt;/td&gt;
 &lt;td&gt;77.7 → 69.8（-10.2%）&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;类型 1：KeyCount 增加&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;B&lt;/td&gt;
 &lt;td&gt;+14.0%&lt;/td&gt;
 &lt;td&gt;211.0 → 235.9（+11.8%）&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;类型 3：两者同时增加&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;因此准确结论是：Spark OOM 来自A key 数量爆发导致partition 内所有 key 的总工作集变大（平均记录/key 下降反而抵消了一部分增长），而不是单个 key 变大（数据倾斜）。&lt;/p&gt;
&lt;h1 id="4根因"&gt;4、根因
&lt;/h1&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;根因：整分区物化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IngestionInPartition&lt;/code&gt; 把整个 partition 收集到一个列表，内存复杂度约为：&lt;/p&gt;
&lt;p&gt;$$
O(partition 内总记录数)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;重要诱因：partition 数量过少&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当前 &lt;code&gt;--partitions=10&lt;/code&gt;，平均每个 partition 承担约总数据量的十分之一。数据增长后，即使分布均匀，单 partition 也可能过大。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;并发内存放大&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;日志显示：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;executor.threadpool.activeTasks, value=3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;说明同一个 Executor 当时可能同时执行 3 个 task。每个 task 都在构造自己的整分区列表，因此 Executor Heap 大致需要同时承受：&lt;strong&gt;3×单分区工作集&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Executor Heap 不足&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Old Gen 已达到约 94.57%，扩容 &lt;code&gt;ArrayList&lt;/code&gt; 时无法再创建更大的数组，于是出现：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;OutOfMemoryError: Java heap space
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ArrayList.grow
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Arrays.copyOf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="5解决办法"&gt;5、解决办法
&lt;/h1&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 O["Executor Java Heap OOM"] --&gt; E["阶段 1：应急恢复"]
 O --&gt; W["阶段 1：降低单 task 工作集"]
 O --&gt; C["阶段 2：长期代码根治"]
 O --&gt; U["阶段 3：按增长类型治理上游"]

 E --&gt; M["spark.executor.memory&lt;br/&gt;例如 8g 提升到 24g"]
 E --&gt; HC["若同 Executor 并发多个大 task&lt;br/&gt;降低 executor cores"]

 W --&gt; P["shuffle partitions&lt;br/&gt;从 10 灰度提升到 50 / 100 / 200"]
 P --&gt; V["通过 Spark UI 验证&lt;br/&gt;每 task records / bytes / peak memory / GC"]

 C --&gt; S["foreachPartition 中逐 key 处理"]
 S --&gt; K["只物化当前 key 的 records"]
 K --&gt; Z["立即序列化为 Blob"]
 Z --&gt; B["达到 batch size 后写入并 clear"]
 B --&gt; F["最后 flush 剩余 batch&lt;br&gt;(通过batch size限制Blob峰值)"]

 U --&gt; U1["类型 1：分析新增/删除 key 来源"]
 U --&gt; U2["类型 2：分析 Retained Key 密度、重复和 fan-out"]
 U --&gt; U3["类型 3：同时执行两套分析"]

 BS["调小 ObjectStore batch size"] -. "当前实现中无效" .-&gt; C
 

 HB["增大 heartbeat/network timeout"] -. "只能缓解次生告警" .-&gt; O

 classDef root fill:#f8d7da,stroke:#b02a37,color:#58151c,stroke-width:3px;
 classDef phase1 fill:#fff3cd,stroke:#d39e00,color:#664d03,stroke-width:2px;
 classDef phase2 fill:#cfe2ff,stroke:#0d6efd,color:#052c65,stroke-width:2px;
 classDef phase3 fill:#d1e7dd,stroke:#198754,color:#0a3622,stroke-width:2px;
 classDef note fill:#e9ecef,stroke:#6c757d,color:#343a40,stroke-width:1px,stroke-dasharray:5 5;

 class O root;

 class E,W,M,HC,P,V phase1;

 class C,S,K,Z,B,F,BS2,BS3 phase2;

 class U,U1,U2,U3 phase3;

 class BS,HB note;&lt;/pre&gt;&lt;h3 id="51-第一阶段恢复运行"&gt;5.1 第一阶段：恢复运行
&lt;/h3&gt;&lt;p&gt;降低本次重跑失败概率，但没有改变内存复杂度&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;spark.executor.memory=24g&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--partitions=10&lt;/code&gt; 灰度提升到 &lt;code&gt;100&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;检查节点是否能提供对应 Executor container 内存。&lt;/li&gt;
&lt;li&gt;如果同一 Executor 仍同时运行 3 个大 task 并持续 Full GC，将 executor cores 降到 1～2。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;改动小；&lt;/li&gt;
&lt;li&gt;能迅速提高 task 的容错空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前算法的内存复杂度没有变化；&lt;/li&gt;
&lt;li&gt;数据继续增长仍会再次 OOM；&lt;/li&gt;
&lt;li&gt;如果每个 executor 同时运行多个 task，24 GiB 会被多个 task 共同消耗；&lt;/li&gt;
&lt;li&gt;更大 Heap 也可能带来更长 Full GC。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="52-第二阶段长期代码根治"&gt;5.2 第二阶段：长期代码根治
&lt;/h3&gt;&lt;p&gt;不再把整个 partition 放入一个 &lt;code&gt;ArrayList&lt;/code&gt; 后再次按 key 分组。&lt;code&gt;groupByKey&lt;/code&gt; 后的每个 &lt;code&gt;Entries.next()&lt;/code&gt; 本身已经对应一个 ObjectStore key，可以：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一次只读取一个 key 的 records，立即构造对应 Bond Blob，将 key/value 放入小批次&lt;/li&gt;
&lt;li&gt;达到 &lt;code&gt;batch_size&lt;/code&gt; 后写 ObjectStore&lt;/li&gt;
&lt;li&gt;清空批次，继续下一个 key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;内存复杂度将从：&lt;/p&gt;
&lt;p&gt;$$
O(整个 partition 的记录数)
$$&lt;/p&gt;
&lt;p&gt;降低为：&lt;/p&gt;
&lt;p&gt;$$
O(单个最大 key 的记录数+batch size)
$$&lt;/p&gt;
&lt;p&gt;所以流式处理后，内存会从数百万至数千万个 Java Record，下降到最多约两千条记录加一个写入批次。这样即使数据总量继续增长，也主要增加 task 运行时间，而不会按总行数线性推高 executor 峰值内存。&lt;/p&gt;
&lt;h3 id="53-第三阶段按增长类型治理上游"&gt;5.3 第三阶段：按增长类型治理上游
&lt;/h3&gt;&lt;p&gt;这里的“按照增长类型治理上游”不是发现数据增长后直接过滤，而是先判断增长来自哪里，再区分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;正常业务增长&lt;/strong&gt;：保留数据，做容量规划和监控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则变化&lt;/strong&gt;：确认是否符合预期，不符合则修正规则并回算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据质量问题&lt;/strong&gt;：修复重复、Join fan-out、ID 不稳定等根因。&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>