当前位置: 首页 > article >正文

java --- 性能优化01

目录


前言

一、过度依赖自动内存管理而不进行内存调优

概述:

1.1  错误场景:

1.2  错误表现及危害:

1.3  解决方法:

1.4  案例:

1.5  示例

1.5.1  优化代码,减少不必要的内存分配

二、不合理的数据库查询

概述:

2.1 错误场景:

2.2 错误表现及危害:

2.3 解决方法:

2.4 案例:

三、过度使用同步机制

3.1 错误场景:

3.2 错误表现及危害:

使用方式

使用方式

3.3 解决方法:

3.4 案例:


前言

在我们深入探讨 Java 性能优化中常见的错误之前,先来简单回顾一下热门技术如何帮助提升性能。
(1)内存管理优化就像整理你的书桌。通过合理设置堆内存的大小、减少垃圾回收的次数、选择合适的垃圾回收器,可以让程序运行得更顺畅。
(2)代码优化技巧类似于给汽车升级发动机和减轻车身重量。优化算法和数据结构、避免过度同步、优化字符串操作、提高代码可读性,都能让程序跑得更快、更高效。
(3)数据库访问优化好比在餐厅高峰期增加服务员数量和简化点餐流程。使用连接池、优化 SQL 查询、采用异步数据库访问,可以大大提升数据库操作的速度和效率。
(4)多线程与并发优化就像在工厂里合理安排工人。通过合理设置线程数量、使用线程池、避免死锁和竞争条件,充分发挥多核处理器的优势,提升整体性能。
(5)性能监控与调优工具如 JProfiler、VisualVM 和 Arthas,就像医生的诊断设备。它们帮助我们深入了解程序的运行状况,及时发现问题并进行优化。


一、过度依赖自动内存管理而不进行内存调优

概述:

        Java 的自动垃圾回收机制(GC)确实方便,但过度依赖而不做内存调优可能导致性能问题,尤其是在处理大数据或高并发时。

1.1  错误场景:

        开发者常常忽视内存调优,依赖默认设置。然而,不同应用的内存需求不同,特别是复杂项目,默认的 GC 设置很难满足要求。比如,处理大量请求的系统如果堆内存过小,GC 会频繁触发,导致程序卡顿。

1.2  错误表现及危害:

  • 堆内存过小:会导致频繁的垃圾回收(GC),影响程序运行速度,类似于过度打扫导致工作中断。
  • 堆内存过大:会浪费系统资源,影响其他进程运行,类似于小房间里装了过大的空调,占用过多空间。

1.3  解决方法:

  • 调整 JVM 参数:合理设置堆内存大小 (-Xms 和 -Xmx),确保符合应用的内存需求。
  • 选择合适的垃圾回收器:根据应用场景,选择适合的 GC,比如 G1 GC 更适合大内存应用。
  • 使用内存分析工具:通过工具如 JProfiler、VisualVM 检测内存泄漏,并优化代码。

1.4  案例:

        某电商系统因堆内存设置过小和选择错误的 GC,导致系统频繁卡顿。通过调整内存设置、切换到 G1 GC 并修复内存泄漏,系统性能提高了 30%,用户体验显著改善。

1.5  示例

1.5.1  优化代码,减少不必要的内存分配

(1)减少对象创建

        通过避免频繁创建短生命周期的对象,可以减少 GC 压力,提高内存效率。

示例:

// 不推荐:每次循环都创建新字符串对象
for (int i = 0; i < 1000; i++) {
    String result = new String("Result: " + i);
}

// 推荐:使用StringBuilder拼接,减少对象创建
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    result.append("Result: ").append(i);
}

作用:StringBuilder 拼接字符串比 String 高效,因为它避免了创建多个中间对象,减少了堆内存的使用。

(2)对象池化

        对于频繁使用的大对象,创建对象池复用实例,避免频繁的创建和销毁。

示例:

// 不推荐:每次都创建新对象,增加GC负担
for (int i = 0; i < 1000; i++) {
    Connection conn = new Connection();  // 假设这是一个重量级对象
    // 使用连接
    conn.close();
}

// 推荐:使用对象池复用对象
ObjectPool<Connection> pool = new ObjectPool<>(Connection::new);  // 假设存在ObjectPool类
for (int i = 0; i < 1000; i++) {
    Connection conn = pool.borrow();
    // 使用连接
    pool.return(conn);
}

作用:通过对象池技术,可以避免频繁的对象创建和销毁,从而减轻 GC 负担,提升系统性能。

(3)内存泄漏检测与避免

        避免内存泄漏的常见做法是确保不必要的对象不会被长期引用。

// 不推荐:使用静态集合会导致对象长期被引用,内存无法回收
private static List<Object> cache = new ArrayList<>();

public void addToCache(Object obj) {
    cache.add(obj);
}

// 推荐:使用局部变量,及时释放对象的引用
public void addToCache(Object obj) {
    List<Object> tempCache = new ArrayList<>();
    tempCache.add(obj);
    // 操作完成后,tempCache将被垃圾回收
}

作用:静态变量会导致对象长期被引用,无法被 GC 回收,可能导致内存泄漏。避免使用静态集合或对象池时,及时清理无用对象。

(4)设置合理的线程池

        合理使用线程池管理并发任务,避免创建过多线程耗尽内存。

// 不推荐:为每个任务创建新线程,浪费资源
for (int i = 0; i < 1000; i++) {
    new Thread(() -> {
        // 执行任务
    }).start();
}

// 推荐:使用线程池管理线程
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
    executor.submit(() -> {
        // 执行任务
    });
}
executor.shutdown();

作用:线程池复用线程,避免了频繁的线程创建和销毁,降低了内存和 CPU 的使用,提高了系统性能。

(5)优化集合的使用

        根据需求,选择合适的集合大小和类型,减少内存浪费。

// 不推荐:使用默认初始大小(例如ArrayList初始容量为10),超出后会频繁扩容
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    list.add("Item " + i);
}

// 推荐:指定合理的初始容量,避免扩容
List<String> list = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
    list.add("Item " + i);
}

作用:指定集合的初始容量避免了集合扩容时的频繁复制,减少了内存的浪费和 CPU 负担。


二、不合理的数据库查询

概述

        数据库查询的优化对 Java 应用程序的性能影响重大。如果查询不合理,极有可能成为性能瓶颈,尤其在数据量大、查询频繁的场景下。

2.1 错误场景:

在很多应用中,数据库查询是非常常见且重要的操作,然而一些常见的错误却容易被忽视:

  • 没有为经常查询的字段创建索引。
  • 进行不必要的全表扫描。
  • 编写复杂且低效的 SQL 查询语句。

2.2 错误表现及危害:

  1. 未创建索引

    • 这是最常见的错误之一。当查询的字段没有索引时,数据库必须遍历整个表来找到匹配的结果。这就像在图书馆里没有目录的情况下寻找一本书,效率极低。
    • 危害:随着数据量增加,查询时间急剧上升,导致响应缓慢,影响用户体验。
  2. 全表扫描

    • 如果查询没有条件限制或者没有使用索引,数据库会执行全表扫描,查找符合条件的记录。这就好比在沙漠中寻找一颗特定的沙粒,浪费大量时间和资源。
    • 危害:全表扫描会占用大量 I/O 资源和 CPU 资源,尤其在大表中,性能下降明显。
  3. 复杂低效的 SQL 查询

    • 编写复杂的 SQL 查询,如嵌套子查询、过多的表连接,容易导致数据库的查询优化器难以生成高效的执行计划。这类似于让一辆跑车在泥泞的路上行驶,无法发挥应有的速度。
    • 危害:复杂查询会增加数据库的处理时间,导致查询响应缓慢,甚至可能锁住表,影响其他查询操作。

2.3 解决方法:

  1. 创建索引

    • 为经常查询的字段(如主键、外键、常用过滤字段)创建索引,就像为图书馆编制目录。索引可以加速查询,减少遍历数据的时间。
    • 示例:在 MySQL 中,可以为 user 表的 email 字段创建索引:
      CREATE INDEX idx_email ON user(email);
  2. 避免全表扫描

    • 尽量通过索引、条件查询或限制返回的记录来避免全表扫描。确保 WHERE 子句中使用了索引字段,避免进行无条件查询。
    • 示例:使用带有条件过滤的查询:
      SELECT * FROM user WHERE email = 'example@example.com';
  3. 优化 SQL 查询

    • 避免使用过于复杂的子查询,尝试将其改写为连接查询。同时,减少不必要的表连接,尽量保持查询简单高效。
    • 示例:将嵌套子查询改写为 JOIN 查询:

    -- 不推荐:嵌套子查询

SELECT * FROM orders WHERE user_id IN (SELECT id FROM user WHERE status = 'active');

    -- 推荐:使用 JOIN

SELECT orders.* FROM orders JOIN user ON orders.user_id = user.id WHERE user.status = 'active';

2.4 案例:

        一个电商平台在促销高峰期遇到了数据库查询缓慢的问题,导致大量用户体验不佳。经过分析,发现热门商品的查询没有创建索引,且某些查询使用了全表扫描。开发团队通过以下措施优化了查询性能:

  • 为关键字段(如商品ID、类别)创建索引
  • 优化了 SQL 查询,减少了不必要的表连接和嵌套子查询
  • 通过条件查询避免全表扫描

优化后,数据库查询响应速度提高了近 50%,用户的下单转化率也随之增加。


三、过度使用同步机制

概述
        在多线程编程中,适当的同步机制可以确保数据一致性,但过度使用同步会降低程序的并发性能,导致资源浪费和性能下降。

3.1 错误场景:

        在开发多线程程序时,开发者通常会使用同步机制(如 synchronized 或 ReentrantLock)来防止数据竞争。但不加选择地使用这些机制会导致线程阻塞,甚至引发性能瓶颈。


同步(Synchronous):简单来说,程序必须等待任务完成才能执行下一步。

异步(Asynchronous):简单来说,程序无需等待,可以同时处理多个任务。


3.2 错误表现及危害:

  1. 过度使用 synchronized 或 ReentrantLock:
    • 使用同步机制时,如果过多的代码块被锁住,会造成大量线程同时争抢锁资源,导致性能大幅下降。这类似于让很多人同时通过一扇非常狭窄的门,大家互相等待,谁也无法顺利通过。
    • 危害:线程阻塞增加,系统的并发处理能力被限制,响应时间变长,尤其是在高并发的场景下,性能会急剧下降。

(1)synchronized 是 Java 内置的同步机制,它可以用来锁住某个方法或代码块,以保证同一时刻只有一个线程可以执行该方法或代码块。

使用方式
  • 同步方法:在方法前加 synchronized,表示整个方法在同一时间只能由一个线程访问。

public synchronized void updateValue() {
    // 只有一个线程可以同时进入这个方法
    value++;
}
  • 同步代码块:将 synchronized 加在特定的代码块上,可以只对需要保护的部分加锁,其他代码部分不受影响。
public void updateValue() {
    // 非线程安全代码
    synchronized (this) {
        // 线程安全部分
        value++;
    }
}

(2)ReentrantLock 是 Java java.util.concurrent.locks 包中的类,功能上类似于 synchronized,但比 synchronized 提供了更灵活的控制方式。

使用方式
  • 锁的基本操作:在代码中显式加锁和释放锁。
import java.util.concurrent.locks.ReentrantLock;

public class Example {
    private final ReentrantLock lock = new ReentrantLock(); // 创建锁对象

    public void updateValue() {
        lock.lock();  // 获取锁
        try {
            value++;  // 线程安全操作
        } finally {
            lock.unlock();  // 释放锁
        }
    }
}

(3)ReentrantLock 的特点:

  • 手动加锁和释放锁:开发者需要手动调用 lock.lock() 来加锁,并在操作完成后使用 lock.unlock() 释放锁。相比于 synchronized,它提供了更明确的锁定机制。
  • 可以尝试获取锁:ReentrantLock 提供了 tryLock() 方法,允许线程尝试获取锁而不会一直等待(synchronized 一旦等待锁,就无法中途退出)。
if (lock.tryLock()) {
    try {
        // 拿到锁后执行
    } finally {
        lock.unlock();
    }
} else {
    // 没有拿到锁
}

公平锁:你可以创建 ReentrantLock 时指定为公平锁,即让等待时间最长的线程优先获取锁,而不是让任意线程随机获取锁。

ReentrantLock lock = new ReentrantLock(true); // 公平锁

3.3 解决方法:

1.  缩小同步代码块的范围

  • 只对真正需要保证线程安全的部分加锁,而不是整个方法或大块代码。将同步范围缩小到最小可行的粒度,减少锁的持有时间。
  • 示例
// 不推荐:同步整个方法
public synchronized void updateValue() {
    // 一些无关的操作
    value++;
}

// 推荐:同步关键代码块
public void updateValue() {
    // 一些无关的操作
    synchronized(this) {
        value++;
    }
}
  • 作用:缩小同步范围可以减少锁的争用,提高并发效率。

2.  使用无锁数据结构或原子类

  • 对于简单的计数或状态更新操作,考虑使用 java.util.concurrent 包中的无锁数据结构或原子操作类(如 AtomicInteger),避免传统的同步机制。
  • 示例
// 使用AtomicInteger替代synchronized
AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet();  // 无需使用锁
}

作用:原子操作类通过无锁机制实现线程安全,可以在高并发场景下显著提高性能。


3.  使用多线程调试工具

  • 借助 Java 的多线程调试工具(如 VisualVM、JConsole 等),可以实时监控线程的状态和竞争情况,发现并解决潜在的线程竞争和锁争用问题。
  • 作用:及时发现过度使用锁导致的性能问题,并通过工具调整锁的策略和使用范围,优化系统性能。

3.4 案例:

某金融交易系统在高并发的情况下,频繁出现交易处理延迟。经过深入分析,发现系统中多个关键方法都使用了 synchronized 进行同步,导致大量线程被阻塞,无法并发执行。开发团队通过以下方法进行了优化:

  1. 缩小了同步代码块的范围,减少不必要的锁争用。
  2. 使用了 AtomicInteger替代同步机制,处理高频计数操作。
  3. 通过 VisualVM 工具监控线程状态,进一步优化了锁的使用。

最终,系统的并发处理能力提升了 30%,交易处理响应时间显著缩短,能够在高峰时段平稳运行。


http://www.kler.cn/a/305307.html

相关文章:

  • #渗透测试#SRC漏洞挖掘#云技术基础02之容器与云
  • git之 revert和rebase
  • Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
  • ORA-01092 ORA-14695 ORA-38301
  • 微服务架构面试内容整理-SpringCloud Netflix‌与Spring Cloud Alibaba比较
  • 即插即用篇 | YOLOv8 引入 代理注意力 AgentAttention
  • Git使用—把当前仓库的一个分支push到另一个仓库的指定分支、基于当前仓库创建另一个仓库的分支并推送到对应仓库(mit6828)
  • 使用 easyX 库实现顺序表插入操作的可视化
  • 并发锁机制之深入理解synchronized
  • Mybatis-plus进阶篇(一)
  • 一种全新的webapi框架C#webmvc初步介绍
  • opencv之傅里叶变换
  • ZYNQ FPGA自学笔记
  • 大屏可视化常用图标效果表达
  • OCR2.0--General OCR Theory
  • 先框架后历元还是先历元后框架?
  • elementui 单元格添加样式的两种方法
  • Web 创建设计
  • RabbitMQ(高阶使用)延时任务
  • 19. 删除链表的倒数第 N 个结点【 力扣(LeetCode) 】
  • 定时任务调用OpenFegin无token认证异常
  • LAMP+WordPress
  • 服务器运维面试题4
  • 【SpringBoot】调度和执行定时任务--Quartz(超详细)
  • Ubuntu 22.04.5 LTS 发布下载 - 现代化的企业与开源 Linux
  • 力扣移除元素(力扣题26)(插空找空位java)