CodeGym /课程 /JAVA 25 SELF /性能剖析与代码优化:工具与方法

性能剖析与代码优化:工具与方法

JAVA 25 SELF
第 63 级 , 课程 4
可用

1. 性能剖析入门

性能剖析就像为你的程序做体检:我们不只是看“温度”(监控),而是要找出应用哪里“疼”、哪里慢、在哪些地方消耗了过多内存或资源。

性能剖析是收集并分析程序运行信息的过程,目的是发现瓶颈(bottlenecks)和低效代码片段。与通常只跟踪总体指标(CPU、内存、线程数)的监控不同,剖析能向内看:了解哪些方法被调用最频繁、它们耗时多少、创建了多少对象,以及内存泄漏究竟发生在哪里。

什么时候真的需要做性能剖析?

  • 应用在“卡顿”,但原因不明。
  • 内存占用突然增长。
  • 更新代码后,某些操作变慢了。
  • 需要弄清为什么服务器资源不够用。

顺带一提,几乎每位开发都至少有过一次优化错地方的经历。为什么?因为凭感觉几乎无法定位瓶颈——这正是分析器存在的意义。

核心剖析指标

  • 方法执行时间(CPU 剖析):哪些方法最耗时?程序在哪些地方“吃”CPU?
  • 内存使用(内存剖析):哪些对象创建最频繁?哪些对象存留时间超出预期?
  • 对象数量:我们是否创建了过多同类对象?
  • 线程:线程是否过多?是否存在锁(deadlock、contention)?
  • 方法调用:调用栈有多深?是否存在没有出口的递归?

2. 剖析工具

在 Java 世界有几款经典(而且免费!)的剖析工具。我们来看看其中的主力。

VisualVM

VisualVM 是一款免费工具,自 JDK 6 起随 JDK 提供。它可以:

  • 连接本地与远程 JVM。
  • 查看内存、线程、CPU、垃圾回收。
  • 生成并分析 heap dump。
  • 对应用进行 CPU 与内存剖析。

如何启动 VisualVM?
通常位于 JDK 目录:<path_to_JDK>/bin/jvisualvm
启动后,选择 Java 应用进程——就能像看鱼缸里游动的小鱼那样观察它的生命体征(只不过这里的“鱼”是对象和线程)。

JProfiler, YourKit

这是商业但非常强大的工具。它们支持:

  • 内存、CPU、线程剖析。
  • 分析内存“快照”(heap dump)。
  • 定位泄漏、长时间锁、慢方法。
  • 与 IDE 与 CI/CD 集成。

入门用 VisualVM 足矣;当你“长大”到大型项目时——不妨考虑这些工具。

Java Flight Recorder (JFR)

JFR 是 JDK 内置的用于收集 JVM 运行事件的工具。它非常轻量,对性能几乎无影响,并可收集以下信息:

  • 方法运行时间。
  • 垃圾回收。
  • 线程、锁、错误。

JFR 非常适合生产环境,在不能拖慢应用的前提下使用。

3. 实战:剖析一个简单应用

我们来写一个迷你计算器,它既会执行耗时计算,又会保存操作历史(这样能有循环、集合与内存操作可看)。

代码示例:“慢速计算器”

import java.util.ArrayList;
import java.util.List;

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // 模拟 "重" 操作
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

接下来——主类:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("操作历史:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

如何剖析这个应用?

  1. 编译并运行应用。
  2. 打开 VisualVMjvisualvm)。
  3. 找到你的进程(通常按类名 Main 识别)。
  4. 进入 CPU Profiler 选项卡并点击 Start。
  5. 让程序运行一会(或者再次触发这个耗时操作)。
  6. 查看哪些方法最耗时。

问题:你认为哪个方法最“重”?
答案:当然是 simulateHeavyOperation()——它在 5_000_000 次循环里调用 Math.sqrt

4. 常见性能问题

算法慢

最常见的原因:算法或数据结构选型不当。比如在列表里线性查找而不是使用 HashMap,或用冒泡排序而不是快速排序。

示例:

// 慢速查找
for (String s : list) {
    if (s.equals("target")) {
        // 找到了
    }
}

最好使用 SetMap 来实现快速查找。

内存泄漏

内存泄漏是指对象依然“存活”(仍有引用指向它们),尽管它们已不再需要。这会导致内存占用上升,最终引发 OutOfMemoryError

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 MB
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

如何发现泄漏?
VisualVM 中生成 heap dump,查看哪些对象占用最多内存,以及为什么仍有引用指向它们。

过度创建对象

如果在循环里创建大量同类对象,不仅会加重垃圾回收负担,还可能拖慢应用。

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // 不好!
}

最好使用常量或字符串池(String pool)。

线程阻塞

当多个线程争用同一资源(例如同步方法)时,可能导致阻塞与性能下降。

public synchronized void doWork() {
    // ...
}

如何定位?
VisualVM 的 Threads 选项卡中可以看到哪些线程“挂起”以及原因。

5. 优化方法

先度量,后优化

优化的首要法则:不要优化并未拖慢系统的部分。

先进行剖析,找到“热点”,再修改代码。有时最“显眼”的代码只占用 1% 的时间,而真正的“怪兽”藏在某个库里或意想不到的地方。

用分析器寻找热点

热点是指占用应用运行时间最大份额的方法或代码片段。

VisualVM 的 CPU Profiler 里你可以:

  • 按执行时间对方法排序。
  • 查看调用栈(stack trace):谁调用了谁。
  • 记住,有时“罪魁祸首”并不是你的代码,而是某个库,甚至是 JDK。

优化实例

示例 1:更换算法
如果发现大部分时间都耗在列表查找上,就把 List 换成 HashSet

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // 快!
}

示例 2:减少分配次数
与其在循环中创建新对象,不如复用,或使用 StringBuilder

// 不好:
for (int i = 0; i < 10000; i++) {
    String s = "结果: " + i;
}

// 更好:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("结果: ").append(i);
    String s = sb.toString();
}

示例 3:缓存
如果某个耗时方法经常以相同参数被调用,请使用缓存。

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. 演示:加速我们的计算器

问题:simulateHeavyOperation() 非常耗时

步骤 1. 剖析
VisualVM 中可以看到,几乎所有时间都耗在 Math.sqrt(i) 上(循环 5_000_000 次)。

步骤 2. 优化
如果只是负载模拟——删除它或减少迭代次数。
如果是实际业务逻辑——考虑是否可以:

  • 缓存结果。
  • 使用更快的算法。
  • 把计算放到独立线程(若对用户体验不敏感)。

优化示例:

private void simulateHeavyOperation() {
    // 原来是 5_000_000,现在是 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

步骤 3. 验证结果
再次进行剖析——程序运行更快了,CPU 负载下降。

7. 可视化:优化流程

flowchart TD
    A[启动应用]
    B["性能剖析(VisualVM)"]
    C[定位瓶颈]
    D[优化代码]
    E[再次剖析]
    F[性能提升]

    A --> B --> C --> D --> E --> F
    E --> C

8. 剖析与优化的常见错误

错误 1:凭感觉优化。 开发者常常在没有度量之前就修改代码。结果是投入很多,收效甚微。

错误 2:在“不真实”的条件下剖析。 剖析应在接近真实负载与数据的环境进行。空载剖析可能无法暴露真正问题。

错误 3:忽视内存泄漏。 如果不看 heap dump、不分析引用链,程序可能会一直“发胖”,最终崩溃。

错误 4:一味追求微小优化。 不要把几天时间花在加速只占 0.1% 运行时间的代码上。先解决主要瓶颈。

错误 5:忽略线程与同步。 在多线程应用中,性能问题往往不是算法本身,而是锁与等待(synchronized、contention)。

错误 6:修改后忘记再剖析。 优化之后一定要复测:有时“优化”反而会变慢!

1
调查/小测验
日志记录第 63 级,课程 4
不可用
日志记录
日志记录、监控和性能分析
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION