記一次內存爆漲分析 , JVM命令使用

問題描述:

tomcat服務突然不可用 , 所有請求均不通 .

第一想法就是服務掛了. 登錄服務器

  • 查詢tomcat進程, 服務還在運行中
    • 服務運行中 , 但接口不可用 .
    • 可以想到硬件到了極限 , CPU , 內存 , 日誌(大日誌文件佔滿服務器)
  • 查詢進程狀態 . java佔用內存超95%
    • 死循環
    • 大對象 map 或 list
  • 查日誌,確定問題所在
    • 一個select查詢, 查了幾十萬條數據,而且多次刷. 把內存佔滿了.
    • 有日誌就是好辦事 .

下面是沒有日誌的問題定位方式 .使用jvm命令. 以下爲我本地的模擬操作

還是前面的邏輯:

確定服務是否還在運行中

$ jps -l
3408 org.apache.catalina.startup.Bootstrap
9920 org.jetbrains.jps.cmdline.Launcher
4892
8716 org.jetbrains.idea.maven.server.RemoteMavenServer
9852 sun.tools.jps.Jps

可以看到 3408爲tomcat進程, 可以列出來,說明服務沒問題 . 在運行中.

進程的硬件狀態

$ top
 PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
3408 root     20   0 5494m 1.7g  11m S  6.7 95.9   1:19.12 java

可以看到 , %MEM爲內存顯示爲百分比 . 內存佔用95.9%

接下去思路:

  1. 突然的情況 , 那麼一定有操作. 先大膽的斷定爲接口異常. 先定位http請求的線程.
    • 大部分情況下,都是http線程的異常
  2. 如果http線程無異常 , 再找其他問題

定位http線程問題

$ jstack 3408 | grep http
"http-apr-80-exec-2" daemon prio=6 tid=0x000000000d60c000 nid=0x2088 runnable [0x000000000fc7a000]
"http-apr-80-exec-1" daemon prio=6 tid=0x000000000d60d800 nid=0x2364 waiting for monitor entry [0x000000000e1de000]
"http-apr-80-AsyncTimeout" daemon prio=6 tid=0x000000000c21e800 nid=0x1f68 waiting on condition [0x00000000102cf000]
"http-apr-80-Acceptor-0" daemon prio=6 tid=0x000000000c21e000 nid=0x15e0 runnable [0x00000000100df000]
"http-apr-80-Sendfile" daemon prio=6 tid=0x000000000c21d000 nid=0x9d4 in Object.wait() [0x000000000febe000]
"http-apr-80-Poller" daemon prio=6 tid=0x000000000c21c800 nid=0x7b0 in Object.wait() [0x000000000fdaf000]

tomcat的http請求默認線程名格式爲:http-apr-80-exec-xx , xx代表線程號 . 我這裏只是模擬 , 只有兩個線程,線程1是waiting狀態 , 線程2 是runnable狀態

那麼一般情況下, 資源被線程過度使用纔會爆. 一般只有一個線程爲runnbale,其他都在等待資源 . 爲waiting狀態.

那麼把線程2的棧信息打印出來

打印異常棧信息

$ jstack 7048 | grep "http-apr-80-exec-2" -A 200
"http-apr-80-exec-2" daemon prio=6 tid=0x000000000d86b000 nid=0x2478 runnable [0x0000000010aea000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.read(SocketInputStream.java:152)
        at java.net.SocketInputStream.read(SocketInputStream.java:122)
        at com.mysql.jdbc.util.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
        at com.mysql.jdbc.util.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
        - locked <0x00000007f52a1398> (a com.mysql.jdbc.util.ReadAheadInputStream)
        at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3001)
        at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3462)
        at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3452)
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3893)
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2526)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2673)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2549)
        - locked <0x00000007f5245990> (a com.mysql.jdbc.JDBC4Connection)
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
        - locked <0x00000007f5245990> (a com.mysql.jdbc.JDBC4Connection)
        at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1192)
        - locked <0x00000007f5245990> (a com.mysql.jdbc.JDBC4Connection)
        .......
        at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:231)
        at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53)
        at com.sun.proxy.$Proxy76.selectByCondition(Unknown Source)
        ........
		at com.sun.proxy.$Proxy77.selectByCondition(Unknown Source)
        at com.test.services.SelectService.selectListByCondition(SelectService.java:63)
        at com.test.controllers.SelectController.selectListByCondition(SelectController.java:142)
        at com.test.controllers.SelectController$$FastClassBySpringCGLIB$$299008ba.invoke(<generated>)
        .........

截取了線程2的一部分信息.

棧的執行順序是從下往上的 . 最上面是最後執行的.

最上面可以看到 , 線程2 執行了一個select查詢,最後在讀取數據 , 那麼就是在讀取數據庫數據.

現在可以定位問題原因, 查詢了大量數據寫到內存裏 ,把內存佔滿了.

順着棧信息向下找 ,可以找到業務方法的信息

at com.sun.proxy.$Proxy77.selectByCondition(Unknown Source)
at com.test.services.SelectService.selectListByCondition(SelectService.java:63)
at com.test.controllers.SelectController.selectListByCondition(SelectController.java:142)

那麼出問題的代碼位置也定位到了.SelectService的63行調用了一個select查詢.查詢了大量數據.沒有做數據量限制.

  • 過千數據不應當在接口中查詢
  • 參數校驗一定要到位
  • 日誌很重要, 日誌輸出也要合理. 不能大量輸出

問題定位結束 .

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章