性能优化随笔

性能优化遵循木桶原则,最短的一块板决定了系统瓶颈,某一时刻只有一个瓶颈点,解决了这个瓶颈点,才能发现下一个瓶颈。

性能优化就是要在现有的资源里(cpu、内存、硬盘io、网络io等),最大限度的把这些资源利用起来;

性能优化需要从3方面:

1. cpu 使用率:如果cpu使用率低,可以尝试增加工作线程数,不能无限制增加,每个应用都有一个最优值,要看cpu密集型操作与io密集型操作占用的时间比例,非cpu操作时间越多,那么线程数需要的也就越多,极端情况下全部都是cpu密集型的操作,那么理论上只需要创建与服务器物理核数一样的线程数即可(一般会创建cpu核数+1个线程,防止内存缺页中断之类的cpu等待),否则会导致频繁的上下文切换,反而使系统性能下降(上下文切换见后面);极端情况下全部都是io密集型操作,那么线程数越多越好,直到到达io设备的瓶颈,即使这样也无法充分将cpu利用起来;

随着工作线程数的增加,cpu使用率应该越来越高,如果增加线程数,发现cpu使用率仍然不能提升,如果io没有到达瓶颈,那么接下来要排查程序里面是否有加锁操作,对某些资源的访问需要进行同步,这些操作导致多线程只能串行执行,并发是不能突破串行的限制,这种瓶颈比较容易发现,通过打印java的线程栈,找到加锁和等待锁的位置,来判断是否是因为这些锁导致的问题;

2. 内存:jvm内存如果跟现有访问量设置的大小不匹配,则会引发频繁的gc操作,降低应用的吞吐率,严重的可以直接导致程序oom,另外gc操作是一项非常消耗cpu的操作,导致cpu接近100%的使用率;另外还要防止系统内存不够用使用swap,swap会将部分内存中的数据保存到磁盘上,一旦需要访问这些数据,就会拖慢系统;

3.io:例如本地io例如读取磁盘, 网络io例如查询mysql,查询redis,查询es,查询mq,调用远程接口,这些都会降低cpu使用率,优化方式一般是使用缓存,本地缓存,分布式缓存;

如果发现应用吞吐量上不去,并且cup利用率很低,尝试增加线程数,如果增加线程数后,cpu利用率仍然上不去,那么就要排查io问题或者是并发锁问题,如果是io瓶颈,那么就要看硬件问题,如果是锁并发导致的,需要对锁进行优化,或采用其它设计方案,消除锁;

另外指标中的wa ,代表cpu等待io的时间,但是io高,wa也不一定会上升,比如cpu请求也频繁的情况下,wa就不会升高很多,如果cpu空闲,只要都是io操作,那么wa会看到明显的升高(请求I/O后等待响应,但进程从核上移开了)

上下文切换

线程切换会导致上下文的切换,上下文切换需要保存线程当前运行状态,还会导致当前线程运行的cpu缓存失效,这些都是额外的开销,cpu密集型的应用如果切换频繁,会导致吞吐率下降严重 ,更详细内容见这篇文章:http://ifeve.com/java-context-switch/

cas锁的使用场景

场景一:

1.业务代码耗时时间短,甚至比线程切换耗时还要少;

2.竞争情况不激烈;

场景二:

操作耗时长,竞争激烈,非公平模式;这种情况下可以先使用cas尝试获取锁,如果获取到了,就可以避免线程切换,避免cpu缓存失效,从而提高吞吐量;缺点是会导致被阻塞的线程获取锁的概率下降,等待锁时间变长;

在java中cas锁会与volatile关键字一起使用,对于读多,写少的变量,可以使用volatile+cas控制。

无锁编程

无锁编程的优点是显而易见的,它可以充分利用多核cpu的计算能力,避免线程切换、缓存失效带来的额外开销,避免锁带来的死锁问题;

很多人认为无锁编程就是cas操作,这样理解其实是不正确的,cas也是一种锁,称之为乐观锁,它可以避免线程切换、缓存失效带来的开销,但是cas仍会使多核的并行执行变为串行执行,为了避免线程阻塞,会做一些无用功,下面的例子我们可以了解一下,除了cas,无锁编程有哪些方式:

例如,计数器,不要设置一个全局的计数器,这样所有线程都竞争一个资源,可以给每个线程持有一个自己的计数器,这样就没有锁竞争的问题了;

例如,客户端load balance功能,有5个服务器,客户端轮询的方式访问这五个服务器,实现负载均衡,一般做法是给每个服务器编号,1、2、3、4、5,然后一个全局变量记录当前访问的服务器编号,每访问一次编号+1,到5的时候就置为1;还有另外一种更好的解决方案,给每个线程一个编号,每个线程使用自己的编号去轮询,实现负载均衡,这样就消除了多线程对一个全局变量的竞争,rocketmq就是这样实现的;

再例如,jvm中线程在年轻带申请内存的时候,会给每个线程预先分配1m大小的TLAB,这个TLAB为线程私有的,线程申请内存空间时会在TLAB上申请,这样就避免了线程在堆上申请内存空间时锁住整个堆;

可以看出无锁编程一种形式就是将资源拆分后分配给每个线程,这样就可避免对资源的竞争,本质也是一种锁的拆分,当拆到每个线程上以后,就不存在锁了;

注意锁的范围

当然不是全部的场景都可以将资源拆分或是拆分给每个线程,如果一定要使用锁,注意锁的范围,加锁执行的每一个操作一定是需要互斥的操作,类锁与对象锁优先选择对象锁;

拆分锁

避免资源集中,导致锁的竞争提高,将一个需要竞争的资源拆分为多个,减小锁的粒度;

例如,计数器,将一个计数器拆分为10个,这样就锁争用冲突情况就变成了原来的1/10;

再例如,读写锁,将读与写分离,而不是使用同一把锁;

锁的小结

通过上面的讨论在多核服务器上,我们一定可以得出以下结论:

无锁性能>cas性能

无锁性能>锁性能

那么下面这个结论是否成立呢?

cas性能>锁操作

显然是不一定成立的,如果你还不明白为什么不一定成立,请回去在读一遍cas锁的使用场景。

自动拆箱装箱导致的性能问题

Integer sum = 0;
 for(int i=0; i<10000; i++){
   sum+=i;
}

sum+=i 这个操作,首先会对sum执行拆箱操作,然后执行+i,最后将结果装箱生成一个新的Integer对象,所以这个for循环会创建将近1w个Integer对象;

StringBuilder与+

现在的java中的字符串连接符“+”在java中会被优化成隐式的StringBuilder操作;

但是下面的语句就存在性能问题

String[] str= new String[]{"aa","bb","cc", ........};

String newStr ="head ";

for(int i = 0 , j = str.length ; i < j ; i++){

    newStr += str[i] ;

}

newStr += str[i] 这个操作每次都会生成一个新的StringBuilder操作对象,所以这个循环执行多少次就会创建出多少个对象来;

另外StringBuilder对象内部实现就是生成一个char[]数组,默认大小为16,如果超过长度,那么就会生成一个新的数组,使用Arrays.copyOf(char[] original, int newLength) 方法将旧的内容拷贝的新的数组里面去;会占用额外的内存和产生额外的操作;所以初始化时尽量指定一个最大的大小;

ArrayList、HashMap等集合类的自动扩容问题

与上面提到的StringBuilder一样,这两个内部实现也是有一个数组;数组是不能扩容的,要想扩容就生成一个新的数组,然后吧旧数组的内容通过 Arrays.copyOf()方法拷贝过来,所以它们初始化时一定要指定一个合理的大小;

另外需要说明的是 Arrays.copyOf()方法是浅拷贝;

能用基本类型一定不要使用对象(除非维护、设计方面的需要)

另外对象类型都会有一个markword,需要分配更多的内存空间;

基本类型直接在栈上分配空间,会大大减轻你的gc压力;

GC

导致频繁ygc的原因:

短时间内生成大量对象;

年轻带设置的过小,业务请求量大;

导致频繁fgc的原因:

老年代设置过小,大量永久对象占用老年代内存空间;

大量大对象直接晋升到老年代;(这种情况可以采用对外内存【内存池】)

年轻带设置不合理,导致大量临时对象过早晋升到老年代;

gc停顿时间过长:

1. gc算法中一般耗时就是标记、扫描、压缩、复制这几部分;

巨型链表数据结构会导致扫描时间过长;

2. CMS remark阶段停顿时间过长,可能是年轻带指向老年代对象过多导致,可以在remark操作之前执行一次ygc;

3. 用到了swap 或者io繁忙被阻塞导致,jvm写gc日志阻塞,此时表现为[Times: user=0.20 sys=0.01, real=18.45 secs] ,user和sys之和大大小于real的情况  https://engineering.linkedin.com/blog/2016/02/eliminating-large-jvm-gc-pauses-caused-by-background-io-traffic 这篇文章详细的说明这种情况,使用 sar -d -p 1命令可以监控io情况;

4. 堆设置过大,垃圾回收器此时应该优先使用G1,在大堆的情况下其它垃圾回收器很难满足低停顿的要求;

5. GC线程数少  [Times: user=25.56 sys=0.35, real=20.48 secs];

触发FullGC

System.gc()会导致FullGC,以下方式会执行System.gc():

1. 直接内存(冰山一角),如果直接内存满了,会主动调用System.gc()去清理;

2. RMI,周期性的调用System.gc(),可以通过以下参数配置它的执行周期:

– Dsun.rmi.dgc.server.gcInterval=n

– Dsun.rmi.dgc.client.gcInterval=n

3. 程序中主动调用System.gc() 或Runtime.getRuntime().gc();

4. jmx中能够调用

注意:不建议使用-XX:+DisableExplicitGC参数禁止System.gc()的调用,原因见第一条;

jmap -histo:live 命令触发FullGC

1. 尽量不要生成大量的临时大对象,大对象直接晋升到老年代,大量的临时大对象会导致频繁的fullgc,大对象大小设置参数为-XX:PretenureSizeThreshold=1000000;

2. 如何判断eden设置是否合理

    使用jstat -gccause pid 1s 观察eden回收频率是否过于频繁,如果过于频繁有两方面原因,一方面可能是服务器访问量高,瞬间的高并发产生了很多临时对象,这种情况是正常的,可以通过扩容年轻带的方式来降低年轻带的回收频率 ,虽然增加年轻带大小会增加单次ygc时间,但是这个关系并不是线性的;另一方面是应用创建了大量的临时对象,这时要根据应用情况判断是否可避免大量临时对象的创建;

3. 如何判断Survivor设置是否合理

     ygc后eden区幸存下来的对象就会进入到Survivor中,如果对象生命周期比较短,经历几次ygc后,在Survivor区就被回收掉了,Survivor是防止临时对象从年轻带晋升到老年代的一个缓冲区;

    对于那些存活时间比较长的对象,不可能让它一直呆在Survivor中来回复制,jvm通过对象的age来控制对象的晋升,Survivor中的对象都有一个age,每经历一次ygc,如果Survivor中的对象没有被回收掉,age就会加1,达到一定的age的对象就晋升到老年代了;

     Survivor可以通过-XX:MaxTenuringThreshold=16 来配置对象最大晋升age,注意是最大晋升age,而不是晋升age;那么如何确定晋升age呢?

     这就涉及到另外一个参数了-XX:TargetSurvivorRatio=50(默认值为50),代表Survivor区域使用量超过50%的对象会全部晋升到老年代,超过50%比例的对象的最小age就是晋升age,但是这个晋升age还不是最终的晋升age,它会与-XX:MaxTenuringThreshold设置的最大值进行比较,选择其中最小的值作为晋升age,大于等于这个age的对象都会晋升到老年代;

    一般Survivor区域不够用(即发生溢出,Survivor to区域大小无法容纳所有eden和Survivor from中的存活对象,溢出的对象直接进入老年代)或者Survivor区域使用量超过50%,对象就会晋升到老年代,我们可以通过增加Survivor区域大小(例如:-XX:SurvivorRatio=6 代表 <2个Surivivor:Eden=2:6>)和调高Survivor区域的使用量晋升百分比(如-XX:TargetSurvivorRatio=70代表将from区利用率到70%才晋升,),将大部分临时对象留在年轻带,需要注意的是要防止Survivor区域溢出,一旦溢出,Survivor to容纳不了的对象直接晋升到老年代,如果这些对象都是临时对象,会加重fullgc

这里有一个参数-XX:+PrintTenuringDistribution可以打印每次ygc后Survivor区域的对象年龄分布情况以及晋升age;

优化手段

1.  处理批量数据(集合类),将单线程变为多线程,可以自己写,也可以使用fock/join框架 、stream;

2.  对于处理耗时、更新不频繁、读取频繁的数据,采用缓存+定时任务异步更新的方式,确定一个可以接受的更新频率;

3.  对于不需要马上返回处理结果,且处理耗时的数据,可以采用mq缓存,后台任务批量处理;

问题排查

jstack排查应用执行慢的问题,两种情况可以使用jstack排查

1. 某段代码执行时间过长;

2. 锁导致并行变串行;

如果是因为线程数太少导致的,通过jstack是发现不了的;所以一般压测,先增加线程数,等到增加线程数qps与cpu使用率都上不去的时候, 再通过jstack进行排查问题

jvm内存溢出问题

1.如果对业务代码熟练,使用jmap histo 导出类的直方图,找到大对象,根据大对象找到业务代码;

2.如果大对象不是业务类,或者对代码不熟悉,则dump堆后,看引用关系,找到引用的代码位置;

推荐工具

http://gceasy.io/

gclog分析工具,狠给力;

http://xxfox.perfma.com/

你假笨的perfma公司出品,狠给力,至少不会让你的参数设置的离谱;

http://www.perfma.com/

笨神的创业产品

2023-01-04T07:45:19.081+0800: 3417975.999: [GC pause (G1 Evacuation Pause) (young), 8.0313063 secs]
   [Parallel Time: 1521.3 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 3417976410.1, Avg: 3417976411.3, Max: 3417976414.7, Diff: 4.6]
      [Ext Root Scanning (ms): Min: 296.6, Avg: 405.8, Max: 686.4, Diff: 389.8, Sum: 3246.2]
      [Update RS (ms): Min: 186.0, Avg: 573.8, Max: 814.0, Diff: 628.0, Sum: 4590.3]
         [Processed Buffers: Min: 1, Avg: 19.8, Max: 76, Diff: 75, Sum: 158]
      [Scan RS (ms): Min: 0.1, Avg: 0.6, Max: 4.6, Diff: 4.5, Sum: 5.2]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
      [Object Copy (ms): Min: 261.8, Avg: 458.1, Max: 639.5, Diff: 377.7, Sum: 3665.2]
      [Termination (ms): Min: 14.5, Avg: 44.8, Max: 68.3, Diff: 53.8, Sum: 358.3]
         [Termination Attempts: Min: 1, Avg: 2.5, Max: 4, Diff: 3, Sum: 20]
      [GC Worker Other (ms): Min: 19.3, Avg: 36.8, Max: 76.0, Diff: 56.8, Sum: 294.4]
      [GC Worker Total (ms): Min: 1516.5, Avg: 1520.0, Max: 1521.1, Diff: 4.6, Sum: 12159.6]
      [GC Worker End (ms): Min: 3417977931.2, Avg: 3417977931.2, Max: 3417977931.3, Diff: 0.1]
   [Code Root Fixup: 0.1 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 5999.0 ms]
   [Other: 510.9 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 10.5 ms]
      [Ref Enq: 0.3 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 128.3 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 88.0 ms]
   [Eden: 6136.0M(6136.0M)->0.0B(508.0M) Survivors: 8192.0K->4096.0K Heap: 6416.1M(10.0G)->277.3M(10.0G)]
 [Times: user=0.41 sys=0.07, real=8.05 secs] 
 

real  远大于   user+sys 的情况, 可能是操作系统开启了swap,也可能是系统磁盘io繁忙,阻塞了gc日志导致的停顿

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值