写点什么

JVM 调优深度剖析:优化 Java 应用的全方位攻略(一)

作者:王中阳Go
  • 2024-10-29
    湖南
  • 本文字数:5518 字

    阅读完需:约 18 分钟

JVM 调优深度剖析:优化 Java 应用的全方位攻略(一)

这是我们就业陪跑训练营学员总结的文章,我觉得不错,和大家分享一下。


Java 应用中,JVM 调优至关重要。本文聚焦 JVM 调优,涵盖主要目标、思路与方法。目标包括减少 GC 停顿、提高吞吐量等; 思路有多步,如选垃圾回收器、设堆内存等。方法分系统参数配置(含不同回收器应用场景与参数调节)、代码优化(避免过度创建对象等)、架构设计(水平扩展等)。掌握这些,可为 Java 应用高效运行助力。


关注我,下一篇为你继续分享:《调优监控工具的使用与调优后实现的优化目标》

1 JVM 调优的主要目标

  1. 减少垃圾回收 GC)的停顿时间:在垃圾回收过程中,应用程序会暂停执行(Stop-The-World,STW),过长的 GC 停顿时间会影响应用的响应时间和性能。

  2. 提高吞吐量:在批量处理或高负载任务中,提升每单位时间内处理的数据量。

  3. 减少内存泄漏和频繁的 FullGC:通过监控和调优减少频繁的 Full GC 和内存泄漏,提升系统的稳定性。

  4. 减少 CPU 和内存的占用:尽量减少 JVM 占用的系统资源,提高资源利用效率。

  5. 提高应用的响应速度:对用户交互频繁的应用(如 Web 应用),需通过调优减少响应延迟,增强用户体验。

2 JVM 调优的思路可以归纳为以下几个步骤:

  1. 选择合适的垃圾回收器

  2. 合理设置堆内存大小

  3. 调整新生代和老年代的内存比例

  4. 调节 GC 停顿时间

  5. 调节 JIT 编译优化

  6. 减少内存碎片和内存泄漏

  7. 优化线程管理和线程堆栈大小

3 调优方法分类

(一)系统参数配置及原因

堆内存大小调整:

  • 指令:通过-Xms 和-Xmx 来实现,一般建议将初始堆大小和最大堆大小设置为相同的值。

  • 原因:当堆内存大小发生变化时,JVM 需要进行一些额外的操作来调整内存布局。例如,在堆内存收缩时,JVM 需要移动对象来释放内存空间;在堆内存扩展时,JVM 需要向操作系统申请更多的内存。这些操作都会带来一定的性能开销。如果初始堆大小和最大堆大小相同,就可以避免这种因内存重新分配而产生的开销,使得内存管理更加稳定和高效。


垃圾回收器选择:


  • 根据不同应用场景选择合适的垃圾回收器。

Serial GC(-XX:+UseSerialGC)

  • 这是最基本的单线程垃圾回收器。它在进行垃圾回收时会暂停所有应用程序线程,直到垃圾回收完成。适用于小型应用程序或者对暂停时间不太敏感的单 CPU 环境。例如,在启动 Java 程序时可以这样设置:

  • java -XX:+UseSerialGC YourMainClass

  • 使用场景

  • 简单的文件加密工具项目

  • 项目描述:开发一个简单的命令行工具,用于对本地文件进行加密。用户通过命令行参数指定要加密的文件路径和加密算法(如 AES),工具读取文件内容,加密后将结果输出到新文件。

  • 使用 Serial GC 的原因:这个工具相对简单,主要涉及文件读取、加密操作和文件写入。它是一个单线程的应用(加密操作本身也是单线程执行更安全),内存占用相对较小。在加密过程中,即使垃圾回收暂停了应用程序,由于操作相对短暂,用户也不太会察觉到明显的延迟。而且对于这种小型工具应用,Serial GC 的简单性使得它更容易集成和调试。例如,在开发过程中,如果出现内存泄漏等问题,通过简单的调试工具和 Serial GC 的单线程回收机制,可以比较容易地定位问题。- 调节 GC 停顿时间:对于 Serial 收集器,由于其单线程的特性,很难直接调节停顿时间。不过,可以通过调整堆内存大小(-Xms 和 -Xmx 参数)来间接影响停顿时间。如果堆内存设置得较小,那么 GC 的工作范围也会相对较小,可能会减少停顿时间,但同时也可能会导致 GC 更加频繁。

Parallel GC(-XX:+UseParallelGC)

  • 它使用多个线程来进行垃圾回收,能够更有效地利用多核 CPU 资源,从而提高垃圾回收的效率。在进行垃圾回收时仍然会暂停应用程序线程。可以通过以下方式启用:

  • java -XX:+UseParallelGC YourMainClass

  • 使用场景

  • 大数据日志分析项目

  • 项目描述:在一个大型企业的服务器集群中,需要对海量的系统日志和业务日志进行分析。日志数据每天会产生数 TB,应用程序需要从分布式存储系统(如 HDFS)中读取日志文件,进行数据清洗(如去除无用的日志字段、格式化日期等),然后通过一些统计分析算法(如计算某个业务操作的频率、用户访问的峰值时间等)来提取有价值的信息。

  • 使用 Parallel GC 的原因:

  • 这个项目是一个典型的后台数据处理应用,运行在多核服务器环境。在数据处理过程中,会产生大量的临时对象,如日志数据的解析对象、统计数据的中间结果对象等。Parallel GC 可以利用多核 CPU 的优势,通过多个线程并行进行垃圾回收。例如,当一个批次的日志数据处理完成后,Parallel GC 能够快速地回收这些临时对象所占用的内存,为下一批次的日志处理腾出空间,从而提高整个日志分析系统的吞吐量,满足企业对海量日志快速分析的需求。

  • 调节 GC 停顿时间:

  • 控制垃圾收集的吞吐量:

  • 可以使用 -XX:MaxGCPauseMillis 参数来设置期望的最大 GC 停顿时间。JVM 会尽量调整垃圾收集的频率和强度,以满足这个停顿时间的要求。不过需要注意的是,设置这个参数可能会导致吞吐量下降。例如,如果将 -XX:MaxGCPauseMillis 设置为 50 毫秒,JVM 会尝试通过调整新生代和老年代的大小、GC 的频率等方式来将 GC 停顿时间控制在 50 毫秒以内。

  • 调整新生代大小: 通过 -Xmn 参数来设置新生代的大小。增大新生代的大小可以减少 Minor GC 的频率,但每次 Minor GC 的时间可能会增加。因为在新生代空间较大的情况下,垃圾收集器需要处理更多的对象。相反,减小新生代的大小会增加 Minor GC 的频率,但每次 Minor GC 的时间可能会缩短。CMS(Concurrent Mark - Sweep)GC(-XX:+UseConcMarkSweepGC)

CMS(Concurrent Mark - Sweep)GC (-XX:+UseConcMarkSweepGC)

  • CMS 垃圾回收器的目标是尽量减少垃圾回收时应用程序的暂停时间。它主要有四个阶段:初始标记、并发标记、重新标记和并发清除。其中初始标记和重新标记阶段会暂停应用程序线程,但时间相对较短;并发标记和并发清除阶段与应用程序线程并发执行。启用 CMS 垃圾回收器可以使用以下指令:

  • java -XX:+UseConcMarkSweepGC YourMainClass

  • 不过,CMS 垃圾回收器也有一些缺点,比如它会占用更多的 CPU 资源用于并发执行,并且可能会产生内存碎片。可以通过-XX:CMSInitiatingOccupancyFraction 参数来设置在老年代空间使用达到多少百分比时触发 CMS 垃圾回收。例如,设置为 70%:

  • java -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction = 70 YourMainClass

  • 高流量电商网站后端项目

  • 项目描述:开发一个电商网站的后端服务,包括商品管理、订单处理、用户认证等多个模块。这个后端服务需要同时处理大量的用户请求,如用户查询商品信息、添加商品到购物车、下单支付等操作。

  • 使用 CMS GC 的原因:对于电商网站这种对响应时间要求极高的应用,用户在浏览商品和下单过程中的体验至关重要。如果垃圾回收导致应用程序长时间暂停,用户可能会遇到页面加载缓慢或者操作无响应的情况。CMS GC 的并发标记和清除阶段可以在很大程度上减少这种暂停时间。例如,当大量用户在促销活动期间访问网站时,CMS GC 可以在后台并发地进行垃圾回收,同时保证前端应用能够及时响应用户的各种操作请求,如更新购物车数量、查询订单状态等,从而提高用户的满意度和购物体验。

  • 调节 GC 停顿时间:

  • 调整初始标记和重新标记阶段的参数:可以通过 -XX:+CMSScandStartInitialMarkAtBoot 参数,在启动时就开始初始标记阶段,这样可以减少应用程序运行过程中的停顿。对于重新标记阶段,可以通过 -XX:ParallelCMSThreads 参数来设置并行的线程数,增加线程数可以加快重新标记的速度,从而减少停顿时间。

  • 调整 CMS 的触发阈值:使用 -XX:CMSInitiatingOccupancyFraction 参数来设置老年代使用达到多少百分比时触发 CMS 收集。如果将这个参数设置得较低,CMS 收集会更频繁,但每次收集的停顿时间可能会较短;如果设置得较高,CMS 收集的频率会降低,但每次收集的停顿时间可能会因为老年代中对象过多而变长。

G1(Garbage - First)GC(-XX:+UseG1GC)

  • G1 垃圾回收器是一种面向服务器端应用的垃圾回收器,它将堆内存划分为多个大小相等的区域(Region),在进行垃圾回收时可以优先回收垃圾最多的区域。它能够在尽量短的时间内完成垃圾回收,并且可以比较好地控制垃圾回收暂停时间。启用 G1 垃圾回收器的指令如下:

  • java -XX:+UseG1GC YourMainClass

  • 可以通过-XX:MaxGCPauseMillis 参数来设置最大垃圾回收暂停时间的目标值。例如,设置最大暂停时间为 200 毫秒:

  • java -XX:+UseG1GC -XX:MaxGCPauseMillis = 200 YourMainClass

  • 使用场景

  • 大型企业级资源管理系统项目

  • 项目描述:

  • 构建一个大型企业的资源管理系统,用于管理企业内部的各种资源,包括人力资源(员工信息、考勤记录等)、物资资源(办公设备、库存物资等)和财务资源(预算、成本核算等)。这个系统有一个庞大的数据库连接池,并且会缓存大量的业务数据(如经常访问的员工信息、物资库存报表等)以提高性能。

  • 使用 G1 GC 的原因:

  • 该系统的堆内存占用很大,因为要存储大量的业务对象和缓存数据。同时,它运行在多处理器服务器环境,并且需要满足不同用户(如人力资源部门、物资管理部门、财务部门等)对系统的不同需求。有些操作(如人力资源部门批量导入员工考勤数据)对吞吐量有要求,而另一些操作(如财务部门实时查询预算使用情况)对响应时间敏感。G1 GC 可以将堆内存划分为多个区域,有针对性地回收垃圾最多的区域,有效地管理内存,并且可以通过参数控制垃圾回收暂停时间,从而很好地满足这个大型企业级资源管理系统复杂的内存管理需求。

  • 调节 GC 停顿时间:

  • 设置期望的最大停顿时间:

  • 通过 -XX:MaxGCPauseMillis 参数来设置期望的最大 GC 停顿时间,与 Parallel 收集器类似,G1 会根据这个参数来调整回收策略。例如,将 -XX:MaxGCPauseMillis 设置为 200 毫秒,G1 会尽量将每次 GC 的停顿时间控制在 200 毫秒以内。

  • 调整 Region 大小和数量:

  • 可以通过 -XX:G1HeapRegionSize 参数来调整 Region 的大小。较小的 Region 大小可以更精细地控制内存回收,但可能会增加管理成本;较大的 Region 大小则相反。同时,G1 会根据 Region 的使用情况和期望的停顿时间来动态调整回收的 Region 数量。

并发线程数调整

  • 指令:通过-XX:ParallelGCThreads 来调整,一般设置为与处理器数目(CPU 核心的数量)相等。

  • 原因:

  • 充分利用硬件资源

  • 如果设置的垃圾回收线程数多于处理器核心数,会导致线程之间过度竞争 CPU 资源。例如,有 8 个垃圾回收线程但只有 4 个处理器核心,那么这些线程就需要争夺有限的 CPU 时间片来执行任务,这可能会导致额外的上下文切换开销。上下文切换是指 CPU 从一个线程切换到另一个线程执行任务时,需要保存当前线程的状态并加载下一个线程的状态,这个过程会消耗一定的时间和系统资源。相反,如果设置的线程数过少,比如只有 2 个垃圾回收线程在 4 核处理器上,那么就无法充分利用所有的处理器核心,造成硬件资源的浪费,垃圾回收的效率也会降低。

  • 避免资源过度竞争和浪费

  • 当设置-XX:ParallelGCThreads 的值等于处理器数目时,垃圾回收器可以为每个处理器核心分配一个线程来进行垃圾回收工作。这样,在垃圾回收阶段,每个核心都能独立地处理一部分垃圾回收任务,实现真正的并行处理。例如,如果计算机有 4 个处理器核心,设置 4 个垃圾回收线程,就像有 4 个工人同时清理垃圾一样,可以最大限度地利用硬件的并行处理能力,从而提高垃圾回收的效率。

JIT 编译器调整

  • 指令:通过-XX:+PrintCompilation 参数观察编译过程,在某些情况下通过-XX:CompileCommand 参数禁用对某个方法或类的编译。

  • java-XX:+PrintCompilation-XX:CompileThreshold=1000YourApp

  • 此处设置 JIT 编译器在方法调用达到 1000 次后进行编译,同时打印编译日志,以帮助分析性能瓶颈。

  • 原因:JIT 编译器的优化可以提高热点代码的执行速度。

内存区域划分调整

  • 指令:通过-XX:NewRatio 和-XX:SurvivorRatio 来调整新生代和老年代的比例。

  • 例如,-XX:NewRatio=4 表示年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5;

  • -XX:SurvivorRatio=4 表示两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6。

  • 原因:新生代和老年代的比例没有固定的标准,需要根据应用程序的特点进行调整。一般来说,新生代的比例可以在 1/3 到 1/2 之间。例如,如果堆内存大小为 4GB,可以将新生代设置为 1.5GB,老年代设置为 2.5GB。内存区域划分的调整是为了根据应用的特点合理分配内存,避免某个区域的内存不足导致频繁的垃圾回收。

(二)代码优化

在代码层面,应避免过度创建对象,以减少垃圾回收的负担。比如,尽量使用局部变量,因为调用方法时传的参数以及在调用中创建的临时变量都保存在栈中,速度较快,而其他变量如静态变量、实例变量等都在堆中创建,速度较慢,且栈中创建的变量随着方法的运行结束就消失了,不需要额外的垃圾回收。选择合适的集合类型也很关键,例如 ArrayList 随机遍历快,LinkedList 添加删除快。同时,要优化循环操作,如 for(int i =0; i<list.size(); i++){...}可替换为 int length = list.size();for(int i =0; i<length; i++){...},这样在 list.size()很大的时候,可以减少很多消耗。

(三)架构设计

水平扩展可以通过增加服务器节点来分担负载,提高系统的吞吐量。异步处理可以使用消息队列等技术,将耗时的操作异步执行,避免阻塞主线程。缓存优化可以减少对数据库等外部资源的访问,提高响应速度。分布式设计可以将系统的不同模块部署在不同的服务器上,提高系统的可扩展性和可用性。


好了,这期内容先和你分享到这里。


关注我,下一篇为你继续分享: 《调优监控工具的使用与调优后实现的优化目标》

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:infoq 面试群。

用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
JVM 调优深度剖析:优化 Java 应用的全方位攻略(一)_Java_王中阳Go_InfoQ写作社区