好的,这是一个非常实际且重要的问题。大对象(如 List、Map)的不当使用是导致内存抖动、GC 频繁甚至 OOM 的常见原因。我们来详细拆解如何正确复用它们。
错误的复用方法
错误的方法通常表现为 线程不安全 、内存泄漏风险 或 性能低下。
1. 在方法内部直接创建(最典型错误)
这是最常见的错误,虽然不算"复用",但它是问题的根源------没有复用。
// 错误示例:每次调用都创建新对象
public void processData() {
List<User> userList = new ArrayList<>(); // 每次进入方法都新建
// ... 填充 userList 并进行操作
// 方法结束,userList 成为垃圾,等待 GC
}
危害:在循环或高频调用的方法中,会瞬间产生大量短生命周期的大对象,迫使 JVM 频繁进行 Young GC,严重时晋升到老年代引发 Full GC。
2. 使用 static字段但缺乏清理(线程不安全 & 内存泄漏)
试图通过静态变量来"复用",但忽略了线程安全和状态污染问题。
public class BadCache {
// 静态字段,所有线程共享
private static List<String> sharedList = new ArrayList<>();
public void addData(String data) {
sharedList.add(data); // 线程不安全!ArrayList 在并发修改时会抛出 ConcurrentModificationException 或导致数据错乱。
}
public void clearAndReuse() {
// 如果只加不减,这个列表会无限增长,最终导致内存泄漏!
// sharedList.clear(); // 必须手动清理,但何时清理?清理时机难以把握。
}
}
3. 使用 ThreadLocal但不调用 remove()(内存泄漏)
ThreadLocal可以实现线程隔离的"伪复用",但如果使用不当,会造成严重的内存泄漏。
public class BadThreadLocalUsage {
private static final ThreadLocal<List<Object>> threadLocalList = new ThreadLocal<>();
public void doSomething() {
List<Object> list = threadLocalList.get();
if (list == null) {
list = new ArrayList<>(1000); // 每个线程首次使用时创建
threadLocalList.set(list);
}
// ... 使用 list
// 错误:方法结束后,没有调用 threadLocalList.remove()
// 由于 ThreadLocalMap 的 Key 是弱引用,但 Value 是强引用,会导致 Value 无法被回收,造成内存泄漏。
}
}
4. 使用线程不安全的集合并自行加锁(性能低下)
意识到线程安全问题,但采用了笨拙的同步方式,性能极差。
public class BadSynchronizedCache {
private final List<Object> synchronizedList = new ArrayList<>();
public void add(Object obj) {
synchronized (this) { // 粗暴的同步,锁粒度太大
synchronizedList.add(obj);
}
}
// 或者使用 Collections.synchronizedList,但其迭代时仍需手动同步,容易出错。
// private final List<Object> syncList = Collections.synchronizedList(new ArrayList<>());
}
正确的复用方法
正确的方法核心在于:保证线程安全、避免内存泄漏、按需清理、提升性能。
1. 使用线程安全的对象池(推荐用于创建成本高的大对象)
对于创建成本极高的大对象(如包含大量预初始化数据的 Map),可以使用对象池模式。Apache Commons Pool 是经典实现。
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
public class ExpensiveMapFactory extends BasePooledObjectFactory<Map<String, Object>> {
@Override
public Map<String, Object> create() {
// 创建代价高的初始化过程
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
map.put("key" + i, expensiveInitMethod());
}
return map;
}
@Override
public PooledObject<Map<String, Object>> wrap(Map<String, Object> map) {
return new DefaultPooledObject<>(map);
}
@Override
public void passivateObject(PooledObject<Map<String, Object>> p) {
// 归还对象时,清空内容,准备下一次使用
p.getObject().clear();
}
}
// 使用
GenericObjectPool<Map<String, Object>> pool = new GenericObjectPool<>(new ExpensiveMapFactory());
Map<String, Object> map = pool.borrowObject(); // 从池中获取
try {
// ... 使用 map
} finally {
pool.returnObject(map); // 务必在 finally 中归还,否则对象池失效
}
优点:严格控制对象数量,避免重复创建的高成本。
缺点:引入第三方依赖,增加了系统复杂性,需合理配置池大小和超时时间。
2. 使用 ThreadLocal并严格遵循"用完即清理"原则(推荐用于线程内复用)
这是最常用的正确方法之一,尤其适用于 Web 应用(一个请求一个线程)。
public class GoodThreadLocalUsage {
private static final ThreadLocal<List<Object>> threadLocalList = new ThreadLocal<>();
public void doSomething() {
List<Object> list = threadLocalList.get();
if (list == null) {
list = new ArrayList<>(1000); // 惰性创建
threadLocalList.set(list);
}
try {
// ... 使用 list
} finally {
// !!! 关键步骤:使用完毕后,立即清理,防止内存泄漏 !!!
list.clear(); // 清空内容,而不是解除 ThreadLocal 绑定
// 或者 threadLocalList.remove(); // 彻底移除,下次使用会重新创建。对于明确生命周期的场景(如一次请求),remove() 更安全。
}
}
}
最佳实践 :在 Web 框架的请求拦截器(Filter/Interceptor)的 afterCompletion方法中统一清理所有 ThreadLocal。
3. 使用并发集合(推荐用于多线程共享只读或可更新状态)
如果只是需要在多个线程间共享数据,并且主要是读取,或者更新操作不冲突,使用高性能的并发集合是最佳选择。
// 场景1:读多写极少,可使用 CopyOnWriteArrayList
private final List<String> readOnlyList = new CopyOnWriteArrayList<>();
// 写入时会复制整个底层数组,所以写性能差,读性能极佳且无锁。
// 场景2:通用的高并发 KV 存储,使用 ConcurrentHashMap
private final ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
// 分段锁/CAS 机制,保证了高并发下的性能。它本身就是一个"复用"的容器。
// 场景3:如果需要一个全局复用的、可更新的列表,但又不想加锁影响性能,可考虑使用不可变对象。
// 每次更新都创建一个新的 List,然后原子性地替换引用(适用于更新不频繁的场景)。
private final AtomicReference<List<String>> atomicListRef = new AtomicReference<>(Collections.unmodifiableList(new ArrayList<>()));
4. 显式缓存与清理(适用于特定生命周期的对象)
在明确的业务周期内复用对象,并在周期结束时统一清理。
public class ReportGenerator {
// 在生成一份大型报告期间复用这个列表
private List<ReportItem> reportItems;
public void generateReport() {
this.reportItems = new ArrayList<>(5000); // 在方法开始时创建
try {
// ... 填充 reportItems
// ... 多次使用 reportItems 进行计算和渲染
} finally {
// 报告生成完毕,生命周期结束,可以置为 null 辅助 GC
// 或者保留,如果下一次 generateReport 能复用其容量(但通常不建议,逻辑易混淆)
this.reportItems = null;
}
}
}
总结对比
| 方法 | 适用场景 | 优点 | 缺点 | 注意事项 |
|---|---|---|---|---|
| 错误:方法内创建 | 任何场景 | 简单 | 性能极差,GC 压力大 | 绝对避免在循环或高频方法中使用 |
| 错误:Static 字段 | 几乎无 | 看似简单 | 线程不安全,极易内存泄漏 | 禁止使用 |
| 错误:ThreadLocal 不 remove | 任何场景 | 无 | 确定性内存泄漏 | 严禁不清理就结束使用 |
| 正确:对象池 | 创建成本极高的对象 | 节省创建成本,控制总量 | 复杂,有额外开销 | 谨慎选择,配置得当 |
| 正确:ThreadLocal + remove | 线程内复用,生命周期清晰(如请求) | 线程安全,无锁,性能好 | 滥用易导致内存泄漏 | 必须在 finally 块中清理 |
| 正确:并发集合 | 多线程共享数据 | API 简单,性能经过高度优化 | 并非所有场景都适用(如需要深拷贝) | 根据场景选择 ConcurrentHashMap, CopyOnWriteArrayList等 |
| 正确:显式缓存与清理 | 对象生命周期与业务流程绑定 | 逻辑清晰,易于管理 | 复用范围有限 | 确保在生命周期结束时清理 |
核心心法 :**复用是为了效率和稳定,但不能以牺牲线程安全和引入内存泄漏为代价。** 在选择方法时,始终问自己三个问题:
-
是否线程安全?
-
是否会内存泄漏?
-
性能是否符合预期?