引言
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。Java语言作为企业应用中的“抗鼎者”,Java生态中也积攒了大量宝贵的性能优化经验。
在应用系统中,性能优化其实可以从各个角度出发考虑,如架构优化、前端调优、中间件调优、网关调优、容器调优、JVM调优、接口调优、服务器调优、数据库调优等,从优化类型上而言,主体可以分为三类:
- ①结构/架构优化:优化应用系统整体架构做到性能提升的目的。如:读写分离、集群热备、分布式架构、引入缓存/消息/搜索中间件、分库分表、中台架构(大数据中台、基础设施中台)等。
- ②配置/参数优化:调整应用系统中各层面的配置文件、启动参数达到优化性能的目标。如:JVM、服务器、数据库、、操作系统、中间件、容器、网关参数调整等。
- ③代码/操作优化:开发者编写程序时,从代码、操作方面进行调节,达到效率更高的初衷。如:代码中使用更优秀的算法思想/设计模式、SQL优化、对中间件的操作优化等。
本章则重点阐述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
,这值得吗?答案显而易见。因此,并没有最好的技术架构,只有最适用的架构,能从现有环境及实际业务出发,选用最为合适的技术体系,这才是我们应该做的事情。如:
- 项目业务中读写参半,单节点难以承载压力,项目集群、双主热备值得参考。
- 项目业务中写大于读,引入消息中间件、DB分库、项目集群也可以考虑。
- 项目业务中读大于写,引入缓存/搜索中间件、动静分离、读写分离是些不错的选择。
- .......
当你的系统原有架构遇到性能瓶颈时,你甚至可以考虑进一步做架构优化,如:设计多级分布式缓存、缓存中间件做集群、消息中间件做集群、Java程序做集群、数据库做分库分表、搜索中间件做集群.....,慢慢的,你的系统会越来越庞大复杂,需要处理的问题也更为棘手,但带来的效果也显而易见,随着系统的结构不断变化,承载百万级、千万级、亿级、乃至更大级别的流量也并非难事。
但只有当你的业务流量/访问压力在选用其他架构无法承载时,你才应该考虑更为庞大的架构。当然,如果项目在起步初期就有预估会承载巨大的流量压力,那么提前考虑也很在理,采用分布式/微服务架构也并非失策,因为对比其他架构体系而言,微服务架构的拓展性更为灵活。但也需要记住:分布式/微服务体系是很好,但它不一定适用于你的项目。
1.3、预防大于一切,调优并非“临时抱佛脚”
当问题出现时再想办法解决,这种策略永远都属于下下策,防范于未然才是最佳方案,提前防范问题出现主要可分为两个阶段:
- ①项目初期预测未来的流量压力,提前根据业务设计出合适的架构,确保上线后可以承载业务的正常增长。
- ②项目上线后,配备完善的监控系统,在性能瓶颈来临前设好警报线,确保能够在真正的性能瓶颈到来之前解决问题。
对于项目初期的架构思考,值得牢记的一点是:不要“卡点”设计,也不能过度设计造成性能过剩,举例:
项目上线后的正常情况下,流量大概在“一木桶”左右,结果你设计时直接整出个“池塘”级别的结构出来了,这显然是不合理的,毕竟架构体系越庞大,项目的成本也自然就越高。
当然,也不能说正常情况下压力在“一木桶”左右,就只设计出一套仅能够承载“一木桶”流量的结构,这种“卡点”设计的策略也是不可取的,因为你需要适当考虑业务增长带来的风险,如果“卡点”设计,那么很容易让项目上线后,短期内就遭遇性能瓶颈。
因此,如果项目正常的访问压力大概在“桶”级别,那将结构设计到“缸”级别是合理的,这样即不必担心过度设计带来的性能过剩,导致成本增高;也无需考虑卡点设计造成的:项目短期遭遇性能瓶颈。
但设计时的这个度,必须由你自己根据项目的业务场景和环境去思量,不存在前篇一律的方法可教。
有人曾说过:“如果你可以根据业务情景设计出一套能确保业务增长,且在线上能稳定运行三年时间以上的结构,那你就是位业内的顶尖架构”,但老话说的好:“计划永远赶不上变化”,就算思考到业务的每个细节,也不可能设计出一套一劳永逸的结构出现,我们永远无法判断意外和明天哪个先来。因而,项目上线后,配备完善的监控警报系统也是必不可少的。不过值得注意的是:
监控系统的作用并不是用来提醒你项目“嗝屁”了的,而是用来提醒你:线上部署的应用系统可能会“嗝屁”或快“嗝屁”了,毕竟当项目灾难已经发生时再给警报,那到时候的情况就是:“亡羊补牢,为时已晚”。
通常情况下,在监控系统上面设置的性能阈值都会比最大极限值要低5~15%
,如:最大极限值是85%
,那设置告警值一般是75%
左右就会告警,不会真达到85%
才告警,只有这样做才能留有足够的时间让运维和开发人员介入排查。当系统发出可能“嗝屁”的警告时,开发和运维人员就应当立即排查相关的故障隐患,然后再通过不断的修改和优化,提前将可能会出现的性能瓶颈解决,这才是性能调优的正确方案。
因此,最终结论为:绝不能等到系统奔溃才去优化,预防胜于一切。
1.4、无需追求完美,理性权衡利弊
“追求极致,做到完美”这点是大部分开发者的通病,很多人会因为这个思想导致自己在面临一些问题时束手无策,比如举个例子:
业务:
MacBookPro
一元购活动,预计访问压力:10000QPS
。
环境:单台机器只能承载2000QPS
,目前机房中还剩余两台空闲服务器。
状况:此时就算将空闲的两台机器加上去,也无法顶住目前的访问压力。
此时你会怎么做?很多人都会茫然,这看起来好像是没办法的事情呀,似乎只能等死了.....
但事实真的如此吗?并非如此,其实这种情况也有多种解决方案,如:
- ①停掉系统中部分非核心的业务,将服务器资源暂时让给该业务。
- ②抛弃掉部分用户的请求,只接受处理部分用户的请求。
- ③........
这些方案是不是可以解决上面的哪个问题呢?答案是肯定的。但完美主义者会认为:
系统中的服务不能停啊,得保持正常服务啊。
用户的请求怎么能抛,用户的访问必须得响应啊。
但事实告诉你的是:类似于京东、淘宝、12306等这些国内的顶级大厂,也照样是这么干的。好比阿里,在双十一的时候都会抽调很多冷门业务的服务器资源给淘宝使用,也包括你在参与这些电商平台的抢购或秒杀类活动时,你是否遇到过如下情况:
- 服务器繁忙,请稍后重试......
- 服务器已满,排队中.....
- 前方拥堵,排队中,当前第
x
位.....
如果当你遇到了这些情况,答案显而易见,你的请求压根就没有到后端,在前端就给你pass
了,然后给你返回了一个字符串,让你傻傻的等待。
这个例子要告诉大家的是:在处理棘手问题或优化性能时,无需刻意追求完美,理性权衡利弊后,适当的做出一些决断,抛弃掉一部分不重要的,起码比整个系统挂掉要好,何况之后照样也可以恢复。
1.5、性能调优的通核心步骤
性能优化永远是建立在性能瓶颈之上的,如果你的系统没有出现瓶颈,那则无需调优,调优之前需要牢记的一点是:不要为了调优而调优,而是需要调优时才调。
而发现性能瓶颈的方式有两种,一种是你的应用中具备完善的监控系统,能够提前感知性能瓶颈的出现。另一种则是:应用中没有搭载监控系统,性能瓶颈已经发生,从而导致应用频繁宕机。大型的系统一般都会搭载完善的监控系统,但大多数中小型项目却不具备该条件,因此,大部分中小型项目发现性能瓶颈时,大多数情况下已经“嗝屁”了。
通常而言,性能优化的步骤可分为如下几步:
- ①发现性能瓶颈:如有监控系统,那它会主动发出警报;如若没有,那出现瓶颈时应用肯定会出问题,如:无响应、响应缓慢、频繁宕机等。
- ②排查瓶颈原因:排查瓶颈是由于故障问题导致的,还是真的存在性能瓶颈。
- ③定位瓶颈位置:往往一个系统都会由多个层面协同工作,然后对外提供服务,当发现性能瓶颈时,应当确定瓶颈的范围,如:网络带宽瓶颈、Java应用瓶颈、数据库瓶颈等。
- ④解决性能瓶颈:定位到具体的瓶颈后对症下药,从结构、配置、操作等方面出发,着手解决瓶颈问题。
本章则重点是阐述Java虚拟机-JVM相关的调优操作,但需要先提前说明的是:
单层面的性能调优其实只能当成锦上添花的作用,但绝对不能成为系统性能高/低、响应快/慢、吞吐量大/小的决定性要素。应用系统的性能本身就还算可以,那么调优的作用是让其性能更佳。但如若项目结构本身就存在问题,那么能够带来的性能提升也是有限的,如果你想让你的项目快到飞起,那么还需要从多个层面共同着手才能达到目的。
二、JVM垃圾收集相关调优策略
在JVM垃圾收集相关的调优实践中,通常都是以最优吞吐量和最短停顿时间来评价JVM的性能:吞吐量越高代表性能越好、暂停时间越短也代表越好。那么如何做到这两点呢?核心思想在于:
- 尽可能让对象在新生代中分配和回收。
- 尽量避免过多对象进入年老代,缩短年老代GC时间。
- 尽量给JVM分配足够多的内存,减少所有区域中的GC次数。
归根结底,本质思想就一点:“尽量让Java中的对象去到它自己该去的位置”,短命的对象就老老实实的进入新生代区域,大对象和长命的对象则进入年老代空间,避免JVM因为对象“乱窜”导致GC频发和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、分配速率的四种状况
- ①分配速率低,回收速率超于分配速率,GC状态无异常,代表系统GC正常。
- ②分配速率高,回收速率略低于或远低于分配速率,代表程序存在OOM隐患。
- ③分配速率高,但回收速率勉强可以跟上,代表系统处于“亚健康”状态。
- ④分配速率低,GC次数频繁,释放空间较少,可能存在内存泄漏。
其中①为正常状况,无需做任何处理,也没必要去对于这类系统做刻意优化,如果你的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
日志来计算的提升速率,拆解前面的计算公式可以分析出整体的计算逻辑:
- 先通过新生代回收前后的已使用容量大小,计算出新生代中减少容量。
- 再通过整堆回收前后的已使用容量大小,计算出整个堆空间的减少容量。
- 再通过新生代减少-整堆减少,这样可以大致算出新生代中提升到年老代的提升量。
- 该方式只能计算出大概的提升量,因为整堆减少会包含年老代、元空间等区域回收。
- 在通过本次GC触发时间-上次GC触发时间,得到本轮GC中程序正常执行的时长。
- 最后通过提示量除执行时长,即可得到JVM的大概提升速率。
不过在计算提升速率的时候,有个点需要额外注意: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、年老代空间调优思想
年老代空间调优的核心就一点:避免或尽量减少过早提升,为何不是降低提升速率呢?因为在业务规模比较大的情况下,提升速率比较高也是合理的。所以在调优年老代时,只需要将过早提升的对象依旧控制在新生代即可。
过早提升的表现
- ①一次
FullGC
后,年老代的空间占用比极速下降。 - ②短时间内频繁触发
FullGC
。 - ③提升速率接近分配速率。
- ④新生代GC发生后,新生代的空间占用比下降到
20%
以内。
过早提升如何解决?
处理过早提升时,需要根据具体的情况来决定采取何种措施:
- ①如果是业务或流量压力变大导致的,那么增大新生代空间即可。
- ②如果是代码中存在问题,如内存泄漏或循环体中创建对象等,优化代码即可。
- ③如果是短命的大对象分配,如大数组,则可以考虑优化数据结构,如换成链表。
2.3、合理的堆空间该如何分配
Java内存各分区的大小对JVM的性能影响很大,不恰当的空间大小可能会埋下很多故障隐患,同时也会直接或间接影响JVM的提升速率、分配速率,所以如何将各分区调整到合适的大小就成了一个棘手的问题。大部分不具备线上JVM调优实操经验的开发者都会茫然,通常会认为设定的越大越好,但答案却并非如此。
在指定各区域大小时,可以依据“活跃数据”大小来进行设定,“活跃数据”是指应用程序稳定运行后长期存活在堆中的对象,也就是FullGC
后年老代中的对象。一般在计算“活跃数据大小”,都会多次采集程序稳定执行后的FullGC
日志,通过取平均值的方式计算出堆中长期存活的年老代总量大小。
计算出“活跃数据大小”后,就可以根据其具体值计算出其他分区恰当的值,比例如下:
- ①堆空间:活跃数据大小的
4~5
倍 - ②新生代:活跃数据大小的
1.5~2
倍 - ③年老代:活跃数据大小的
2.5~3
倍 - ④元空间:活跃数据大小的
1.2~1.8
倍
假设此时观测出的“活跃数据大小”为800MB
,那堆空间的各区域的大小:
- ①堆空间:
3200MB
- ②新生代:
1200MB
- ③年老代:
2000MB
当然,这仅作为初始值参考,具体情况取决于应用业务的特性和需求。
但需注意的是:实际过程中,
-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方面:
MinorGC、FullGC
停顿时长、GC触发间隔、GC回收总量等。 - ..........
GC调优时的收益排序:改善代码 > 装配合适的GC回收器 > 重新设置内存比例/大小 > 调整JVM参数。
但需重点注意的是:上述的GC调优理论都是基于
G1
之前的分代垃圾收集器而言的,G1
之后的不分代收集器,如:ZGC、ShenandoahGC
等压根没必要刻意优化,自身的机制本就足够优异,而且后续的不分代收集器对外暴露的可操作参数也并不多。
三、阿里在线排除工具 - Arthas
Arthas(阿尔萨斯)是阿里开源的一款Java在线诊断工具,官网原话:当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到JVM的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从JVM内查找某个类的实例?
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、基础命令
help
:查看Arthas
命令帮助信息。cls
:清空当前屏幕中的所有信息,类似于clear
命令。session
:查看当前会话的信息。reset
:重置所有增强类,还原Arthas
增强过的所有类(stop
时生效)。version
:显示当前的Arthas
版本信息。history
:输出历史执行过的所有命令。quit
:退出当前的Arthas
会话,其他会话不受影响。shutdown
:关闭所有Arthas
会话后,退出Arthas
。stop
:强制关闭Arthas
并中断所有会话。keymap
:输出Arthas
中所有默认的以及自定义的快捷键。options
:查看或设置Arthas
的全局开关。pwd
:返回当前的工作目录位置,同Linux的pwd
命令。
3.2.2、类命令
sc
:查看JVM已加载的类信息,可选项如下:class-pattern
:类名表达式匹配(必填),如sc java.lang.String
。-E
:开启正则表达式匹配,默认为通配符匹配。-c
:指定class
的类加载器的哈希码。-d
:显示当前类的详细信息,包含来源、声明、类加载相关等信息。-f
:输出当前类的属性成员信息,与-d
一同使用。-x
:指定输出静态变量时属性的遍历深度,默认为0
。-n
:具有详细信息的匹配类的最大数量(默认为100)。
sm
:查看已加载类的方法信息,可选项如下:class-pattern
:类名表达式匹配(必填),如sm java.lang.String
。-E
:开启正则表达式匹配,默认为通配符匹配。-d
:查看方法的详细信息,配合方法名使用,如sm -d java.lang.String toString
。-c
:同sc -c
作用相同。-n
:同sc -h
作用相同。
jad
:反编译指定已加载类的源码,可选项如下:-c、-E
都与前面的作用相同,举几个案例演示用法。jad --source-only java.lang.String
:只显示反编译后的Java源码。jad java.lang.String
:反编译指定类。jad java.lang.String toString
:反编译指定类的某个方法。
mc
:内存编译器,编译.java
源文件为.class
类文件,可选项如下:-c
:指定类加载器(以哈希码的方式指定)。-d
:指定编译后的类文件输出位置。
redefine
:加载外部的.class
文件,重新加载JVM已加载的类。- 推荐使用
retransform
代替redefine
。
- 推荐使用
retransform
:作用与redefine
相同,热部署的作用,用于线上替换类方法。- 注意点:
- ①重新替换JVM中被加载的类时,不能新增方法或属性。
- ②正在执行的方法不能替换。
- 注意点:
dump
:导出已加载类的字节码数据到指定目录,可选项如下:-c、-E
作用与之前的相同。-d
:指定输出的路径,如dump -d /usr/data/byteCode java.lang.String
。
classloader
:查类加载器的继承树,urls,类加载信息,可选项如下:-a
:显示所有类加载器加载的所有类。-c
:查看指定的类加载器的加载路径,如classloader -c 14ae5a5
。-l
:统计每个类加载器的加载信息。-r
:查找某个的资源路径,配合-c
使用,如classloader -c 33909752 -r java/lang/String.class
。-t
:以树结构列出每个类加载器之间的父子关系。-u
:显示类加载器的url统计信息,如加载总数、父子关系、加载范围等。-i
:查看每种类加载器的实例数量及其加载总量。
3.2.3、JVM命令
dashboard
:资源监控仪表盘,包含线程、内存、GC、运行环境等信息,可选项如下:-i
:刷新实时数据的间隔时间,默认为5000ms
。-n
:刷新实时数据的次数,默认为一直持续刷新,按ctrl+c
退出。
thread
:查看当前线程的堆栈信息,可选项如下:-n
:显示最活跃的n
条线程信息,如thread -n 5
。-i
:指定活跃性统计的采样间隔时间,如thread -i 5000
。-b
:自动检测出应用中当前阻塞其他线程的线程。--state
:查询目前程序中处于指定状态的线程,如thread --state BLOCKED
。id
:查看某个线程的详细信息,如thread 21
。
jvm
:查看JVM信息,包含线程/内存/OS/内存结构/编译/类加载/运行环境等信息。sysprop
:查看或修改当前JVM的系统属性,如sysprop java.home
。sysenv
:,查看当前JVM的环境参数。vmoption
:查看或修改JVM的运行时参数,如:vmoption PrintGC
:查看PrintGC
是否开启。vmoption PrintGC true
:更改PrintGC
参数。
logger
:查看logger信息,更新logger level。getstatic
:查看类的静态属性,用法:getstatic class_nmae field_name
。ognl
:执行ognl表达式,使用方式可参考:官方指南、特殊用法。heapdump
:类似于jmap
工具的堆dump
功能,使用方式:heapdump /usr/data/dump/heap.hprof
:导出堆快照到指定文件。heapdump --live /usr/data/dump/heap.hprof
:只导出存活对象的快照。
mbean
:查看Mbean
的信息,详情参考:官方文档。memory
:查看JVM的内存划分、内存结构以及占用率。
3.2.4、字节码增强命令
tt
:记录指定方法每次执行的数据,并能在不同的时间下调用观测,可选项如下:<class_pattern> <method_pattern>
:指定要观测的类名+方法名。-t
:记录下方法每次执行的情况,如tt -t demo.MathGame primeFactors
。-i <index>
:查看某条执行记录的执行详情,如tt -i 1000
。-d <index>
:删除某条执行记录,配合-i
使用,tt- d -i 1000
。-n
:设置执行次数,如tt -t -n 10 demo.MathGame primeFactors
。-l
:显示目前已存在的所有执行记录。-p
:重新执行某条执行记录,配合-i
使用,如tt -i 1001 -p
。-s
:通过OGNL
表达式进行查找。-M
:指定接收结果的字节上限,默认为1KB
。---replay-times
:配合-p
使用,指定重新执行N
次。--replay-interval
:执行多次时,每次执行时的间隔时间。- 重新执行
3
次某记录,每次间隔500ms
:tt -i 1001 -p --replay-times 3 --replay-interval 500
。
watch
:观测指定方法的执行情况,可选项如下:-b
:在方法调用之前观测。-s
:在方法成功执行后观测。-e
:在方法异常执行后观测。-f
:在方法结束后进行观测(默认)。-n
:指定观测的次数。- 使用示例:
watch -s -n 10 demo.MathGame primeFactors
monitor
:对指定的方法执行进行监控,可选项如下:-c
:指定监控的周期,默认为60s
。-n
:指定监控的周期次数。- 使用示例:
monitor -c 10 -n 3 demo.MathGame primeFactors
stack
:输出当前方法被调用的调用路径。trace
:方法内部调用路径,并输出方法路径上的每个节点上耗时,可选项如下:-i
:跳过JVM的本地方法。-n
:和之前的-n
同义。
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占用过高问题
- ①使用
thread -n 10
命令查看CPU占用资源最高的10条线程。 - ②使用
thread
命令查看前几条线程的详细执行信息,定位到具体的方法。 - ③使用
monitor
命令对前面定位到的方法进行监控,查看方法的调用次数与耗时。 - ④分析
monitor
命令查询出的结果,定位问题根源,确定是由于调用过于频繁导致的,还是内部代码逻辑问题。 - ⑤使用
jad
命令反编译class
文件,根据前面分析的原因排查代码并改善。
3.3.2、排查线程阻塞问题
- ①使用
thread
查看所有线程信息,再筛选所有阻塞状态的线程。 - ②根据线程名称定位具体的业务模块,再选中该业务中的一条线程查看堆栈信息。
- ③根据线程堆栈信息定位导致阻塞的具体方法,再利用
stack
命令查看方法堆栈信息。 - ④利用
jad
工具反编译源码,分析业务逻辑代码并改善。
3.3.3、排查死锁问题
- ①利用
Arthas
来检测死锁特别简单,只需要执行一行命令thread -b
即可。
3.3.4、排查方法执行过慢问题
- ①通过
trace
命令排查方法执行速度,trace xx类 xx方法 '#cost>50ms'
,观测执行时间大于50ms
的该方法的调用信息。 - ②可以结合正则表达式,同时排查多个类、多个方法,
trace -E ClassA|ClassB method1|method2|method3
。
3.3.5、动态修改线上代码
有些项目编译可能需要两小时,好容易编译完成上线之后,发现代码有一处小地方存在逻辑错误需要更改,此时难度需要重新将其下线,重新更改后打包部署吗?有了Arthas
之后的你完全不需要这样干。
- ①通过
jad
将要修改的类反编译为.java
文件,输出到指定目录。 - ②本地纠正
.java
文件后,通过mc
命令重新编译.java
文件。 - ③通过
redefine
或retransform
命令将刚编译的.class
文件再次加载到JVM中。
这个功能是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
日志进行计算。不过基于“活跃数据”计算出的大小也可以根据业务进行调整。
- ①对象存活较高的业务,
Survivor
区与Eden
区比值建议为2:4
,即-XX:SurvivorRatio=4
。 - ②对象晋升年龄阈值建议:
- 对象存活率较低的业务:保留默认值,即
15
。 - 对象存活率较高的业务:建议调小,如
-XX:MaxTenuringThreshold=7
,可以减少大量存活对象在幸存区反复横跳带来的性能开销。
- 对象存活率较低的业务:保留默认值,即
- ③JIT编译的热点代码缓存区至少
64M
,即-XX:ReservedCodeCacheSize=64m
。 - ④TLAB线程私有区域可以调整为
Eden
区的1/10
,即-XX:TLABWasteTargetPercent=10
。 - ⑤记得打开
OOM
时Dump
堆的参数,以及执行脚本可以指定为重启应用。
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、选择垃圾收集器
选用合适的垃圾收集器往往能够让你的应用性能提升一大截,但合适的收集器也需要根据运行环境及业务场景去选择,那如何选择最合适的收集器呢?
- ①、将堆空间调整到合适的大小后,优先让JVM自行根据配置选择。
- ②、如果内存小于
100MB
或部署在单核/双核机器,使用串行收集器。 - ③、JDK8及以前追求低延迟(响应速度)选
ParNew+CMS
,追求高吞吐则选PS+PO
。 - ④、后续新版本的JDK中,
8GB
以上可以考虑选用G1
,上百GB
规模可采用ZGC
。
4.2.2、ParNew+CMS组合参数推荐
- 使用
ParNew+CMS
组合:-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
。 - ①并行收集GC线程数建议为CPU核数,即
-XX:ParallelCMSThreads=CPU*core
。 - ②内存碎片整理方面(
MSC
工作):-XX:+UseCMSCompactAtFullCollection
:内存碎片化严重时开启MSC
整理。- 建议将每次
FullGC
后的内存整理改为2-3
轮触发一次,即-XX:CMSFullGCsBeforeCompaction
。
- ③因为是追求响应速度的组合,因此目标停顿时间可以适当偏小一些,即
-XX:MaxGCPauseMillis
。 - ④激进优化策略:
-XX:+CMSParallellnitialMarkEnabled
:在初始阶段采用多线程执行。-XX:+CMSParallelRemarkEnabled
:在重新标记阶段采用多线程执行。-XX:+CMSScavengeBeforeRemark
:在重新标记阶段前触发一次新生代GC。
4.2.3、ParallelScavenge+ParallelOld组合参数推荐
- 使用
PS+PO
组合:-XX:+UseParallelGC -XX:+UseParallelOldGC
。 - ①并行收集GC线程数建议为CPU核数,即
-XX:ParallelGCThreads=CPU*core
。 - ②因为是追求吞吐的组合,因此吞吐比尽量可以调高,即
-XX:GCTimeRatio
,如若无经验没法预估准确值,那则可以开启JVM的自适应调整策略:-XX:+UseAdaptiveSizePolicy
。
4.2.4、G1整堆收集器参数推荐
- 使用
G1
收集器:-XX:+UseG1GC
。 - ①不要强制使用
-Xmn
参数设置年轻代的大小,因为G1是通过动态调整年轻代大小达到目标暂停时间的目的。 - ②如果分配的对象平均体积过大,可以适当调大每个分区的
Size
,但必须要为2
的次幂,即通过-XX:G1HeapRegionSize
调整,正常情况下尽量不要手动调整。 - ③尽量可以将并发线程数调整的大一些,即
-XX:ConcGCThreads
,一般推荐为CPU核数+1~2
。 - ④手动指定触发混合GC的阈值,关闭
IHOP
适应分析,消除自适应计算的耗时,-XX:InitiatingHeapOccupancyPercent=45 -XX:-G1UseAdaptiveIHOP
。 - ⑤混合GC时间过长可微调该三个参数:
-XX:G1MixedGCCountTarget=8 -XX:G1MixedGCLiveThresholdPercent=88 -XX:G1HeapWastePercent=5
。
4.3、性能激进优化策略
在JDK1.7及其之后的版本中,JVM推出了很多激进优化的策略,但在1.8及其之后的环境中,大部分的参数都是默认开启的,因此我们没有必要显式再次开启。但其实JVM中的一些激进优化参数默认也并未打开,如果你的程序堆空间足够大,也可以尝试开启后优化程序性能。
- ①
-XX:ParGCCardsPerStrideChunk=4096
:CMS激进优化策略,增大GC线程扫描卡表的范围,默认为256
,三个最佳值为32768、4K、8K
。 - ②
-XX:+AlwaysPreTouch
:开启物理内存分配替换虚拟内存分配,优化分配率。 - ③
-XX:+UseLargePages
:启用内存大页面分配技术。 - ④
-XX:-UseBiasedLocking
:关闭偏向锁,在并发较高的系统中关闭反而可以提升性能。 - ⑤
-XX:AutoBoxCacheMax=20000
:加大IntrgerCache
的缓存。 - ⑥
-XX:-UseCounterDecay
:关闭JIT即时编译器的热度衰减机制(会消耗一定内存)。 - ⑦
-XX:-TieredCompilation
:关闭C1
静态编译器编译,直接使用C2
编译。 - ⑧
-XX:MaxDirectMemorySize
:直接内存大小如果确认用的比较少,可以调小,如果用的比较多,可以适当调大。
4.4、不同的启动方式参数设置方式
Idea/Ecalipse
:在运行时的选项卡中配置,如IDEA的Configurations... -> VM Options
中。Tomcat
:bin
目录下的catalina.sh
文件中的JAVA_OPTS
的值上写JVM参数即可。jar
包方式启动直接将VM参数跟在后面即可。
五、总结
对于性能优化这个内容而言,没有绝对正确或最佳的参数,也包括本章的内容你可以适当参考但不能照搬于生产环境,安全第一,项目能够稳定执行是根本,性能优化永远要建立在应用健康运转但遭遇瓶颈的基础上,不要随便调优,更不要刻意调优。
同时,对于JDK不同版本中的默认值,如果你不清楚其具体作用,那建议保留默认值,毕竟JDK默认将其设为此值总有它的理由,默认值至少能够满足绝大部分的项目需求。因此,如若你没有丰富的激进优化经验,再次重申:不要随意更改一些性能参数的默认值。