2020 年,一个使用 Solr 做全文检索的项目出现了奇怪的现象:访问量很低,其他页面看起来正常,但每次输入内容执行全站检索,都会观察到两次 GC。

最后找到的主要问题,不是先调大堆,而是搜索列表带回了不需要的完整明细。

先把触发动作和分配变化放在一起看

当时新生代配置约 300 MB,几乎没有并发。观察搜索前后的 GC 统计,发现两三秒内新生代就被填满。

以支持该工具的 Java 8 HotSpot 为例,可以在获授权的目标 JVM 上观察:

jstat -gc 12345 100 30

12345 是示意 PID,100 是毫秒间隔,30 是采样次数,不应照抄到任意进程。jstat 参数说明

统计能帮助对照各区域使用量和 GC 计数,却不能只凭这些数字识别具体业务对象,也不能把一次年轻代回收直接叫作内存泄漏。

堆分析发现了大量数组,但还没找到业务原因

随后导出堆转储,用 MAT 分析,看到 byte[] 占用了一百多 MB。它提示短时间存在很多字节数据,但仅按类名汇总还不足以知道是哪段代码创建的。

如果继续分析,应看持有关系、引用链及相关业务对象。堆转储是某一时刻的快照,不完整记录此前所有分配;还要结合 GC 日志或分配观测理解短命对象。

Java 8 jmap 支持堆转储,但这类操作可能暂停应用、消耗磁盘,并把令牌、用户数据和业务内容带入文件。应先确认目标 JVM、维护窗口、容量与访问权限,不在故障中盲目导出。jmap 说明

转折出现在接口响应里

反复看代码没有发现明显异常后,我打开浏览器开发者工具,查看搜索接口返回值,发现每条结果都携带很长的文章或词条明细。

当时界面里看到约 4 MB 的内容量,但没有留下足以区分单字段、单条记录或整个响应的测量口径;它不能直接当作精确的堆内存大小。真正有用的观察是:列表不需要这些完整正文,却把它们一起加载和返回了。

读取结果、组装 DTO、序列化以及响应缓冲都可能增加分配。浏览器看到的字节大小与 JVM 对象实际占用也不是同一个数。

删除不需要的字段,优先在源头减少数据

当时去掉列表中的明细字段后,再做相同操作,原先那种紧随检索出现的频繁 GC 现象不再复现。这是那次处理的观察,不表示 Java 应用从此不应有 GC。

实现时应检查两层:

  • 查询结果是否只读取列表需要的字段。
  • 对外 DTO 是否只暴露列表真正使用的内容。

如果 Solr API 和字段配置支持,可用 fl 明确返回字段。例如,Solr 8.11 风格的字段列表参数可以写成:

fl=id,title,score

字段名必须符合实际 schema 和页面需求;这里不是把原系统版本补定为 8.11。Solr 返回字段列表

最后对照相同查询和返回条数,比较分配、响应大小、延迟与回收情况,同时确认列表和详情功能仍正确。不是所有检索内存问题都来自响应字段,但“这次请求究竟带回了什么”,值得在调 JVM 参数前先看一眼。