摆脱性能问题和内存泄漏!
如何确定 Java 线程池大小:综合指南
线程池可减少创建线程的开销(JVM 和操作系统的延迟和额外工作)。但是管理线程会增加开销,因此是否使用线程池并不明确。线程池还可以帮助管理线程使用的资源。
调整线程池大小以从系统中提取最佳性能并平稳应对峰值工作负载。
线程池大小不应超过数据存储请求的连接池大小(否则线程将等待连接,这是低效的);池大小也不应超过池正在使用的任何外部服务的处理能力(以免压垮该服务);此外,线程池大小应配置为低于压垮可用 CPU 所需的阈值,但这取决于线程处理的任务的性质,即 CPU 密集型(小池,不超过核心数)与 IO 密集型(池大小低于压垮整体 CPU 和整体 IO 处理的大小)。监控上下文切换可以帮助决定这些情况。
如果有空闲的 CPU,您可以将 CPU 密集型任务划分为更小的子任务,并将这些子任务分布到多个 CPU 核心上,从而优化 CPU 密集型任务。
您可以通过以下方式优化 IO 密集型任务:缓存经常访问的数据;负载平衡:跨多个线程执行任务;使用 SSD 而不是旋转磁盘;使用高效的数据结构,例如哈希表和 B 树;消除不必要的文件操作(例如,不要多次打开和关闭同一个文件)。
对于 CPU 密集型任务,确定线程池大小的常见经验规则是使用可用的 CPU 核心数。
对于 I/O 密集型任务,调整线程池的大小是为了拥有足够多的线程,使 I/O 设备保持繁忙但又不会过载。
确定线程池大小的公式是线程数 = 可用核心数 * 目标 CPU 利用率 * (1 + (等待时间 / 服务时间)),其中目标 CPU 利用率是您希望应用程序使用的 CPU 时间百分比,等待时间是线程等待 I/O 操作完成所花费的时间,服务时间是线程执行计算所花费的时间。
Java 性能工具箱一览
为了最小化容器大小,您可以使用剥离的基础映像(例如 jre-slim)并安装应用程序所需的最小组件。
jlink 让您生成自定义的剥离 JRE,它可以是您的应用程序的最小运行时(但请注意,如果您剥离我们的可服务性工具,如 jcmd,您将难以对生成的 JVM 进行故障排除)。jdeps 让您找到运行您的应用程序所需的剥离图像的最小模块集。
jcmd 可以从正在运行的 JVM 获取统计信息(例如 GC 统计信息),但过于频繁地运行 jcmd 会增加 JVM 的开销。
仅在需要时启用 -XX:NativeMemoryTracking,因为它会给 JVM 增加相当大的开销。
您可以使用任何 JMX 客户端(例如 jconsole)触发堆转储。
jstat 从正在运行的 JVM 获取统计数据。
jmap 让您从正在运行的 JVM 中获取堆直方图和堆转储。
可以启用 JFR 记录并让您调查性能。您可以将 JFR 记录从 JVM 中流出。
减少 Java 应用程序中的网络调用
缓存网络调用,其中来自前一次调用的数据至少在某些时间/某些调用中足够。缓存元素过期可以基于时间,也可以基于其他一些失效标准。
批量网络调用,即对一个端点进行多次调用,并且调用可以等待很短的时间。这减少了往返次数并提高了网络效率。
在通过网络发送数据之前对其进行压缩可以减少传输的数据量。
异步调用允许应用程序在网络调用进行时继续执行。这减少了应用程序被阻塞的时间。
优化数据库查询以消除不必要的数据传输并最大限度地减少检索的数据量。
检查网络调用,看是否完全没有必要,并将其消除。
解决 Java 应用程序中的本机内存问题
可以通过堆转储来查找内存泄漏的原因,但堆直方图(例如通过
jmap -histo ...
)可能会提供提示并且成本较低。本机内存不足错误可能是由于其他进程耗尽了系统内存而导致的。您可以移除或减少它们的内存使用量,和/或减少 JVM 上使用的堆以提供更多本机内存。
JVM 内存包括:堆、元空间(类和元数据)、代码缓存(字节码和编译后的代码)、JVM 管理空间、已加载的 jar 和本机库、线程堆栈和线程本地存储、JNI/JVMTI 分配的内存、NIO 分配、直接字节缓冲区。
要确认是否存在本机内存泄漏,您需要监视 JVM 的驻留集大小(如果使用多映射,则监视成比例的集大小)。查找持续增加的内存大小,然后通过将其与变化的进程内存大小进行比较来排除堆内存泄漏。
本机内存跟踪 (-XX:NativeMemoryTracking=summary / -XX:NativeMemoryTracking=detail) 可用于查看 JVM 已知的本机内存。这会对性能造成 5%-10% 的影响。可以使用 获取
jcmd
报告VM.native_memory [baseline, detail.diff, summary.diff]
。本机内存泄漏的一个常见原因是类生成。在这种情况下,限制元空间 -XX:MaxMetaspaceSize 可以防止主机耗尽资源(尽管 JVM 仍会遇到 OutOfMemroy 错误)对于 JVM 不管理的内存的本机内存跟踪,您需要使用本机内存跟踪工具,如 jemalloc、valgrind、dbx、purify、pmap 和核心转储文件。
本机内存泄漏分析的总体过程是:1. 确定机器 / 容器是否具有足够的本机内存可用于预期的内存使用量;2. 检查 JVM 进程内存是否持续增加;3. 强制进行垃圾收集并消除堆内存作为原因(或修复它);4. 使用 NativeMemoryTracking 消除 JVM 管理的本机内存作为原因(或修复它);5. 使用本机内存分析器或转储分析来查找 JVM 管理的本机内存之外的本机内存泄漏的原因。
常见的本机内存泄漏情况:使用与低内存基数中的可用空间冲突的压缩 oop(使用 -XX:HeapBaseMinAdress=n 修复);直接字节缓冲区(确保 Java 包装器已被释放);NIO API(确保 Java 包装器已被释放);膨胀器/收缩器(确保你使用 end() 结束它们);终结器(确保终结器处理线程已运行)。
防止内存泄漏的策略包括:确保对象仅在需要时才处于范围内;谨慎使用静态字段;避免无限增长的静态集合;在不再需要时,始终取消注册侦听器和回调;限制缓存的大小;在不再需要对象时,从集合中删除它们;谨慎使用内部类实例;使用后始终关闭资源(例如文件,流,连接);分析应用程序的内存使用情况;单元和集成测试以检查内存泄漏。
Java 内存泄漏几大原因总结
内存泄漏的一个常见原因是无限增长的静态集合。为防止泄漏,请限制集合的大小或定期清除它。
内存泄漏的一个常见原因是未关闭流、连接或文件句柄等资源。为防止泄漏,请确保使用 finally 块或 try-with-resources 关闭资源。
内存泄漏的一个常见原因是非静态内部类比外部类存在时间长 - 因为这些内部类隐式引用了外部类。要防止泄漏,请使用静态内部类或单独的类。
内存泄漏的一个常见原因是将缓存对象留在缓存中的时间过长。为防止泄漏,请使用支持过期的缓存库,如 EhCache 或 Guava 的 Cache。
内存泄漏的一个常见原因是线程池中不会终止的线程中的 ThreadLocal 变量。为防止泄漏,请在不再需要 ThreadLocal 变量时将其删除。
内存泄漏的一个常见原因是注册侦听器而不注销它们。为了防止泄漏,请始终提供注销侦听器的机制。
内存泄漏的一个常见原因是单例类持有大对象。为了防止泄漏,请确保单例类持有对象的时间不要超过必要的时间。
内存泄漏的一个常见原因是未关闭数据库连接。为防止泄漏,请在使用后始终关闭数据库连接。
内存泄漏的一个常见原因是频繁加载和卸载类。为了防止泄漏,请谨慎处理类加载器和已加载类的生命周期。
每个开发人员和架构师都必须知道的最重要的性能问题 - 第 2 部分 - 并发
运行多个同时线程是一项简单的任务,只要它们不与可变共享对象(由多个线程共享或访问但也可以被多个线程更改的对象)交互。
不可变的共享对象不会给多线程代码带来问题。
当两个或多个线程需要多个共享资源来完成其任务,并且它们以不同的顺序或不同的方式访问这些资源时,就会发生死锁。
过多的同步会导致大量线程缓慢且停滞。
在活锁中,两个或多个线程不断在彼此之间传输状态。它们不会像死锁那样等待,而是执行无进展的无用工作。通过检查无效状态(例如,当队列抛出异常时,重新将消息添加到队列)来避免活锁。
线程池的大小对性能很重要。如果线程池太小,那么当有资源可用来处理请求时,请求将不必要地等待;但如果线程池太大,那么太多线程将同时执行,争夺处理资源,从而导致上下文切换并降低整体效率。
处理 Java 中对共享资源的并发访问
当您拥有共享资源时,并发性会变得很困难。多个线程对共享资源的访问和更新是一种竞争条件。如果没有并发协调,访问和更新的结果将是不确定的。
Java
synchronized
和锁(例如 ReentrantReadWriteLock)提供对共享资源的互斥访问,这使得它们的同时使用具有确定性。volatile
变量提供对共享资源的独占访问和更新,但仅适用于一项操作,而不是一组操作。要使复合操作确定性地并发执行,您需要使用synchronized
或锁。当多个线程尝试以不同的顺序锁定多个
synchronized
监视器时,就会发生死锁。一个线程获取了第一个监视器并尝试获取第二个监视器,而第二个线程已经获取了第二个监视器并正在尝试获取第一个监视器 - 死锁。如果使用,虚拟线程将固定底层平台线程
synchronized
,因此请小心避免将其synchronized
用于任何非短暂操作。乐观锁定是一种不同的并发技术,在这种技术中,尝试执行操作,如果在此期间没有其他任何东西改变共享资源,则该操作会成功,但如果共享资源已发生改变,则该操作会失败。
Java 死锁预防和故障排除技巧
死锁是指两个或多个线程被阻塞并等待对方释放资源的情况。死锁可以通过以下方式检测:线程堆栈转储;JConsole、VisualVM 等许多工具;以及 LockSupport 中的代码。
防止死锁的策略:始终以固定顺序获取锁;避免长时间持有锁;减少持有锁的代码块的范围;使用 ReentrantLock 和类似的并发锁类来管理锁,例如使用 tryLock() 来获取锁而不阻塞;获取锁时使用超时以防止线程无限期等待;使用非阻塞算法和数据结构来完全避免死锁。
Java 堆内存优化以改善查询延迟
堆外内存映射的速度可能很快,但仍然比堆内内存慢。因此,请根据延迟要求对数据进行分段,并仅将延迟要求最低的数据保留在堆上。
-XX:+StringDeduplication(仅适用于 G1 GC)在空闲 CPU 周期内删除重复字符串(将重复字符串的 char[] 设置为指向相同的 char[])。这有利于减少内存使用量,但会产生一些 CPU 开销,并且会影响低延迟响应(由于竞争并发 CPU 开销)。
Guava interners 支持隔离缓存,是一种广泛使用的重复数据删除技术。
FALF - 固定大小数组,无锁 - 用于重复数据删除对象的内部器 - 代码。
较小的堆往往可以通过降低 GC 成本来改善延迟。
低延迟模式
通用代码很容易产生严重的尾部延迟。由于延迟会累积,如果最终用户请求击中多个内部应用请求,许多用户将达到中间请求的尾部延迟。
最大延迟很难优化,因此请重点关注接近最大延迟进行优化。
使用对数 x 轴或 eCDF 可视化来可视化延迟。
通过以下方式减少延迟:避免移动数据、最小化操作和避免等待。
通过以下方式避免数据移动:避免网络调用或尽量减少调用所产生的网络距离;共置数据;将数据复制到需要的地方;以及缓存数据。
通过减少操作来减少延迟:使用更简单的算法;使用最小化操作开销的内存结构(链表和图表通常是低延迟的糟糕选择);优化代码;避免 CPU 密集型计算;避免在快速/关键代码路径中分配内存;避免需求分页(确保正在使用的内存已经分页)。
优化代码以实现低延迟意味着:减少 CPU 周期、减少 CPU 缓存未命中等。将长时间运行的任务拆分为多个短任务。使用分析器查找效率低下的代码。请注意,您经常会用性能来换取其他东西(内存或整体延迟)。
通过以下方式避免等待:消除同步(例如,按核心划分数据并仅在核心上处理);使用无等待算法;将代码保留在用户空间(避免内核调用或尽可能绕过它);避免上下文切换(使用专用的线程到核心);使用异步/非阻塞 IO;使用忙轮询;使共享数据结构只读;使用单生产者+单消费者队列在核心之间传输;使用 TCP_NODELAY;不要处理来自网络的请求,将它们从队列中取出并单独处理,以便较长的延迟请求不会阻塞队列。
通过以下方式隐藏延迟:并行化请求处理;将请求发送到多个服务器并采取最快的响应;使用轻量级线程。
调整系统以实现低延迟:配置 CPU 频率为恒定(性能),将 CPU 隔离到特定线程,禁用交换,配置网络堆栈中断亲和性。
为了降低延迟,事务性需要尽可能简单。当你必须进行复杂的事务时,幂等性可以显著降低恢复的复杂性。
针对低延迟应用程序优化 Java
低延迟应用程序垃圾收集的关键问题是停止世界暂停。
选择正确的垃圾收集器对于优化应用程序性能至关重要:串行 GC 适用于内存占用低、CPU 要求低的小型应用程序,但由于 GC 暂停时间较长,因此不适用于低延迟应用程序;并行 GC(吞吐量收集器)通过使用多个线程进行垃圾收集来最大化应用程序吞吐量,但由于 GC 暂停时间较长,因此也不适用于低延迟应用程序;CMS(并发标记清除)旨在通过与应用程序线程并发工作来最大限度地减少暂停时间,虽然暂停时间较短,但容易受到碎片的影响,最终导致长时间暂停;G1(垃圾优先收集器)试图在吞吐量和暂停时间之间提供良好的平衡,但并不能提供最短的暂停时间;ZGC(Z 垃圾收集器)和 Shenandoah 专为低延迟应用程序设计,旨在实现少于 10 毫秒的暂停时间,即使在大型堆上也是如此。
垃圾收集调优的最佳实践:优化堆大小,太小的堆会导致频繁的 GC 循环,太大的堆可能会导致更长的 GC 暂停;了解应用程序的分配模式以调整不同 GC 代的大小,避免填充旧代对于避免大多数 GC 算法的长 GC 至关重要;使用 GC 日志记录和监控来了解应用程序中 GC 的行为,识别频繁或长时间暂停等问题。
高效的编码可提高执行速度并减少垃圾收集开销。技术包括:选择正确的数据结构;在适当的情况下使用原始类型以避免使用装箱类型;根据特定需求构建自定义数据结构。
避免内存泄漏:确保流、连接和其他 I/O 对象等资源在使用后正确关闭;使用 try-with-resources;对可以重新创建的缓存和大型数据结构使用弱引用;进行分析以识别和修复内存泄漏。
算法效率的微小变化可能会对性能产生重大影响。
最小化循环内的工作、避免循环内的方法调用和循环展开等简单的技术可以提高性能。
对于性能热点,延迟计算或对象创建直到绝对必要为止。
使用细粒度锁或无锁数据结构。
定期进行概要分析和性能测试。
正确设置初始(-Xms)和最大(-Xmx)堆大小以防止频繁的垃圾收集,但请注意,过大的堆大小可能会导致更长的 GC 暂停。
为应用程序选择最佳的垃圾收集器,例如 -XX:+UseG1GC、-XX:+UseConcMarkSweepGC 或 -XX:+UseZGC 等。
启用 GC 日志记录。
高级 JVM 调优技术包括:微调垃圾收集器;使用堆外内存;调整 JVM 的代码缓存(例如 -XX:InitialCodeCacheSize 和 -XX:ReservedCodeCacheSize);其他编译器标志,如 -XX:CompileThreshold。
调优时:进行单一更改并根据监控结果逐步调整;在真实负载条件下进行测试;定期检查和更新 JVM 设置以适应应用程序更改和 JVM 更新。
设计应用程序以有效利用多个核心和线程是低延迟系统的关键。确保线程高效安全地协同工作,在并发执行以加快处理速度和管理并发带来的开销之间取得适当的平衡。
在网络和 I/O 密集型应用程序中,使用异步模式防止线程在等待数据时处于空闲状态。
有效缓存:确定缓存什么、何时缓存以及缓存多长时间,以优化应用程序的数据访问需求,同时确保缓存的数据保持相关性和最新性。
事件驱动架构在对外部变化的实时响应至关重要的应用程序中特别有用。
编写可扩展 Java 应用程序:最佳实践和策略
可扩展性是指在不影响性能的情况下处理增加的负载 - 以高效可靠的方式处理更多请求。可扩展性可以垂直实现(向单个节点添加更多资源)和水平实现(通过向系统添加更多节点)。水平扩展通常因其在可用性和容错方面的优势而受到青睐。
一些有助于扩展的架构实践是:将系统分解为提供特定服务子集的模块化单元,以独立扩展这些服务(例如微服务);松散耦合,例如与事件驱动的请求流;和容器化。
异步处理(例如支持背压的反应系统)提高了扩展能力。
高效的内存管理对于扩展组件至关重要:消除内存泄漏,使用适当的数据结构,并通过选择和调整垃圾收集器来优化代码中的垃圾收集。
有效的并发管理对于可扩展性至关重要:使用 java.util.concurrent 数据结构和控制机制,高效地管理线程,并且优先使用不可变对象。
定期分析和优化有助于保持可扩展性:识别和优化代码瓶颈。
调整 JVM:调整堆大小 - 更多的内存可以减少垃圾收集的频率,但太多会导致资源浪费和长时间的 GC 暂停;选择和调整垃圾收集器;。
有效缓存。根据数据和请求类型适当地选择本地缓存和分布式缓存,并确保缓存失效策略提供有效的缓存命中率。
高效的数据库交互通常涉及连接池和优化查询,以及针对应用程序的更新和访问模式优化数据库结构。
有效的监控对于理解和提高性能至关重要:确保系统具有应用程序性能监控、日志框架、日志分析工具、收集指标并执行健康检查。
https://www.youtube.com/watch?v=ZKe4jGe1sL4
Kubernetes pod 中运行的 Java 进程的内存设置
由于 JVM 仅遵守堆大小限制,而不遵守非堆内存限制,因此无法保证 Java 进程的完整内存边界。非堆内存很难预测,因为它取决于多种因素(代码生成、线程数、GC 算法、加载的类数等)。
如果使用的本机内存超出容器内存限制,容器将被 OOM Killer 终止。由于 JVM 使用的内存量是可变的,并且只有堆内存受到限制,因此您需要确保 JVM 堆限制充分低于容器限制,以使 JVM 非堆内存使用量不会使容器本机内存使用量超出限制。
需要注意的是,-XX:MaxRAMPercentage 不会限制 Java 进程可以使用的内存总大小。它具体指的是 JVM 堆大小。
已提交内存是 JVM 已分配的系统内存的一部分。Xms 堆内存在启动时已提交,并根据 JVM+应用程序的需求增加到 Xmx。如果堆中不再需要已提交的内存,某些垃圾收集器可以将其返回,但除非发生这种情况,否则堆使用量可能远低于已提交的内存。
设置容器 JVM 内存的建议步骤是:1. 从 MaxRAMPercentage 的 75% 开始;2. 如果最大堆使用率保持较高水平,则需要增加容器内存,但如果堆使用率正常但进程大小接近容器限制,则需要减少 MaxRAMPercentage(或提供更多容器内存)。
https://medium.com/codex/running-jvm-applications-on-kubernetes-beyond-java-jar-a095949f3e34
在 Kubernetes 上运行 JVM 应用程序:超越 java -jar
JVM GC 选择(如果未设置显式 GC 算法标志):如果 CPU 数量大于或等于 2,且内存量大于 1792MB,则所选 GC 为 G1 GC(或 Java 9 之前的 ParallelGC)。如果这两个条件中的任何一个低于上述值,则所选 GC 为 SerialGC。
当未设置 -Xmx 时,JVM 将最大堆值配置为可用(容器)内存的四分之一,除非当最大堆值为 50% 时可用内存低于 256MB,或者当最大堆值约为 127MB 时可用内存介于 256MB 和 512MB 之间。
要找出您的 JVM 正在使用哪种 GC 实现,请执行
java -XX:+PrintFlagsFinal -version | grep "Use" | grep "GC"
(或在 Windows 上执行 findstr 而不是 grep)。同样,过滤“MaxHeapSize”将显示 Xmx 值。确保 JVM 不限于仅有 1 个可用 CPU,从而避免在高并发应用中使用 SerialGC。如有必要,请将 -XX:ActiveProcessorCount 标志设置为大于 1 的值,但请注意,实际可用 CPU 与提供的值之间的差异可能会导致 JVM 效率低下。
指定所需的 GC 实现。一个简单的建议是,对于 4GB 以下的堆使用 ParallelGC,对于 4GB 以上的堆使用 G1。选项包括 -XX:+UseSerialGC / -XX:+UseParallelGC / -XX:+UseG1GC / -XX:+UseShenandoahGC / -XX:+UseZGC。
最好使用 CPU 限制为 2000m 或更高的容器。通常,在具有 2000m CPU 限制的容器中安装单个 JVM 比在具有 1000m CPU 限制的两个容器中安装两个单独的 JVM 更好。
最好使用 -Xmx 参数或 -XX:MaxRAMPercentage 明确配置 JVM 堆大小,而不是让 JVM 选择一个值。
JVM 内存区域包括堆和非堆(本机内存/堆外)。非堆中包括元空间、代码缓存、堆栈内存、GC 数据等。因此,您应该将 JVM 的堆大小配置为可用内存的 50% 到 75% 之间,将剩余值保留给非堆和操作系统。
使用负载测试来验证配置的堆大小是否适合您的应用程序,检查是否出现 OOM(包括来自 OOM 杀手的)。
如果要避免 JVM 处理内存分配和释放任务,请将 Xms 设置为等于 Xmx 的值。但这意味着 JVM 进程大小永远不会最小化。
对于 Kubernetes 容器,请使用内存请求相等限制。如果您的 JVM 将进行非同步处理,则使用可突发容器(CPU 限制大于请求),让 JVM 使用其他容器未同时使用的备用 CPU 资源。
基于 CPU 的水平 Pod 自动扩缩可以适用于 JVM,因为负载较高的 JVM 通常会生成更多对象,因此会使用更多 CPU 密集型的 GC。
实用性能分析
在衡量绩效时,您需要在开始之前设定一个目标,以便知道要衡量什么以及目标是什么。
稳定状态负载测试是在正常生产负载下进行测试;极限负载测试是增加负载直到资源饱和,以找出应用程序的极限并确定哪些资源限制了规模。它还会告诉您当负载随后减少时系统是否会恢复,或者极限负载是否破坏了服务。
有用的操作系统级别变化:增加打开文件的数量,扩展临时端口范围,将 CPU 调节器更改为性能(阻止 CPU 频率动态改变)。
一个好的 JVM 配置是 JVM 21 或更高版本,具有生成 ZGC(-XX:+UseZGC -XX:+ZGenerational)和 -XX:+DebugNonSafepoints(当然 -Xmx 足够大,适合您的应用程序,这样它就不会因为堆太小而受到限制)。
至少跟踪这些资源:CPU、网络、SSD、性能、故障、GC、线程、连接、响应(时间、类型、错误)。
始终测量基线 - 在逐渐增加且系统没有问题之后测量正常负载的测试 - 以便您可以将未来的异常测量结果与之进行比较。
USE 方法:利用率、饱和度、错误。首先查找错误。然后是饱和度 - 每种资源的最大容量。最后是利用率。利用率是一段时间内资源使用量的平均值,饱和度是确定资源在一段时间内何时达到容量。当利用率不是 100% 时,您可以拥有饱和资源,因为饱和发生的时间段比利用率的平均计算时间更短。
如果负载测试发现存在不足,则应分析导致不足的组件。使用没有安全点偏差的分析器。
确保在负载测试中产生负载的客户端本身没有饱和,从而导致客户端限制。
寻找 Java 的隐藏性能陷阱
要找到性能问题的原因,请按照以下步骤操作:1. 查看分布式跟踪以了解请求延迟的位置;2. 查看影响步骤 1 中已识别组件的指标并查找异常值;3. 对已识别的组件进行分析(例如使用 JFR)。
分布式跟踪让您看到低效率,例如重复的相同查询,不必要的额外跳数,可以并行化的子请求。
性能问题的常见原因包括:线程池太小、GC 暂停、等待从池中获取连接的时间太长、缓存命中率太低、错误太多、日志记录效率低下(例如构建从未使用过的字符串)。检查您的指标是否存在常见问题。
一旦您解决了性能问题,您就应该公开一个可以识别类似情况的指标,并在再次发生时发出警报。
避免在锁定的方法(例如事务)中进行网络调用,因为方法中的资源(例如 JDBC 连接)在调用时处于空闲状态但被锁定。
驯服野外性能问题:JVM 分析实用指南
性能调优就是以更高效的方式利用资源。通过分析来识别低效之处是实现此目标的好方法之一。
仅从考虑系统的角度来看,抽象的层次和级别太多而无法知道什么是低效的——您需要衡量资源使用情况并找出低效率。
任何受到安全点偏差影响的分析器所提供的信息量都会比没有受到安全点偏差影响的分析器少——仅从安全点进行分析会错过潜在的重要信息并导致误导性分析。
async-profiler 是更好的 Java 分析器之一,它没有安全点偏差,可以配置为低影响,可以看到本机堆栈帧以及 Java 堆栈帧,具有集成的火焰图,并且具有多种分析模式(包括 CPU、挂钟、锁、分配、缓存未命中等)。
如果线程过多,挂钟分析将使用线程采样 - 否则采样将花费太长时间,并且分析将无效。这意味着您可能会丢失重要数据,尤其是在较短的分析周期内。
使用正则表达式来调查火焰图以查看哪些框架占用时间。
事件驱动框架有来自抽象层的开销。
对错误的资源(即不会导致效率低下的资源)进行分析会提供误导性或无用的信息。您可能需要在不同级别进行多次调查,以确定哪些资源需要进行分析才能进行有效调整。
确保测试时不会测量热身效果,除非您特别想测量热身效果。
高 CPU 使用率需要 CPU 分析,但低 CPU 使用率需要其他分析,例如挂钟分析。
对于任何需求量很大的资源,缓存都是一个很好的解决方案,通过提供预先计算的数据可以减少需求。
日志记录的开销可能很高,并且误用日志记录框架或使用效率低下的现象很常见。
瓶颈可能会掩盖其他瓶颈,因此请注意,提高瓶颈的效率可能不会使应用程序更高效。您需要反复进行调整,直到实现目标。
使用错误的系统时钟源会产生非常低效的计时代码。深入到内核调用的分析器将显示此类低效。
最强大的优化是删除代码和缓存。
TLAB 中的分配比 TLAB 外的分配效率高得多,因此使用显示这些不同分配的分析器很有用。
通过垂直可扩展性和有效测试最大化金融交易系统的性能和效率
分配少量内存是高效的,但您尝试分配的内存越多、越大或同时分配的内存越多,就越有可能达到 CPU 缓存共享限制并大大减慢分配时间(数量级!)。对于在高并发性下分配的小对象,分配开销远大于 GC 开销。
所有 JVM 和线程的最大总分配率为 10-15 GB/秒。降低分配率通常可以提高吞吐量。
使用中央服务器提供时间戳的分布式唯一时间戳至少需要 100 微秒。UUID 需要 300 纳秒,但不能直接映射到时间戳。通过采用纳秒时间戳并用主机 ID 替换最低 2 位数字,您可以在 40 纳秒内获得一个没有中央服务器的唯一时间戳(使用共享内存使其在每个主机上都是唯一的)。
延迟的最大来源往往是持久性(DB、持久消息、HA、磁盘存储等)。通过最小化持久性,您可以优化延迟。另一台机器上的冗余副本通常比本地磁盘上的副本快得多。
将 Java 推向极限:在 2 秒内处理 10 亿行数据
整数运算比浮点运算快得多。如果小数点后有固定数量的小数值,则可以使用整数来表示它,并在需要最终结果时只需除以数量级即可。
如果并行处理数据,则每个核心加载一个块来处理比加载一个大块并并行处理更有效率,因为一个大块将在 CPU 缓存之间共享,效率要低得多。
通过内存映射加载文件数据比使用文件读取来处理文件要快得多。
一次从内存(而非磁盘)读取 8 个字节比逐字节读取更有效,因为有一个 CPU 指令可用于此(参考 SWAR)。
如果除以 2 的幂,则移位比除法更快。
如果您只需要一系列任务末尾的字符串,则以长整型表示字符串会非常有效。
无分支编程(没有 if 语句,没有替代类实现)非常适合 CPU 管道,因此效率极高。
使用前向探测的开放地址哈希映射比带有链接节点的哈希映射更快,因为您避免了额外的节点创建和链接操作。
每个核心使用一个数据结构,然后在处理结束时合并数据结构意味着 CPU 缓存使用单独的数据并且不会争用,从而速度更快。
避免创建对象非常有效,但编码困难并且往往会降低可维护性。