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(堆内存):

原因分析
- JDK版本变更带来的内存回收逻辑差异:原openjdk 17.0.2与新版本JDK(17.0.7及以上)在直接ByteBuffer的Cleaner机制、Reference队列处理逻辑上可能存在调整,导致直接内存无法及时被回收。
- gRPC与新版JDK的兼容性问题:gRPC默认依赖Netty作为传输层,Netty的内存池实现与新版JDK的NIO组件(如ByteBuffer、Selector)行为不匹配,大消息处理后未正确释放直接缓冲区。
- 镜像底层系统差异:amazoncorretto镜像基于AlmaLinux 2023,原openjdk镜像可能基于不同Linux发行版,系统层面的内存管理特性(如透明大页、内存分配器)差异,影响了直接内存的回收效率。
- 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
相关产品推荐
相关产品推荐

