跳到主要内容

Java性能优化:衡量指标、理论方法与技术手段详解

·6679 字·14 分钟

本文深入探讨了Java性能优化,涵盖衡量指标、理论方法及七大优化手段:复用优化、结果集优化、高效实现、算法优化、计算优化、资源冲突优化和JVM优化。通过具体示例和代码片段,详细解析了各技术的应用场景与实现方式。

1. 衡量指标 #

  1. 响应时间
    1. 秒开率
  2. 吞吐量和响应速度
    1. 并发量
  3. CPU使用率
  4. 内存使用率
  5. GC情况

1.1 响应时间 (Response Time) #

  • 定义:从用户或系统发出请求到收到响应之间的时间。
  • 参数:通常以毫秒(ms)为单位,具体取决于应用场景。例如,在Web应用中,理想的响应时间应该在几百毫秒内;对于实时性要求高的系统(如金融交易),响应时间可能需要控制在几十毫秒甚至更低。
  • 如何衡量:可以通过日志记录每个请求的开始时间和结束时间,计算差值即为响应时间。也可以使用专业的性能测试工具如JMeter、Gatling等来模拟大量并发请求并统计平均响应时间。
  • 指标:
    • 平均响应时间
      • 是最常用的指标,指标能够体现服务接口的平均处理能力。它的本质是把所有的请求耗时加起来,然后除以请求的次数。
    • 百分位数
      • 它能够反映出应用接口的整体响应情况。

在这些高稳定性系统中,目标就是要干掉严重影响系统的长尾请求。这部分接口性能数据的收集,采用更加详细的日志记录方式,而不仅仅靠指标。比如,将某个接口,耗时超过1s的入参及执行步骤,详细地输出在日志系统中。

1.2 吞吐量 (Throughput)和响应速度 #

  • 定义:单位时间内处理的工作量,可以是请求数、事务数等。
  • 参数:一般用每秒处理的请求数(TPS, Transactions Per Second)或者每分钟处理的数据量来表示。
  • 如何衡量:同样可以借助性能测试工具进行测量,在稳定状态下观察系统的最大吞吐量。

分布式的高并发应用并不能把单次请求作为判断依据,它往往是一个统计结果。其中最常用的衡量指标就是吞吐量和响应速度。

通常通过以下三个指标进行描述:

  • QPS:每秒查询的数量
  • TPS:每秒事务的数量
  • HPS:每秒HTTP请求数量

响应速度是串行执行的优化,通过执行步骤解决问题;

吞吐量是并行执行的优化,通过合理利用计算资源达到目标。

在高并发的互联网应用中,使用有限的硬件资源,力求在响应速度和吞吐量之间找到一个平衡点。

并发量:

  • 并发量是指系统同时能处理的请求数量,这个指标反映了系统的负载能力。
  • 在高并发应用中,仅仅高吞吐是不够的,它还必须同时能为多个用户提供服务

并发高时,会导致很严重的共享资源争用问题,需要减少资源冲突,以及长时间占用资源的行为。

  • 针对于响应时间进行设计,一般来说是万能的。

因为响应时间减少,同一时间能够处理的请求必然会增加。

1.3 CPU 使用率 (CPU Usage) #

  • 定义:程序运行时占用CPU资源的比例。
  • 参数:百分比形式展示,0%表示完全空闲,100%则意味着满负荷运转。
  • 如何衡量:Linux系统下可通过top命令查看进程级别的CPU使用情况;Windows平台可利用任务管理器;还可以通过编程方式调用操作系统的API获取相关信息。

1.4 内存使用率 (Memory Usage) #

  • 定义:应用程序在运行过程中所占用的物理内存大小。
  • 参数:以字节(Byte)为单位,通常转换成KB、MB、GB等形式更易于理解。
  • 如何衡量:Java中可以使用Runtime.getRuntime().totalMemory()和freeMemory()方法分别获取JVM分配给程序的总内存以及剩余未使用的内存,从而推算出已使用的内存量。

1.5 GC 情况 (Garbage Collection) #

  • 定义:垃圾回收机制清理不再使用的对象所花费的时间及频率。
  • 参数:主要包括GC次数、每次GC耗时、总的GC耗时等。
  • 如何衡量:启用JVM的GC日志功能,分析其中的关键数据;也可以借助VisualVM等可视化工具直观地监控GC行为。

2. 理论方法 #

2.1 Amdahl定律 #

  • 该定律描述了当部分代码被加速后整个程序速度提升的程度。公式为(Speedup=\frac{1}{(1-f)+\frac{f}{S}}),其中(f)代表可并行化的比例,(S)表示这部分代码的速度提升倍数。根据Amdahl定律可知,并非所有地方都值得去优化,因为即使将某一部分无限加快,如果它在整个程序中占比很小,对整体性能的改善也非常有限。因此,在做性能优化之前,要先找出瓶颈所在,优先考虑那些能够带来显著收益的部分。

2.2 Little定律 #

  • Little定律揭示了系统中平均客户数量与到达率、停留时间之间的关系,表达式为(L=\lambda W)。这里(L)表示平均客户数,(\lambda)为单位时间内的到达率,(W)则是每个客户的平均停留时间。这个定律可以帮助我们理解高并发场景下的队列长度变化规律,进而指导如何调整线程池大小、连接池配置等参数以达到最优性能。

2.3 木桶理论 #

  • 组成系统的组件,在速度上是良莠不齐的。系统的整体性能,就取决于系统中最慢的组件。
  • 比如:在数据库应用中,制约性能最严重的是落盘的I/O问题。硬盘是这个场景下的短板,首要的任务就是补齐这个短板。

2.4 基准测试 #

  • 并不是简单的性能测试,是用来测试某个程序的最佳性能
  • 应用接口往往在刚启动后都有短暂的超时
    • 在测试之前,对应用进行预热,消除JIT编译器等因素的影响
    • 在Java里就有一个组件,即JMH,就可以消除这些差异

2.5 局部性原理 #

  • 基于计算机硬件架构特性,包含时间局部性与空间局部性。时间局部性指程序在近期内频繁访问同一内存地址的数据,例如循环中频繁使用的变量,将其缓存能减少重复从内存加载的开销;空间局部性意味着相邻内存地址的数据常被一同访问,如数组元素的连续读取,合理组织数据结构使相关数据连续存放,可提升缓存命中率,加快数据访问速度,进而优化整体性能。

2.6 池化思想 #

  • 创建和销毁资源往往伴随着高昂成本,池化旨在提前创建并维护一定数量的资源,应用需要时从池中获取,使用完毕归还而非直接销毁。以线程池为例,避免了线程频繁创建与销毁的开销,同时可根据系统负载灵活调整线程数量,保障资源合理利用与系统高效运行。类似的还有数据库连接池、对象池等,都是运用这一思想提升性能。

2.7 分治策略 #

  • 将复杂问题分解为多个规模较小、相对独立且易于解决的子问题,分别求解后再合并结果。如在大规模数据排序任务中,可采用归并排序算法,它递归地将数据分成两半分别排序,最后合并有序子序列。相比一次性处理全部数据,分治有效降低问题复杂度,利用多核 CPU 并行处理子问题的能力,显著提升运算效率。

2.8 线程安全与锁争用 #

  • 多线程环境下,多个线程同时访问共享资源可能导致数据不一致的问题,这就需要引入同步机制保证线程安全。然而,过度使用锁会引发严重的锁争用现象,降低系统并发度。因此,合理设计线程模型,尽量减少锁的粒度和持有时间,采用无锁/乐观锁算法代替传统互斥锁,都是提高性能的有效途径。

3. 性能优化的技术手段 #

3.1 复用优化 #

核心在于避免重复创建高成本资源,通过复用降低资源消耗。以数据库连接为例,频繁创建与销毁连接会产生显著开销。采用连接池技术,如 Apache Commons DBCP,预先初始化一定数量连接置于池中,应用按需取用、用完归还,削减连接创建销毁成本,提升整体效率。

谈到数据复用,首先想到的就是缓冲和缓存,两者意义是完全不同的。

  • 缓冲(Buffer):常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地、缓慢地随机写。
  • 缓存(Cache):常见于对已读取数据的复用。通过将它们缓存在相对高速的区域,缓存主要针对的是读操作。

  1. 适用场景:

适用于频繁创建和销毁对象或资源的场景。像线程、数据库连接、网络连接等。

  1. 举例说明:

通过对象池技术复用已经创建的对象,避免频繁的内存分配和垃圾回收。例如,数据库连接池可以有效管理数据库连接,减少连接建立和断开的开销;线程池则可以复用线程,避免频繁创建和销毁线程带来的性能损耗。

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ConnectionPool {
    private final BlockingQueue<Connection> pool;
    private final int maxSize;

    public ConnectionPool(int maxSize) throws Exception {
        this.maxSize = maxSize;
        pool = new ArrayBlockingQueue<>(maxSize);
        for (int i = 0; i < maxSize; i++) {
            pool.add(DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "user", "password");
        }
    }

    public Connection getConnection() throws InterruptedException {
        return pool.take();
    }

    public void releaseConnection(Connection connection) {
        try {
            pool.put(connection);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

3.2 结果集优化 #

聚焦数据库查询结果处理,精准提取所需数据,避免 “数据冗余”。编写 SQL 查询时,运用 SELECT 字段列表精准锁定目标列,摒弃 “SELECT *” 这种粗放式查询,减少不必要的数据传输与内存占用;同时合理设置查询分页,如 MySQL 的 LIMIT 语句,应对大数据集查询,降低单次结果集规模,加速查询响应。

  1. 适用场景:

在数据密集型应用与数据库频繁交互,且查询结果集较大、数据敏感度高的场景尤为关键,如企业级报表生成、大数据分析平台。

  1. 举例说明:
  • XML的表现形式是非常好的,那为什么还有JSON呢?
    • 一个重要的原因就是体积变小,传输效率和解析效率变高。
  • Google的Protobuf体积更小,虽然可读性低,但在一些高并发场景下(如RPC),能够显著提高效率,这是典型的对结果集的优化。
  • 像Nginx,一般都会开启GZIP压缩,使得传输的内容保持紧凑。客户端只需要一小部分计算能力,就可以方便解压。由于这个操作是分散的,所以性能损失是固定的。
  • 对于一些对时效性要求不高,但对处理能力有高要求的业务,要吸取缓冲区的经验,尽量减少网络连接的交互,采用批量处理的方式,增加处理速度。
  • 优化SQL查询语句,选择合适的数据结构存储结果集,分页加载数据等。例如,在一个大数据量的报表系统中,可以通过分页查询逐步加载数据,而不是一次性读取所有记录,从而减轻内存压力。

注:可采用bitmap、位图等优化。

-- 分页查询
SELECT * FROM orders LIMIT ?,?;
public List<Order> getOrders(int pageNumber, int pageSize) {
    // 执行分页查询
    return jdbcTemplate.query(
        "SELECT * FROM orders LIMIT ? OFFSET ?",
        new Object[]{pageSize, (pageNumber - 1) * pageSize},
        new OrderRowMapper()
    );
}

3.3 高效实现 #

从代码编写细节雕琢,以更优编程范式提升性能。例如在字符串拼接场景,摒弃频繁使用 “+” 运算符(在 Java 中底层会创建多个临时对象),改用 StringBuilder 或 StringBuffer(线程安全需求时用后者),减少对象创建开销,优化内存利用。又如在集合遍历中,优先选用迭代器模式而非传统 for 循环索引遍历,避免因索引操作带来的额外开销,使代码执行更流畅高效。

  1. 适用场景:
  • 适用于代码局部 “热点” 区域,如高频执行方法体、循环逻辑内部,微小改进能汇聚显著性能提升。
  • 适用于需要频繁执行的操作或性能敏感的模块。
  1. 举例说明:
  • 在平时的编程中,尽量使用一些设计理念良好、性能优越的组件。
    • 比如有了Netty,就不用再选择比较老的Mina组件。
  • 而在设计系统时,从性能因素考虑,就不要选SOAP这样比较耗时的协议。
  • 比如一个好的语法分析器(比如使用JavaCC),其效率会比正则表达式高很多。
  • 选择合适的算法和数据结构,避免不必要的计算和内存拷贝。
    • 例如,使用StringBuilder代替String进行字符串拼接,可以显著提高性能;
    • 采用HashMap而非ArrayList进行查找操作,可以在O(1)时间内完成。

注:适配器模式很重要,现有工具上抽象出一层来使用。

// 使用StringBuilder进行字符串拼接
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();

3.4 算法优化 #

算法是程序灵魂,不同算法在时空复杂度上存在 “天壤之别”。如在海量日志数据检索场景,朴素的线性搜索算法时间复杂度高达 ,而引入二分查找(前提数据有序,时间复杂度 )或哈希表查找(平均时间复杂度接近 )能大幅提速。依问题特性择取最优算法,是从根基上撬动性能杠杆。

算法能够显著提高复杂业务的性能。但在实际的业务中,往往都是变种。

由于存储越来越便宜,在一些CPU非常紧张的业务中,往往采用空间换取时间的方式,来加快处理速度。

算法属于代码调优,代码调优涉及很多编码技巧。

有时对算法、数据结构的灵活使用,也是代码优化的一个重要内容。

比如常用的降低时间复杂度的方式,就有递归、二分、排序、动态规划等。

  1. 适用场景:
  • 面对大规模数据处理、复杂计算任务,算法抉择成为性能 “分水岭”,如搜索引擎索引构建、金融风险模型计算。
  1. 举例说明:
  • 通过改进算法降低时间复杂度和空间复杂度。例如,在排序问题中,快速排序的平均时间复杂度为O(n log n),而冒泡排序的时间复杂度为O(n²)。选择更高效的算法可以大幅提高性能。
// 快速排序示例
public static void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int pivotIndex = partition(arr, low, high);
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr, i, j);
        }
    }
    swap(arr, i + 1, high);
    return i + 1;
}

private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

3.5 计算优化 #

关注代码中的数学、逻辑运算细节,削减不必要计算量。比如在循环条件判断中,避免重复执行复杂函数调用。若循环需判断某数组元素是否满足特定数学条件,提前计算并缓存该条件结果,而非每次循环都重新计算,节省 CPU 算力;对于浮点数运算,留意精度损失风险,必要时采用 BigDecimal 等高精度类,保障计算准确性与性能平衡。

当然,计算优化还有另一层含义:

  1. 并行执行

现在的CPU发展速度很快,绝大多数硬件,都是多核。要想加快某个任务的执行,最快最优的解决方式,就是让它并行执行。

多机

多进程

多线程(java采用此方式)

  1. 变同步为异步

通常涉及变成模型的改变。请求会一直阻塞,直到有成功,或者失败结果的返回。

虽然编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。

  1. 惰性加载

使用一些常见的设计模式来优化业务,提高体验,比如单例模式、代理模式等。

比如在绘制Swing窗口时,如果要显示比较多的图片,就可以先加载一个占位符,然后通过后台线程慢慢加载所需要的资源,这就可以避免窗口的僵死。

  1. 适用场景:
  • 涉及密集计算任务,尤其是科学计算、金融数值处理领域,细微优化能累积可观性能红利。
  • 涉及大量数学运算或浮点运算的场景。
  1. 举例说明:
  • 利用硬件特性(如SIMD指令集)加速计算,减少不必要的类型转换和精度损失。例如,在图像处理领域,可以使用Java的FloatBuffer和DoubleBuffer类直接操作底层缓冲区,提高浮点运算效率。
  • 多线程处理
  • 同步变异步
  • 惰性加载
import java.nio.FloatBuffer;

public class ImageProcessing {
    private FloatBuffer buffer;

    public ImageProcessing(int width, int height) {
        buffer = FloatBuffer.allocate(width * height * 4); // RGBA
    }

    public void processImage() {
        // 直接操作FloatBuffer中的数据
        for (int i = 0; i < buffer.capacity(); i++) {
            float value = buffer.get(i);
            // 进行图像处理操作
            buffer.put(i, value * 2.0f);
        }
    }
}

3.6 资源冲突优化 #

在多线程并发场景,资源竞争是性能 “暗礁”。运用锁机制(如 Java 的 synchronized、ReentrantLock)精细控制共享资源访问权限,确保线程安全同时避免过度锁竞争。例如多个线程同时读写同一文件,合理划分读写锁,读操作共享锁并发执行,写操作独占锁串行化,平衡并发与不数据一致性;借助线程本地变量(ThreadLocal)为每个线程分配独立资源副本,化解共享冲突,提升并发性能。

在平常的开发中,会涉及很多共享资源:

  • 单机的,比如一个HashMap
  • 外部存储,比如一个数据库行
  • 单个资源,比如Redis某个key的Setnx
  • 多个资源的协调,比如事务、分布式事务等

按照锁级别,锁可分为乐观锁悲观锁乐观锁在效率上更高

按照锁类型,锁又分为公平锁非公平锁,在对任务的调度上,有一些细微的差别。

注:无锁模式下解决资源冲突是比较优雅的。

  1. 适用场景:
  • 多线程或多进程环境下存在资源竞争的场景。如 Web 服务器会话管理、分布式缓存更新。
  1. 举例说明:
  • 通过合理的锁机制、无锁算法、消息队列等方式解决资源冲突问题。例如,在生产者-消费者模式中,使用阻塞队列可以有效协调生产者和消费者的节奏,避免死锁和饥饿现象。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumerExample {
    private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

    public void produce(int item) throws InterruptedException {
        queue.put(item);
    }

    public Integer consume() throws InterruptedException {
        return queue.take();
    }
}

3.7 JVM优化 #

JVM 作为 Java 运行基石,参数调优举足轻重。当应用面临内存溢出(OutOfMemoryError)、频繁垃圾回收引发 “性能抖动” 时,精准调校 JVM 参数是 “对症良方”。如依据应用内存需求,通过 “-Xms” 与 “-Xmx” 设定初始与最大堆内存,保障运行期内存稳定;调整新生代、老生代比例,适配对象生命周期特性,优化垃圾回收频率与停顿时间,为应用性能 “保驾护航”。

因为Java是运行在JVM虚拟机之上,它的诸多特性,就要受到JVM的制约。对JVM虚拟机进行优化,也能在一定程度上能够提升Java程序的性能。如果参数配置不当,甚至会造成OOM等比较严重的后果。

目前被广泛使用的垃圾回收期是G1,通过很少的参数配置,内存即可高效回收。

  1. 适用场景:
  • 应对 Java 应用因内存管理、垃圾回收导致的性能瓶颈,是保障系统稳定高效运行的关键 “兜底” 手段。
  1. 举例说明:
  • 适当增大堆内存(-Xms,-Xmx),设置合理的新生代与老年代比例(-XX:NewRatio),选择合适的垃圾收集器(-XX:+UseG1GC)等。例如,对于一个大数据处理平台,由于其涉及到大量的临时对象创建与销毁,我们可以增加新生代空间,使更多对象能够在年轻代就被回收,减少Full GC发生的频率。
java -Xms2g -Xmx2g -XX:NewRatio=3 MyApp 
上述参数设置初始堆内存和最大堆内存为 2GB,新生代与老生代比例为 1:3,可根据实际应用情况灵活调整。