作者推荐 |【深入了解系统性能优化】「实战技术专题」全方面带你透彻探索服务优化技术方案(JVM优化篇)

发布时间:2023年12月17日


在这里插入图片描述

JVM性能优化和调优

JVM(Java虚拟机)是Java程序的运行环境,它负责将Java程序编译后的字节码转换成机器码并执行。JVM的性能对Java程序的运行速度和稳定性有着至关重要的影响。本文将介绍JVM性能优化和调优的一些技巧和方法。

内存管理

在这里插入图片描述

JVM的内存管理对程序的性能有着重要的影响。JVM的内存分为堆内存和非堆内存。堆内存用于存储对象实例,而非堆内存用于存储JVM本身的数据结构和代码。以下是一些内存管理的技巧:

  • 调整堆内存大小:JVM的堆内存大小可以通过-Xmx和-Xms参数来调整。-Xmx参数用于设置JVM最大可用的堆内存大小,而-Xms参数用于设置JVM启动时的堆内存大小。如果JVM的堆内存过小,程序可能会频繁进行垃圾回收,从而影响程序的性能。如果堆内存过大,会导致程序启动缓慢和内存浪费。

  • 使用垃圾回收器:JVM提供了多种垃圾回收器,每种垃圾回收器都有其优缺点。例如,CMS垃圾回收器适用于大型应用程序,而G1垃圾回收器适用于需要更好的响应时间的应用程序。选择合适的垃圾回收器可以提高程序的性能。

  • 避免内存泄漏:内存泄漏是指程序中的对象无法被垃圾回收器回收,从而导致内存占用不断增加。内存泄漏会导致程序的性能下降和系统崩溃。避免内存泄漏的方法包括及时释放对象、避免循环引用等。

线程管理

在这里插入图片描述

JVM的线程管理对程序的性能也有着重要的影响。以下是一些线程管理的技巧:

  • 避免线程死锁:线程死锁是指两个或多个线程互相等待对方释放锁,从而导致程序无法继续执行。线程死锁会导致程序的性能下降和系统崩溃。避免线程死锁的方法包括避免循环等待、按照相同的顺序获取锁等。

  • 使用线程池:线程池可以避免创建和销毁线程的开销,从而提高程序的性能。JVM提供了ThreadPoolExecutor类来实现线程池。

  • 避免线程上下文切换:线程上下文切换是指CPU从一个线程切换到另一个线程的过程。线程上下文切换会导致程序的性能下降。避免线程上下文切换的方法包括减少线程数量、使用协程等。

代码优化

在这里插入图片描述

代码优化是指通过改进代码结构和算法来提高程序的性能。以下是一些代码优化的技巧:

  • 减少对象创建:对象的创建和销毁会占用CPU和内存资源。减少对象的创建可以提高程序的性能。例如,可以使用对象池来避免频繁创建对象。

  • 使用高效的算法和数据结构:高效的算法和数据结构可以减少程序的运行时间和内存占用。例如,使用哈希表可以快速查找数据。

  • 避免过度优化:过度优化可能会导致代码难以维护和理解。优化应该在保证代码可读性和可维护性的前提下进行。

  • 选择合适的数据结构:选择 ArrayList 和 LinkedList 对我们的程序性能影响很大,为什么?因为 ArrayList 内部是数组实现,存在着不停的扩容和数据复制。

  • 选择更优的算法

最大子列和问题:

给定一个整数序列,a0, a1, a2, …… , an(项可以为负数),求其中最大的子序列和。如果所有整数都是负数,那么最大子序列和为 0;

例如(a[1],a[2],a[3],a[4],a[5],a[6])=(-2,11,-4,13,-5,-2)时,最大子段和为 20,子段为 a[2],a[3],a[4]。

  • 最坏的算法:穷举法,所需要的的计算时间是 O(n^3).
  • 一般的算法:分治法的计算时间复杂度为 O(nlogn).
  • 最好的算法:最大子段和的动态规划算法,计算时间复杂度为 O(n)

n越大,时间就相差越大,比如 10000 个元素,最坏的算法和最好的算法之间的差距绝非多线程或者集群化能轻松解决的。

  • 编写更少的代码:同样正确的程序,小程序比大程序要快,这点无关乎编程语言。

JVM与JIT编译器相关的优化

对JVM性能影响最大的是编译器。选择编译器是运行 java 程序首先要做的选择之一

热点编译的概念

对于程序来说,通常只有一部分代码被经常执行,这些关键代码被称为应用的热点,执行的越多就认为是越热。将这些代码编译为本地机器特定的二进制码,可以有效提高应用性能。

选择编译器类型

  • -server,更晚编译,但是编译后的优化更多,性能更高
  • -client,很早就开始编译
  • -XX:+TieredCompilation,开启分层编译,可以让 jvm 在启动时启用 client 编译,随着代码变热后再转为 server 编译。

缺省编译器取决于机器位数、操作系统和 CPU 数目。32 位的机器上,一般默认都是client 编译,64 位机器上一般都是 server 编译,多核机器一般是 server 编译。
在这里插入图片描述 mix mode 一般指编译时机:

  • -Xint 表示禁用 JIT,所有字节码都被解释执行,这个模式的速度最慢的。
  • -Xcomp 表示所有字节码都首先被编译成本地代码,然后再执行。
  • -Xmixed,默认模式,让 JIT 根据程序运行的情况,有选择地将某些代码编译成本地代
    码。
  • -Xcomp 和-Xmixed 到底谁的速度快,针对不同的程序可能有不同的结果,基本还是推荐用默认模式。

代码缓存相关

在编译后,会有一个代码缓存保存编译后的代码,一旦这个缓存满了,jvm 将无法继续编译代码。

当jvm提示: CodeCache is full,就表示需要增加代码缓存大小。-XX:ReservedCodeCacheSize=N 可以用来调整这个大小。

JVM类型代码缓存的默认大小
32位client Java832MB
32位server,分层编译,Java8240MB
64位server , 分层编译,Java8240MB
32位client,Java732MB
32位server,Java732MB
64位server,Java748MB
64位server,分层编译,Java796MB

编译阈值

代码是否进行编译,取决于代码执行的频度,是否到达编译阈值。

编译阈值

计数器有两种:方法调用计数器方法里的循环回边计数器,一个方法是否达到编译阈值取决于方法中的两种计数器之和。编译阈值调整的参数为:-XX:CompileThreshold=N

方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。

半衰周期

当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。

开启半衰周期

进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。

设置半衰周期值

可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。

编译线程

进行代码编译的时候,是采用多线程进行编译的。

方法内联

开启内联机制

内联默认开启,-XX:-Inline,可以关闭,但是不要关闭,一旦关闭对性能有巨大影响。方法是否内联取决于方法有多热和方法的大小,

设置方法的较热的方法内联大小阈值

很热的方法如果方法字节码小于325字节才会内联,这个大小由参数 -XX:MaxFreqInlinesSzie=N调整,但是这个很热与热点编译不同,没有任何参数可以调整热度。

设置方法的内联的方法大小的触发阈值

方法小于35个字节码,一定会内联,这个大小可以通过参数-XX:MaxInlinesSzie=N调整。

逃逸分析

JVM 所做的最激进的优化,最好不要调整相关的参数。

GC 调优

目标:GC的时间够小,GC的次数够少,发生 Full GC 的周期足够的长,时间合理,最好是不发生。

调优的原则和步骤

  1. 大多数的 java 应用不需要 GC 调优
  2. 大部分需要 GC 调优的的,不是参数问题,是代码问题
  3. 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多;
  4. GC 调优是最后的手段
GC 调优的最重要的三个选项
  • 第一位:选择合适的 GC 回收器
  • 第二位:选择合适的堆大小
  • 第三位:选择年轻代在堆中的比重

步骤

监控GC的状态

使用各种JVM工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和 GC 执行时间,觉得是否进行优化。

分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁 GC,则必须优化;

如果满足下面的指标,则一般不需要进行 GC
  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行 beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

推荐策略

年轻代大小选择
响应时间优先的应用

尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择),在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用

尽可能的设置大,可能到达GB的程度,因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。避免设置过小,当新生代设置过小时会导致:

  1. YGC次数更加频繁
  2. 可能导致YGC对象直接进入老年代,如果此时老年代满了,会触发FGC。
年老代大小选择
响应时间优先的应用

年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。

最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在年轻代和年老代回收上的时间比例。

吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代,原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

总结

JVM性能优化和调优是Java程序开发中的重要环节。通过合理的内存管理、线程管理和代码优化,可以提高程序的性能和稳定性。在进行优化时,应该根据具体情况选择合适的优化方法,并在保证代码可读性和可维护性的前提下进行优化。

全方面带你透彻探索服务优化技术方案

文章来源:https://blog.csdn.net/l569590478/article/details/135029047
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。