Java性能优化:那些年踩过的坑和学到的教训

做Java开发这么多年,性能优化踩的坑真不少。有些问题看起来简单,但实际解决起来很麻烦。今天就把我遇到的一些典型问题和解决方案都分享出来,希望能帮到大家。
在这里插入图片描述

内存泄漏:最隐蔽的杀手

内存泄漏是最难排查的问题之一,特别是生产环境,可能运行好几天才OOM。

案例1:ThreadLocal没清理

这是我踩的第一个坑。项目里用了ThreadLocal存用户信息,结果没清理,导致内存泄漏。

// 错误示例
public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    
    public static User getUser() {
        return userThreadLocal.get();
    }
    
    // 问题:没有remove,导致内存泄漏
}

问题分析:

  • ThreadLocal是弱引用,但value是强引用
  • 线程池复用线程,ThreadLocal不会被清理
  • 时间长了就OOM了

解决方案:

public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    
    public static User getUser() {
        return userThreadLocal.get();
    }
    
    // 关键:使用完必须remove
    public static void remove() {
        userThreadLocal.remove();
    }
}

// 使用示例:用try-finally确保清理
try {
    UserContext.setUser(user);
    // 业务逻辑
} finally {
    UserContext.remove(); // 必须清理
}

案例2:监听器没取消注册

项目里用了事件监听器,结果监听器持有对象的引用,导致对象无法回收。

// 错误示例
public class OrderService {
    private EventBus eventBus;
    
    public void createOrder(Order order) {
        // 创建订单
        saveOrder(order);
        
        // 注册监听器
        eventBus.register(new OrderListener(order)); // 问题:监听器持有order引用
    }
}

class OrderListener {
    private Order order; // 持有引用,导致order无法回收
    
    public OrderListener(Order order) {
        this.order = order;
    }
}

解决方案:

// 监听器只存ID,不存对象
class OrderListener {
    private Long orderId; // 只存ID
    
    public OrderListener(Long orderId) {
        this.orderId = orderId;
    }
    
    @Subscribe
    public void handleEvent(OrderEvent event) {
        if (event.getOrderId().equals(orderId)) {
            // 处理事件
        }
    }
}

// 或者用弱引用
class OrderListener {
    private WeakReference<Order> orderRef;
    
    public OrderListener(Order order) {
        this.orderRef = new WeakReference<>(order);
    }
}

GC调优:不是越大越好

很多人觉得堆内存越大越好,其实不是。堆大了,GC时间也会变长,可能导致应用暂停时间过长。

案例:Full GC频繁

项目部署后,发现Full GC很频繁,应用经常卡顿。

排查过程:

  1. 看GC日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
  1. 分析GC日志:发现老年代很快就满了,触发Full GC。

  2. 分析内存使用:用jmap导出堆转储,用MAT分析,发现有很多大对象。

问题原因:

  • 缓存设置太大,导致老年代经常满
  • 有内存泄漏,对象无法回收

解决方案:

// 优化缓存策略
@Configuration
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000) // 限制大小
            .expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间
            .recordStats()); // 记录统计信息
        return cacheManager;
    }
}

GC参数调整:

# 使用G1GC(推荐)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200  # 目标暂停时间
-XX:G1HeapRegionSize=16m

# 或者ZGC(Java 17+,低延迟)
-XX:+UseZGC

集合使用不当:隐藏的性能杀手

集合用不对,性能差很多。

案例1:ArrayList vs LinkedList

很多人不知道什么时候用哪个,结果用错了。

// 错误:频繁插入删除用ArrayList
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add(0, "item" + i); // ArrayList插入头部很慢,要移动元素
}

// 正确:用LinkedList
List<String> list = new LinkedList<>();
for (int i = 0; i < 10000; i++) {
    list.add(0, "item" + i); // LinkedList插入头部很快
}

选择原则:

  • 随机访问多:用ArrayList
  • 插入删除多:用LinkedList
  • 不确定:用ArrayList(通常更快)

案例2:HashMap初始化大小

HashMap如果知道大概大小,最好初始化时就指定容量。

// 错误:不知道大小,会多次扩容
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
    map.put("key" + i, "value" + i);
}

// 正确:指定初始容量
Map<String, Object> map = new HashMap<>(1000); // 避免扩容
for (int i = 0; i < 1000; i++) {
    map.put("key" + i, "value" + i);
}

案例3:字符串拼接

大量字符串拼接,用StringBuilder。

// 错误:用+拼接,每次创建新对象
String result = "";
for (int i = 0; i < 1000; i++) {
    result += "item" + i; // 很慢
}

// 正确:用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("item").append(i);
}
String result = sb.toString();

数据库查询优化:N+1问题

这是最常见的性能问题。

案例:查询用户和订单

// 错误:N+1查询
public List<User> getUsersWithOrders() {
    List<User> users = userRepository.findAll();
    for (User user : users) {
        List<Order> orders = orderRepository.findByUserId(user.getId()); // N次查询
        user.setOrders(orders);
    }
    return users;
}

如果有100个用户,就会执行1 + 100 = 101次查询。

解决方案1:使用JOIN FETCH

@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();

解决方案2:批量查询

public List<User> getUsersWithOrders() {
    List<User> users = userRepository.findAll();
    List<Long> userIds = users.stream()
        .map(User::getId)
        .collect(Collectors.toList());
    
    // 一次查询所有订单
    Map<Long, List<Order>> ordersMap = orderRepository.findByUserIdIn(userIds)
        .stream()
        .collect(Collectors.groupingBy(Order::getUserId));
    
    // 组装数据
    users.forEach(user -> {
        user.setOrders(ordersMap.getOrDefault(user.getId(), Collections.emptyList()));
    });
    
    return users;
}

并发问题:锁竞争

高并发场景下,锁竞争是性能瓶颈。

案例:同步方法导致性能问题

// 错误:整个方法加锁,并发性能差
@Service
public class OrderService {
    private final Map<Long, Order> cache = new HashMap<>();
    
    public synchronized Order getOrder(Long id) { // 整个方法锁住
        Order order = cache.get(id);
        if (order == null) {
            order = loadOrder(id);
            cache.put(id, order);
        }
        return order;
    }
}

解决方案1:减小锁粒度

@Service
public class OrderService {
    private final Map<Long, Order> cache = new ConcurrentHashMap<>();
    
    public Order getOrder(Long id) {
        return cache.computeIfAbsent(id, this::loadOrder); // 只锁一个key
    }
}

解决方案2:读写分离

@Service
public class OrderService {
    private final Map<Long, Order> cache = new ConcurrentHashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public Order getOrder(Long id) {
        lock.readLock().lock();
        try {
            Order order = cache.get(id);
            if (order != null) {
                return order;
            }
        } finally {
            lock.readLock().unlock();
        }
        
        // 缓存未命中,加写锁
        lock.writeLock().lock();
        try {
            // 双重检查
            Order order = cache.get(id);
            if (order == null) {
                order = loadOrder(id);
                cache.put(id, order);
            }
            return order;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

工具推荐

性能优化离不开工具:

  1. JProfiler / VisualVM:分析CPU和内存
  2. MAT:分析堆转储
  3. Arthas:在线诊断工具
  4. GCViewer:分析GC日志

总结

性能优化是个持续的过程,不是一蹴而就的。关键是要:

  1. 监控:及时发现性能问题
  2. 分析:找到问题根源
  3. 优化:针对性优化
  4. 验证:验证优化效果

避免过度优化,先解决主要问题。性能优化没有银弹,要根据实际情况来。

更多推荐