什么是预热?它有什么作用?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念理解:面试官想知道你是否真正理解 "预热" 这个概念在高并发场景下的本质,而不是只会背名词。预热的本质是什么?是把系统从冷态提前推到热态。

  2. 实战经验:是否经历过双 11、秒杀、大促这种高并发场景,知道在大流量来之前要做哪些准备。这块只有真干过大促的人才有体感。

  3. 系统性思维:能否从 JVM、缓存、连接池、服务发布等多个维度全面梳理预热的落地方式,而不是只说出一个点。

核心答案

预热(Warmup / Prewarm),说白了就是在正式流量到来之前,提前把系统从 "冷状态" 推到 "热状态"。

什么是 "热状态"?就是系统已经准备好随时承接大流量了:热点代码被 JIT 编译成了机器码,热点数据已经进缓存了,连接池里的连接也建好了,核心线程也拉起来了,依赖服务之间的长连接也握完手了。

一句话总结:让一切就绪,避免冷启动。

预热的典型场景有这几类:

预热类型 解决的问题 典型手段
JVM 预热 JIT 还没编译热点代码,解释执行性能差 跑核心接口、关闭分层编译用 CompileThreshold 调小
缓存预热 首次请求打到 DB,可能压垮数据库 启动时加载热点数据到 Redis
连接池预热 首次请求才建连接,耗时高 initialSize / minimum-idle
线程池预热 核心线程没创建,请求来了才现建 prestartAllCoreThreads()
服务发布预热 新实例刚上线性能差就接全量流量 小流量灰度 + 健康检查
秒杀/大促预热 库存、商品数据需提前加载 活动前批量加载到 Redis

高并发预热机制
高并发预热机制

深度解析

一、为什么需要预热——冷启动到底有多痛

先看一个典型的 "冷启动翻车" 场景:

冷启动雪崩流程
冷启动雪崩流程

上图展示的就是典型的冷启动雪崩链路。系统刚启动那会儿,所有东西都是 "冷" 的:JIT 还没编译热点代码,响应慢个几倍甚至十几倍;缓存里空空如也,请求全都穿透到 DB;连接池里没连接,排队建连接很耗时;线程池里没线程,临时建线程开销也大。

这时候如果有大流量瞬间涌入,几个问题会叠加:

  • JVM 层面:解释执行比编译执行慢 5~10 倍,单个请求的处理时间成倍增加
  • DB 层面:缓存为空,所有请求直接打穿到数据库,DB 瞬间被压垮
  • 线程层面:连接池打满、线程池排队,请求开始堆积
  • 雪崩效应:上游超时重试,进一步放大流量,最终整个系统挂掉

预热就是要把这些 "冷" 的东西提前变 "热",让流量进来时一切就绪。

冷启动请求堆积
冷启动请求堆积

二、JVM 预热——JIT 编译器的痛

JVM 是典型的 "解释执行 + JIT 编译" 混合模式。代码先被解释执行(慢),当某个方法被调用次数达到阈值时,JIT 才会把它编译成本地机器码(快)。

关于阈值有几个细节:

  • -XX:CompileThreshold:C2(server 编译器)默认 10000 次,C1(client 编译器)默认 1500 次。但这个默认值只有在关闭分层编译-XX:-TieredCompilation)时才生效。
  • 分层编译-XX:+TieredCompilation,JDK 8 默认开启):JVM 不再使用单一阈值,而是用一组分层阈值(如 Tier4InvocationThresholdTier4BackEdgeThreshold)来决定何时从解释执行 → C1 → C2 逐级升级。

所以 Java 应用刚启动时,前几千次调用都是解释执行,性能比稳定运行时差好几倍。这就是为什么很多公司上线后会先跑一段时间核心接口,或者用流量回放工具把核心链路压一遍,让 JIT 把热点代码编译好。

三、缓存预热——避免缓存雪崩

缓存预热是高并发场景最常考的预热场景。秒杀、大促这种活动,如果等用户请求来了才去 DB 查数据放缓存,第一个洪峰就会把 DB 压垮。

典型做法:

@PostConstruct
public void preloadCache() {
    log.info("开始预热秒杀商品缓存...");
    List<Item> hotItems = itemMapper.findHotItems();
    for (Item item : hotItems) {
        String key = "item:" + item.getId();
        redisTemplate.opsForValue().set(key, JSON.toJSONString(item),
                                         1, TimeUnit.HOURS);
        // 同时预热布隆过滤器,防止缓存穿透
        bloomFilter.put(item.getId());
    }
    log.info("缓存预热完成,共加载 {} 条", hotItems.size());
}

这段代码做了两件事:把热点商品数据预加载到 Redis,同时把商品 ID 放进布隆过滤器,防止恶意请求穿透缓存打 DB。

缓存预热防DB穿透
缓存预热防DB穿透

四、连接池 / 线程池预热

这一块经常被忽视,但其实挺关键。

数据库连接池(以 HikariCP 为例):

spring:
  datasource:
    hikari:
      minimum-idle: 10        # 最小空闲连接数,启动即创建
      maximum-pool-size: 50
      connection-timeout: 30000

minimum-idle 配置后,HikariCP 启动时就会创建对应数量的连接放入池中。如果没配,第一个请求来了才建连接,TCP 三次握手 + 认证 + SSL 握手,单次几十毫秒甚至上百毫秒的开销。

HikariCP 官方还有个建议:把 minimum-idle 设成和 maximum-pool-size 一样,让连接池固定大小,避免运行期频繁建连销毁连接。

线程池预热

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize, keepAliveTime,
    TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)
);
// 预创建所有核心线程,避免请求来了才建线程
pool.prestartAllCoreThreads();

prestartAllCoreThreads() 这个方法很多同学不知道,它会在创建完线程池后立刻把所有核心线程建好。方法返回值就是成功创建的线程数。秒杀场景特别有用。

连接池线程池预热
连接池线程池预热

五、服务发布预热——灰度引流

服务发布也是预热的高频场景。新版本上线时,JVM 是冷的、缓存是空的、连接是空的,如果直接接入全量流量,第一个洪峰就把新实例打挂了。

典型做法(K8s 场景):

服务发布预热流程
服务发布预热流程

这套机制的核心思路是:先让新实例接小流量跑热,再逐步放量

  • liveness:进程是否存活(能不能被重启)
  • readiness:是否准备好接流量(能不能进负载均衡)
  • 灰度放量:用网关权重、Dubbo 路由规则等控制流量比例

阿里内部有一套叫 "微服务预热" 的机制,新 Provider 上线后会先用极小流量跑一段时间,等 RT、CPU 稳定了再放全量,效果很明显。

服务发布灰度预热
服务发布灰度预热

面试高频追问

  1. 追问一:预热和懒加载矛盾吗?

    不矛盾。懒加载是 "用到才加载",目的是加快启动速度;预热是 "提前加载热点",目的是避免冷启动。两者可以结合:启动时懒加载保证快速可用,跑起来后用定时任务预热热点数据。

  2. 追问二:预热加载太多数据会不会内存爆炸?

    会的,所以预热要选热点、设过期、限数量。一般用 LRU 算法的本地缓存,或者 Redis 的 maxmemory-policy 配合淘汰策略。同时监控预热后的内存使用,别把 JVM 堆撑爆。

  3. 追问三:JVM 预热有没有更精细的手段?

    早期有 JEP 295 AOT 编译(Java 9 引入的实验性特性),但这个在 JDK 17 已经被 JEP 410 移除了。现代 Java 的 AOT 方案是 GraalVM Native Image,把 Java 代码在构建期直接编译成原生可执行文件,启动几乎是毫秒级。Spring 6 + Spring Boot 3 也原生支持了 GraalVM Native Image,是目前主流的 "去掉冷启动" 方案。

常见面试变体

  • "秒杀系统为什么要预热?怎么预热?"
  • "缓存雪崩和预热有什么关系?"
  • "Java 服务刚启动为什么慢?怎么优化?"
  • "K8s 的 liveness 和 readiness 探针有什么区别?跟预热啥关系?"

记忆口诀

预热 = 把冷的变热的:JIT 热点编译、缓存先填好、连接池先建好、线程先拉起、流量先小后大。

总结

预热是高并发系统的 "战前准备"。核心就一句话:别让流量撞上冷启动。从 JVM、缓存、连接池、线程池到服务发布,能预热的地方不少,每一处提前做一点,系统就稳一点。能在面试里把这几层都讲清楚,加分稳稳的。