jstat和jmap使用

简介: jstat和jmap使用

背景

最近在做Spring Websocket后台程序的压力测试,但是当并发数目在10个左右时,服务器的CPU使用率一直在160%+,出现这个问题后,一开始很纳闷,虽然服务器配置很低,但也不至于只有10个并发吧。。服务器的主要配置如下:

  • CPU:2核 Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
  • 内存:4GB

使用top命令查看资源占用情况,发现pid为9499的进程占用了大量的CPU资源,CPU占用率高达170%,内存占用率也达到了40%以上。 

问题排查

首先使用jstat命令来查看一下JVM的内存情况,如下所示:

jstat -gcutil 9499 1000

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8129 1147.010 1147.661
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8136 1148.118 1148.768
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8143 1149.139 1149.789
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8150 1150.148 1150.799
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8157 1151.160 1151.811
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8164 1152.180 1152.831
  0.00   0.00 100.00  94.92  97.44  95.30     24    0.651  8170 1153.051 1153.701
  0.00   0.00 100.00  94.92  97.45  95.30     24    0.651  8177 1154.061 1154.712
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8184 1155.077 1155.728
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8191 1156.089 1156.739
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8198 1157.134 1157.785
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8205 1158.149 1158.800
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8212 1159.156 1159.807
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8219 1160.179 1160.830
  0.00   0.00 100.00  94.93  97.45  95.30     24    0.651  8225 1161.047 1161.697

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看到,Eden区域内存占用高达100%,Old区占用高达94.9%,元数据空间区域占用高达97.4%。Young GC的次数一直是24,但是Full GC的次数却高达几千次,而且在程序运行期间,频繁发生Full GC,导致FGC的次数一直增加。 
虽然FGC次数一直在增加,但是却没有回收到任何空间,导致一直在运行FGC,根据这些信息,基本可以确定是程序代码上出现了问题,可能存在内存泄漏问题,或是创建了不合理的大型对象。


基于上述分析,我们知道应该是程序的问题,要定位问题,我们需要先获取后台程序的堆转储快照,我们使用jmap工具来生成Java堆转储快照:

jmap -dump:live,format=b,file=problem.bin 9499

Dumping heap to /root/problem.bin ...
Heap dump file created
  • 1
  • 2
  • 3
  • 4

下面就是对Java堆转储快照进行分析了,我使用了Eclipse Memory Analyzer(MAT)来对快照进行分析,在MAT打开快照文件之前,要将其后缀名修改为hprof,打开文件之后,可以发现如下问题:

9 instances of "org.apache.tomcat.websocket.server.WsFrameServer", loaded by "java.net.URLClassLoader @ 0xc533dc70" occupy 566,312,616 (75.57%) bytes. 

Biggest instances:
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xce4ef270 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xce4f1588 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf934b10 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf936e28 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xcf9620f8 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd21c6158 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd5dc8b30 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xd727bcf8 - 62,923,624 (8.40%) bytes. 
•org.apache.tomcat.websocket.server.WsFrameServer @ 0xe768bd68 - 62,923,624 (8.40%) bytes. 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到WsFrameServer的实例占用了75.57%的内存空间,而这也就是问题所在了,那WsFrameServer为什么会占用这么高的内存呢?我继续用MAT来查看WsFrameServer实例的内存分布情况: 

 
 
可以看到,WsFrameServer实例中,有两个类型的变量占了WsFrameServer的绝大部分,它们分别是java.nio.HeapCharBuffer类的实例变量messageBufferText、java.nio.HeapByteBuffer类的实例变量messageBufferBinary。

WsFrameServer继承自WsFrameBase ,messageBufferText和messageBufferBinary属性就在WsFrameBase里,然后我们来debug程序,看看这两个属性是如何被赋值的。

public WsFrameBase(WsSession wsSession, Transformation transformation) {
    inputBuffer = ByteBuffer.allocate(Constants.DEFAULT_BUFFER_SIZE);
    inputBuffer.position(0).limit(0);
    messageBufferBinary = ByteBuffer.allocate(wsSession.getMaxBinaryMessageBufferSize());
    messageBufferText = CharBuffer.allocate(wsSession.getMaxTextMessageBufferSize());
    wsSession.setWsFrame(this);
    this.wsSession = wsSession;
    Transformation finalTransformation;
    if (isMasked()) {
        finalTransformation = new UnmaskTransformation();
    } else {
        finalTransformation = new NoopTransformation();
    }
    if (transformation == null) {
        this.transformation = finalTransformation;
    } else {
        transformation.setNext(finalTransformation);
        this.transformation = transformation;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们首先看debug结果: 


可以看到,这两个变量的capacity都是20971520,它们是根据WsSession返回的大小来分配大小的,我们来看WsSession的方法的返回值:

private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;


static final int DEFAULT_BUFFER_SIZE = Integer.getInteger(
            "org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE", 8 * 1024)
            .intValue();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这两个变量的大小默认都是8192,那如果它们只占用8K的内存大小,应该也不会出现问题啊,那这两个变量一定是在其他地方被修改了,我们继续看源代码,在WsSession的构造方法中有如下两行代码:

this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize();
this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();
  • 1
  • 2
@Override
public int getDefaultMaxBinaryMessageBufferSize() {
    return maxBinaryMessageBufferSize;
}

@Override
public int getDefaultMaxTextMessageBufferSize() {
    return maxTextMessageBufferSize;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

webSocketContainer是在WsSession的构造方法中传入的,webSocketContainer这两个方法分别返回maxBinaryMessageBufferSize和maxTextMessageBufferSize的值,它们默认为:

private int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
private int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE;
  • 1
  • 2

即这两个方法的默认返回值仍然是Constants.DEFAULT_BUFFER_SIZE,即8192,那它们是在哪里改变成20971520了呢? 
WsWebSocketContainer类中还有以下几个方法:

@Override
public int getDefaultMaxBinaryMessageBufferSize() {
    return maxBinaryMessageBufferSize;
}

@Override
public void setDefaultMaxBinaryMessageBufferSize(int max) {
    maxBinaryMessageBufferSize = max;
}

@Override
public int getDefaultMaxTextMessageBufferSize() {
    return maxTextMessageBufferSize;
}

@Override
public void setDefaultMaxTextMessageBufferSize(int max) {
    maxTextMessageBufferSize = max;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这几个方法分别可以获取和设置maxBinaryMessageBufferSize和maxTextMessageBufferSize的值,那是不是通过这几个方法来修改的值呢? 
ServletServerContainerFactoryBean类中有如下一段代码:

public void afterPropertiesSet() {
    Assert.state(this.servletContext != null,
            "A ServletContext is required to access the javax.websocket.server.ServerContainer instance");
    this.serverContainer = (ServerContainer) this.servletContext.getAttribute(
            "javax.websocket.server.ServerContainer");
    Assert.state(this.serverContainer != null,
            "Attribute 'javax.websocket.server.ServerContainer' not found in ServletContext");

    if (this.asyncSendTimeout != null) {
        this.serverContainer.setAsyncSendTimeout(this.asyncSendTimeout);
    }
    if (this.maxSessionIdleTimeout != null) {
        this.serverContainer.setDefaultMaxSessionIdleTimeout(this.maxSessionIdleTimeout);
    }
    if (this.maxTextMessageBufferSize != null) {
        this.serverContainer.setDefaultMaxTextMessageBufferSize(this.maxTextMessageBufferSize);
    }
    if (this.maxBinaryMessageBufferSize != null) {
        this.serverContainer.setDefaultMaxBinaryMessageBufferSize(this.maxBinaryMessageBufferSize);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这个方法将在bean所有的属性被初始化后调用,其实这两个值就是在这修改的了。 
为什么这么说呢,我们看着两个截图: 
项目启动时serverContainer的相关属性值
WsSession的构造方法中传入的wsWebSocketContainer的属性值

对比这两张图片可知,WsSession的构造方法中传入的wsWebSocketContainer与项目启动时的serverContainer是同一个实例。所以,在afterPropertiesSet()方法中设置的值就是在wsWebSocketContainer中设置的值。

ServletServerContainerFactoryBean类的相关属性如下:

@Nullable
private Integer maxTextMessageBufferSize;

@Nullable
private Integer maxBinaryMessageBufferSize;
  • 1
  • 2
  • 3
  • 4
  • 5

这两个属性的初始值是在servlet中设置的:

<bean class="org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean">
    <property name="maxTextMessageBufferSize" value="20971520"/>
    <property name="maxBinaryMessageBufferSize" value="20971520"/>
</bean>
  • 1
  • 2
  • 3
  • 4

总结

通过上述分析,也就解释了为什么WsFrameServer占用了很大的内存。那程序中为什么一开始将这两个值设置这么大呢?原因是在很久以前,我们刚测试Websocket通信时,发现只能传输小于8K的消息,大于8K的消息都不能进行传输,所以我们干脆把它调大,也就直接设置为了20M,这也就导致了现在的这个问题。 
但是程序中发送的消息大小都是100K+的,那我也不能将他们设置太小,所以我们将其改小,设置为200K,然后重新测试,能够达到50并发。但是,50并发感觉还是不太行,不知道能不能有其他的解决办法~_~我再想想。

目录
相关文章
|
10月前
|
Java Android开发
jvm的OOM
jvm的OOM
37 0
|
12月前
|
SQL 算法 安全
五、JDK命令行工具jps、jstat、jmap、jstack、jhat、jinfo
一、jps,查看虚拟机进程的命令 参数说明 -q:只输出进程 ID -m:输出传入 main 方法的参数 -l:输出完全的包名,应用主类名,jar的完全路径名
152 0
|
数据可视化 Java
利用jstat命令排查OOM和内存泄漏
利用jstat命令排查OOM和内存泄漏
337 0
利用jstat命令排查OOM和内存泄漏
|
监控 数据可视化 Java
JVM系列之:GC调优基础以及初识jstat命令
JVM系列之:GC调优基础以及初识jstat命令
307 0
JVM系列之:GC调优基础以及初识jstat命令
|
Java
jstat -gcutil 命令使用
jstat -gcutil 命令使用
489 0
jstat -gcutil 命令使用
|
缓存 监控 算法
JVM GC
前面我们已经在整体上简单地介绍了一下 JVM 组成部分,本文着重介绍其中 GC 相关的内容
|
Java
JVM的GC日志
查看jvm默认gc收集器 cmd执行命令: java -XX:+PrintCommandLineFlags -version gc类型
857 0
|
监控 Java 应用服务中间件
JVM性能调优监控工具jps、jstack、jmap、jhat、jstat、hprof使用详解
 现实企业级Java应用开发、维护中,有时候我们会碰到下面这些问题: OutOfMemoryError,内存不足 内存泄露 线程死锁 锁争用(Lock Contention) Java进程消耗CPU过高 ......     这些问题在日常开发、维护中可能被很多人忽视(比如有的人遇到上面的问题只是重启服务器或者调大内存,而不会深究问题根源),但能够理解并解决这些问题是Java程序员进阶的必备要求。
2741 0
|
监控 Java
Java命令行监控工具(jmap,jstack,jstat,jinfo,jps)
项目部署上线之后,线上项目是不能轻易修改的,定位问题的难度也会变大。因此监控是非常重要的一个环节,有了监控,我们才能更好的定位系统中的问题,从而排查。监控的工具有很多种, 但是java自带的命令行监控工具,是必须要掌握的。
3041 0