面试官:说说你对Java服务的调优都有哪些?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 面试官:说说你对Java服务的调优都有哪些?

Java应用性能优化是一个老生常谈的话题,典型的性能问题如页面响应慢、接口超时,服务器负载高、并发数低,数据库频繁死锁等。尤其是在“糙快猛”的互联网开发模式大行其道的今天,随着系统访问量的日益增加和代码的臃肿,各种性能问题开始纷至沓来。Java应用性能的瓶颈点非常多,比如磁盘、内存、网络I/O等系统因素,Java应用代码,JVM GC,数据库,缓存等。笔者根据个人经验,将Java性能优化分为4个层级:应用层、数据库层、框架层、JVM层,如图1所示。image.png

每层优化难度逐级增加,涉及的知识和解决的问题也会不同。比如应用层需要理解代码逻辑,通过Java线程栈定位有问题代码行等;数据库层面需要分析SQL、定位死锁等;框架层需要懂源代码,理解框架机制;JVM层需要对GC的类型和工作机制有深入了解,对各种JVM参数作用了然于胸。围绕Java性能优化,有两种最基本的分析方法:现场分析法和事后分析法。

现场分析法通过保留现场,再采用诊断工具分析定位。现场分析对线上影响较大,部分场景(特别是涉及到用户关键的在线业务时)不太合适。

事后分析法需要尽可能多收集现场数据,然后立即恢复服务,同时针对收集的现场数据进行事后分析和复现。下面我们从性能诊断工具出发,分享搜狗商业平台在其中的一些案例与实践。

性能诊断工具

性能诊断一种是针对已经确定有性能问题的系统和代码进行诊断,还有一种是对预上线系统提前性能测试,确定性能是否符合上线要求。本文主要针对前者,后者可以用各种性能压测工具(例如JMeter)进行测试,不在本文讨论范围内。针对Java应用,性能诊断工具主要分为两层:OS层面和Java应用层面(包括应用代码诊断和GC诊断)。

OS 诊断

OS的诊断主要关注的是CPU、Memory、I/O三个方面。

CPU 诊断

对于CPU主要关注平均负载(Load Average),CPU使用率,上下文切换次数(Context Switch)。

通过top命令可以查看系统平均负载和CPU使用率,图2为通过top命令查看某系统的状态。image.png

平均负载有三个数字:63.66,58.39,57.18,分别表示过去1分钟、5分钟、15分钟机器的负载。

按照经验,若数值小于0.7*CPU个数,则系统工作正常;若超过这个值,甚至达到CPU核数的四五倍,则系统的负载就明显偏高。

图2中15分钟负载已经高达57.18,1分钟负载是63.66(系统为16核),说明系统出现负载问题,且存在进一步升高趋势,需要定位具体原因了。

通过vmstat命令可以查看CPU的上下文切换次数,如图3所示:image.png上下文切换次数发生的场景主要有如下几种:

1)时间片用完,CPU正常调度下一个任务;

2)被其它优先级更高的任务抢占;

3)执行任务碰到I/O阻塞,挂起当前任务,切换到下一个任务;

4)用户代码主动挂起当前任务让出CPU;

5)多任务抢占资源,由于没有抢到被挂起;

6)硬件中断。

Java线程上下文切换主要来自共享资源的竞争。一般单个对象加锁很少成为系统瓶颈,除非锁粒度过大。

但在一个访问频度高,对多个对象连续加锁的代码块中就可能出现大量上下文切换,成为系统瓶颈。

比如在我们系统中就曾出现log4j 1.x在较大并发下大量打印日志,出现频繁上下文切换,大量线程阻塞,导致系统吞吐量大降的情况,其相关代码如清单1所示,升级到log4j 2.x才解决这个问题。

清单 1. log4j 1.x 同步代码片段

for(Category c = this; c != null; c=c.parent) {
     // Protected against simultaneous call to addAppender, removeAppender,…
     synchronized(c) {
         if (c.aai != null) {
         write += c.aai.appendLoopAppenders(event);
     }
     }
}

Memory

从操作系统角度,内存关注应用进程是否足够,可以使用free –m命令查看内存的使用情况。

通过top命令可以查看进程使用的虚拟内存VIRT和物理内存RES,根据公式VIRT = SWAP + RES可以推算出具体应用使用的交换分区(Swap)情况,使用交换分区过大会影响Java应用性能,可以将swappiness值调到尽可能小。

因为对于Java应用来说,占用太多交换分区可能会影响性能,毕竟磁盘性能比内存慢太多。

I/O

I/O包括磁盘I/O和网络I/O,一般情况下磁盘更容易出现I/O瓶颈。通过iostat可以查看磁盘的读写情况,通过CPU的I/Owait可以看出磁盘I/O是否正常。

如果磁盘I/O一直处于很高的状态,说明磁盘太慢或故障,成为了性能瓶颈,需要进行应用优化或者磁盘更换。

除了常用的top、ps、vmstat、iostat等命令,还有其他Linux工具可以诊断系统问题,如mpstat、tcpdump、netstat、pidstat、sar 等。Brendan总结列出了Linux不同设备类型的性能诊断工具,如图4所示,可供参考。image.png

Java 应用诊断工具

应用代码诊断

应用代码性能问题是相对好解决的一类性能问题。通过一些应用层面监控报警,如果确定有问题的功能和代码,直接通过代码就可以位;

或者通过top+jstack,找出有问题的线程栈,定位到问题线程的代码上,也可以发现问题。

对于更复杂,逻辑更多的代码段,通过Stopwatch打印性能日志往往也可以定位大多数应用代码性能问题。

常用的Java应用诊断包括线程、堆栈、GC等方面的诊断。

jstack

jstack命令通常配合top使用,通过top -H -p pid定位Java进程和线程,再利用jstack -l pid导出线程栈。

由于线程栈是瞬态的,因此需要多次dump,一般3次dump,一般每次隔5s就行。

将top定位的Java线程pid转成16进制,得到Java线程栈中的nid,可以找到对应的问题线程栈。

image.pngJProfiler

JProfiler可对CPU、堆、内存进行分析,功能强大,如图7所示。同时结合压测工具,可以对代码耗时采样统计。

image.png

GC 诊断

Java GC解决了程序员管理内存的风险,但GC引起的应用暂停成了另一个需要解决的问题。JDK提供了一系列工具来定位GC问题,比较常用的有jstat、jmap,还有第三方工具MAT等。

jstat

jstat命令可打印GC详细信息,Young GC和Full GC次数,堆信息等。其命令格式为jstat –gcxxx -t pid,如图8所示。image.pngjmap

jmap打印Java进程堆信息jmap –heap pid。通过jmap –dump:file=xxx pid可dump堆到文件,然后通过其它工具进一步分析其堆使用情况。

MAT

MAT是Java堆的分析利器,提供了直观的诊断报告,内置的OQL允许对堆进行类SQL查询,功能强大,outgoing reference和incoming reference可以对对象引用追根溯源。image.png图9是MAT使用示例,MAT有两列显示对象大小,分别是Shallow size和Retained size,前者表示对象本身占用内存的大小,不包含其引用的对象,后者是对象自己及其直接或间接引用的对象的Shallow size之和,即该对象被回收后 GC 释放的内存大小,一般说来关注后者大小即可。

对于有些大堆 (几十G) 的Java应用,需要较大内存才能打开MAT。通常本地开发机内存过小,是无法打开的,建议在线下服务器端安装图形环境和MAT,远程打开查看。或者执行mat命令生成堆索引,拷贝索引到本地,不过这种方式看到的堆信息有限。

为了诊断GC问题,建议在JVM参数中加上-XX:+PrintGCDateStamps。常用的GC参数如图10所示。

image.png对于Java应用,通过top+jstack+jmap+MAT可以定位大多数应用和内存问题,可谓必备工具。有些时候,Java应用诊断需要参考OS相关信息,可使用一些更全面的诊断工具,比如Zabbix(整合了OS和JVM监控)等。在分布式环境中,分布式跟踪系统等基础设施也对应用性能诊断提供了有力支持。

性能优化实践

在介绍了一些常用的性能诊断工具后,下面将结合我们在Java应用调优中的一些实践,从JVM层、应用代码层以及数据库层进行案例分享。

JVM 调优:GC 之痛

搜狗商业平台某系统重构时选择RMI作为内部远程调用协议,系统上线后开始出现周期性的服务停止响应,暂停时间由数秒到数十秒不等。

通过观察GC日志,发现服务自启动后每小时会出现一次Full GC。由于系统堆设置较大,Full GC一次暂停应用时间会较长,这对线上实时服务影响较大。

经过分析,在重构前系统没有出现定期Full GC的情况,因此怀疑是RMI框架层面的问题。

通过公开资料,发现RMI的GDC(Distributed Garbage Collection,分布式垃圾收集)会启动守护线程定期执行 Full GC来回收远程对象,清单 中展示了其守护线程代码。

清单 2.DGC 守护线程源代码

private static class Daemon extends Thread {
 public void run() {
 for (;;) {
     //…
 long d = maxObjectInspectionAge();
 if (d >= l) {
    System.gc();
 d = 0;
 }
 //…
 }
     }
}

定位问题后解决起来就比较容易了。一种是通过增加-XX:+DisableExplicitGC参数,直接禁用系统GC的显示调用,但对使用NIO的系统,会有堆外内存溢出的风险。

另一种方式是通过调大-Dsun.rmi.dgc.server.gcInterval和-Dsun.rmi.dgc.client.gcInterval参数,增加Full GC间隔,同时增加参数-XX:+ExplicitGCInvokesConcurrent,将一次完全Stop-The-World的Full GC调整为一次并发GC周期,减少应用暂停时间,同时对NIO应用也不会造成影响。从图11可知,调整之后的Full GC次数在3月之后明显减少。image.pngGC调优对高并发大数据量交互的应用还是很有必要的,尤其是默认JVM参数通常不满足业务需求,需要进行专门调优。GC日志的解读有很多公开的资料,本文不再赘述。

GC调优目标基本有三个思路:降低GC频率,可以通过增大堆空间,减少不必要对象生成;降低GC暂停时间,可以通过减少堆空间,使用CMS GC算法实现;避免Full GC,调整CMS触发比例,避免Promotion Failure和Concurrent mode failure(老年代分配更多空间,增加GC线程数加快回收速度),减少大对象生成等。

应用层调优:嗅到代码的坏味道

从应用层代码调优入手,剖析代码效率下降的根源,无疑是提高Java应用性能的很好的手段之一。

某商业广告系统(采用Nginx进行负载均衡)某次日常上线后,其中有几台机器负载急剧升高,CPU使用率迅速打满。我们对线上进行了紧急回滚,并通过jmap和jstack对其中某台服务器的现场进行保存。image.png堆栈现场如图12所示,根据MAT对dump数据的分析,发现最多的内存对象为byte[]和java.util.HashMap,且Entry对象存在循环引用。

初步定位在该HashMap的put过程中有可能出现了死循环问题(图中 java.util.HashMap$Entry 0x2add6d992cb8和0x2add6d992ce8的next引用形成循环)。

查阅相关文档定位这属于典型的并发使用的场景错误 (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6423457) ,简要的说就是HashMap本身并不具备多线程并发的特性,在多个线程同时put操作的情况下,内部数组进行扩容时会导致HashMap的内部链表形成环形结构,从而出现死循环。

针对此次上线,最大的改动在于通过内存缓存网站数据来提升系统性能,同时使用了懒加载机制,如清单3所示。

清单 3. 网站数据懒加载代码

private static Map<Long, UnionDomain> domainMap = new HashMap<Long, UnionDomain>();
    private boolean isResetDomains() {
        if (CollectionUtils.isEmpty(domainMap)) {
            // 从远端 http 接口获取网站详情
            List<UnionDomain> newDomains = unionDomainHttpClient
                    .queryAllUnionDomain();
            if (CollectionUtils.isEmpty(domainMap)) {
                domainMap = new HashMap<Long, UnionDomain>();
                for (UnionDomain domain : newDomains) {
                    if (domain != null) {
                        domainMap.put(domain.getSubdomainId(), domain);
                    }
                }
            }
            return true;
        }
        return false;
    }

可以看到此处的domainMap为静态共享资源,它是HashMap类型,在多线程情况下会导致其内部链表形成环形结构,出现死循环。

通过对前端Nginx的连接和访问日志可以看到,由于在系统重启后Nginx积攒了大量的用户请求,在Resin容器启动,大量用户请求涌入应用系统,多个用户同时进行网站数据的请求和初始化工作,导致HashMap出现并发问题。

在定位故障原因后解决方法则比较简单,主要的解决方法有:

(1)采用ConcurrentHashMap或者同步块的方式解决上述并发问题;

(2)在系统启动前完成网站缓存加载,去除懒加载等;

(3)采用分布式缓存替换本地缓存等。

对于坏代码的定位,除了常规意义上的代码审查外,借助诸如MAT之类的工具也可以在一定程度对系统性能瓶颈点进行快速定位。

但是一些与特定场景绑定或者业务数据绑定的情况,却需要辅助代码走查、性能检测工具、数据模拟甚至线上引流等方式才能最终确认性能问题的出处。

以下是我们总结的一些坏代码可能的一些特征,供大家参考:(1)代码可读性差,无基本编程规范;(2)对象生成过多或生成大对象,内存泄露等;(3)IO流操作过多,或者忘记关闭;(4)数据库操作过多,事务过长;(5)同步使用的场景错误;(6)循环迭代耗时操作等。

数据库层调优:死锁噩梦

对于大部分Java应用来说,与数据库进行交互的场景非常普遍,尤其是OLTP这种对于数据一致性要求较高的应用,数据库的性能会直接影响到整个应用的性能。

搜狗商业平台系统作为广告主的广告发布和投放平台,对其物料的实时性和一致性都有极高的要求,我们在关系型数据库优化方面也积累了一定的经验。

对于广告物料库来说,较高的操作频繁度(特别是通过批量物料工具操作)很极易造成数据库的死锁情况发生,其中一个比较典型的场景是广告物料调价。客户往往会频繁的对物料的出价进行调整,从而间接给数据库系统造成较大的负载压力,也加剧了死锁发生的可能性。

下面以搜狗商业平台某广告系统广告物料调价的案例进行说明。

某商业广告系统某天访问量突增,造成系统负载升高以及数据库频繁死锁,死锁语句如图13所示。image.png其中,groupdomain表上索引为idx_groupdomain_accountid (accountid),idx_groupdomain_groupid(groupid),primary(groupdomainid) 三个单索引结构,采用Mysql innodb引擎。

此场景发生在更新组出价时,场景中存在着组、组行业(groupindus 表)和组网站(groupdomain 表)。

当更新组出价时,若组行业出价使用组出价(通过isusegroupprice 标示,若为1则使用组出价)。同时若组网站出价使用组行业出价(通过isuseindusprice标示,若为1则使用组行业出价)时,也需要同时更新其组网站出价。由于每个组下面最大可以有3000个网站,因此在更新组出价时会长时间的对相关记录进行锁定。

从上面发生死锁的问题可以看到,事务1和事务2均选择了idx_groupdomain_accountid的单列索引。根据Mysql innodb引擎加锁的特点,在一次事务中只会选择一个索引使用,而且如果一旦使用二级索引进行加锁后,会尝试将主键索引进行加锁。

进一步分析可知事务1在请求事务2持有的idx_groupdomain_accountid二级索引加锁(加锁范围“space id 5726 page no 8658 n bits 824 index”),但是事务2已获得该二级索引 (“space id 5726 page no 8658 n bits 824 index”) 上所加的锁,在等待请求锁定主键索引PRIMARY索引上的锁。由于事务2等待执行时间过长或长时间不释放锁,导致事务1最终发生回滚。

通过对当天访问日志跟踪可以看到,当天有客户通过脚本方式发起大量的修改推广组出价的操作,导致有大量事务在循环等待前一个事务释放锁定的主键PRIMARY索引。

该问题的根源实际上在于Mysql innodb引擎对于索引利用有限,在Oracle数据库中此问题并不突出。解决的方式自然是希望单个事务锁定的记录数越少越好,这样产生死锁的概率也会大大降低。最终使用了(accountid, groupid)的复合索引,缩小了单个事务锁定的记录条数,也实现了不同计划下的推广组数据记录的隔离,从而减少该类死锁的发生几率。

通常来说,对于数据库层的调优我们基本上会从以下几个方面出发:

(1)在SQL语句层面进行优化:慢SQL分析、索引分析和调优、事务拆分等;

(2)在数据库配置层面进行优化:比如字段设计、调整缓存大小、磁盘I/O等数据库参数优化、数据碎片整理等;

(3)从数据库结构层面进行优化:考虑数据库的垂直拆分和水平拆分等;

(4)选择合适的数据库引擎或者类型适应不同场景,比如考虑引入NoSQL等。

总结与建议

性能调优同样遵循2-8原则,80%的性能问题是由20%的代码产生的,因此优化关键代码事半功倍。

同时,对性能的优化要做到按需优化,过度优化可能引入更多问题。对于Java性能优化,不仅要理解系统架构、应用代码,同样需要关注JVM层甚至操作系统底层。

总结起来主要可以从以下几点进行考虑:

1)基础性能的调优

这里的基础性能指的是硬件层级或者操作系统层级的升级优化,比如网络调优,操作系统版本升级,硬件设备优化等。比如F5的使用和SDD硬盘的引入,包括新版本Linux在NIO方面的升级,都可以极大的促进应用的性能提升;

2)数据库性能优化

包括常见的事务拆分,索引调优,SQL优化,NoSQL引入等,比如在事务拆分时引入异步化处理,最终达到一致性等做法的引入,包括在针对具体场景引入的各类NoSQL数据库,都可以大大缓解传统数据库在高并发下的不足;

3)应用架构优化

引入一些新的计算或者存储框架,利用新特性解决原有集群计算性能瓶颈等;或者引入分布式策略,在计算和存储进行水平化,包括提前计算预处理等,利用典型的空间换时间的做法等;都可以在一定程度上降低系统负载;

4)业务层面的优化

技术并不是提升系统性能的唯一手段,在很多出现性能问题的场景中,其实可以看到很大一部分都是因为特殊的业务场景引起的,如果能在业务上进行规避或者调整,其实往往是最有效的。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
28天前
|
缓存 算法 Java
Java 实现的局域网管控软件的性能调优
局域网管控软件在企业网络管理中至关重要,但随着网络规模扩大和功能需求增加,其性能可能受影响。文章分析了数据处理效率低下、网络通信延迟和资源占用过高等性能瓶颈,并提出了使用缓存、优化算法、NIO库及合理管理线程池等调优措施,最终通过性能测试验证了优化效果,显著提升了软件性能。
34 1
|
1月前
|
算法 Java 测试技术
java性能调优涉及哪些方面
本文详细探讨了性能调优的各个方面,包括Java编程、多线程、JVM监控、设计模式和数据库调优。文章还介绍了性能调优的标准制定、介入时机、系统性能的影响因素,以及如何衡量和判断系统的性能与负载承受能力。最后,提出了性能调优的具体策略,包括代码、设计、算法优化及参数调整,并讨论了限流、智能化扩容等兜底策略。
java性能调优涉及哪些方面
|
8天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
9天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
30 6
|
5天前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
10天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
33 4
|
16天前
|
监控 前端开发 Java
Java SpringBoot –性能分析与调优
Java SpringBoot –性能分析与调优
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
410 37
|
1月前
|
监控 Java Linux
Java 性能调优:调整 GC 线程以获得最佳结果
Java 性能调优:调整 GC 线程以获得最佳结果
69 11