人工智能

导航菜单



当前位置: 首页  /  python  /  正文

JVM成神路之性能调优篇:GC调优、Arthas工具详解及线上最佳配置

阅读:25

JVM成神路之性能调优篇:GC调优、Arthas工具详解及线上最佳配置

引言

“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。Java语言作为企业应用中的“抗鼎者”,Java生态中也积攒了大量宝贵的性能优化经验。
在应用系统中,性能优化其实可以从各个角度出发考虑,如架构优化、前端调优、中间件调优、网关调优、容器调优、JVM调优、接口调优、服务器调优、数据库调优等,从优化类型上而言,主体可以分为三类:

本章则重点阐述Java中,JVM虚拟机相关的全面优化,如:内存、GC、即时编译、JVM参数配置等。

一、系统中性能优化的核心思维

建立在经验的基础之上才能做好的,对于调优要实事求是,任何的调优手段或技巧不要纸上谈兵,只有经过实践的才能用于生产环境,千万不要将一些没有实际依据的调优策略用于线上环境,否则可能会导致原本好好的程序反而调优调崩溃。

1.1、单个节点层面调优的核心思想

在一个程序中,所有的业务执行实体都为线程,应用程序的性能跟线程是直接挂钩的。而程序中的一条线程必须要经过CPU的调度才可执行,线程执行时必然也会需要数据、产生数据,最终也会和内存、磁盘打交道。因而单个节点的性能表现,不可避免的会跟CPU、内存、磁盘沾上关系。
线程越多,需要的CPU调度能力也就越强,需要的内存也越大,磁盘IO速率也会要求越快。因此CPU、内存、磁盘,这三者之间的任意之一达到了瓶颈,程序中的线程数量也会达到极限。达到极限后,系统的性能会成抛物线式下滑,从而可能导致系统整体性能下降乃至瘫痪。

由于如上原因,在考虑性能优化时,必然不能让CPU、内存、磁盘等资源的使用率达到95%+,一般而言,最大利用率控制在80-85%左右的最佳状态。

同时,前面也分析过,因为程序的性能跟线程挂钩,所以线程的模型也是影响性能的重要因素。目前程序设计中主要存在三种线程处理模型:BIO、NIO、AIO(NIO2),BIO是Java中传统的线程一对一处理模型,NIO的最佳实践为reactor模型,而proactor模型又作为了NIO2/AIO的落地者。绝大部分情况下,AIO的性能优于NIO,而NIO的性能又远超于BIO。

所以在做性能优化时,你应该要清楚系统的性能瓶颈在哪儿,到底是要调哪个位置?是线程模型?或是CPU调度?还是内存回收?亦是磁盘IO速率?针对不同层面有不同的优化方案,并非为了追求“热词/潮流”而盲目的调优。

1.2、优秀且适用的系统架构胜过千万次调优

一个单体架构(Tomcat+MySQL)部署的系统遇到性能问题时,能力再强,本事再大,任凭使出浑身解数也无法将其调到处理万级并发的程序,正常服务器部署的一台MySQL服务做到极致调优也难以在一秒内承载5000+的QPS。一味地追求极致的优化,其实也难以解决真正大流量下的并发冲击,因此一套优秀的系统架构胜过自己千万次的调优。

当然,也并非说项目实现时,越多的技术加进来越好,一套完善的分布式架构就必然比单体架构要好吗?其实也不见得,因为当引入的技术越多,所需要考虑的问题也会更多,耗费的成本也会越高,一个项目收益60W,结果用上最好的配置(高端的开发者+顶级的服务器+完善的分布式架构)成本耗费200W,这值得吗?答案显而易见。因此,并没有最好的技术架构,只有最适用的架构,能从现有环境及实际业务出发,选用最为合适的技术体系,这才是我们应该做的事情。如:

当你的系统原有架构遇到性能瓶颈时,你甚至可以考虑进一步做架构优化,如:设计多级分布式缓存、缓存中间件做集群、消息中间件做集群、Java程序做集群、数据库做分库分表、搜索中间件做集群.....,慢慢的,你的系统会越来越庞大复杂,需要处理的问题也更为棘手,但带来的效果也显而易见,随着系统的结构不断变化,承载百万级、千万级、亿级、乃至更大级别的流量也并非难事。

但只有当你的业务流量/访问压力在选用其他架构无法承载时,你才应该考虑更为庞大的架构。当然,如果项目在起步初期就有预估会承载巨大的流量压力,那么提前考虑也很在理,采用分布式/微服务架构也并非失策,因为对比其他架构体系而言,微服务架构的拓展性更为灵活。但也需要记住:分布式/微服务体系是很好,但它不一定适用于你的项目。

1.3、预防大于一切,调优并非“临时抱佛脚”

当问题出现时再想办法解决,这种策略永远都属于下下策,防范于未然才是最佳方案,提前防范问题出现主要可分为两个阶段:

对于项目初期的架构思考,值得牢记的一点是:不要“卡点”设计,也不能过度设计造成性能过剩,举例:

项目上线后的正常情况下,流量大概在“一木桶”左右,结果你设计时直接整出个“池塘”级别的结构出来了,这显然是不合理的,毕竟架构体系越庞大,项目的成本也自然就越高。
当然,也不能说正常情况下压力在“一木桶”左右,就只设计出一套仅能够承载“一木桶”流量的结构,这种“卡点”设计的策略也是不可取的,因为你需要适当考虑业务增长带来的风险,如果“卡点”设计,那么很容易让项目上线后,短期内就遭遇性能瓶颈。
因此,如果项目正常的访问压力大概在“桶”级别,那将结构设计到“缸”级别是合理的,这样即不必担心过度设计带来的性能过剩,导致成本增高;也无需考虑卡点设计造成的:项目短期遭遇性能瓶颈。
但设计时的这个度,必须由你自己根据项目的业务场景和环境去思量,不存在前篇一律的方法可教。

有人曾说过:“如果你可以根据业务情景设计出一套能确保业务增长,且在线上能稳定运行三年时间以上的结构,那你就是位业内的顶尖架构”,但老话说的好:“计划永远赶不上变化”,就算思考到业务的每个细节,也不可能设计出一套一劳永逸的结构出现,我们永远无法判断意外和明天哪个先来。因而,项目上线后,配备完善的监控警报系统也是必不可少的。不过值得注意的是:

监控系统的作用并不是用来提醒你项目“嗝屁”了的,而是用来提醒你:线上部署的应用系统可能会“嗝屁”或快“嗝屁”了,毕竟当项目灾难已经发生时再给警报,那到时候的情况就是:“亡羊补牢,为时已晚”。
通常情况下,在监控系统上面设置的性能阈值都会比最大极限值要低5~15%,如:最大极限值是85%,那设置告警值一般是75%左右就会告警,不会真达到85%才告警,只有这样做才能留有足够的时间让运维和开发人员介入排查。当系统发出可能“嗝屁”的警告时,开发和运维人员就应当立即排查相关的故障隐患,然后再通过不断地修改和优化,提前将可能会出现的性能瓶颈解决,这才是性能调优的正确方案。
因此,最终结论为:绝不能等到系统奔溃才去优化,预防胜于一切。

1.4、无需追求完美,理性权衡利弊

“追求极致,做到完美”这点是大部分开发者的通病,很多人会因为这个思想导致自己在面临一些问题时束手无策,比如举个例子:

业务:MacBookPro一元购活动,预计访问压力:10000QPS。
环境:单台机器只能承载2000QPS,目前机房中还剩余两台空闲服务器。
状况:此时就算将空闲的两台机器加上去,也无法顶住目前的访问压力。
此时你会怎么做?很多人都会茫然,这看起来好像是没办法的事情呀,似乎只能等死了.....

但事实真的如此吗?并非如此,其实这种情况也有多种解决方案,如:

这些方案是不是可以解决上面的哪个问题呢?答案是肯定的。但完美主义者会认为:
系统中的服务不能停啊,得保持正常服务啊。
用户的请求怎么能抛,用户的访问必须得响应啊。
但事实告诉你的是:类似于京东、淘宝、12306等这些国内的顶级大厂,也照样是这么干的。好比阿里,在双十一的时候都会抽调很多冷门业务的服务器资源给淘宝使用,也包括你在参与这些电商平台的抢购或秒杀类活动时,你是否遇到过如下情况:

如果当你遇到了这些情况,答案显而易见,你的请求压根就没有到后端,在前端就给你pass了,然后给你返回了一个字符串,让你傻傻的等待。

这个例子要告诉大家的是:在处理棘手问题或优化性能时,无需刻意追求完美,理性权衡利弊后,适当地做出一些决断,抛弃掉一部分不重要的,起码比整个系统挂掉要好,何况之后照样也可以恢复。

1.5、性能调优的通核心步骤

性能优化永远是建立在性能瓶颈之上的,如果你的系统没有出现瓶颈,那则无需调优,调优之前需要牢记的一点是:不要为了调优而调优,而是需要调优时才调
而发现性能瓶颈的方式有两种,一种是你的应用中具备完善的监控系统,能够提前感知性能瓶颈的出现。另一种则是:应用中没有搭载监控系统,性能瓶颈已经发生,从而导致应用频繁宕机。大型的系统一般都会搭载完善的监控系统,但大多数中小型项目却不具备该条件,因此,大部分中小型项目发现性能瓶颈时,大多数情况下已经“嗝屁”了。

通常而言,性能优化的步骤可分为如下几步:

本章则重点是阐述Java虚拟机-JVM相关的调优操作,但需要先提前说明的是:

单层面的性能调优其实只能当成锦上添花的作用,但绝对不能成为系统性能高/低、响应快/慢、吞吐量大/小的决定性要素。应用系统的性能本身就还算可以,那么调优的作用是让其性能更佳。但如若项目结构本身就存在问题,那么能够带来的性能提升也是有限的,如果你想让你的项目快到飞起,那么还需要从多个层面共同着手才能达到目的。

二、JVM垃圾收集相关调优策略

在JVM垃圾收集相关的调优实践中,通常都是以最优吞吐量和最短停顿时间来评价JVM的性能:吞吐量越高代表性能越好、暂停时间越短也代表越好。那么如何做到这两点呢?核心思想在于:

归根结底,本质思想就一点:“尽量让Java中的对象去到它自己该去的位置”,短命的对象就老老实实的进入新生代区域,大对象和长命的对象则进入年老代空间,避免JVM因为对象“乱窜”导致GC频发和GC时间变长,如:

因此,GC调优的目的就相当于给JVM做“保养”,让其每个区域按照设计的初衷正常工作。

通常情况下,当JVM存在性能问题时,都会牵扯到两个概念,分配速率(Allocation Rate)和提升速率(Promotion Rate),这也是分析性能问题时常用的两个指标,其中分配速率影响新生代的垃圾回收,提升速率影响年老代的垃圾回收。

2.1、新生代-分配速率(Allocation Rate)

分配速率代表固定时间内分配的内存量,通常情况下以MB/S为单位,分配速率高,其实并不是什么好事,对于这点我们稍后再做阐述。先来具体如何计算分配的速率。

2.1.1、分配速率如何计算?

一般而言可以通过GC日志计算出来,比如:

0.751: [GC (Allocation Failure) [PSYoungGen: 30705K->5115K(38400K)]
    30705K->12385K(125952K), 0.0187498 secs]
    [Times: user=0.00 sys=0.00, real=0.02 secs] 
1.514: [GC (Allocation Failure) [PSYoungGen: 38395K->5120K(71680K)]
    45665K->35687K(159232K), 0.0570688 secs] 
    [Times: user=0.09 sys=0.00, real=0.06 secs] 
3.018: [GC (Allocation Failure) [PSYoungGen: 70326K->5104K(71680K)]
    108940K->105240K(172032K), 0.0866792 secs] 
    [Times: user=0.30 sys=0.02, real=0.09 secs] 
复制代码

分配速率计算公式:(本轮GC前使用容量-上轮GC后使用容量)/(本轮GC时间-上轮GC时间)

GC轮数

时间差值

上轮GC后容量

本轮GC前容量

容量差值

分配速率

第一轮

751ms

0KB

30705KB

30705KB

≈41MB/S

第二轮

763ms

5115KB

38395KB

33280KB

≈44MB/S

第三轮

1504ms

5120KB

70326KB

65206KB

≈43MB/S

每轮均速

NULL

NULL

NULL

NULL

≈43MB/S

通过GC日志中的信息可以初步计算出,该Java程序中的对象分配速率大概在43MB/S左右。

2.1.2、分配速率对JVM的影响

前面曾提及过,分配速率高并不是好事,为什么这么说呢?因为Java程序的分配速率越高时,也代表着堆中分配的对象会越多,对象越多也就会让GC的频率更频繁。因此,当分配速率越高,会导致JVM的GC开销越大,分配速率的变化会增加或降低STW的频率,从而影响吞吐量。

但高分配速率的标准是相对而言的,要根据具体的Eden区大小来判断,一个堆大小为32GB的分配速率是1000MB/S,一个500MB的堆空间分配速率为100MB/S,前者可被称为是高分配速率吗?并非如此,因为前者的堆有32G,1000MB/S的速率也需要一段时间才能触发GC,但后者100MB/S的速率对于500M的堆空间而言,则可被称为高速率,因为对于500MB的堆空间而言,会在极短的时间内触发GC。因此,分配速率高低是要根据实际的堆大小来判断。

2.1.3、分配速率的四种状况

其中①为正常状况,无需做任何处理,也没必要去对于这类系统做刻意优化,如果你的Java应用的JVM处于该状态,但程序整体吞吐量依旧上不去,或响应速度缓慢,那应该从其他层面入手解决。

如果Java应用出现第③种情况,其实应用本身是没有任何问题的,这种情况一般是由于分配的堆空间不足,分配速率过快,导致频繁触发GC回收阈值,因此造成GC负载过重,对于这类情况应该适当调大堆空间,从而使GC频繁下降。

②、④则都是程序中存在隐患会出现的状况,通常情况下都是由于程序中存在不规范的代码导致的。状况②是因为代码在堆中生成了大量对象,造成分配速率很高,回收速度无法跟上分配速度,从而导致应用有可能内存溢出。
状况④则是明显的内存泄露问题,因为GC开销较大,但实际回收后释放的空间较小,代表内存中有大量对象无法回收,这可能是由于内存泄漏导致的。同时,也正因为GC次数比较频繁,所以导致应用中的用户线程暂停了工作,停止了对象分配,因而出现了分配速率低的“假象”。
对于②、④状况则需要优化代码,前者需要降低分配速率,后者则需要解决内存泄漏。

2.1.4、新生代空间调优思想

新生代空间的调优核心思想就是需要降低分配速率,简单来说就是少创建对象、多分配空间,以减少GC次数,加大系统吞吐量。但需要值得理解的是:为新生代分配更大的堆空间,反而会使分配速率提高,但新生代空间大了,触发GC的阈值自然会增加,从而能够达到减少GC频率的目的。

2.2、年老代-提升速率(Promotion Rate)

前面分析的分配速率仅会对新生代空间造成影响,而影响年老代空间的则是另外一个指标:提升速率,也就是指定时间内,新生代升入年老代空间的对象总量,通常单位也为MB/S。

在前面谈论分配速率时,可以根据GC日志计算新生代的分配占比,但新生代升入年老代空间的提升速率又该如何计算呢?因为MajorGC一般都是伴随着FullGC一起发生的,所以无法根据MajorGC计算,比较FullGC时会回收整堆空间。

2.2.1、提升速率如何计算?

同样计算提升速率时,依旧是通过MinorGC日志来计算:

1.514: [GC (Allocation Failure) [PSYoungGen: 38395K->5120K(71680K)]
    45665K->35687K(159232K), 0.0570688 secs] 
    [Times: user=0.09 sys=0.00, real=0.06 secs] 
3.018: [GC (Allocation Failure) [PSYoungGen: 70326K->5104K(71680K)]
    100894K->105240K(172032K), 0.0866792 secs] 
    [Times: user=0.30 sys=0.02, real=0.09 secs] 
复制代码

提升速率计算公式:((新生代回收前使用总量-新生代回收后使用总量)-(整堆回收前使用总量-整堆回收后使用总量))/(本轮GC时间-上轮GC时间)

GC轮数

时间差值

新生代减少

整堆减少

提升量

提升速率

第一轮

763ms

33275KB

9978KB

23297KB

≈30MB/S

第二轮

1504ms

65222KB

3700KB

61522KB

≈40MB/S

每轮均速

NULL

NULL

NULL

NULL

≈35MB/S

结果如上表,此刻是通过MinorGC日志来计算的提升速率,拆解前面的计算公式可以分析出整体的计算逻辑:

不过在计算提升速率的时候,有个点需要额外注意:Java应用启动后的第一条GC日志不能参与计算,因为第一条GC日志是程序启动后,初次触发GC时输出的,此时堆空间刚从“冷状态”启动,因此测算出的速率并非程序正常执行时的提升速率。

2.2.2、提升速率对JVM的影响

和分配速率相同,提升速率也一样会影响GC,但它影响的是年老代空间,速率越快也就代表着提升的对象越多,年老代空间被填满的时间会更短,MajorGC被触发的频率也会越快。不过通常情况下,年老代的GC一般会伴随着FullGC一起发生,因此,提升速率越高会最终导致FullGC频率越快。

2.2.3、进入年老代的三种异常情况

当代码中存在内存泄漏时,会造成堆内存被一点点蚕食,最终导致新生代空间没有空闲内存分配新对象,从而触发JVM的空间分代担保机制,开启对象动态晋升阈值判定,将大量原本未达晋升标准的对象提前迁入年老代空间,以确保新生代拥有足够的空闲内存维护Java应用的正常执行。
常发性内存泄漏、偶发性内存泄漏、一次性内存泄漏、隐式内存泄漏,不同性质的内存泄漏造成的提升速率增长也不同,后两者引发的速率增长并不大,但前两者,尤其是常发性内存泄漏会带来很大的隐患,最终必然会引发OOM。

在分代堆中有这么一条法则:“超过指定阈值的大对象会被直接送往年老代空间”,这条结论是依据对象特性而制定的,正常情况下,大对象都不会是“朝生夕死”的对象,一般都能够“活”到成功晋升。因此,为了节省大对象在两个Survivor区中反复挪动带来的开销,JVM会将超过阈值标准的大对象直接分配到年老代。
大对象直接进入年老代是合理的,但频繁的大对象分配是不合理的,会导致年老代被快速填满,因而频繁触发FullGC。
大对象直接进入年老代空间,因此大对象分配是不参与前述的提升速率计算公式的。

当系统业务暴涨时,巨大的流量和并发冲击会导致业务线程创建更多的新对象,因而会导致新生代的GC阈值被频繁触发,加快了新生代整体的晋升速度,从而导致提升速率暴涨。
对于这类正常业务增长导致的提升速率变高,这是系统中的常事,这种情况下只需依照具体业务流量的增长,合理的调大堆空间即可。

其实归根结底,上述三点都是在围绕着“对象被过早提升到年老代”这一核心思想展开。对于年老代而言,新生代空间中的所有对象,按部就班的活到15岁再晋升是最佳的状态,因为能够在新生代熬过十多轮GC的对象晋升后,绝大多数情况下会再存活很长一段时间。
但如果是由于上述三种状况导致对象过早提升到年老代空间,则会带来很大的不稳定因素,有可能很多提早晋升的对象刚晋升,没熬过几轮GC就“死”了,从而违背了“年老代存放长命对象”的设计初衷。同时,过早提升还会造成年老代会被快速填满,从而频繁触发FullGC,最终导致Java应用暂停时间过长,影响系统整体的吞吐量。

2.2.4、年老代空间调优思想

年老代空间调优的核心就一点:避免或尽量减少过早提升,为何不是降低提升速率呢?因为在业务规模比较大的情况下,提升速率比较高也是合理的。所以在调优年老代时,只需要将过早提升的对象依旧控制在新生代即可。

过早提升的表现

过早提升如何解决?

处理过早提升时,需要根据具体的情况来决定采取何种措施:

2.3、合理的堆空间该如何分配

Java内存各分区的大小对JVM的性能影响很大,不恰当的空间大小可能会埋下很多故障隐患,同时也会直接或间接影响JVM的提升速率、分配速率,所以如何将各分区调整到合适的大小就成了一个棘手的问题。大部分不具备线上JVM调优实操经验的开发者都会茫然,通常会认为设定的越大越好,但答案却并非如此。
在指定各区域大小时,可以依据“活跃数据”大小来进行设定,“活跃数据”是指应用程序稳定运行后长期存活在堆中的对象,也就是FullGC后年老代中的对象。一般在计算“活跃数据大小”,都会多次采集程序稳定执行后的FullGC日志,通过取平均值的方式计算出堆中长期存活的年老代总量大小。
计算出“活跃数据大小”后,就可以根据其具体值计算出其他分区恰当的值,比例如下:

假设此时观测出的“活跃数据大小”为800MB,那堆空间的各区域的大小:

当然,这仅作为初始值参考,具体情况取决于应用业务的特性和需求。

但需注意的是:实际过程中,-Xmx、-Xms两个参数设定的值必须一致,这样做的好处在于可以避免动态伸缩时带来的性能损耗与空间震荡,因为当JVM内存不足向OS申请内存时都会触发一次全局GC。

2.4、GC调优实操思路

前面几点所提及的都是GC调优的一些方法论以及衡量指标,但在真正需要处理GC调优时,上面几点只能给你提供辅导,并不能建立完善的调优思路,因此,接下来再一同论述GC调优的具体实操思想。

GC调优时,一般会根据Java程序所装配的垃圾收集器以及具体的GC日志来作为基础进行操作,但不同的垃圾回收器执行的GC日志都是不同的,因此并没有万能的调优策略可以满足所有的性能指标,GC优化要建立在具体的业务场景及环境中,才能达到事半功倍的效果。不过通常GC调优核心步骤如下:

调优前首先需要确定的就是优化目标,到底是需要减少GC停顿,还是增大程序吞吐等,然后再根据目标排除GC日志,分析后根据日志中的分配速率、提升速率、GC频率、GC各阶段停顿时间等指标,实行具体的优化操作。

同时,也不必奢求一次优化到位,GC调优通常是需要多次进行的,一次优化往往无法达到目标预期,需要不断的根据优化后的GC日志再次制定优化策略,从而最终达到优化目标。

但GC调优的根本其实是在调“对象”,如果程序本身代码就存在问题,好比代码中存在频繁创建对象的逻辑,就算你调出花来也无济于事,必须还得从根源上解决问题,这种情况下应当采用jmap工具分析堆使用情况,查看对象分布,从而反向定位代码中的问题并加以解决。

2.5、GC优化总结

凡是涉及性能调优的内容,几乎都必须建立在监控系统之上,不一定要全面,但至少能让调优前有指标数据可参考。对于监控系统中,JVM-GC这块建议统计的信息:

GC调优时的收益排序:改善代码 > 装配合适的GC回收器 > 重新设置内存比例/大小 > 调整JVM参数。

但需重点注意的是:上述的GC调优理论都是基于G1之前的分代垃圾收集器而言的,G1之后的不分代收集器,如:ZGC、ShenandoahGC等压根没必要刻意优化,自身的机制本就足够优异,而且后续的不分代收集器对外暴露的可操作参数也并不多。

三、阿里在线排除工具 - Arthas


Arthas(阿尔萨斯)是阿里开源的一款Java在线诊断工具,官网原话:当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

Arthas支持JDK6+,支持Linux/Mac/Winodws,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。

3.1、Arthas快速上手

对于Arthas工具如果不会使用,其实阿里提供的在线的Terminal学习方式(传送门),可以帮助大家快速上手,下面在本篇中也快速概述一下。

依照官方的案例演示,先下载并启动提供好的Java案例:

$ wget https://arthas.aliyun.com/math-game.jar
$ java -jar math-game.jar
复制代码

再启动一个新的Terminal窗口,下载并启动Arthas工具:

$ wget https://arthas.aliyun.com/arthas-boot.jar
$ java -jar arthas-boot.jar
复制代码

紧接着Arthas会将本机中所有的Java进程查询出来,类似于jps/ps的作用:

[INFO] arthas-boot version: 3.5.5
[INFO] Found existing java process.......
* [1]: 161 math-game.jar
复制代码

如果你的机器中启动了多个Java应用,此时会查询出来一个应用列表,我们可以根据前面的序号选择自己要操作的Java应用,如上情况中,再输入1即可:

$ 1
复制代码

最终,Arthas成功启动,接下来再通过Arthas提供的指令进行操作即可:

3.2、Arthas命令详解

Arthas从最初的发布开始,随着后续社区的活跃性增强及用户群体的不断壮大,指令也越发完善与丰富,至目前为止提供了基础命令、JVM命令、class命令以及字节码增强命令等几大类。

3.2.1、基础命令

3.2.2、类命令

3.2.3、JVM命令

3.2.4、字节码增强命令

3.2.5、Arthas的OGNL表达式

Arthas中的很多进阶操作都需要依赖于OGNL表达式进行编写,因此想要玩转Arthas,自然需要对于OGNL也具备一定的基本功,接下来演示一些常规操作,详细的使用方式可参考:官方指南、特殊用法。

①、调用静态属性

ognl '@类的全限定名@静态属性名'

示例:

[arthas@80573]$ ognl '@demo.MathGame@random'
复制代码

②、调用静态方法

ognl '@类的全限定名@静态方法名("参数")'

示例1:调用入参为基本数据类型和集合的方法:

[arthas@80573]$ ognl '@demo.MathGame@print(100,{1,2,3,4})' -x 1
null
复制代码

示例2:调用入参为对象类型的方法:

[arthas@80573]$ ognl '#obj=new java.lang.Object(),@xxx.xxx@xxx(#obj)' -x 1
复制代码

示例3:调用入参为Map类型的方法:

[arthas@80573]$ ognl '#map={"k1":"v1","k2":"v2"},@xxx.xxx@xxx(#map)' -x 1
复制代码

示例4:将一个方法的执行结果作为另一个方法的入参:

[arthas@80573]$ ognl '#result=@xx.xx@A(),@xx.xx@xx(#result)' -x 1
复制代码

③、调用构造方法

ognl 'new 类的全限定名()'

示例1:调用无参创建对象

[arthas@80573]$ ognl 'new java.lang.Object()'
复制代码

示例2:调用有参创建对象

[arthas@80573]$ ognl 'new xxx.xx.xxx("xx",x,{1,2,3})'
复制代码

示例3:调用存在对象引用类型的构造函数创建对象

[arthas@80573]$ ognl '#obj=new new java.lang.Object(),new xxx.xx.xxx(#obj)'
复制代码

④、读取不同类型的值

示例1:读取引用对象类型的属性值

[arthas@80573]$ ognl '@类全限定名@方法名("参数").属性名称'
复制代码

示例2:读取List类型的指定元素

[arthas@80573]$ ognl '@类全限定名@方法名("参数")[下标]'
复制代码

示例3:读取Map类型的指定元素

[arthas@80573]$ ognl '@类全限定名@方法名("参数")["key"]'
复制代码

⑤.........

详细的OGNL语法可参考:官方指南,在线上排查时往往会结合tt、watch、monitor、stack、trace等多个命令共同使用。

3.3、Arthas线上常用场景

Arthas中集成了大部分JDK工具的功能实现,因此,在线上情况时,可以通过它快速的帮助我们解决问题,如CPU占用过高、线程阻塞、死锁、代码动态修改、方法执行缓慢、排查404等。

3.3.1、排查CPU占用过高问题

3.3.2、排查线程阻塞问题

3.3.3、排查死锁问题

3.3.4、排查方法执行过慢问题

3.3.5、动态修改线上代码

有些项目编译可能需要两小时,好容易编译完成上线之后,发现代码有一处小地方存在逻辑错误需要更改,此时难度需要重新将其下线,重新更改后打包部署吗?有了Arthas之后的你完全不需要这样干。

这个功能是Arthas非常实用的一个功能,往往在线上环境被用于代码纠错、日志级别修改、Java配置文件修改等场景。

3.3.6、...........

显然,Arthas还有更多的应用场景等待你去探索,根据不同的业务场景以及遇到的不同问题,利用Arthas都可以实现很好的排查与解决,上述中仅列出一些常见的应用场景。

四、不同场景下的最佳配置推荐

线上JVM的最佳参数配置往往要根据实际的业务场景以及运行环境进行思量,首先需要弄明白业务是追求响应速度还是吞吐量,再者需要结合所部署的硬件配置及服务器环境综合考虑,下面提供一些配置参数给予大家用作参考。

4.1、运行时数据区

4.1.1、堆空间

之前曾提及到,运行时数据区最佳的空间大小,以“活跃数据大小”进行作为基础参考,然后进行设置:


无论你的项目是追求响应速度,亦或是吞吐量,都可根据“活跃数据”计算的大小作为基础进行调整,依照“活跃数据”计算出的大小也恰巧能够符合Sun公司官方给出的推荐,如:

新生代空间的最佳占比应当在堆总大小的3/8,换算成百分比为37.5%。
通过上图中根据“活跃数据”获取的各分区大小进行计算:
1200MB(Eden)/3200MB(Heap)=0.375(37.5%),和官方的推荐完全一致。

那么实际项目上线时,“活跃数据大小”如何获取呢?可以在测试阶段进行压测,然后通过GC日志进行计算。不过基于“活跃数据”计算出的大小也可以根据业务进行调整。

1.8及以上版本的JDK大多数情况下,只需要调整好每个分区的大小即可,其他的优化参数,大多数JVM都会默认开启。
-Xms、-Xmx两参数的值需保持一致,防止由于内存动态伸缩时造成抖动影响性能。

4.2、元空间

元空间的大小建议:一般在“活跃数据”的1.2倍左右足够,如果程序内使用大量动态代理,可以尝试加大到1.5、1.8倍。

4.3、栈空间

HotSpot中,Java虚拟机栈和本地方法栈合二为一了,因此这里的栈空间涵盖了这两个概念。
JDK1.5之前默认栈大小为256K,1.5之后默认为1M大小,对于该值的调整要基于业务来决定,如果业务执行时,方法调用链不会太长,可以适当缩小到512k,即-Xss512K,这样做的好处在于:在物理内存相同的情况下,该值越小,程序中就能产生更多的线程,从而能够拥有更多的线程处理客户端到来的请求。

但操作系统不可能允许一个进程无限制的创建线程,因此单个进程中的线程数量一般最多控制3000~5000最佳。

4.2、GC垃圾收集

GC方面也是JVM调优中“操作性”最大的部分,因此,这部分在JVM调优额外重要。

4.2.1、选择垃圾收集器

选用合适的垃圾收集器往往能够让你的应用性能提升一大截,但合适的收集器也需要根据运行环境及业务场景去选择,那如何选择最合适的收集器呢?

4.2.2、ParNew+CMS组合参数推荐

4.2.3、ParallelScavenge+ParallelOld组合参数推荐

4.2.4、G1整堆收集器参数推荐

4.3、性能激进优化策略

在JDK1.7及其之后的版本中,JVM推出了很多激进优化的策略,但在1.8及其之后的环境中,大部分的参数都是默认开启的,因此我们没有必要显式再次开启。但其实JVM中的一些激进优化参数默认也并未打开,如果你的程序堆空间足够大,也可以尝试开启后优化程序性能。

4.4、不同的启动方式参数设置方式

五、总结

对于性能优化这个内容而言,没有绝对正确或最佳的参数,也包括本章的内容你可以适当参考但不能照搬于生产环境,安全第一,项目能够稳定执行是根本,性能优化永远要建立在应用健康运转但遭遇瓶颈的基础上,不要随便调优,更不要刻意调优。

同时,对于JDK不同版本中的默认值,如果你不清楚其具体作用,那建议保留默认值,毕竟JDK默认将其设为此值总有它的理由,默认值至少能够满足绝大部分的项目需求。因此,如若你没有丰富的激进优化经验,再次重申:不要随意更改一些性能参数的默认值。

标签

JVM成神性能GCArthas工具详解最佳配置


相关文章列表

【JAVA虚拟机】全面解析:JVM、堆、GC、直接内存、性能调优大揭秘

【JAVA虚拟机】全面解析:JVM、堆、GC、直接内存、性能调优大揭秘

一、JVM(JAVA虚拟机)JVM(Java虚拟机):是一个抽象的计算模型。如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境,...


深入了解Java的GC原理,掌握JVM 性能调优!

深入了解Java的GC原理,掌握JVM 性能调优!

对于 Java 开发人员来说,进行程序的性能优化是很有挑战的工作,也是很有意义的一件事。本篇主要根据 JVM 内存模型和垃圾回收的详细讲解,可以更好的理解JVM的调优的根本原理。JVM内存模型JVM ...


JVM全面详解:执行引擎子系统、JIT即时编译原理与分派实现

JVM全面详解:执行引擎子系统、JIT即时编译原理与分派实现

引言执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。但虚拟机与物理机最大的不...


Java垃圾回收机制GC全面指南,深入解析JVM运行原理

Java垃圾回收机制GC全面指南,深入解析JVM运行原理

1、GC过程1)先判断对象是否存活(是否是垃圾)可以通过引用计数算法和可达性分析算法来判断,由于引用计数算法无法解决循环引用的问题,所以目前使用的都是可达性分析算法2)再遍历并回收对象(回收垃圾)可以...


Java性能优化指南:精华笔记分享 | 第二版性能分析工具详解

Java性能优化指南:精华笔记分享 | 第二版性能分析工具详解

1. 性能分析工具1.1. 必须有足够大的堆来处理数据1.2. 运行性能分析工具时开启并发GC算法1.3. 不合时宜的Full GC暂停会导致缓冲区的数据溢出1.4. 性能分析的一个缺陷就是在应用程序...


8个用于数据挖掘的最佳开源工具

8个用于数据挖掘的最佳开源工具

在机器学习的流程中数据挖掘是重要的一环。数据挖掘是从大量数据中提取隐藏的或未知,但可能有用信息的过程。这些数据最终会被加上标签,用于模型的训练。很多的数据科学家和机器学习工程师都有其熟悉的数据挖掘工具...


友情链接