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 1000S0 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.6610.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8136 1148.118 1148.7680.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8143 1149.139 1149.7890.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8150 1150.148 1150.7990.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8157 1151.160 1151.8110.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8164 1152.180 1152.8310.00 0.00 100.00 94.92 97.44 95.30 24 0.651 8170 1153.051 1153.7010.00 0.00 100.00 94.92 97.45 95.30 24 0.651 8177 1154.061 1154.7120.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8184 1155.077 1155.7280.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8191 1156.089 1156.7390.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8198 1157.134 1157.7850.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8205 1158.149 1158.8000.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8212 1159.156 1159.8070.00 0.00 100.00 94.93 97.45 95.30 24 0.651 8219 1160.179 1160.8300.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 9499Dumping 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的方法的返回值:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
這兩個變量的大小默認都是8192,那如果它們只占用8K的內存大小,應該也不會出現問題啊,那這兩個變量一定是在其他地方被修改了,我們繼續看源代碼,在WsSession的構造方法中有如下兩行代碼:
this.maxBinaryMessageBufferSize = webSocketContainer.getDefaultMaxBinaryMessageBufferSize(); this.maxTextMessageBufferSize = webSocketContainer.getDefaultMaxTextMessageBufferSize();- 1
- 2
- 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類中還有以下幾個方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
這幾個方法分別可以獲取和設置maxBinaryMessageBufferSize和maxTextMessageBufferSize的值,那是不是通過這幾個方法來修改的值呢??
ServletServerContainerFactoryBean類中有如下一段代碼:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
這個方法將在bean所有的屬性被初始化后調用,其實這兩個值就是在這修改的了。?
為什么這么說呢,我們看著兩個截圖:?
對比這兩張圖片可知,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并發感覺還是不太行,不知道能不能有其他的解決辦法~_~我再想想。
總結
以上是生活随笔為你收集整理的jstat和jmap使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里云域名备案
- 下一篇: Java并发编程实战(chapter_3