Java性能优化:那些年踩过的坑和学到的教训
·
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很频繁,应用经常卡顿。
排查过程:
- 看GC日志:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
-
分析GC日志:发现老年代很快就满了,触发Full GC。
-
分析内存使用:用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();
}
}
}
工具推荐
性能优化离不开工具:
- JProfiler / VisualVM:分析CPU和内存
- MAT:分析堆转储
- Arthas:在线诊断工具
- GCViewer:分析GC日志
总结
性能优化是个持续的过程,不是一蹴而就的。关键是要:
- 监控:及时发现性能问题
- 分析:找到问题根源
- 优化:针对性优化
- 验证:验证优化效果
避免过度优化,先解决主要问题。性能优化没有银弹,要根据实际情况来。
更多推荐




所有评论(0)