当前位置: 首页 > 科技观察

面试这样回答Java调优,至少加1K!!!

时间:2023-03-20 16:01:09 科技观察

Java应用程序性能优化是一个老生常谈的话题。典型的性能问题包括页面响应慢、接口超时、服务器负载高、并发度低、数据库死锁频繁等。尤其是在今天“粗暴快速”的互联网开发模式下,随着系统访问量的不断增加,代码臃肿,各种性能问题开始浮出水面。Java应用性能存在很多瓶颈,比如磁盘、内存、网络I/O等系统因素,Java应用代码、JVMGC、数据库、缓存等,笔者根据个人经验,将Java性能优化分为四个层次:应用层、数据库层、框架层、JVM层,如图1所示。图1.Java性能优化分层模型每层优化的难度递增,涉及的知识和需要解决的问题解决的也会不同。例如,应用层需要了解代码逻辑,通过Java线程栈定位有问题的代码行等;数据库层需要分析SQL,定位死锁等;框架层需要看懂源码,了解框架机制;JVM层需要对GC的类型和工作机制有深刻的理解,对JVM各种参数的作用有清晰的认识。围绕Java性能优化,最基本的分析方法有两种:现场分析和事后分析。现场分析法是保留现场,然后利用诊断工具进行分析定位。现场分析对线上影响较大,有些场景(特别是涉及用户线上重点业务时)不适合。事后分析包括尽可能多地收集现场数据,然后立即恢复服务,并对收集到的现场数据进行事后分析和复现。下面我们从性能诊断工具入手,分享一些案例和实践。1性能诊断工具性能诊断的一种是对存在性能问题的系统和代码进行诊断。本文主要关注前者,后者可以通过各种性能压力测试工具(如JMeter)进行测试,不在本文讨论范围之内。对于Java应用,性能诊断工具主要分为两层:OS层和Java应用层(包括应用代码诊断和GC诊断)。OSDiagnosisOS诊断主要集中在三个方面:CPU、Memory和I/O。2CPU诊断主要关注CPU的平均负载(LoadAverage)、CPU使用率、上下文切换次数(ContextSwitch)。可以通过top命令查看系统的平均负载和CPU占用率。图2显示了通过top命令查看的系统状态。图2.top命令示例loadaverage有三个数字:63.66、58.39、57.18,分别代表机器在过去1分钟、5分钟、15分钟的负载。根据经验,如果该值小于0.7*CPU个数,则系统正常工作;如果超过这个值,甚至达到CPU核心数的四五倍,系统负载明显偏高。图2中,15分钟负载高达57.18,1分钟负载为63.66(系统16核),说明系统负载有问题,并且有进一步上升的趋势,具体原因需要定位。CPU的上下文切换次数可以通过vmstat命令查看,如图3所示:图3.vmstat命令示例图中发生上下文切换次数的场景主要如下:1)时间片用完,CPU正常调度下一个任务;2)被其他优先级更高的任务抢占;3)执行任务遇到I/O阻塞,暂停当前任务,切换到下一个任务;4)用户代码主动挂起当前任务让出CPU;5)多个任务抢占资源,因为没有抢占而被挂起;6)硬件中断。Java线程上下文切换主要来源于对共享资源的竞争。通常,锁定单个对象很少会成为系统瓶颈,除非锁定粒度太大。但是,在一个访问频率高且连续锁定多个对象的代码块中,可能会发生大量的上下文切换,成为系统瓶颈。比如我们的系统,log4j1.x在大并发、频繁的上下文切换、大量线程阻塞的情况下打印了大量日志,导致系统吞吐量大幅下降。相关代码如清单1所示。升级到log4j2.x只是解决了这个问题。for(Categoryc=this;c!=null;cc=c.parent){//ProtectedagainstsimultaneouscalltoaddAppender,removeAppender,...synchronized(c){if(c.aai!=null){write+=c.aai.appendLoopAppenders(event);}…}}3内存从操作系统的角度来看,内存关系到应用进程是否足够。您可以使用free–m命令查看内存使用情况。通过top命令可以查看进程使用的虚拟内存VIRT和物理内存RES。根据公式VIRT=SWAP+RES,可以计算出具体应用使用的交换分区(Swap)。如果交换分区太大,会影响Java应用程序的性能。你可以将swappiness的值调的越小越好。因为对于Java应用来说,占用过多的swap分区可能会影响性能,毕竟磁盘性能比内存慢很多。4I/OI/O包括磁盘I/O和网络I/O。通常,磁盘更容易出现I/O瓶颈。可以通过iostat查看磁盘的读写状态,通过CPU的I/O等待可以查看磁盘I/O是否正常。如果磁盘I/O一直处于高状态,说明磁盘太慢或有故障,已经成为性能瓶颈,需要进行应用优化或更换磁盘。除了常用的top、ps、vmstat、iostat等命令外,还有其他可以诊断系统问题的Linux工具,如mpstat、tcpdump、netstat、pidstat、sar等。Brendan总结并列出了不同类型Linux设备的性能诊断工具,如图4所示,供参考。图4.Linux性能观察工具5Java应用诊断及工具应用代码性能问题相对容易解决。通过一些应用层面的监控告警,如果确定有问题的功能和代码,可以直接通过代码定位;或者用top+jstack找出有问题的线程栈,定位到有问题线程的代码,也能找到问题所在。对于比较复杂和逻辑的代码段,通过Stopwatch打印性能日志往往可以定位到大部分应用代码性能问题。常用的Java应用程序诊断包括对线程、堆栈和GC的诊断。jstackjstack命令通常与top结合使用,通过top-H-ppid定位Java进程和线程,然后使用jstack-lpid导出线程栈。由于线程堆栈是瞬态的,因此需要多次转储,通常是3次转储,通常每5秒一次。将top定位的Java线程pid转成16进制,得到Java线程栈中的nid,找到对应的问题线程栈。图5.使用top–H-p查看运行时间较长的Java线程。如图5所示,线程24985运行时间较长,可能存在问题。转成16进制后,通过Java线程栈找到对应线程0x6199栈如下,从而定位问题点,如图6所示。图6.jstack查看线程栈JProfilerJProfiler可以分析CPU,heap,和内存,功能强大,如图7所示。同时结合压测工具,可以对代码进行耗时采样和统计。图7.通过JProfiler进行内存分析图6GC诊断JavaGC解决了程序员管理内存的风险,但GC引起的应用程序暂停是另一个需要解决的问题。JDK提供了一系列工具来定位GC问题。比较常用的有jstat、jmap和第三方工具MAT。jstatjstat命令可以打印GC详细信息、YoungGC和FullGC时间、堆信息等。命令格式为jstat–gcxxx-tpid,如图8所示。图8.示例jstat命令jmapjmap打印Java进程堆信息jmap–heappid。使用jmap–dump:file=xxxpid将heap转储到文件中,然后使用其他工具进一步分析其heap使用情况。MATMAT是一个分析Java堆的工具,提供直观的诊断报告,内置的OQL允许对堆进行类似SQL的查询,功能强大,传出引用和传入引用可以追溯对象引用的来源。图9.MAT示例图9是MAT使用示例。MAT有两列显示对象大小,分别是Shallowsize和Retainedsize。直接或间接引用的对象的Shallowsizes的总和,即对象被回收后GC释放的内存大小,一般来说只关注后者的大小。对于一些大堆(几十G)的Java应用,打开MAT需要更大的内存。通常本地开发机内存太小打不开。建议在离线服务器上安装图形环境和MAT,远程打开查看。或者执行mat命令生成堆索引,并将索引复制到本地,但是这种方式看到的堆信息有限。为了诊断GC问题,建议在JVM参数中加上-XX:+PrintGCDateStamps。常用的GC参数如图10所示。图10.常用的GC参数对于Java应用,top+jstack+jmap+MAT可以定位大部分应用和内存问题,是必不可少的工具。Java应用诊断有时需要参考OS相关信息,可以使用一些更全面的诊断工具,如Zabbix(集成OS和JVM监控)等。在分布式环境中,还需要分布式跟踪系统等基础设施。为应用性能诊断提供强有力的支持。7性能优化实践介绍完一些常用的性能诊断工具,下面结合我们在Java应用调优方面的一些实践,从JVM层、应用代码层、数据库层分享案例。JVM调优:GC之痛XX商业平台某系统重构时选择了RMI作为内部远程调用协议。系统上线后,服务开始周期性停止响应,停顿时间从几秒到几十秒不等。通过观察GC日志,发现自服务启动后,每小时都会发生一次FullGC。由于系统堆设置较大,FullGC一次会导致应用挂起时间较长,对在线实时业务影响较大。经分析,重构前系统并没有定期的FullGC,所以怀疑是RMI框架层面的问题。通过公开资料发现RMI的GDC(DistributedGarbageCollection)会启动一个守护线程周期性的执行FullGC回收远程对象,其守护线程代码如清单2所示。清单2.DGC守护线程源码privatestaticclassDaemonextendsThread{publicvoidrun(){for(;;){//...longd=maxObjectInspectionAge();if(d>=l){System.gc();d=0;}//…}}}一旦你找到问题,更容易解决。一种是通过添加-XX:+DisableExplicitGC参数直接禁止系统GC的显示调用,但是对于使用NIO的系统,存在堆外内存溢出的风险。另一种方法是通过增加-Dsun.rmi.dgc.server.gcInterval和-Dsun.rmi.dgc.client.gcInterval参数来增加FullGC间隔,增加参数-XX:+ExplicitGCInvokesConcurrent可以完全停止-TheFullThe-World的GC调整为并发GC周期,减少应用停顿时间,不会影响NIO应用。从图11可以看出,调整后的FullGC数量在3月之后明显下降。图11FullGC监控统计对于高并发、大数据量交互的应用,GC调优还是很有必要的,尤其是默认的JVM参数通常不能满足业务需求,需要专门调优。GC日志的解读公开资料很多,本文不再赘述。GC调优目标基本上有3种思路:降低GC频率,可以增加堆空间,减少不必要的对象生成;减少GC停顿时间,可以减少堆空间,使用CMSGC算法;避免FullGC,调整CMStriggerRatio,避免PromotionFailure和Concurrentmodefailure(老年代分配更多空间,增加GC线程数加速回收),减少大对象的产生等应用层调优:闻代码的臭味从应用层代码调优入手,分析代码效率下降的根本原因,无疑是提升Java应用性能的最佳途径之一。一个商业广告系统(使用Nginx做负载均衡)每天上线后,里面的几台机器负载急剧上升,CPU占用率很快就满了。我们在线进行了紧急回滚,通过jmap和jstack保存了其中一台服务器的站点。图12.通过MAT分析栈场景栈场景如图12所示,根据MAT分析dump数据,发现内存对象最多的是byte[]和java.util.HashMap$Entry,而java.util.HashMap$Entry对象有一个循环引用。初步定位在HashMap的put过程中可能会出现死循环问题(图中java.util.HashMap$Entry的下一个引用0x2add6d992cb8和0x2add6d992ce8形成一个循环)。查阅相关文档定位这是典型的并发场景错误(http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457)。总之,HashMap本身不具备多线程并发的特性。在多个线程同时进行put操作的情况下,内部数组的扩容会导致HashMap的内部链表形成环形结构,导致死循环。对于本次上线,最大的变化是通过在内存中缓存网站数据来提升系统性能,同时使用了懒加载机制,如清单3所示。清单3.网站数据懒加载代码privatestaticMapdomainMap=newHashMap();privatebooleanisResetDomains(){if(CollectionUtils.isEmpty(domainMap)){//从远程http接口获取网站详情ListnewDomains=unionDomainHttpClient.queryAllUnionDomain();if(CollectionUtils.isEmpty(domainMap)){domainMap=newHashMap();for(UnionDomaindomain:newDomains){if(domain!=null){domainMap.put(domain.getSubdomainId(),domain);}}}returntrue;}returnfalse;}可以看到这里的domainMap是一个静态共享资源,是HashMap类型的。在多线程的情况下,它的内部链表会形成一个环形结构。有一个无限循环。通过前端Nginx的连接和访问日志可以看出,系统重启后,Nginx已经积累了大量的用户请求。Resin容器启动时,大量用户请求涌入应用系统,多个用户同时请求并初始化网站数据。有效,导致HashMap出现并发问题。定位故障原因后,解决方法就比较简单了。主要的解决方案是:(1)使用ConcurrentHashMap或者同步块来解决上面的并发问题;(2)在系统启动前完成网站缓存加载,去除延迟加载等;(3)用分布式缓存代替本地缓存等。对于坏代码的定位,除了常规意义上的codereview之外,还可以通过MAT等工具在一定程度上快速定位系统性能瓶颈。但在一些与特定场景或业务数据绑定的情况下,需要辅助代码走查、性能测试工具、数据模拟,甚至线上引流,最终确认性能问题的根源。以下是我们总结的一些不良代码的一些可能特征,供大家参考:(1)代码可读性差,没有基本的编程规范;(2)生成对象过多或生成大对象,内存泄漏等;(3)IO流操作太多,或者忘记关闭;(4)数据库操作过多,事务过长;(5)同步使用场景错误;(6)循环迭代等耗时操作数据库层调优:死锁噩梦对于大多数Java应用来说,与数据库交互的场景是很常见的,尤其是OLTP等对数据一致性要求高的应用,性能数据库将直接影响整个应用程序的性能。表现。搜狗商业平台系统作为广告主的广告发布投放平台,对其素材的实时性和一致性有着极高的要求。我们在关系型数据库优化方面也积累了一定的经验。对于广告素材库,操作频率高(尤其是通过批量素材工具操作)很容易造成数据库死锁。比较典型的场景之一就是广告物料的价格调整。客户经常频繁地调整物料报价,间接对数据库系统造成较大的负载压力,增加了出现死锁的可能性。以下是搜狗商业平台广告系统中的广告素材调价案例。某商业广告系统,某天访问量突然增加,导致系统负载增加,数据库频繁死锁。死锁语句如图13所示。图13.死锁语句其中groupdomain表上的索引为idx_groupdomain_accountid(accountid)、idx_groupdomain_groupid(groupid)、primary(groupdomainid)三个单索引结构,使用Mysqlinnodb引擎。这个场景出现在更新一个groupbid的时候,场景中存在group,groupindustry(groupindus表)和groupsite(groupdomain表)。更新组标时,如果组行业标使用组标(用isusegroupprice标记,如果为1则使用组标)。同时,如果团站竞价使用团业竞价(isuseindusprice标记,为1则使用团业竞价),团网竞价也需要同时更新。由于每个组最多可以有3000个网站,更新组标时相关记录会被长期锁定。从上面的死锁问题可以看出,事务1和事务2都选择了idx_groupdomain_accountid这个单列索引。根据Mysqlinnodb引擎加锁的特点,一次事务只选择一个索引使用,一旦使用二级索引加锁,就会尝试加锁主键索引。进一步分析可知,事务1锁定了事务2持有的`idx_groupdomain_accountid`二级索引(锁定范围"spaceid5726pageno8658nbits824index"),但是事务2已经获取到二级索引("spaceid上加的锁5726页号8658n位824索引”)正在等待请求锁定主键索引PRIMARY索引。事务1最终回滚是因为事务2等待执行时间过长或者长时间没有释放锁。通过跟踪当天的访问日志可以看出,当天某客户通过脚本发起大量修改促销组出价的操作,导致大量交易循环等待释放锁定的主键PRIMARY以前交易的索引。问题的根源其实是Mysqlinnodb引擎对索引的使用有限制,而这个问题在Oracle数据库中并不突出。解决办法自然是希望单个事务锁定的记录数越少越好,这样出现死锁的概率就会大大降低。最后通过一个(accountid,groupid)的复合索引来减少单个事务锁定的记录数,同时也实现了不同计划下推广组数据记录的隔离,从而降低此类死锁发生的概率。一般来说,对于数据库层的调优,我们基本上从以下几个方面入手:(1)SQL语句层面的优化:慢SQL分析、索引分析调优、事务拆分等;数据库配置层面的优化:如字段设计、缓存大小调整、磁盘I/O等数据库参数优化、数据碎片整理等;(3)数据库结构层面的优化:考虑数据库的垂直拆分和水平拆分等;(4)选择合适的数据库引擎或类型以适应不同的场景,比如考虑引入NoSQL。8总结与建议性能调优也遵循2-8原则,80%的性能问题是由20%的代码产生的,所以优化关键代码更有效。同时性能优化要按需优化,过度优化可能会引入更多的问题。对于Java性能优化,不仅需要了解系统架构和应用代码,还需要关注JVM层乃至操作系统底层。总结起来,主要可以考虑以下几点:1)基础性能调优这里的基础性能指的是硬件层面或者操作系统层面的升级优化,比如网络调优、操作系统版本升级、硬件设备优化等.比如F5的使用和SDD硬盘的引入,包括新版Linux在NIO方面的升级,都可以极大促进应用的性能提升;2)数据库性能优化包括常见的事务拆分、索引调优、SQL优化、NoSQL的引入等,比如事务拆分时异步处理的引入,最后实现一致性的方法的引入,包括引入针对特定场景的各种NoSQL数据库,可以大大缓解传统数据库在高并发下的缺点;3)应用架构优化引入一些新的计算或存储框架,使用新特性解决原有的集群计算性能瓶颈等;或引入分布式策略对计算和存储进行分级,包括预计算和预处理等,采用典型的空间变化时间等做法;可以在一定程度上降低系统负载;4)业务层面的优化技术并不是提高系统性能的唯一手段。在很多出现性能问题的场景中,可以看出很大一部分是因为特殊的业务场景导致的,如果能够在业务中避免或者调整,往往是最有效的。