JVM调优--理论篇

在对Java应用进行性能优化时,JVM的调优是一个绕不开的话题。本文重点介绍下如何对JVM进行调优,以期提高Java应用的性能、稳定性、响应时间等性能目标。JVM的调优过程符合Java应用的调优过程,主要分为三步:性能监控、性能分析、性能调优。此外,本文讨论的JVM均是指HotSpot VM,对于其他JVM,可以参考相关原理,并不能拿来就用。

性能优化概述

性能优化的目标主要是提高应用的整体性能表现,确保应用能够满足业务需求。具体来说,主要包括等方面。具体描述如下:

(1) 降低资源消耗。减少CPU、内存、磁盘I/O和网络I/O等系统资源的占用,降低运营成本,实现更高效的资源利用。对任何应用来说,同一个任务消耗更少的CPU资源,都是一个永恒的话题。除了CPU资源,还有内存资源、磁盘I/O、网络I/O。成本问题,永远是企业最关注的问题之一。

(2) 提高吞吐量。吞吐量是对单位时间内处理工作量的度量。提高吞吐量,可以提升系统处理请求的能力,从而支持更高的并发用户数和更大的数据量。设计吞吐量需求时,一般不考虑它对延迟或响应时间的影响。通常情况下,增加吞吐量的代价是延迟的增加或内存使用的增加。吞吐量需求的一个典型例子是,应用每秒需要完成一定次数的事务。

(3) 减少延迟或提高响应性。延迟或者响应性,是对应用收到指令开始工作直到完成该工作所消耗时间的度量,可以确保系统能够快速响应用户操作或处理外部请求。定义延迟或响应性时并不考虑程序的吞吐量,通常情况下,提高响应性或缩小延迟的代价是更低的吞吐量、或更多的内存消耗(或者二者同时发生)。延迟或响应性的一个典型例子是,应用应该在一定时间范围内完成请求的工作,如一个业务接口应保证1s内返回。

注意,减少资源消耗、提高吞吐量、提高响应性并不能同时满足,在进行性能优化时,应有所权衡,以期设计出一个满足业务需求,且成本低廉,吞吐量可观、响应及时的应用。

性能优化主要涉及三个活动:性能监控、性能分析、性能调优。其中:

性能监控是一种以非侵入方式收集或查看应用运行性能数据的活动。监控通常是指一种在生产、质量评估或开发环境中实施的带有预防或主动性的活动。当应用干系人报出性能问题却没有给出足以定位根本原因的线索时,就需要先进行性能监控,随后是性能分析。性能监控也有助于找出对应用响应性或吞吐量尚未造成严重影响的潜在问题。

性能分析是一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。性能分析是针对性能监控或是对干系人所报问题的回应,关注的范围通常比性能监控更集中。性能分析很少在生产环境中进行,通常在质量评估、测试或开发环境中执行。

性能调优时一种为改善应用响应性或吞吐量而更改参数、源代码或参数配置的活动。性能调优通常是在性能监控或性能分析之后进行。

JVM性能监控

JVM是应用软件栈的重要组成部分,应该像监控应用自身和操作系统那样监控JVM。分析JVM监控数据,可以知道何时需要JVM调优。JVM版本变更、操作系统变更、应用版本更新、或者在应用输入发生重大变动时,都应考虑JVM调优。所以,监控JVM非常重要。生产环境中,应自始至终地监控应用JVM。

根据对HotSpot VM的了解,可知JVM性能监控应重点监控的活动有:垃圾收集、JIT编译器、类加载活动、线程间锁竞争,等。接下来将一一说明。

监控JVM的垃圾收集

监控JVM的垃圾收集非常重要,因为它对应用的吞吐量和延迟都有着深刻的影响。现代JVM,如HotSpot VM,可以将每次GC的数据直接输出成日志文件,以文件方式查看GC统计数据,或用GUI监控工具查看。

重要的垃圾收集数据

重要的垃圾收集数据包括:

(1) 当前使用的Java虚拟机

(2) 当前使用的垃圾收集器

(3) Java堆的大小

(4) 新生代和老年代的大小

(5) 永久代的大小

(6) Minor GC的持续时间

(7) Minor GC的频率

(8) Minor GC的空间回收量

(9) Full GC的持续时间

(10) Full GC的频率

(11) 每个并发垃圾收集周期内的空间回收量

(12) 垃圾收集前后Java堆的占用量

(13) 垃圾收集前后新生代和老年代的占用量

(14) 垃圾收集前后永久代的占用量

(15) 是否老年代或永久代的占用触发了Full GC

(16) 应用是否显式调用了System.gc()

垃圾收集报告

HotSpot VM报告垃圾收集数据几乎没有什么额外开销,建议在生产环境中使用。这里说明下各种生成垃圾收集统计数据的命令行选项,并对这些数据进行解释。

一般来说,垃圾收集分为两种:次要垃圾收集(Minor GC)和主要垃圾收集(Full GC或Major GC)。Minor GC收集新生代,Full GC通常会收集整个堆,包括新生代和老年代,除了将新生代中活跃的对象提升到老年代,还会压缩整理老年代。因此,Full GC之后,新生代为空,老年代也已压缩整理并且只有活跃对象。

默认情况下,Full GC在执行之前,会触发执行一次Minor GC。但是,在执行ParallelGC的时候,可以控制关闭这次Minor GC。在开始-XX:UseParallelGC或-XX:UseParallelOldGC时,如果关闭-XX:-ScavengeBeforeFullGC,HotSpot VM在Full GC之前不会进行Minor GC。需要说明的是,但是,-XX:-ScavengeBeforeFullGC并不是一个标准的JVM参数,且在Java 9中已弃用。而且在Full GC之前禁止Minor GC条件比较苛刻,很可能设置后与预期不符,不建议使用该参数。
-XX:PrintGCDetails参数

-verbose:gc用于在标准输出上打印详细的垃圾收集日志。这些日志提供了关于JVM如何管理内存和执行垃圾收集操作的有用信息。而-XX:PrintGCDetails参数可以打印比-verbose:gc参数更多有价值的垃圾收集信息。注意,在生产环境中启用-verbose:gc或-XX:PrintGCDetails参数,或其他详细的GC日志可能会产生大量的输出,这可能会对应用程序的性能产生轻微的影响。因此,通常建议在开发和测试阶段使用这些参数,以便在必要时进行调优和分析。

1.包含时间戳

-XX:PrintGCDetails参数打印的日志包含时间戳前缀,这个时间是自JVM启动以来的秒数。为了生成标准时间戳,形如YYYY-MM-DD-T-HH-MM-SS.mmm-TZ,可以补充-XX:PrintGCDateStamps参数。

YYYY: 年,4位数字

MM: 月,2位数字,不足则高位补0

DD: 日,2位数字,不足则高位补0

T: 分隔符,左边为日期,右边为时间

HH:小时,2位数字,不足则高位补0

MM:分钟,2位数字,不足则高位补0

SS:秒,2位数字,不足则高位补0

mmm:毫秒,3位数字,不足则高位补0

TZ:时区

日志中包含时区,时间是该时区的本地时间,而不是GMT时间(格林尼治标准时间,零度经线的时间,是所有时区时间的基准)。

时间戳可以更好的计算MinorGC和Full GC的持续时间和频率,也能推算它们的预期值,如果不符合应用需求,可以考虑对其调优。

2.将垃圾收集的统计数据直接输出到文件

使用-Xloggc:<filename>可以将垃圾收集的统计数据直接输出到文件(filename是保存的文件名),以便离线分析。离线分析可以处理时间范围更广的垃圾收集数据,查找问题时也不会直接影响线上应用。

结合使用-XX:PrintGCDetails参数和-Xloggc:<filename>参数,可以在不使用-XX:PrintGCDateStamps参数的情况下,自动添加时间戳前缀,打印格式与-XX:PrintGCDateStamps参数相同。

3.应用停止时间和应用并发时间

使用-XX:+PrintGCApplicationConcurrentTime和-XX:PrintGCApplicationStoppedTime,HotSpot VM可以报告应用在安全点操作之间的运行时间,以及阻塞Java线程的时间。利用这两个命令行选项观察安全点操作有助于理解和量化延迟对JVM的影响,也可以用来辨别是JVM安全点操作还是应用程序引入的延迟。

4.显式垃圾收集

显式的垃圾收集比较容易识别,因为垃圾收集日志中会有特定文字(包含System),说明垃圾收集是显式调用System.gc()所引起的。如无特殊理由,不建议显式垃圾收集。因为即使显式调用调用System.gc(),也不能保证立即执行垃圾收集。

垃圾收集数据的离线分析

离线分析是为了汇总垃圾收集数据并从中查找重要的数据模式。垃圾收集数据的离线分析方法有多种,如将数据载入数据表格或者图形工具。常用的离线分析工具有GCViewer、GCHisto、MAT等。推荐使用MAT(Eclipse Memory Analyzer)。这里不展开MAT工具的使用,有兴趣的同学,可以自行查找学习。

垃圾收集数据的在线分析

除了离线分析垃圾收集数据,还可以在线分析。在线分析工具需要连接到Java进程,并实时监控应用的性能指标。相比离线分析工具,在线分析工具会带来一定的系统性能损耗,但可以获得实时的监控数据。比较推荐的在线分析工具有:JConsole、Visual VM、Visual GC等。推荐使用JConsole。这里不展开JConsole工具的使用,有兴趣的同学,可以自行查找学习。

监控JIT编译器

监控HotSpot JIT编译活动有多种方法。虽然JIT编译加快了应用的运行,但它也需要计算资源,如CPU周期和内存。当需要找出哪些方法被优化,或者某些情况下的逆优化(Deoptimized)或重新优化(Reoptimized)时,监控JIT编译就很用。JIT编译器优化时会有一些初始假设,如果之后发现不正确,就可能会发生逆优化或重新优化。在这种情况下,JIT编译器会放弃之前所做的优化而基于获得的新信息重新优化。
-XX:PrintCompilation参数

可以使用-XX:PrintCompilation监控HotSpot JIT编译器。-XX:PrintCompilation为每次编译生成一行日志。也有一些工具可以监控JIT编译,但它们提供的信息不如-XX:PrintCompilation详尽。

监控类加载活动

许多应用都会使用自定义的类加载器,有时称为用户类加载器。JVM类加载器负责加载类,也负责卸载类。何时加载或卸载类取决于JVM运行时环境和所用的类加载器。监视类加载活动是有价值的,特别是在应用使用自定义类加载器的时候。图形化工具JConsole、Visual VM和Visual GC插件都可以监视类加载。

监控线程间锁竞争

快速定位Java应用中的锁竞争,常用的技巧是用JDK的jstack工具抓取线程转储信息。在jstack日志中查找锁竞争的关键在于,从多个线程的栈追踪信息中查找相同的锁地址,然后找到等待该锁地址的线程。如果发现多个线程的栈追踪信息都视图锁住相同的锁地址,说明应用正面临锁竞争。抓取多份jstack日志,如果在同一个锁上一直出现类似的锁竞争,那么应用极有可能正面临高度锁竞争问题。注意,栈追踪信息提供了发生锁竞争的代码在源代码中的位置。从Java应用的源代码中找到发生高度锁竞争的位置是一件非常困难的事,而用jstack方法追踪应用的锁竞争则简化了这一过程。

JVM性能分析

性能分析是一种以侵入方式收集运行性能数据的活动,它会影响应用的吞吐量或响应性。性能分析是对性能监控或是对干系人所报问题的回应,关注的范围通常比性能监控更集中。性能分析很少在生产环境中进行,通常是在质量评估、测试或开发环境中,常常是性能监控之后再采取的行动。性能分析常常表现为一种响应性的活动,是对性能问题报告人的回应。与性能监控相比,性能分析关注的点更集中。

性能分析的工具有很多,比较推荐的有:Oracle Solaris Studio Performance Analyzer、NetBeans Profiler,Arthas等。根据工具的特点和业务需要选择合适的工具。这里不展开介绍,重点介绍下常见的性能分析场景。

系统或内核态CPU使用

如果CPU时钟周期被用于执行操作系统或内核代码,这部分时钟周期就无法用于执行应用程序。因此,改善应用性能的策略之一就是减少消耗在系统或内核CPU上的时钟数。但是,这一策略不适用于在系统或内核上消耗时间极少的应用。监控操作系统在系统或内核上CPU的使用情况能够为决策是否采用该策略提供依据。

增加并行性

现代CPU架构将多核、多硬件执行线程技术摆到了程序员面前。这意味着程序员可以利用更多的CPU资源做更多的工作。然而,要利用好这些额外的CPU资源,运行于其上的程序必须能够并行工作。也就是说,这些程序需要按照多线程的方式构造或设计,才能充分利用额外的硬件线程。JVM是一种多线程设计,所以在对JVM进行性能分析时,要考虑如何调优以更好的利用其并行性的能力。

锁竞争

导致系统态CPU使用过高的另一个方面是应用中可能存在严重的锁竞争。早期JVM的实现中,对Java Monitor对象的操作往往直接委托给操作系统的Monitor对象或者互斥原语。这种设计导致一旦Java应用发生锁竞争,系统态CPU使用就很高,因为操作系统的互斥原语会触发系统调用。现代JVM对Monitor对象的操作大多通过JVM自身的用户态代码来实现,不再直接把这些操作委托给操作系统原语。这种改变意味着使用现代JVM后,即使Java应用出现锁竞争,也不一定会使用系统态CPU。应用尝试获取锁时,首先使用用户态CPU,直到最后才委托给操作系统原语,使用系统态CPU,只有出现了非常严重的锁竞争时,才会发生系统态CPU高的情况。

启动时间

启动时间是应用初始化所消耗的时间。此外,Java应用中另一个值得关注的指标是现代JVM完成应用热区(Hot Portion)优化,初始化所消耗的时间。Java应用初始化的完成时间取决于很多因素,如初始化时载入的类的数量、需要初始化的对象的数量、这些对象如何初始化以及HotSpot VM的运行时环境选择,等等。启动时间需求的一个典型例子是,应用的初始化需要在一定时间内完成。如笔者所在的服务团队要求,一个服务应保证1min内能启动成功,在2min内可以对外提供服务。当然,不同的团队、不同的服务类型,该指标会有差异。

应该在应用启动过程中,JVM初始化消耗的时间,以及占有的比例。如果JVM称为瓶颈,则考虑对JVM进行调优,或调整应用的启动时间要求。

JVM性能调优

和性能监控和性能分析相比,性能调优是一种为改善应用响应性或吞吐量而更改参数、源代码或属性配置的活动。性能调优通常是在性能监控或性能分析之后进行。

现代Java虚拟机是一种非常复杂的软件,它能灵活地适应不同应用领域及多种应用的需要。虽然大多数应用使用JVM的默认配置就能很好地工作,仍然有不少应用需要对JVM进行额外的配置,才能达到其期望的性能要求。现代JVM为了满足各种应用的需要,为程序运行提供了大量的JVM配置选项。但是,针对一个应用进行的JVM调优(配置),可能并不适用于另一个应用。因此,理解如何进行JVM调优就变得非常有必要。

对现代JVM进行性能调优是一门艺术,但也有一些基础性的理论和原则,理解这些理论并遵循这些原则会让性能调优的工作变得更加轻松。

JVM调优流程

JVM调优大致可以按照如下流程进行:

在开始调优之前,开发人员需要对应用的性能要求有一个清晰的了解,这些性能需求源于应用干系人所定义的优先级。对性能需求的这种分类称为系统需求。与功能需求不同,系统需求关注应用运行的特定方面,如吞吐量、响应时间、内存消耗、启动时间、可用性、可管理性,等等。功能需求关注的是应用按照何种方式运行,产生何种输出。

JVM性能调优涉及多个方面的取舍,需要全盘考虑各方面的影响。关注某一个系统需求时,往往又会牺牲系统的另一方面的需求。如减少内存消耗常常会影响系统的吞吐量以及系统延迟。又或者,为了改善系统的可管理性,减少应用部署的数量,然而这又牺牲了应用的可用性。由于决定系统调优重点时存在着诸多的取舍,了解应用干系人更关注哪些性能指标,在JVM调优过程中至关重要。

一旦明确了哪些是最重要的系统需求,下一步就是选择JVM部署模式。开发人员需要抉择将应用部署到多个JVM上运行,还是在单个JVM上运行。可用性、可管理性以及内存使用都是选择JVM部署模式时需要考虑的因素。目前主流的架构是微服务架构。对一个应用来说,常常需要拆分成多个独立的微服务。对于每个微服务来说,为了保证其高可用,通常需要多个实例。

接下来是选择JVM的运行时环境。HotSpot VM提供了多种运行时环境选项,包括client模式,这种运行时环境能提供更短的启动时间和较小的内存使用;server模式,这种运行时环境专注提供更高的吞吐量。系统需求在吞吐量、响应性、启动及初始化时间方面的要求主导了JVM运行时环境的选择。在Java 7及之后的版本,还引入了混合模式,该模式可以很好地融合client模式和Server模式的主要特征。

接下来是垃圾收集器的调优阶段,通过优化垃圾收集器,帮助应用达到内存使用、停顿时间/延迟、吞吐量的要求。调优一般是从调节垃圾收集器,满足程序的内存使用需求开始,之后是时间延迟的要求,最后是吞吐量的要求。

JVM调优是一个根据性能测试结果不断优化配置的多次迭代过程。在达到应用的系统需求指标之前,每个步骤都可能经历多次迭代。此外,为达到某一方面的指标,有可能需要对之前的性能参数进行调整,进而需要重做之前的调优步骤。假设经过几个迭代的垃圾收集器调节后,程序在延迟上的表现仍不能令人满意。这种情况下,改变JVM的部署模式就变得非常重要,否则就只能修改应用或重新定义应用的系统需求。

对系统需求划分优先级

性能调优的第一步就是划分应用的系统需求优先级,在这一阶段,开发人员需要与应用的重要干系人一起讨论,并就其优先级达成一致。由于这项工作明确定义了哪些是应用最重要的需求,所以应该作为应用架构设计的一部分。

性能调优的过程中,从应用干系人的角度出发,依据重要性对系统需求进行排序是非常重要的。最重要的系统需求左右了刚开始的很多决定。这也符合软件设计没有银弹的思想。不同应用关注的系统需求的侧重点不同,对应的业务决策也有差异。

选择JVM部署模式

JVM部署模式选择指的是将应用部署到单个JVM实例上,还是部署多多个JVM实例上。目前应用的主流架构是微服务架构。对一个应用来说,常常需要拆分成多个独立的微服务。对于每个微服务来说,为了保证其高可用,通常需要多个实例。

选择JVM运行模式

为Java应用选择JVM运行模式,就是指定以何种方式运行JVM,可选项有:client模式、server模式和混合模式。注意,混合模式是在Java 7及之后的版本才引入。推荐使用混合模式,这也是Java 8及之后的版本默认的运行模式。该模式可以很好地融合client模式和Server模式的主要特征。

除了Java运行模式,还有就是JVM部署时,是选择32位JVM,还是64位JVM。HotSpot VM默认的执行位数取决于所安装的JDK版本和操作系统。在32位操作系统上,只能运行32位的JDK,因此HotSpot虚拟机也只能在32位模式下执行。而在64位操作系统上,可以选择安装32位或64位的JDK。如果安装了64位的JDK,那么HotSpot虚拟机将默认在64位模式下执行。

此外,还需要选择初始的垃圾收集器。HotSpot VM提供了多种垃圾收集器:Serial收集器、Throughput收集器、CMS收集器、G1收集器等。垃圾收集器的选择,要根据业务的特征,进行充分的测试。如果应用对停顿时间要求较为严格(即不希望因为垃圾收集而导致过多的暂停),那么可能需要选择那些停顿时间较短的垃圾收集器,如CMS收集器或G1收集器。相反,如果应用对停顿时间要求不那么严格,可以考虑使用串行收集器或并行收集器,它们在垃圾收集时可能会暂停所有工作线程。如果对较小的堆或较低配置的硬件,可能更适合使用简单的串行收集器或者并行收集器。对于大内存堆和多核CPU的环境,可以考虑使用G1等垃圾收集器,它们更适合大规模的堆和高配置的硬件。

垃圾收集调优基础

垃圾收集主要关注的性能属性有:吞吐量、延迟、内存占用,垃圾收集器调优也有三个基本原则:每次Minor GC都尽可能多地收集垃圾对象;处理吞吐量和延迟问题时,垃圾收集器能处理的内存越大,垃圾收集的效果越好;在吞吐量、延迟、内存占用中任意选择两个进行垃圾收集器调优。对于Java虚拟机调优来说,理解不同属性选择所带来的取舍、调优的原则以及收集什么信息都是非常重要的。

性能属性

(1) 吞吐量:是评价垃圾收集器能力的重要指标之一,指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。

(2) 延迟:也是评价垃圾收集器能力的重要指标,度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。

(3) 内存占用:垃圾收集器流畅运行所需要的内存数量。

这其中任何一个属性性能的提高几乎都是以另一个或两个属性性能的损失作代价的。换句话说,某一个属性上的性能提高总是牺牲另一个或两个属性。然而,对大多数应用来说,极少出现这三个属性的重要程度都同等的情况。很多时候,某一个或两个属性的性能要比另一个重要。哪个属性最重要,并将其映射到应用的系统需求,对应用而言非常重要。

原则

JVM垃圾收集器调优有三个需要理解的基本原则:

(1) 每次Minor GC都尽可能多地收集垃圾对象。这称作"Minor GC回收原则"。遵循这一原则可以减少应用发生Full GC的频率。Full GC的持续时间总是最长的,是应用无法达到其延迟或吞吐量要求的首要原因。

(2) 处理吞吐量和延迟问题时,垃圾收集器能处理的内存越大,垃圾收集的效果越好,应用运行也越流畅。这称作"GC内存最大化原则"。

(3) 在吞吐量、延迟、内存占用三个属性中任意选择两个进行垃圾收集器调优。这称作"GC调优的三选二原则"。

对于Java虚拟机调优来说,理解不同属性选择所带来的取舍、调优的原则以及收集什么信息都是非常重要的。调优JVM垃圾收集的过程中,谨记这三条原则能帮助更轻松地调优垃圾收集,达到应用的性能要求。

命令行选项及GC日志

GC日志是收集调优所需信息的最好途径。这意味着开发者需要通过命令行开启虚拟机的GC统计信息采集功能。为了定位问题,即便在生产系统上开启GC日志也是个不错的选择。但是,一旦完成问题定位,要及时关闭GC日志收集。开启GC日志对性能的影响不大,但是会产生比平时更多的日志。开启GC日志可以提供丰富的数据,将应用层的事件与垃圾收集或JVM层面的事件关联起来。HotSpot VM提供了多个GC日志相关的命令行选项,推荐使用如下的命令行集合:

− X X : P r i n t G C D a t e S t a m p s − X X : P r i n t G C D e t a i l s − X l o g g c : < f i l e n a m e > -XX:PrintGCDateStamps -XX:PrintGCDetails -Xloggc:<filename> −XX:PrintGCDateStamps−XX:PrintGCDetails−Xloggc:<filename>

其中,-XX:PrintGCDetails参数用来表示打印比-verbose:gc参数更多有价值的垃圾收集信息。-XX:PrintGCDetails参数打印的日志包含时间戳前缀,这个时间是自JVM启动以来的秒数。为了生成标准时间戳,形如YYYY-MM-DD-T-HH-MM-SS.mmm-TZ,可以补充-XX:PrintGCDateStamps参数。使用-Xloggc:<filename>可以将垃圾收集的统计数据直接输出到文件(filename是保存的文件名),以便离线分析。

此外,针对高延迟问题调优HotSpot VM时,下面的两个命令行选项很有用,通过它们可以获得应用由于执行JVM安全点操作而阻塞的时间以及两个安全点操作之间应用运行的时间。

− X X : + P r i n t G C A p p l i c a t i o n C o n c u r r e n t T i m e − X X : P r i n t G C A p p l i c a t i o n S t o p p e d T i m e -XX:+PrintGCApplicationConcurrentTime -XX:PrintGCApplicationStoppedTime −XX:+PrintGCApplicationConcurrentTime−XX:PrintGCApplicationStoppedTime

安全点操作使JVM进入到一种状态:所有的Java应用都被阻塞,执行本地代码的线程都被禁止返回VM执行Java代码。安全点操作常用于虚拟机需要进行内部操作时,此时所有的Java线程都被显式地置于阻塞状态且不能修改Java堆的情况。

如果应用某些时间段的响应时间超过了应用要求,使用命令行选项-XX:PrintGCApplicationConcurrentTime可以帮助判定应用是否运行,运行了多长时间。

确定内存占用

活跃数据的大小是确定运行应用所需Java堆大小不错的切入点。同时,它也决定了开发者是否需要重新考量应用的内存占用量需求,或者是否需要修改应用以满足应用的内存占用需求。

活跃数据的大小是指,应用稳定运行时长期存活对象所占用的Java堆内存量。换句话说,它是应用运行于稳定态时,Full GC之后Java堆所占用的空间大小。

约束

JVM可以使用的物理内存量要有一个上限。无论是在物理机器还是虚拟机,还是云平台上,JVM可用的内存量需要有一个上限。如果是物理机或虚拟机,受限于物理机或虚拟机自身的内存上限,对于云平台来说,受限于指定的最大内存。

HotSpot VM堆的布局

HotSpot VM根据对象存活周期的不同,将内存划分三部分:老年代(Tenured Generation)和新生代(Young Generation)、永久代(Permanet Generation)。其中,老年代和新生代组合了堆空间,而永久代则在堆空间之外。在新生代中,存放新分配的对象,在老年代中,存放旧的或长期存活或大的对象,在永久代中,存放JVM加载的类元数据(如类的结构信息、常量池等)、类静态变量,等。在Java 8中,永久代被元空间所取代,元空间使用本地内存,可以动态扩展。HotSpot VM堆布局如下所示:

上图中,新生代被划分为一个Eden空间和两个Survivor空间。两块Survivor空间中,一块标记为"From"的Survivor空间,另一块空间标记为"To"的Survivor空间。当Eden空间被填满时机会发生Minor GC。活跃对象会从Eden空间复制到标记为"To"d的Survivor空间,同时"From"的Survivor空间存活下来的对象也会复制到"To"的Survivor空间中。一旦完成Minor GC,Eden空间会被清空,"From"的Survivor空间也会被清空。之后,Survivor空间将互换标记为下一次的Minor GC做准备。现在已经清空的"From"的Survivor空间换上了"To"的标识,而之前"To"的Survivor空间换成了"From"的标识。

如果Minor GC时,"To"的Survivor空间不足以容纳所有从Eden空间和"From"的Survivor空间复制过来的对象,那么超出的部分将会提升到老年代空间。

Java应用分配Java对象时,首先在新生代空间中分配对象,经历过几次Minor GC之后,还保持存活的对象会被提升进入老年代空间。永久代空间中存放VM和Java类的元数据以及类静态变量等。

-Xmx和-Xms设置堆的初始值和最大值。其中-Xmx设定了初始值及最小值,-Xmx设定了最大值。当-Xms指定的值小于-Xmx的值时,新生代及老年代空间大大小会根据应用的需求动态的扩展或缩减。对于吞吐量和延迟关注的Java应用应将-Xms和-Xmx设定为同一值。这样,就不会因为新生代和老年代空间的动态的扩展或缩减触发Full GC,从而降低应用的吞吐量和更长的延迟。如果-Xms指定的值比-Xmx的值大,那么会导致JVM在启动时就会因为无法满足内存分配要求而失败,并可能抛出一个错误。所以业务环境上,不可能存在-Xms值大于-Xmx值的情况。

HotSpot VM不指定Java堆的大小时,会根据物理机或虚拟机的物理内存或分配的物理内存的大小来计算默认的堆大小。具体的计算方式可能因JVM版本和操作系统而略有不同,但大致遵循如下规则:

(1) 对于32位HotSpot VM,默认的堆大小通常是较小的,因为32位JVM本身能够使用的内存空间就有限。具体大小可能因JVM版本和操作系统而异。

(2) 对于64位JVM,默认的堆大小会根据物理内存的大小来计算。在物理内存小于192MB时,JVM的初始和最大堆大小通常为物理内存的一半。当物理内存大于192MB但小于1GB时,JVM的最大堆大小通常为物理内存的1/4。而当物理内存大于或等于1GB时,JVM的最大堆大小可能会根据JVM版本和操作系统的不同而有所变化,但通常会限制在一个合理的范围内,以避免过度消耗系统资源。

一般情况下,默认的堆大小不能够充分的利用物理内存,所以均需要手动设置JVM的堆大小。通常建议将JVM的最大堆大小设置为物理内存的1/4至1/2之间,但这只是一个大致的指导原则,具体设置需要根据实际情况进行调整。JVM除了堆空间会占用内存,栈空间、系统其他资源也会消耗内存,所以不建议把JVM的堆设置过大。应该在保证系统正常运行、满足业务需要、充分测试等前提下,合理设置JVM的堆大小。不建议JVM的堆最大值大于物理内存的80%。举例来说,对于物理内存为2GB的环境来说,如果当前环境,主要的业务进程就是Java应用,那么预留400MB的空间已经足够其他对象或进程使用,在经过充分测试后,可以设置Java堆的最大值为1600MB。

新生代空间可以通过以下命令行选项设置:

(1) -XX:NewSize=<n>[g|m]。新生代空间的初始值,也是最小值,[g|m]指大小的度量单位,分布是GB、MB。

(2) -XX:MaxNewSize=<n>[g|m]。新生代空间的最大值,[g|m]指大小的度量单位,分布是GB、MB。-XX:NewSize参数和-XX:MaxNewSize应同时使用,单独使用意义不大。

(3) -Xmn<n>[g|m]。新生代空间的固定值。[g|m]指大小的度量单位,分布是GB、MB。-XX:NewSize参数和-XX:MaxNewSize应同时使用,单独使用意义不大。

在HotSpot VM中,如果同时设置了-Xmn以及-XX:NewSize或-XX:MaxNewSize,那么-Xmn参数通常具有更高的优先级,会以-Xmn的值为准,因为它直接指定了年轻代的大小。这意味着-XX:NewSize和-XX:MaxNewSize的设置可能会被忽略,或者它们的值会被调整为与-Xmn一致。为了避免潜在的冲突和混淆,只应使用其中一个参数来设置年轻代的大小。

老年代空间的大小会根据新生代的大小间接获得。老年代大小=堆大小-新生代生代。如果新生代设置了-Xmn,那么老年代的固定大小为-Xmx减去-Xmn。如果新生代设置了-XX:NewSize和-XX:MaxNewSize,那么老年代的初始值大小为-Xms减去-XX:NewSize,老年代的最大大小为-Xmx减去-XX:MaxNewSize。

此外,还可以通过-XX:NewRatio来指定新生代和老年代的大小比例。如果当前对大小为4GB,且-XX:NewRatio=3,那么新生代和老年代的大小比例就是1:3。这意味着新生代的大小将会是1GB,而老年代的大小将会是3GB。

-XX:NewRatio和-XX:NewSize、-XX:MaxNewSize和-Xmn在功能上有重叠,-Xmn的优先级最高,其次是-XX:NewSize、-XX:MaxNewSize,最后是-XX:NewRatio。

永久代空间大小可以通过以下命令行选项设置:

(1)-XX:PermSize=<n>[g|m]。永久代空间的初始值,也是最小值,[g|m]指大小的度量单位,分布是GB、MB。

(2) -XX:MaxPermSize=<n>[g|m]。永久代空间的最大值,[g|m]指大小的度量单位,分布是GB、MB。

注意,在Java 8中,引入了元空间(Metaspace)来替代永久代(PermGen),用于存储类的元数据、常量池、静态变量等。因此,在Java 8及以后的版本中,设置永久代大小(如使用-XX:MaxPermSize参数)已经没有意义。

为了解决永久代的大小是有限的问题,Java 8引入了元空间。元空间使用本地内存(native memory)来存储类的元数据,其大小可以根据需要进行动态调整。这大大减少了类加载过程中的内存限制,并提高了应用程序的灵活性。

元空间大小可以通过以下命令行选项设置:

(1) -XX:MetaspaceSize=<n>[g|m]。元空间的初始值,也是最小值,[g|m]指大小的度量单位,分布是GB、MB。

(2) -XX:MaxMetaspaceSize=<n>[g|m]。元空间的最大值,[g|m]指大小的度量单位,分布是GB、MB。默认情况下,元空间的大小是没有上限的,即它的大小受限于机器的物理内存。然而,为了避免因元空间无限增长而导致的内存溢出问题,可以设置一个合理的最大值。

新生代没有足够的空间满足Java对象分配时,HotSpot VM会进行Minor GC以释放空间。经历过几次Minor GC后,仍然存活的对象最终会被提升到老年代。老年代不足以容纳新提升的对象时,就会触发Full GC。此外,对于大对象,直接从老年代获取内存空间,而不是从新生代。

堆大小调优

为了开始堆大小调优,需要一个切入点。首先选择一个垃圾收集器。垃圾收集器的选择,要根据业务需要,如果关注吞吐量,可以优先选择吞吐量优先的垃圾收集器Throughout收集器,如果关注延迟或响应性,可以优先选择CMS收集器或G1收集器。需要说明的是,垃圾收集器的初次选择只是一个开始,后续还会进行测试,只有通过测试验证的垃圾收集器才能被真正被采纳。

接下来就是堆大小的选择。如果清楚Java应用的Java堆空间,可以将Java堆大小作为调优的入手点,使用-Xmx和-Xms设置Java堆的最大大小和初始大小。如果不清楚Java应用需要多大的Java堆,可以使用HotSpot自动选取Java堆的大小。这样,我们就有了一个基点,接下来就是基于这个基点进行调优,后面会逐渐调整Java堆的大小。

无论是通过命令行选项显式指定Java堆的大小,还是采用默认值,目的都是希望将应用调整到最典型的工作场景,即它的稳定态阶段。开发人员需要产生足够的负荷,同时驱动应用处理这些根据生产环境模拟的负荷。

尝试将应用推进到稳定状态的过程中,如果观察到GC日志中出现了OOM,就要查看老年代或永久代是否已经耗尽。优先尝试增加JVM可用物理内存缓解,如将80%的物理内存分配给JVM使用。如果设置后,仍还有OOM问题,则需进一步观察处理。如果是老年代耗尽,则通过设置-Xms和-Xmx增加其值。如果是永久代耗尽,则通过设置-XX:PerSize和-XX:MaxPermSize增加其值。调整完其值后,再次在稳定负载下观察GC日志是否出现OOM。

计算活跃数据大小

活跃数据大小是应用处于稳定态(常稳负载)时,长期存活的对象在Java堆中占用的空间大小。换句话说,活跃数据大小是应用处于稳定态,FullGC之后Java堆中老年代占用的空间大小和永久代占用的空间大小(Java 8之后是元空间)。Java应用的活跃数据大小包括:

(1) 应用处于稳定态时,老年代占用的Java堆大小;

(2) 应用处于稳定态时,永久代占用的空间大小;

为了更好地度量应用的活跃数据大小,最好在多次Full GC之后再查看Java堆的占用情况。也即多取几次Full GC数据,通过取平均值的方式计算Java堆占用及GC时间。另外,需要确保Full Gc发生时,应用正处于稳定态。如果应用没有发生Full GC或不经常发生Full GC,那么可以使用工具或执行jmap命令强制进行Full GC。

初始堆大小空间设置

根据活跃数据大小定义初始Java堆大小时,还需考虑Full GC的影响。推荐的做法是基于最差延迟进行估算。

通用法则之一,将Java堆的初始值-Xms和最大值设置为老年代活跃数据大小的3-4倍。如果Full GC之后,老年代占用的空间大小为300MB,那么应用建议的堆的初始值和最大值为900MB到1200MB。

通用法则之二,永久代的初始值-XX:PermSize及最大值-XX:MaxPermSize应该比永久代活跃数据大1.2-1.5倍。如果Full GC之后永久代空间为40MB,那么应用建议的永久代的初始值和最大值为48MB到60MB。

补充法则,新生代空间应该为老年代空间活跃数据的1-1.5倍。如果Full GC之后,老年代占用的空间大小为300MB,那么应用建议的新生代的初始值和最大值为100MB到200MB。如果Java堆的初始值及最大值为活跃数据大小的3-4倍、新生代为活跃数据的1-1.5倍,那么老年代应该设置为活跃数据大小的2-3倍。

其他考量因素

计算出来Java堆大小并不代表Java应用的总内存占用。如果需要了解Java应用的总内存使用情况,更好的方法是使用操作系统提供的工具监控应用,如Linux上的top命令。另外,Java堆不一定是最耗应用内存的。如应用的线程栈可能需要较多的内存。线程的数目越多,消耗在线程栈上的内存就越多。应用中,方法调用的层次越深,线程栈占用的空间也越大。还可能由于应用使用的三方库分配内存及I/O缓存导致使用较多的内存。

计算出Java堆大小仅仅只是一个出发戴拿。后面的调优过程中,这些值可能会根据情况进行修改,具体的情况取决于应用需求。

调优延迟/响应性

这一步调优的目标是达到程序的延迟性需求,包括多个活动的迭代:优化Java堆大小的配置、评估GC的持续时间和频率、是否可能切换到不同的垃圾收集器以及发生垃圾收集器切换之后进一步的内存调优。

评估垃圾收集器堆延迟影响的过程中将进行以下的活动:

(1) 测量Minor GC的持续时间;

(2) 统计Minor GC的次数;

(3) 测量Full GC的最差(最长)持续时间;

(4) 统计最差情况下,Full GC的频率。

测量GC的持续时间及频率对优化Java堆的大小至关重要。Minor GC的持续时间及频率决定了优化后新生代的大小。最差情况下的Full GC持续时间及频率决定了老年代的大小及垃圾收集器的切换:是否需要从Throughout收集器转向CMS收集器,或者从CMS收集器转向G1收集器。一旦发生切换,也需要针对新的垃圾收集器重新进行内存调优。

输入

调优延迟/响应性有多个输入,都源于应用的系统性需求。

(1) 应用可接受的平均停滞时间。平均停滞时间将与测量出的Minor GC持续时间进行比较。

(2) 可接受的Minor GC(会导致延迟)的频率。Minor GC的频率将与可容忍的值进行比较。对应用干系人来说,GC持续的时间往往比GC发生的频率更重要。

(3) 应用干系人可接受的应用的最大停顿时间。最大停顿时间将与最差情况下Full GC的持续时间进行比较。

(4) 应用干系人可接受的最大停顿发生的频率。最大停顿发生的频率基本上就是Full GC的频率。同样,对于大多数应用来说,相对于GC的频率,更关心GC持续的平均停顿时间和最大停顿时间。

优化新生代的大小

根据垃圾收集的统计数据、Minor GC的持续时间和频率可以确定新生代空间的大小。Minor GC需要的时间与新生代中可访问的对象数直接相关。通常情况下,新生代空间越小,Minor GC持续的时间越短。不考虑这对于Minor GC持续时间的影响,减少新生代空间又会增大Minor GC的频率。这是因为以同样的对象分配频率,较小的新生代空间在很短时间内就会被填满,增大新生代空间可以减少Minor GC的频率。

分析GC数据时,如果发现Minor GC的间隔时间较长,修正的方法是减少新生代空间。如果Minor GC频率太高,修正的方法是增加新生代空间。

计算平均持续时间和频率时,Minor GC的次数越多,平均持续时间及频率的统计也就越准确。另外,使用应用运行于稳定阶段时的Minor GC的值也是非常重要的。

下一步是比较观察到的Minor GC平均持续时间及应用的平均延迟要求。如果观测到的平均GC持续时间大于应用的延迟要求,可以适当减少新生代空间的大小,之后再进行测试,收集GC统计数据后再次进行评估。如果观测到Minor GC频率大于应用的延迟要求(发生太频繁),增大新生代空间,之后再测试,收集GC统计数据后再次进行评估。真正达到应用的平均延迟要求之前可能需要经历多次迭代。注意,调整新生代空间大小时,尽量保持老年代空间大小恒定。

调整新生代空间时,需要谨记以下几个原则:

(1) 老年代空间大小不应该小于活跃数据大小的1.5倍。

(2) 新生代空间至少应为Java堆大小的10%。新生代过小会导致频繁的Minor GC。

(3) 增大Java堆空间时,需要注意不要超过JVM可用的物理内存。堆占用过多内存将导致底层系统交换到虚拟内存,反而会造成垃圾收集器和应用的性能降低。

这个阶段,如果只考虑Minor GC引起的延迟,而调整新生代的大小又无法满足应用的平均停顿时间或延迟性要求,就只能修改应用或修改应用的平均延迟要求。如果仅通过监控Minor GC就能达到应用的延迟性要求,就可以直接进入到老年代空间的调整,调优应用的最差停顿时间和最差停顿评率。

优化老年代大小

这一步的目标是评估Full GC引入的最差停顿时间及Full GC的执行频率。老年代的优化也需要采集垃圾收集的统计数据。这里关注的重点是Full GC持续的时间和频率。发生于稳定态的Full GC的持续时间是应用的最差Full GC停顿时间。如果多个Full GC在稳定态发生,就按平均最差停顿时间计算。取样的数据越多,预测的结果越准确。

从老年代中减去活跃数据的大小可以得到可用老年代空间大小。需要多长时间才能填满老年代的空闲空间取决于新生代到老年代的提升率。提升率可以依据老年代空间占用的增长量以及每次Minor GC后新生代的空间占用计算得出。老年代的空间占用情况可以通过Minor GC之后Java堆的占用情况减去同一次Minor GC后新生代的空间占用得到。

如果预期或观测到Full GC的频率已经远不能达到应用的最差Full GC频率要求,就应该增大老年代空间的大小。这个方法可以帮助降低Full GC的评率。增大老年代空间时,注意保持新生代空间大小恒定。

如果修改老年代空间大小后,只观察到Full GC,很可能是老年代与新生代空间大小失去了平衡,导致应用只进行Full GC。这一情况通常源于即使老年代经过Full GC,仍不足以容纳所有从新生代提升的对象。标识老年代空间不够大的一个线索是每次Full GC之后,老年代几乎没有任何空间被回收,而新生代总有大量的对象占用空间。当老年代没有足够的空间接纳从新生代提升的对象时,这些对象机会被"退还"到新生代空间中。

如果通过老年代空间大小调整几次迭代之后,能满足应用的最差延迟要求,JVM自身的调优就已完成。如果由于Full GC持续时间过长,导致无法达到应用的最差延迟性要求,可以改用并行垃圾收集器或CMS收集器或G1收集器。

为CMS调优延迟

使用CMS收集器时,老年代垃圾收集线程与应用线程能实现最大的并行度。这为同时降低最差延迟出现的频率以及最差延迟的持续时间,避免发生长时间的GC提供了机会。

调优CMS收集器的目的是避免发生Stop-The-World的压缩式GC。然后,这实际上是件"说易做难"的事情。在内存占用有限的情况下,这更是不能保证的。

使用CMS收集器时,如果老年代空间用尽,就会触发一个单线程Stop-The-World压缩式的垃圾收集。现对于Throughout收集器来说,CMS的Full GC通常持续的时间更长。因此,采用CMS的绝对最差延迟要比Throughout收集器的最差延迟时间要长。因此,尽量避免用尽老年代空间是非常重要的。从Throughout收集器迁移到CMS收集器时需要遵循一个通用原则是,将老年代空间增大20%-30%。这样才能更有效地运行CMS收集器。

CMS调优的主要挑战有以下几点:

(1) 对象从新生代提升到老年代的速率。

(2) 并行老年代垃圾收集线程回收空间的速率。

(3) 老年代空间的碎片化。

有多种方法可以解决碎片化问题。其中之一是压缩老年代空间。通过Stop-The-World压缩式GC对老年代空间进行压缩。这个方法不能从根本上解决碎片化问题,且会带来应用的延迟,但是可以推迟老年代空间碎片化到必须进行时再压缩。通常情况下,老年代空间的内存越多,处理碎片压缩是时间就越长。处理碎片问题的另一个方法是减少对象从新生代提升到老年代的比率,即"Minor GC回收原则"。晋升阈值是控制新生代中的对象如何提升到老年代。

晋升阈值

这里"晋升"和"提升"是一个含义,都表示对象提升到老年代。

HotSpot VM在每次Minor GC时,都会计算晋升阈值以决定何时对一个对象进行提升。或者说,晋升阈值就是对象的年龄。一个对象的年龄就是它所经历的Minor GC次数。对象首次分配时,其年龄是0。下一次Minor GC之后,如果该对象还在新生代,其年龄加一,以此类推。新生代中年龄大于HotSpot VM计算出的晋升阈值的对象被提升到老年代。晋升阈值计算的依据是Minor GC之后新生代要容纳的可达对象需要的空间大小及目标Survivor空间占用的空间大小(如果Survivor空间过小,一次Minor GC可能会导致Survivor溢出,溢出的对象将直接提升到老年代)。可以使用-XX:MaxTenuringThreshold=<n>指定在对象年龄超过阈值后,将其提升到老年代。在Open JDK的版本中,MaxTenuringThreshold的默认值是15。但是,这个默认值可能会在不同的JVM实现或版本中有所不同。可以使用-XX:+PrintTenuringDistribution选项来启动JVM。这个选项会在每次Minor GC之后打印出Survivor区中对象的年龄分布信息,同时也会显示当前的MaxTenuringThreshold设置值。

不建议将晋升阈值设置过小,这会导致最近分配的对象很快提升到老年代,同时造成老年代空间的迅速增大,引起频繁的Full GC。同时,也不建议将晋升阈值设置为远大于实际可能的最大值。这会造成对象长期存在于Survivor空间,直到最后溢出。一旦溢出,对象将会被全部提升到老年代,从而造成短期存在对象在长期存在对象之前被提升到老年代,严重影响对象老化机制的有效性。

监控晋升阈值

晋升阈值可以通过HotSpot VM的命令行选项-XX:+PrintTenuringDistribution观察Survivor空间中的对象是如何老化的。在-XX:+PrintTenuringDistribution生成的输出中,需要关注的是随着对象年龄的增加,各对象年龄上字节数减少的情况,以及HotSpot VM计算出的晋升阈值是否等于或接近设置的晋升阈值。-XX:+PrintTenuringDistribution会输出每次Minor GC时晋升分布的情况。

调整Survivor空间的容量

调整Survivor空间容量一个应该谨记于心的重要原则:调整Survivor空间容量时,如果新生代空间不变,增大Survivor空间会减少Eden空间;而减少Eden空间会增加Minor GC的频率。因此,为了同时满足应用Minor GC频率的要求,就需要增大当前新生代的空间;即增大Survivor空间大小时,Eden空间的大小应保持不变。如果可以增大Minor GC的频率,则可以选择用一部分Eden空间来增大Survivor空间。如果内存足够,相对于减少Eden空间,增大新生代大小通常是更好的选择。保持Eden空间不变,Minor GC的频率就不会因为Survivor空间增大而发生变化。再次提醒,减少Eden空间会导致更频繁的Minor GC。

显式的垃圾收集

可以使用-XX:+DisableExplicitGC命令行选项通知HotSpot VM忽略显式的System.gc()调用。禁用显式的垃圾收集时应该慎重,它可能会对应用的性能造成较大的影响。还有可能出现这样场景,开发人员需要及时的对对象引用做处理,但与之对应的是垃圾收集却跟不上其节奏。建议除非有非常明确的理由,否则不要轻易地禁用显式的垃圾收集。与此同时,也建议只有在明确理由下才能在应用中使用System.gc()。

其他

完成上述操作后,就可以知道使用垃圾收集器能否达到应用的延迟要求。如果仍无法达到应用的延迟要求,就只能重新回顾应用的延迟要求,对应用进行修改,可能还需要进行一些性能分析以定位出问题域,或考虑使用垃圾收集器,等等。

调优吞吐量

吞吐量是调优的最后一步。在这一步中,将对应用的吞吐量进行测量,并根据结果对JVM进行微调以优化其性能。

吞吐量调优的主要输入是应用的吞吐量要求。应用的吞吐量通常在应用层面,而不是在JVM的层面进行度量。因此,应用必须必要要有一些吞吐量的性能指标报告,或根据应用的操作能衍生出某种吞吐量指标。之后再讲观测的吞吐量与应用的吞吐量要求进行比较。当观测的应用吞吐量满足或超过预期的吞吐量要求时,整个调优过程就圆满结束。如果需要进一步调优应用的吞吐量,那就需要进行额外的JVM调优工作。

吞吐量调优的另一个重要输入是可用于部署Java应用的内存使用量。Java堆可用的内存越多,应用的性能越好。这一原则不仅适用于吞吐量的性能,也适用于延迟性能。

CMS收集器吞吐量调优

使用CMS收集器时,为了获得更大的吞吐量性能提升需要使用一些配置选项,这些选项与下面的因素或因素的组合密切相关:

(1) 增加新生代空间大小。增加新生代可以降低Minor GC的频率,从而减少固定时间内Minor GC的次数。

(2) 增加老年代空间的大小。增加老年代空间可以降低CMS周期的频率并减少内存碎片,最终减少并发模式失效以及Stop-The-World发生的几率。

(3) 优化新生代空间的大小,调整新生代中Eden空间和Survivor空间以及优化对象老化,减少由新生代提升到老年代的对象数目,最终减少CMS周期的发生数。

以上任何一个选项,或几个选项的组合都可以减少垃圾收集器消耗的CPU周期数,从而将更多的CPU周期用于执行应用。

一个指导原则是,CMS包括Minor GC所带来的开销应该小于10%。通常情况下,如果观察到CMS垃圾收集的开销在3%或更少,则说明通过CMS调优提升吞吐量的可提升空间及其有限了。

Throughout收集器吞吐量调优

对Throughout收集器吞吐量调优的目标是尽可能避免发生Full GC,或者更理想的情况是,在稳定态永不发生Full GC。为了达到这个目标需要优化对象老化频率。通过显式地微调Survivor空间可以实现对象老化的优化。可以将Eden空间设置的更大,从而降低Minor GC的频率,确保老年代有足够的空间持有应用的活跃数据。如果对象没有理想的老化频率,一些非长期存活对象被提升到了老年代时,可以增加一些额外的老年代空间来应对这一情况。

Throughout收集器提供的吞吐量性能是HotSpot VM提供的垃圾收集器中最好的。且Throughout收集器默认启用了一个自适应大小调整的特性。自适应大小调整会根据对象分配及存活率自动地对新生代的Eden和Survivor空间进行调整,以最优化对象老化频率。在对Throughout收集器进行吞吐量调优之前,可以使用-XX:-UseAdaptiveSizePolicy禁用自适应大小调整。

一个指导原则是,Throughout收集器时,垃圾收集的开销应该小于5%。如果可以将垃圾收集的开销减少到1%或更少,那基本上就是极限了。

其他性能命令行选项

还有一些命令行选项并没有介绍,使用这些选项通过优化JIT编译器生成的代码,以及一些其他的方法,可以提升、改善HotSpot VM及应用的性能。

-XX:AggressiveOpts参数

-XX:AggressiveOpts是HotSpot VM的一个参数选项,它的主要作用是启用一系列积极的性能优化。这些优化通常是针对特定场景或基准测试而设计的,旨在提高某些类型应用的性能。

然而,值得注意的是,-XX:AggressiveOpts 标志已经被视为具有潜在风险和不稳定性,因为它包含了一些实验性的和可能不兼容的优化选项。随着Java的发展和更新,许多这些优化已经被集成到标准的JVM性能优化中,或者因为潜在的问题而被移除。

在Java 8 及以后的版本中,-XX:AggressiveOpts 的作用已经变得非常有限。根据Oracle的官方文档,-XX:AggressiveOpts只设置了两个参数:AutoBoxCacheMax = 20000 和 BiasedLockingStartupDelay = 500。这两个参数分别用于调整自动装箱缓存的大小和偏向锁启动的延迟。

虽然-XX:AggressiveOpts在早期版本的Java中可能是一个有用的工具,但在现代版本的Java中,它通常不被推荐使用,除非非常清楚需要启用的具体优化,并且这些优化对应用有显著的正面影响。

逃逸分析

逃逸分析(Escape Analysis)是一种评估Java对象可见范围的技术。尤其是指由某个执行线程创建的Java对象在另一个线程中可以访问,此时称该对象"逃逸"了。如果Java对象不发生逃逸,可以采用其他方法进行调优。可以使用

-XX:+DoEscapeAnalysis参数来启用该功能。从逃逸分析的定义可知,逃逸分析主要应用于多线程场景,对于单线程场景,则不会生效。而且,即使是多线程场景,也不能保证逃逸分析一定生效。逃逸分析并不是一种完美的技术,它可能会受到代码复杂性、编译器优化等因素的影响。因此,即使启用了-XX:+DoEscapeAnalysis参数,JVM也可能无法对所有对象都进行逃逸分析。

偏向锁

偏向锁是一种偏向于最后获得对象锁的线程的优化技术。当只有一个线程锁定该对象,没有锁冲突的情况下,其锁开销可以接近lock-free。在Java 8及以后的版本,默认开启偏向锁。经验表明,这个功能对于大多数应用而言是有效。对于不需要偏向锁的特殊场景,可以通过-XX:-UseBiasedLocking来关闭偏向锁。

大页面支持

计算机系统的内存被划分为称为"页"的固定大小的块。为了减少每次内存访问页表的代价,通常的做法是使用一块缓存,对虚拟地址到物理地址的转换进行缓存。这块缓存称为转译快查缓存(TLB)。

使用TLB完成从虚拟地址到物理地址的映射比遍历整个页表的方式要快的多。TLB通常只能容纳固定数量的条目。TLB中的一条记录就是按页面大小统计的一块内存地址区域的映射。

HotSpot VM在Linux上支持大页面。页面大小还可能随着处理器的不同而有所不同。另外,为了使用大页面,还可能需要对操作系统进行配置。

在Linux操作系统上使用大页面,除了需要在HotSpot VM启动时添加-XX:+UseL argePages参数,还需修改操作系统的配置。不同Linux发行版和内核的不同,需要进行的修改也不同。

参考

《Java性能优化权威指南》 Charlie Hunt, Binu John 著, 柳飞, 陆明刚 译
https://zhuanlan.zhihu.com/p/243064867 JVM常用参数配置
https://segmentfault.com/a/1190000044547802 JVM 8 调优指南:如何进行JVM调优,JVM调优参数

相关推荐
程序猿进阶3 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
阿龟在奔跑12 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
王佑辉12 小时前
【jvm】方法区常用参数有哪些
jvm
王佑辉12 小时前
【jvm】HotSpot中方法区的演进
jvm
Domain-zhuo12 小时前
什么是JavaScript原型链?
开发语言·前端·javascript·jvm·ecmascript·原型模式
Theodore_10221 天前
7 设计模式原则之合成复用原则
java·开发语言·jvm·设计模式·java-ee·合成复用原则
我是苏苏1 天前
Web开发:ORM框架之使用Freesql的DbFrist封装常见功能
java·前端·jvm
天草二十六_简村人2 天前
Java语言编程,通过阿里云mongo数据库监控实现数据库的连接池优化
java·jvm·数据库·mongodb·阿里云·微服务·云计算
老码沉思录2 天前
Android开发实战班 - 数据持久化 - Room 数据库应用
android·jvm·数据库
起名字真南2 天前
【C++】深入理解 C++ 中的继承进阶:多继承、菱形继承及其解决方案
java·jvm·c++·chatgpt·aigc