延迟加载不是“要不要做”,而是“在哪做、怎么做得干净”:聚焦构造开销大、使用概率低、依赖运行时上下文的三类对象,优先用@lazy、lazy、orelseget、静态内部类等原生机制,避免手写同步逻辑,并注意首次阻塞、异常缓存、线程安全与i/o避坑。

Java 变量延迟加载不是“要不要做”的问题,而是“在哪做、怎么做得干净”的问题。核心目标很明确:把高成本初始化动作从启动瞬间挪到首次真正需要时,从而压低内存峰值、缩短启动耗时、减少无效 GC 和资源占用。
哪些变量值得加延迟?
别给每个字段都套上 Lazy 或 Supplier。聚焦三类对象:
- 构造开销大:读取百 MB 配置文件、解析嵌套 JSON、加载机器学习模型、建立数据库连接池
- 使用概率低:错误追踪上报器、离线日志归档服务、管理员后台诊断工具、冷门功能模块的 UI 控件树
- 依赖运行时上下文:需 Spring 上下文注入的 Bean、依赖请求头或用户会话的策略对象、尚未初始化完成的第三方 SDK 实例
按场景选对实现方式
避免手写 if (obj == null) obj = new X() —— 容易出线程安全问题,也难维护。优先用语言/框架原生机制:
-
Spring Bean 级延迟:在
@Bean方法或组件类上加@Lazy;全局启用可配spring.main.lazy-initialization=true -
普通成员变量:用
private final Lazy<t> field = new Lazy(() -> expensiveInit());</t>,访问时取.Value -
静态单例(真正懒):不用静态代码块,改用静态内部类 Holder 模式,
Holder.INSTANCE首次访问才触发初始化 -
Optional 默认值:用
orElseGet(() -> createExpensiveDefault())替代orElse(new ExpensiveDefault()),避免无谓创建
注意初始化边界与风险点
延迟不等于异步,也不代表绝对安全:
-
首次访问即阻塞:调用
.Value或get()时,工厂函数同步执行,若含 I/O 或远程调用,可能卡住当前线程 - 异常会被缓存:初始化失败抛出异常后,后续所有访问都会重抛同一异常,不会重试
-
多线程默认安全但有开销:
Lazy<t></t>和双重校验锁保障只初始化一次,但存在 Monitor 竞争;若确认单线程上下文(如 Spring 启动主线程),可关掉线程安全模式 -
I/O 类初始化慎用同步延迟:不要在
Supplier或Lazy工厂里直接await;应封装为Lazy<task>></task>,由上层异步消费
配合其他策略形成合力
单靠延迟初始化效果有限,需组合推进:
- 关闭非核心 Bean 自动装配,用
@ConditionalOnProperty或@Profile隔离测试/调试组件 - JVM 启动参数设为
-Xms2g -Xmx2g,避免堆动态扩容引发 GC 波动 - 对预热型资源(如本地词典、缓存快照),改用后台线程 + 信号量控制,在主流程就绪后再渐进加载
- MyBatis 关联查询开启
lazyLoadingEnabled=true且aggressiveLazyLoading=false,让一对多关系按需查库
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











