开发环境:

  • JDK: 1.8
  • Netty: 4.1.21.Final

1 起因

由于我之前写过一个接收医疗设备数据的项目,于是为了压力测试,就又按照该协议实现了一个发送设备数据的项目。这个项目在使用的时候出现了一个严重的 BUG,在同时模仿多个设备发送数据,运行一段时间之后,就会出现服务器 CPU 超过 200% 的问题。

2 定位问题

先用 top 命令看一下,发现 CPU 持续 200%,导致整个机子的服务都出了问题,ssh 登陆进去也感觉卡卡的。

top 看到的 200%

然后用 jstat -gcutil <pid> 1000 查看一下 JVM 的状况,发现 1s 内出现 10 次 FULLGC。

jstat 看到的 FULLGC 的情况

在发现大量 FULLGC 之后使用 jmap -histo:live <pid> 查看一下堆内存的情况。

jmap 看到的堆内存情况

发现其中有大量的 [B 也就是 byte[],这就很奇怪,程序里面哪里保存了这么多的二进制字节。

3 解决问题

发现这个大量二进制 gc 不掉的问题以后,就开始想,哪里存在这种可能性。然后就找到了原因,这个问题是由于 ByteBuf 使用不当造成的,由于 Netty 的 ByteBuf 处理二进制字节非常顺手,于是我就写了如下代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CustomEncoder extends MessageToByteEncoder<CMPProtocol> {

    private ByteBuf byteBuf = new ByteBuf();

    private int searialNo = 0;

    @Override
    protected void encode(ChannelHandlerContext ctx, CMPProtocol msg, ByteBuf out) throws Exception {
        FrameHeader frameHeader = msg.getFrameHeader();
        FrameBody frameBody = msg.getFrameBody();
        // 写入序列号
        frameHeader.setSerialNO((short) searialNo);
        // 写入包长
        byteBuf.writeShortLE(FrameHeader.HEAD_LENGTH + frameBody.getContent().length);
        // 写入命令字
        byteBuf.writeBytes(frameHeader.getCommandId());
        // 写入轮询序列号
        byteBuf.writeShortLE(frameHeader.getSerialNO());
        // 写入默认校验码
        byteBuf.writeBytes(MDA.DEFAULT_CHEDK_SUM);
        // 写入包内容
        byteBuf.writeBytes(frameBody.getContent());
        // 获取字节数组
        byte[] frameData = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(frameData);
        // 计算校验码
        byte[] checkSum = CmpCheckSumUtil.calculateCheckSum(frameData);
        // 将校验码填到相应的位置
        System.arraycopy(checkSum, 0, frameData, 6, checkSum.length);
        // 将数据发送到服务端
        out.writeBytes(frameData);
    }
}

这个代码在 byteBuf.readBytes(frameData); 这里就出了问题,由于前面一直在向 byteBuf 中写入数据,但是后面并没有调用 byteBuf.clear(); 去重制读写指针,导致这个 byteBuf 越写越大,也这里引发内存泄漏。

找到原因之后解决问题就很简单了,直接在下面加上 byteBuf.clear(); 就可以解决问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CustomEncoder extends MessageToByteEncoder<CMPProtocol> {

    private ByteBuf byteBuf = new ByteBuf();

    private int searialNo = 0;

    @Override
    protected void encode(ChannelHandlerContext ctx, CMPProtocol msg, ByteBuf out) throws Exception {
        FrameHeader frameHeader = msg.getFrameHeader();
        FrameBody frameBody = msg.getFrameBody();
        // 写入序列号
        frameHeader.setSerialNO((short) searialNo);
        // 写入包长
        byteBuf.writeShortLE(FrameHeader.HEAD_LENGTH + frameBody.getContent().length);
        // 写入命令字
        byteBuf.writeBytes(frameHeader.getCommandId());
        // 写入轮询序列号
        byteBuf.writeShortLE(frameHeader.getSerialNO());
        // 写入默认校验码
        byteBuf.writeBytes(MDA.DEFAULT_CHEDK_SUM);
        // 写入包内容
        byteBuf.writeBytes(frameBody.getContent());
        // 获取字节数组
        byte[] frameData = new byte[byteBuf.readableBytes()];
        byteBuf.readBytes(frameData);
        byteBuf.clear();
        // 计算校验码
        byte[] checkSum = CmpCheckSumUtil.calculateCheckSum(frameData);
        // 将校验码填到相应的位置
        System.arraycopy(checkSum, 0, frameData, 6, checkSum.length);
        // 将数据发送到服务端
        out.writeBytes(frameData);
    }
}

之后程序再次运行,连续跑几天也没有出现这个问题,内存泄漏完美解决。