美文网首页原理收藏-技术篇
Java程序性能分析之道

Java程序性能分析之道

作者: 郭彦超 | 来源:发表于2020-02-14 11:58 被阅读0次

    一、背景描述#

    基于Netty做了一个消息推送服务,将消息总线中的消息推送给与消息推送服务器进行socket连接的client。

    推送服务描述

    二、问题描述#

    进行压力测试,当消息发送速率达到5w条/秒时:

    1、 JVM进程出现频繁FullGC,且每次FullGC并不能回收年老代堆内存空间;

    2、 top命令查看服务器资源使用情况,服务器内存被打满;

    3、负责FullGC的线程不断循环执行导致CPU100%;

    三、问题排查#

    1、查询占用CPU较多线程的堆栈信息####

    排查步骤

    1)jps或ps命令获取jvm进程的pid;

    2)top -H p pid查询jvm进程中各个线程占用的系统资源;

    3)jstack pid获取jvm进程的堆栈信息->stack.txt;

    4)将占用CPU资源较多线程的tid转换为十六进制,从堆栈信息文件stack.txt中匹配对应堆栈信息;

    截取几个堆栈信息如下

    at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)

    at io.netty.channel.nio.NioEventLoop.select(NioEventLoop.java:622)

    at sun.nio.ch.FileDispatcherImpl.write0(Native Method)

    at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)

    - locked <0x0000000080d00f38> (a java.lang.Object)

    at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:270)

    at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)

    at io.netty.channel.ChannelOutboundBuffer$Entry.newInstance(ChannelOutboundBuffer.java:693)

    at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:658)

    at io.netty.handler.stream.ChunkedWriteHandler.doFlush(ChunkedWriteHandler.java:304)

    at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)

    at sun.misc.Unsafe.freeMemory(Native Method)

    at java.nio.DirectByteBuffer$Deallocator.run(DirectByteBuffer.java:94)

    at io.netty.util.internal.Cleaner0.freeDirectBuffer(Cleaner0.java:66)

    at io.netty.channel.AbstractChannelHandlerContext.invokeFlush(AbstractChannelHandlerContext.java:688)

    分析堆栈信息,基本是Netty的IO线程进行select、write、flush和freeDirectBuffer的操作。

    2、jvm堆内存使用分析####

    jstat -gcutil pid 1s 命令,查询JVM进程GC情况,发现Full GC短期大量执行,且无法回收年老代内存空间。

    jmap -histo pid > histo.txt命令,打印JVM进程中每个class的实例数目,内存占用,类全名信息,如下:

    正常情况刚Yong GC完时的内存占用

      1:        457482      35,387,296  [C

      2:        16678      13,773,280  [I

      3:        63772        9,237,488  [B

      4:        122,459        7,837,376  java.nio.DirectByteBuffer

      5:        53,091        7,193,480  [Ljava.lang.Object;

      6:        90076        6485472  io.netty.channel.DefaultChannelHandlerContext

      7:        59706        4298832  io.netty.buffer.UnpooledUnsafeDirectByteBuf

      8:        122777        3928864  java.util.HashMap$Node

      9:        120320        2887680  java.lang.String

      10:        43714        2447984  io.netty.buffer.UnpooledHeapByteBuf

      Total      2360002      138,412,016

    正常情况发生Yong GC前的内存占用

      1:      5800859      438,101,408  [C

      2:        717064      105,584,984  [B

      3:      1375972      88,062,208  java.nio.DirectByteBuffer

      4:        675654      48,647,088  io.netty.buffer.UnpooledUnsafeDirectByteBuf

      5:      1526018      36,624,432  java.lang.String

      6:        621771      34,819,176  io.netty.buffer.UnpooledHeapByteBuf

      7:        620271      34,735,176  java.nio.StringCharBuffer

      8:        621910      29,851,680  io.netty.channel.DefaultChannelPromise

      9:        621523      29833104  java.nio.HeapByteBuffer

      10:      1235512      29652288  java.lang.StringBuilder

    Total      19200129    1,093,142,504

    不断进行Full GC时的内存占用

      1:      14,315,627      687,150,096  io.netty.channel.DefaultChannelPromise

      2:      14,314,546      572,581,840  io.netty.channel.AbstractChannelHandlerContext$WriteAndFlushTask

      3:      14,314,631      458,068,192  io.netty.util.Recycler$DefaultHandle

      4:      14,061,517      337,476,408  com.youzan.platform.ordermsgpush.server.handler.MacdonaldMessageHandler$$Lambda$34/1592197748

      5:        339988      11723152  [C

      6:        443875      10653000  com.youzan.platform.ordermsgpush.server.common.CommandOut

      7:        339909        8157816  java.lang.String

      8:        252671        6064104  com.youzan.platform.ordermsgpush.server.common.CommandIn

      9:        252451        6058824  com.youzan.platform.ordermsgpush.server.dispatcher.network.NetWorkEventDispatcher$1

      10:        252451        6058824  com.youzan.platform.ordermsgpush.server.dispatcher.network.

      Total      59789703    2,146,390,000

    分析结论:

    Netty使用DirectByteBuffer缓存待发送的数据,正常情况下,会存在DirectByteBuffer对象,实际内存空间在堆外申请;

    不断发生Full GC异常时,JVM堆内存被大量的DefaultChannelPromise、WriteAndFlushTask、Recycler对象填满,可见是因为writeAndFlush任务的产生速度大大超过socket连接的TCP发送缓冲区消化速度,导致Netty不断申请堆外内存在应用层缓存待发送的数据,最终耗尽系统内存资源;

    3、产生问题的代码####

    发送数据的代码片段如下:

                channelHandlerContext.writeAndFlush(commandOut).addListener(future -> {

                    if (!future.isSuccess()) {

                        logger.warn("unexpected push. msg:{} fail:{}", msg, future.cause().getMessage());

                    }

                });

    由于ChannelHandlerContext.writeAndFlush()只是产生write任务和flush任务,交由Netty的IO线程处理,其ChannelFuture返回时并不保证flush任务已执行完毕并将数据写入到了连接对应的TCP发送缓冲区。

    如果不对writeAndFlush()的调用速率进行限制,当应用对socket进行写操作的速率超过socket实际发送速率时,对应连接的TCP发送缓冲区会被填满,后续写操作的待发送数据会由Netty不断申请内存缓存,最终导致内存溢出。

    解决方法:

                if(channelHandlerContext.channel().isWritable()){

                    channelHandlerContext.writeAndFlush(commandOut).addListener(future -> {

                        if (!future.isSuccess()) {

                            logger.warn("unexpected push. msg:{} fail:{}", msg, future.cause().getMessage());

                        }

                    });

                }else{

                    try {

                        channelHandlerContext.writeAndFlush(commandOut).sync();

                        logger.info("publish macdonaldMsg,sended. remoteAddress:[{}], msg:[{}]", channelHandlerContext.channel().remoteAddress(), msg);

                    } catch (InterruptedException e) {

                        logger.info("write and flush msg exception. msg:[{}]",msg,e);

                    }

                }

    调用writeAndFlush()方法前先判断当前Channel的可写入状态,若状态为可写入说明待发送数据量并未堆积超限,可以进行异步发送;否则需要执行sync()方法等待发送数据真正成功写入socket的TCP发送缓冲区,然后在执行下一次的write操作

    相关文章

      网友评论

        本文标题:Java程序性能分析之道

        本文链接:https://www.haomeiwen.com/subject/vtedfhtx.html