4、Java虚拟机(JVM)监控:堆内存、GC、线程池、类加载的监控与调优

搜索引擎跑得稳不稳,JVM 是那个“压舱石”。

我见过太多案例,业务代码看着没问题,一到高峰期就卡死。最后查下来,全是 JVM 配置不当惹的祸。说白了,搜索引擎本质上就是个内存密集型应用,倒排索引要加载,缓存要驻留,查询请求要处理——哪一步离得开 JVM?

4.1 堆内存监控:别等 OOM 了才想起来看

堆内存是 JVM 管理的大头。搜索引擎的索引数据、查询结果缓存、各种中间对象,全在这里面待着。

监控什么?

  • 堆使用率:老年代、新生代分别用了多少。我个人习惯看老年代,如果它持续上涨且 GC 后降不下来,那就有内存泄漏的嫌疑。
  • 堆峰值:业务高峰期堆能冲到多高。我建议留出 30% 的余量,别卡着上限跑。
  • 元空间(Metaspace):类加载多了,元空间也会涨。搜索引擎如果动态加载了很多分词器或插件,这里容易出问题。

实战命令:

# 查看堆内存使用情况(推荐 jstat)
jstat -gc <pid> 1000 10

# 或者用 jmap 做堆转储(谨慎使用,会触发 Full GC)
jmap -heap <pid>

我在项目中遇到过,某次上线后堆内存使用率从 60% 飙到 95%。查了半天,发现是索引加载时一个缓存 Map 没设上限,数据越积越多。嗯,这种坑踩过一次就记住了。

4.2 GC 监控:STW 是搜索引擎的“天敌”

GC 暂停(Stop-The-World)对搜索引擎来说是致命的。你想,用户正查着东西呢,结果服务“卡”了几百毫秒——体验直接崩了。

关键指标:

指标 说明 告警阈值(我常用的)
Young GC 频率 每秒或每分钟发生多少次 > 10 次/分钟
Full GC 频率 老年代回收次数 > 1 次/小时
GC 暂停时间 单次 STW 时长 > 200ms
吞吐量 应用运行时间 / 总时间 < 95%

调优思路:

  • 选对 GC 算法:搜索引擎对延迟敏感,我推荐用 G1 或 ZGC。G1 能控制暂停时间在 200ms 以内,ZGC 甚至可以做到 10ms 以下。
  • 调整新生代大小:新生代太小,对象频繁晋升到老年代,Full GC 就多了。我一般把新生代设为堆的 30%-40%。
  • 避免“大对象”直接进入老年代:比如一次加载了 10MB 的索引段,直接进老年代,容易触发 Full GC。可以调大 -XX:PretenureSizeThreshold

我的小技巧:

jstat -gcutil <pid> 1000 实时看 GC 占比。如果 YGCTFGCT 持续增长,说明 GC 时间占比太高了,得赶紧调。

4.3 线程池监控:别让线程“饿死”

搜索引擎处理查询,靠的就是线程池。线程池要是炸了,请求全排队,响应时间直线上升。

监控点:

  • 活跃线程数:当前正在处理任务的线程。如果长期接近最大线程数,说明负载太高了。
  • 队列积压:等待队列里的任务数。我见过队列积压了几万条,结果请求超时率飙升到 50%。
  • 拒绝策略:线程池满了之后怎么处理?默认的 AbortPolicy 会抛异常,搜索引擎里我建议用 CallerRunsPolicy,让调用线程自己跑,至少不丢请求。

代码示例(监控线程池状态):

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));

// 定期打印线程池状态
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    System.out.println(String.format(
        "活跃线程: %d, 队列大小: %d, 已完成任务: %d",
        executor.getActiveCount(),
        executor.getQueue().size(),
        executor.getCompletedTaskCount()
    ));
}, 0, 5, TimeUnit.SECONDS);

避坑指南:

我曾经把线程池的核心线程数设得太大,结果 CPU 上下文切换开销直接占了 30%。后来我学乖了,核心线程数一般按 CPU 核数 * 2 来设,再根据实际压测结果微调。

4.4 类加载监控:小心“类爆炸”

类加载这块,平时不太引人注意,但一出事就是大事。

监控什么?

  • 已加载类总数:如果持续增长,说明有类加载器泄漏。比如每次重新加载索引都创建新的类加载器,旧的没释放。
  • 卸载类数量:正常情况下,类应该能被卸载。如果卸载数为 0,那元空间迟早撑爆。

常用命令:

# 查看类加载情况
jstat -class <pid>

# 输出示例:
# Loaded  Bytes  Unloaded  Bytes     Time
#   12345  23456      0     0.0       1.23

如果 Unloaded 一直是 0,而 Loaded 还在涨,那就要小心了。我遇到过插件热加载框架没做好清理,每次更新插件都多出几百个类,最后元空间满了,应用直接挂掉。

4.5 综合调优建议

调优不是一锤子买卖。我习惯按这个步骤来:

  1. 先监控,后调优:别凭感觉改参数。先跑一周监控数据,看看 GC 频率、堆使用率、线程池积压情况。
  2. 一次只改一个参数:改多了你都不知道是哪个起了作用。我吃过这个亏,一次改了堆大小和 GC 算法,结果性能下降了,查了半天才发现是堆设太大了导致 GC 变慢。
  3. 压测验证:调完后用搜索引擎的典型查询流量压一下,看看响应时间有没有改善。
  4. 保留基线:每次调优前记录下关键指标,方便对比。

总结一句话:

JVM 监控不是玄学,是科学。把堆内存、GC、线程池、类加载这四个维度盯住了,搜索引擎的稳定性至少能上一个台阶。

好了,这一章就聊到这儿。下一章我们聊聊操作系统层面的监控——CPU、内存、磁盘 I/O,这些底层指标怎么跟搜索引擎的表现关联起来。