You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java gRPC服务更换JDK镜像后大请求下直接内存泄漏排查

直接缓冲区内存泄漏问题排查分析

环境与代码概述

我们有一个基于Java开发的gRPC服务,采用SpringBoot 2.5.7版本。为定位问题,已将项目简化为仅包含一个处理文件上传的Unary gRPC端点,入站消息最大尺寸设为400MB,文件以ByteString形式作为gRPC请求的一部分发送:

// Upload an object with a unique ObjectIdentifier and corresponding object.
rpc UploadObject (UploadObjectRequest) returns (google.protobuf.Empty);

message UploadObjectRequest {
    ObjectIdentifier object_identifier = 1;
    bytes object = 2; // A byte array representing single object data.
}

服务端收到请求后,立即通过responseObserver返回成功响应,ByteString会被直接丢弃,不会写入任何存储位置:

@Slf4j
@GRpcService
public class MyEndpoint extends MyServiceImplBase {
    @Override
    public void uploadObject(UploadObjectRequest request, StreamObserver<Empty> responseObserver) {
        responseObserver.onNext(Empty.getDefaultInstance());
        responseObserver.onCompleted();
    }
}

该服务部署在Kubernetes单个Pod中,原使用已废弃的openjdk:17.0.2-jdk镜像,迁移至amazoncorretto:17.0.7-al2023镜像后,出现直接缓冲区(非堆)内存泄漏问题。测试多个替代镜像(如amazoncorretto:21.0.0-al2023、eclipse-temurin:latest)均存在相同问题,仅原openjdk镜像无此现象。

为复现问题,编写了重复调用端点的测试方法,每次发送200MB文件:

@SneakyThrows
@Test
public void test() {
    while (true) {
        Channel channel = grpcClientChannelFactory.createGrpcChannel(host, port);
        MyServiceBlockingStub blockingStub = newBlockingStub(channel);

        final int numOfBytes = 200_000_000;
        byte[] bytes = new byte[numOfBytes];
        new Random().nextBytes(bytes);
        UploadObjectRequest uploadObjectRequest = UploadObjectRequest.newBuilder()
                .setObjectIdentifier(
                        ObjectIdentifier.newBuilder()
                                .setNamespace("namespace")
                                .setKey("testFile")
                )
                .setObject(ByteString.copyFrom(bytes))
                .build();

        blockingStub
                .withCallCredentials(new JwtCallCredential(JWT))
                .uploadObject(uploadObjectRequest);
        Thread.sleep(5000);
    }
}

内存指标对比

使用openjdk:17.0.2-jdk

  • container_memory_usage_bytes:容器内存使用
  • jvm_buffer_memory_used_bytes(直接内存):直接内存使用
  • jvm_buffer_count_buffers(直接内存缓冲区数量):直接内存缓冲区数量
  • jvm_memory_used_bytes(堆内存):堆内存使用

使用amazoncorretto:17.0.7-al2023

  • container_memory_usage_bytes:容器内存使用
  • jvm_buffer_memory_used_bytes(直接内存):直接内存使用
  • jvm_buffer_count_buffers(直接内存缓冲区数量):直接内存缓冲区数量
  • jvm_memory_used_bytes(堆内存):堆内存使用

原因分析

  1. JDK版本变更带来的内存回收逻辑差异:原openjdk 17.0.2与新版本JDK(17.0.7及以上)在直接ByteBuffer的Cleaner机制、Reference队列处理逻辑上可能存在调整,导致直接内存无法及时被回收。
  2. gRPC与新版JDK的兼容性问题:gRPC默认依赖Netty作为传输层,Netty的内存池实现与新版JDK的NIO组件(如ByteBuffer、Selector)行为不匹配,大消息处理后未正确释放直接缓冲区。
  3. 镜像底层系统差异:amazoncorretto镜像基于AlmaLinux 2023,原openjdk镜像可能基于不同Linux发行版,系统层面的内存管理特性(如透明大页、内存分配器)差异,影响了直接内存的回收效率。
  4. JVM默认参数变更:新版JDK可能调整了-XX:MaxDirectMemorySize等直接内存相关参数的默认值,或修改了垃圾回收触发条件,导致直接内存无法被及时清理。

进一步排查建议

  • 分析堆外内存快照:使用jcmd <pid> VM.native_memory summary获取原生内存统计,或结合VisualVM、Eclipse Memory Analyzer工具分析直接内存的持有对象,定位泄漏根源。
  • 逐步验证JDK版本:从17.0.2开始逐步升级JDK版本,找到首次出现泄漏的版本,对照JDK Release Notes排查NIO或内存管理相关的变更。
  • 调整gRPC传输配置:尝试切换gRPC传输层为OkHttp,或修改Netty的内存池参数(如禁用直接内存池、调整缓冲区大小),验证是否为Netty兼容性问题。
  • 监控JVM垃圾回收细节:添加-XX:+PrintGCDetails -XX:+PrintReferenceGC参数,观察垃圾回收时Reference的处理情况,确认Cleaner是否正常工作;明确设置-XX:MaxDirectMemorySize参数,验证是否能限制内存增长。
  • 排查系统层面影响:在新镜像中禁用透明大页,切换内存分配器为jemalloc,观察泄漏是否缓解,排除系统层面的干扰。

内容的提问来源于stack exchange,提问作者Jonathan Shifman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:30:09