(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: “在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。

引言

   “在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。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,前者可被称为是高分配速率吗?并非如此,因为前者的堆有32G1000MB/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

1.png

   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提供的指令进行操作即可:

2.png

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次某记录,每次间隔500mstt -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文件。
  • ③通过redefineretransform命令将刚编译的.class文件再次加载到JVM中。

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

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

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

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

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

4.1、运行时数据区

4.1.1、堆空间

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

3.png

无论你的项目是追求响应速度,亦或是吞吐量,都可根据“活跃数据”计算的大小作为基础进行调整,依照“活跃数据”计算出的大小也恰巧能够符合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
  • ⑤记得打开OOMDump堆的参数,以及执行脚本可以指定为重启应用。

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中。
  • Tomcatbin目录下的catalina.sh文件中的JAVA_OPTS的值上写JVM参数即可。
  • jar包方式启动直接将VM参数跟在后面即可。

五、总结

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

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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
56 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
15天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
23天前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
48 2
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
43 3
|
1月前
|
存储 缓存 监控
聊聊JIT是如何影响JVM性能的!
聊聊JIT是如何影响JVM性能的!
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
1月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
56 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4