别急着加内存:我用 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

关于资源售价和售后服务的说明?
本站所有资源的标价均为本站收集资源的辛苦费,不代表资源本身的价值。软件是高智慧高价值的商品,不可能是白菜价。本站资源标价只是赞助费用,收取的赞助费仅用来维持本站的日常运营!毕竟收集整理资料需要投入云计算资源和站长大量的精力。
代码有没有售后服务和技术支持?
由于代码的运行具有不可预见性,本站不保证代码完整可运行,不提供技术支持和售后服务。 本站原创代码都是站长自己开发的,可以有偿提供技术支持服务。 网站里标明【亲测】的代码都是站长亲测过的,其他的代码由于精力有限,没有一一测试,不能保证代码就一定能够使用,更没有技术支持服务,下载前请自行斟酌。
有没有搭建服务?
由于搭建服务比较费时费力,所以本站除了原创代码外均不提供搭建服务。本站分享代码纯属兴趣爱好,不以盈利为目的,请勿咨询有没有搭建服务,谢谢理解。
链接地址失效了怎么办?
请带上资源链接地址联系客服,工作时间内我们看到后将第一时间回复。
关于解压密码
本站资源一般都没有加密,如果发现需要解压密码的,那么就输入 hao.35dc.com 试试。

发表评论

资源库,由老程序员细心甄别、精心筛选,只为提供优质的源码资源

关于我们 联系我们