发布新日志

  • 详细记录一下JAVA应用程序服务出现内存溢出的利用MAT分析过程

    2018-03-05 18:24:13

    说明:本次以系统在并发情况下后台出现java.lang.OutOfMemoryError:GC overhead limit exceeded错误来分析整个性能测试分析的一个过程。中间用到的工具包括了:loadrunner 、AWR 报告、jstack、MAT等

     

    1使用LoadRunner进行50用户的并发测试,先进行2分钟的预热测试,为了系统能用到缓存的地方都先进行缓存,然后进行5分钟的施压测试。

    2在施压5分钟的后半段时间,应用后台开始出现了“java.lang.OutOfMemoryError”的错误信息;

    具体错误信息如下:

     





    3、既然出现了OutOfMemoryError的错误信息,一般出现该错误信息都是堆内存的溢出,所以我们需要考虑捕捉一下堆内存的信息,捕捉堆内存的信息有2种方式:

    3.1 通过在应用中间件(weblogic、tomcat 等)上加入相应的JVM参数,具体参数如下(加入参数后,系统在出现OutOfMemoryError错误的时候便会自动生成类似java_pid9388.hprof的这样一个文件):

    -Xloggc:D:\heapdump\managed1_gc.log

    -XX:+HeapDumpOnOutOfMemoryError

    -XX:HeapDumpPath=D:\heapdump

    3.2 使用JDK自带的JMAP工具,具体使用方法如下:

    第一步:先使用jps.exe命令找到相应的java进程ID,一般找Server PID的;

    第二步:jmap.exe-dump:format=b,file=d:\dump\java_pid(第一步查询到的PID号).hprof PID(该地方一定要空格后跟着相应的PID号)

    如果只dump heap中的存活对象,则加上选项-live,如下:

    jmap.exe  -dump:live,format=b,file=/path/heap_pid. hprof 进程ID(PID)

    4.使用MAT工具来分析生成的hprof文件内容

    4.1打开需要分析的hprof文件:



    4.2 在Overview(概述)界面利用饼图的摘要信息来分析哪些对象比较占内存

     

    4.3分析Action部分内容:


    4.3.1点击“Leak Suspects”后的结果如下:

     

    4.3.2 在怀疑问题的第点Details



    4.3.3查看有问题的的类所引用的所有对象。此时使用鼠标左键点击,然后弹出菜单中进行如下选择:List Objects->with outgoing references

    (说明:

    图中的Shallow Heap(浅堆):指对象自身占用内存的大小,不包括它引用的对象。

    图中的 Retained Heap(深堆):指当前对象大小+当前对象可直接或间接引用到对象的大小总和

     


    此时可以点击鼠标左键,将sql语句的内容进行拷贝.



    此时就找到了问题。

  • 【JAVA 工具】jstack简单使用,定位死循环、线程阻塞、死锁等问题

    2018-03-05 17:24:48

    转载地址:http://www.cnblogs.com/chenpi/p/5377445.html


    当我们运行java程序时,发现程序不动,但又不知道是哪里出问题时,可以使用JDK自带的jstack工具去定位;

    废话不说,直接上例子吧,在window平台上的;

    死循环

    写个死循环的程序如下:

    复制代码
    package concurrency;
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            while (true) {
    
            }
        }
    }
    复制代码

    先运行以上程序,程序进入死循环;

    打开cmd,输入jps命令,jps很简单可以直接显示java进程的pid,如下为7588:

    或者输入tasklist,找到javaw.exe的PID,如下为7588:

    输入jstack 7588命令,找到跟我们自己代码相关的线程,如下为main线程,处于runnable状态,在main方法的第八行,也就是我们死循环的位置:

    Object.wait()情况

    写个小程序,调用wait使其中一线程等待,如下:

    复制代码
    package concurrency;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    class TestTask implements Runnable {
        @Override
        public void run() {
    
            synchronized (this) {
                try {
                    //等待被唤醒
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
    
            ExecutorService ex = Executors.newFixedThreadPool(1);
            ex.execute(new TestTask());
    
        }
    }
    复制代码

    同样我们先找到javaw.exe的PID,再利用jstack分析该PID,很快我们就找到了一个线程处于WAITING状态,在Test.java文件13行处,正是我们调用wait方法的地方,说明该线程目前还没等到notify,如下

    死锁

    写个简单的死锁例子,如下:

    复制代码
    package concurrency;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    class TestTask implements Runnable {
        private Object obj1;
        private Object obj2;
        private int order;
    
        public TestTask(int order, Object obj1, Object obj2) {
            this.order = order;
            this.obj1 = obj1;
            this.obj2 = obj2;
        }
    
        public void test1() throws InterruptedException {
            synchronized (obj1) {
                //建议线程调取器切换到其它线程运行
                Thread.yield();
                synchronized (obj2) {
                    System.out.println("test。。。");
                }
    
            }
        }
        public void test2() throws InterruptedException {
            synchronized (obj2) {
                Thread.yield();
                synchronized (obj1) {
                    System.out.println("test。。。");
                }
    
            }
        }
    
        @Override
        public void run() {
    
            while (true) {
                try {
                    if(this.order == 1){
                        this.test1();
                    }else{
                        this.test2();
                    }
                    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            Object obj1 = new Object();
            Object obj2 = new Object();
    
            ExecutorService ex = Executors.newFixedThreadPool(10);
            // 起10个线程
            for (int i = 0; i < 10; i++) {
                int rder = i%2==0 ? 1 : 0;
                ex.execute(new TestTask(order, obj1, obj2));
            }
    
        }
    }
    复制代码

    同样我们先找到javaw.exe的PID,再利用jstack分析该PID,很快jstack就帮我们找到了死锁的位置,如下所示:

    等待IO

    写个简单的等待用户输入例子:

    复制代码
    package concurrency;
    
    
    import java.io.IOException;
    import java.io.InputStream;
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException, IOException {
    
            InputStream is = System.in;
            int i = is.read();
            System.out.println("exit。");
    
        }
    }
    复制代码

    同样我们先找到javaw.exe的PID,再利用jstack分析该PID,很快jstack就帮我们找到了位置,Test.java文件12行,如下所示:

     

    其它

    像调用sleep使线程进入睡眠,suspend()暂停线程等就不举例了,都是类似的;

  • 瓶颈分析

    2018-02-23 10:51:54

    CMS GC知识

    CMS,全称Concurrent Mark and Sweep,用于对年老代进行回收,目标是尽量减少应用的暂停时间,减少full gc发生的机率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。

    一次CMS至少会给Full GC的次数 + 2,因为Full GC的次数是按照老年代GC时stop the world的次数而定的

    我们可以认为Major GC == Full GC,他们是一个概念,就是针对老年代/永久代进行GC。

    1. 应用系统负载分析:

    服务器负载瓶颈经常表现为,服务器受到的并发压力比较低的情况下,服务器的资源使用率比预期要高,甚至高很多。导致服务器处理能力严重下降,最终有可能导致服务器宕机。实际性能测试工作中,经常会用以下三类资源指标判定是否存在服务器负载瓶颈:

    • CPU使用率
    • 内存使用率
    • Load

    一般cup的使用率应低于50%,如果过高有可能程序的算法耗费太多cpu,或者某些代码块进行不合理的占用。Load值尽量保持在cpuS+2 或者cpuS*2,其中cpu和load一般与并发数成正比。

    • 内存可以通过2种方式来查看:

    1) 当vmstat命令输出的si和so值显示为非0值,则表示剩余可支配的物理内存已经严重不足,需要通过与磁盘交换内容来保持系统的稳定;由于磁盘处理的速度远远小于内存,此时就会出现严重的性能下降;si和so的值越大,表示性能瓶颈越严重。

    2) 用工具监控内存的使用情况,如果出现下图的增长趋势(used曲线呈线性增长),有可能系统内存占满的情况:

    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95007

    如果出现内存占用一直上升的趋势,有可能系统一直在创建新的线程,旧的线程没有销毁;或者应用申请了堆外内存,一直没有回收导致内存一直增长。

    3)内存分页监控sar -B 5 10

    输出项说明:
    pgpgin/s:表示每秒从磁盘或swap置换到内存的字节数(KB)
    pgpgout/s:表示每秒从内存置换到磁盘或SWAP的字节数(KB)
    fault/s:每秒钟系统产生的缺页数,即主缺页与次缺页之和。
    majflt/s:每秒钟产生的主缺页数。
    pgfree/s:每秒被放入空闲队列中的页个数。是已经扫描到了的空闲页,扫描的空闲page越大,代表内存空余越大 
    pgscank/s:每秒被kswapd扫描的页个数。
    pgscand/s:每秒直接被扫描的页个数。
    pgsteal/s:每秒钟从cache中被清除来满足内存需要的页个数。
    %vmeff:每秒清除的页(pgsteal)占总扫描页(pgscank+pgscand)的百分比。pgsteal/s 除以( pgscank/s + pgscand/s ),pgsteal/s表示每秒释放的cache((pagecache and swapcache)),这个其实就是cache可以释放的页,当CPU需要的page不在cache中时,需要释放cache中的原来的page,从内存和swap中加载需要的page,pgscank/s表示每秒扫描swap的page , pgscand/s表示每秒扫描内存的page,实际上pgscank/s+pgscand/s就是RAM+SWAP 。当vmeff的值变大时,表示从RAM和Swap中置换到cache的page变多,这就意味着用到swap的机会变多。这个参数用来衡量页面置换效率,  超30% 证明物理内存已经处理不过来。这个值越大,则请求越多,越代表内存有问题。
    4.2 Jvm瓶颈分析

    对于java应用来说,过高的GC频率也会在很大程度上降低应用的性能。即使采用了并发收集的策略,GC产生的停顿时间积累起来也是不可忽略的,特别是出现cmsgc失败,导致fullgc时的场景。下面举几个例子进行说明:

    1. Cmsgc频率过高,当在一段较短的时间区间内,cmsGC值超出预料的大,那么说明该JAVA应用在处理对象的策略上存在着一些问题,即过多过快地创建了长寿命周期的对象,是需要改进的。或者old区大小分配或者回收比例设置得不合理,导致cms频繁触发,下面看一张gc监控图(蓝色线代表cmsgc)
    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95008

    由图看出:cmsGC非常频繁,后经分析是因为jvm参数-XX:CMSInitiatingOccupancyFraction设置为15,比例太小导致cms比较频繁,这样可以扩大cmsgc占old区的比例,降低cms频率注。

    调优后的图如下:

    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95009

    1. fullgc频繁触发

    当采用cms并发回收算法,当cmsgc回收失败时会导致fullgc:

    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95010

    由上图可以看出fullgc的耗时非常长,在6~7s左右,这样会严重影响应用的响应时间。经分析是因为cms比例过大,回收频率较慢导致,调优方式:调小cms的回比例,尽早触发cmsgc,避免触发fullgc。调优后回收情况如下

    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95011

    可以看出cmsgc时间缩短了很多,优化后可以大大提高。从上面2个例子看出cms比例不是绝对的,需要根据应用的具体情况来看,比如应用创建的对象存活周期长,且对象较大,可以适当提高cms的回收比例。

    1. 疑似内存泄露,先看下图
    %e6%94%af%e4%bb%98%e5%ae%9d%e7%9a%84%e6%80%a7%e8%83%bd%e6%b5%8b%e8%af%95012

    分析:每次cmsgc没有回收干净,old区呈上升趋势,疑似内存泄露

    最终有可能导致OOM,这种情况就需要dump内存进行分析:

    • 找到oom内存dump文件,具体的文件配置在jvm参数里:
    • -XX:HeapDumpPath=/home/admin/logs

    -XX:ErrorFile=/home/admin/logs/hs_err_pid%p.log

    • 借助工具:MAT,分析内存最大的对象。

    结论

    1. 在单接口压测时,我们用“请求总数/总时长”得到吞吐量;然后再用“吞吐量/平均响应时间”得到实际并发,此举可用来观察系统实际承受的并发;
    2. 在多接口压测时,由于短板效应,同一个流程中的所有接口获得的请求总数和总时长都一样,显然“请求总数/总时长”计算各个子接口的吞吐量不合适,所以改用“并发/平均响应时间”,其中的并发数应在压测工具中埋点统计,不可简单使用工具线程数。
  • 性能测试问题排查一例——网络带宽瓶颈

    2017-09-27 19:52:34

    近期在做一个项目的性能测试时,在打压时发现压力达到100hps后就一直打不上去,同时还会报读redis服务器超时的错误。查看了下打压服务器的cpu和内存占用,没有发现什么异常。

      近期在做一个项目的性能测试时,在打压时发现压力达到100hps后就一直打不上去,同时还会报读redis服务器超时的错误。查看了下打压服务器的cpu和内存占用,没有发现什么异常。

      Cpu占用:

      内存占用:

      1、由于会报redis链接超时错误,首先定位到的是redis服务器挂了,找到开发将log中添加具体连接超时的redis服务器ip信息后,重新跑了一遍。

      依然会报连接redis服务器超时错误,开发立即查看了下对应ip的redis服务器。发现运行情况没有出现任何问题,各项指标均正常。

      2、于是查看压力服务器的各项指标来定位问题。

      用sar命令看了下磁盘性能,发现每秒写扇区的次数达到300以上,怀疑是写入次数过多导致的,于是查了下开发的脚本,发现开发每一步判断逻辑中都加了写errorlog操作。于是怀疑是写log导致的。

      将开发的写log操作大部分都关闭(除了读redis服务器错误)后,重新跑了一下,发现写扇区的次数降到100左右,但是hps依然打不上去。排出了磁盘写入的问题。

      3、接下来安装了nmon工具后,重新跑了一遍,看了下网络传输,发现hps达到100左右时,网络出口占用为120M/s!这是千兆网卡的满载速率了。于是定位到网络成为主要的瓶颈。

      网络I/O传输表:可以发现eth0-write的速率达到120千KB,也就是120M(注意这里的单位是“千”)

      4、查了下自己的打压脚本,发现部分请求的返回数据大小为4M。果断将请求的返回改为200K后重新打压后,压力可以成功达到2000hps以上。同时也没有再出现读redis超时的错误。

      至此,此次问题排查圆满结束。同时向大家着力推荐一下nmon工具。里面记录的参数很全,基本上定位性能的指标(比如cpu、内存、每个cpu、每个磁盘分区的读写、磁盘busy情况、网络吞吐、网络包数据等)都能够统计到。

  • 性能测试瓶颈定位——磁盘IO和线程切换过多

    2017-09-27 19:39:01

    近期在一个性能测试项目中遇到了一个调优的过程。分享一下给大家。

      1、 第一次打压时,发现A请求压力80tps后,cpu占用就非常高了(24核的机器,每个cpu占用率全面飙到80%以上),且设置的检查点没有任何报错。

      2、 了解了一下后台实现逻辑:大体是这样的:服务器接到请求后,会再到另一台kv服务器请求数据,拿回来数据后,根据用户的机器码做个性化运算,最后将结果返回给客户端,期间会输出一些调试log。

      查了下,kv服务器正常,说明是本机服务服务器的问题。具体用vmstat命令看一下异常的地方。

      3、 从图中可以直观的看出,bi、bo、in、cs这四项的值都很高,根据经验,bi和bo代表磁盘io相关、in和cs代表系统进程相关。一个一个解决吧,先看io。

      4、 用iostat –x命令看了下磁盘读写,果然,磁盘慢慢给堵死了。

      5、 看了下过程,只有写log操作才能导致频繁读写磁盘。果断关闭log。重新打压试下。

      6、 Bi和bo降到正常值了,说明磁盘的问题解决了。但是上下文切换数竟然达到了每秒40万次!好可怕~

      7、 只知道上下文切换数很大,怎么知道是在哪些进程间切换呢?

      到网上搜了一个脚本,这个脚本用来统计特定时间内进程切换的top20并打印出来。

      #! /usr/bin/env stap

      #

      #

      global csw_count

      global idle_count

      probe scheduler.cpu_off {

      csw_count[task_prev, task_next]++

      idle_count+=idle

      }

      function fmt_task(task_prev, task_next)

      {

      return sprintf("%s(%d)->%s(%d)",

      task_execname(task_prev),

      task_pid(task_prev),

      task_execname(task_next),

      task_pid(task_next))

      }

      function print_cswtop () {

      printf ("%45s %10s\n", "Context switch", "COUNT")

      foreach ([task_prev, task_next] in csw_count- limit 20) {

      printf("%45s %10d\n", fmt_task(task_prev, task_next), csw_count[task_prev, task_next])

      }

      printf("%45s %10d\n", "idle", idle_count)

      delete csw_count

      delete idle_count

      }

      probe timer.s($1) {

      print_cswtop ()

      printf("--------------------------------------------------------------\n")

      }

      保存成cs.stp后,用stap cswmon.stp 5命令执行下。

      8、发现是discover进程在反复和系统进程进行切换。从此消耗了大量资源。

      9、从网上查了下减少切换进程的一些方法:

      开发随后改了下:将线程数开大了一倍,控制在一个进程中。

      重新打压了一下。发现上下文切换数降低到25万次左右。

      此时的性能数据可以达到每秒260次左右,远远高于之前的80次。已经达到可以上线的需求

      但是由于页面中断书和上下文切换数还是很高,后续还是需要优化~

Open Toolbar