别急着加内存:我用 5 个改动让 Java 服务少吃 40% 内存
别急着加内存:我用 5 个改动让 Java 服务少吃 40% 内存

很多团队一遇到 ,第一反应是:“加内存、加节点、扩 Pod”。但我越来越相信一句话:扩容能掩盖问题,优化才能暴露真相。
这次我们在流量不变、功能不变、硬件不变的前提下,把 Boot 服务的平均内存从 ~850MB/Pod 压到了 ~510MB/Pod,峰值从 ~980MB 掉到 ~620MB,并且高峰期 归零。
下面按我实际执行的顺序,把关键动作讲清楚(每一条都能落地,不是玄学)。
0. 先交代背景:我们到底在优化什么
我们的运行环境大致是这样:
最重要的一句话是:“Heap 并没有打满别急着加内存:我用 5 个改动让 Java 服务少吃 40% 内存,但容器内存很高。”
这通常意味着:不是扛不住,是默认值+使用习惯在浪费。
1) 第一步:先量再改(否则就是“玄学调参”)
我做的第一件事不是改参数,而是把“可观测性”补齐:
做到这一步,你就能把问题拆开:
是堆?是线程栈?是 off-heap?还是缓存/大对象分配造成的 GC 压力?
2) 发现一:线程栈浪费(立省 ~12%)
我们之前的 默认配置类似:
server:
tomcat:
max-threads: 200
但真实平均并发只有 ~60。
更要命的是:平台线程默认栈大小常见在 ~1MB 级别(不同平台可能不同),线程一多,光栈就很吓人。
于是我做了两件事:
1)把线程栈从默认值收紧:
-Xss256k
2)把 线程上限调到更贴近真实并发:
server:
tomcat:
max-threads: 100
结果是什么?

从“200 线程 × ~1MB ≈ 200MB”变成“100 × 256KB ≈ 25MB”,直接省掉 ~175MB。
注意:-Xss 不是越小越好。递归深、栈帧大的代码(或部分三方库)可能触发 ,所以我建议:先 512k,再 256k,逐步压,配合压测与线上异常监控。
3) 发现二:无界缓存(立省 ~15%)
我们定位到多个地方在用这种“看起来很无害”的缓存写法:
private final Map cache = new ConcurrentHashMap();
它不是泄露(没有 引用),但它无上限,真实流量下会“慢慢长大”,最后把内存吃掉。
解决方式很简单:给缓存加边界。我把所有裸 Map 缓存替换成 (关键是 + 过期策略):
@Bean
public Cache userCache() {
return Caffeine.newBuilder()
.maximumSize(20_000)
.expireAfterWrite(20, TimeUnit.MINUTES)
.build();
}
这样做的效果是:堆开始稳定,不再缓慢爬升java 内存泄露 排查,平均又省了 ~120MB。
4) 发现三:一次性加载大表(立省 ~8%)
最典型的“内存杀手”长这样:
@GetMapping("/orders")
public List getAllOrders() {
return orderRepository.findAll();
}
有的租户数据能到 200k+ 行,一次请求把整表拉进 JVM:
我的处理策略是“两条线”:
4.1 页面展示统一分页
@GetMapping("/orders")
public Page getOrders(Pageable pageable) {
return orderRepository.findAll(pageable);
}
4.2 导出走流式(不要 到内存)
@Transactional(readOnly = true)
public void streamOrders(OutputStream out) {
orderRepository.streamAll().forEach(order -> writeToStream(order, out));
}
结果:分配速率下降、峰值堆下降、GC 更轻松。
5) 发现四:DTO/实体“过度返回”(立省 ~5%)
很多接口为了省事直接返回实体:
class User {
Long id;
String name;
String address;
String phone;
String metadataJson;
String preferencesJson;
...
}
但前端其实只要 id + name。这会带来双重浪费:
我改成 DTO( 很顺手):
public record UserSummary(Long id, String name) {}
@Query("select new com.app.UserSummary(u.id, u.name) from User u")
List findAllSummaries();

收益不止内存别急着加内存:我用 5 个改动让 Java 服务少吃 40% 内存,还有响应体大小与吞吐。
6) 发现五: (off-heap)没上限(立省 ~5%)
这条特别容易踩坑:
你 heap dump 看着“没问题”,但容器还是在涨java 内存泄露 排查,最后 ——因为涨的是 /off-heap。
我们用的是 (底层 Netty),于是我把 明确封顶:
-XX:MaxDirectMemorySize=256m
封顶后容器内存曲线明显稳定。
关键认知:** 不在 Java heap 里,但它算在容器内存账上。**所以“堆没满但 ”很常见。
7) / After:结果别靠感觉,拿数据说话
优化前:
优化后:
并且因为内存 可以从 1Gi 下调到 ~600Mijava 内存泄露 排查,Pod 密度更高、节点更省,成本是实打实下降的。
8) 我最想强调的“高级工程师思维”
初级:“我们需要更多内存。”
高级:“我们为什么用掉这么多内存?”
Boot 并不是天生内存大户。
真正吃内存的,往往是:你没审视过的默认值。
9) 可复制的落地清单(你按这张表走就行)缓存数据访问-heap观测
如果你是 Java / 后端 / 全栈开发者,关注我一起走。
本号持续输出:
1. 本站所有资源来源于用户上传和网络,如有侵权请联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系站长处理!
6. 本站不售卖代码,资源标价只是站长收集整理的辛苦费!如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
7. 站长QQ号码 2205675299
资源库 - 资源分享下载网 » 别急着加内存:我用 5 个改动让 Java 服务少吃 40% 内存
常见问题FAQ
- 关于资源售价和售后服务的说明?
- 代码有没有售后服务和技术支持?
- 有没有搭建服务?
- 链接地址失效了怎么办?
- 关于解压密码

