Java大對(duì)象(如?List、Map)如何復(fù)用以及錯(cuò)誤和正確的方法講解
前言
好的,這是一個(gè)非常實(shí)際且重要的問(wèn)題。大對(duì)象(如 List、Map)的不當(dāng)使用是導(dǎo)致內(nèi)存抖動(dòng)、GC 頻繁甚至 OOM 的常見(jiàn)原因。我們來(lái)詳細(xì)拆解如何正確復(fù)用它們。
錯(cuò)誤的復(fù)用方法
錯(cuò)誤的方法通常表現(xiàn)為 線程不安全、內(nèi)存泄漏風(fēng)險(xiǎn)? 或 性能低下。
1. 在方法內(nèi)部直接創(chuàng)建(最典型錯(cuò)誤)
這是最常見(jiàn)的錯(cuò)誤,雖然不算“復(fù)用”,但它是問(wèn)題的根源——沒(méi)有復(fù)用。
// 錯(cuò)誤示例:每次調(diào)用都創(chuàng)建新對(duì)象
public void processData() {
List<User> userList = new ArrayList<>(); // 每次進(jìn)入方法都新建
// ... 填充 userList 并進(jìn)行操作
// 方法結(jié)束,userList 成為垃圾,等待 GC
}危害:在循環(huán)或高頻調(diào)用的方法中,會(huì)瞬間產(chǎn)生大量短生命周期的大對(duì)象,迫使 JVM 頻繁進(jìn)行 Young GC,嚴(yán)重時(shí)晉升到老年代引發(fā) Full GC。
2. 使用 static字段但缺乏清理(線程不安全 & 內(nèi)存泄漏)
試圖通過(guò)靜態(tài)變量來(lái)“復(fù)用”,但忽略了線程安全和狀態(tài)污染問(wèn)題。
public class BadCache {
// 靜態(tài)字段,所有線程共享
private static List<String> sharedList = new ArrayList<>();
public void addData(String data) {
sharedList.add(data); // 線程不安全!ArrayList 在并發(fā)修改時(shí)會(huì)拋出 ConcurrentModificationException 或?qū)е聰?shù)據(jù)錯(cuò)亂。
}
public void clearAndReuse() {
// 如果只加不減,這個(gè)列表會(huì)無(wú)限增長(zhǎng),最終導(dǎo)致內(nèi)存泄漏!
// sharedList.clear(); // 必須手動(dòng)清理,但何時(shí)清理?清理時(shí)機(jī)難以把握。
}
}3. 使用 ThreadLocal但不調(diào)用 remove()(內(nèi)存泄漏)
ThreadLocal可以實(shí)現(xiàn)線程隔離的“偽復(fù)用”,但如果使用不當(dāng),會(huì)造成嚴(yán)重的內(nèi)存泄漏。
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); // 每個(gè)線程首次使用時(shí)創(chuàng)建
threadLocalList.set(list);
}
// ... 使用 list
// 錯(cuò)誤:方法結(jié)束后,沒(méi)有調(diào)用 threadLocalList.remove()
// 由于 ThreadLocalMap 的 Key 是弱引用,但 Value 是強(qiáng)引用,會(huì)導(dǎo)致 Value 無(wú)法被回收,造成內(nèi)存泄漏。
}
}4. 使用線程不安全的集合并自行加鎖(性能低下)
意識(shí)到線程安全問(wèn)題,但采用了笨拙的同步方式,性能極差。
public class BadSynchronizedCache {
private final List<Object> synchronizedList = new ArrayList<>();
public void add(Object obj) {
synchronized (this) { // 粗暴的同步,鎖粒度太大
synchronizedList.add(obj);
}
}
// 或者使用 Collections.synchronizedList,但其迭代時(shí)仍需手動(dòng)同步,容易出錯(cuò)。
// private final List<Object> syncList = Collections.synchronizedList(new ArrayList<>());
}正確的復(fù)用方法
正確的方法核心在于:保證線程安全、避免內(nèi)存泄漏、按需清理、提升性能。
1. 使用線程安全的對(duì)象池(推薦用于創(chuàng)建成本高的大對(duì)象)
對(duì)于創(chuàng)建成本極高的大對(duì)象(如包含大量預(yù)初始化數(shù)據(jù)的 Map),可以使用對(duì)象池模式。Apache Commons Pool? 是經(jīng)典實(shí)現(xiàn)。
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() {
// 創(chuàng)建代價(jià)高的初始化過(guò)程
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) {
// 歸還對(duì)象時(shí),清空內(nèi)容,準(zhǔn)備下一次使用
p.getObject().clear();
}
}
// 使用
GenericObjectPool<Map<String, Object>> pool = new GenericObjectPool<>(new ExpensiveMapFactory());
Map<String, Object> map = pool.borrowObject(); // 從池中獲取
try {
// ... 使用 map
} finally {
pool.returnObject(map); // 務(wù)必在 finally 中歸還,否則對(duì)象池失效
}優(yōu)點(diǎn):嚴(yán)格控制對(duì)象數(shù)量,避免重復(fù)創(chuàng)建的高成本。
缺點(diǎn):引入第三方依賴,增加了系統(tǒng)復(fù)雜性,需合理配置池大小和超時(shí)時(shí)間。
2. 使用 ThreadLocal并嚴(yán)格遵循“用完即清理”原則(推薦用于線程內(nèi)復(fù)用)
這是最常用的正確方法之一,尤其適用于 Web 應(yīng)用(一個(gè)請(qǐng)求一個(gè)線程)。
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); // 惰性創(chuàng)建
threadLocalList.set(list);
}
try {
// ... 使用 list
} finally {
// !!! 關(guān)鍵步驟:使用完畢后,立即清理,防止內(nèi)存泄漏 !!!
list.clear(); // 清空內(nèi)容,而不是解除 ThreadLocal 綁定
// 或者 threadLocalList.remove(); // 徹底移除,下次使用會(huì)重新創(chuàng)建。對(duì)于明確生命周期的場(chǎng)景(如一次請(qǐng)求),remove() 更安全。
}
}
}最佳實(shí)踐:在 Web 框架的請(qǐng)求攔截器(Filter/Interceptor)的 afterCompletion方法中統(tǒng)一清理所有 ThreadLocal。
3. 使用并發(fā)集合(推薦用于多線程共享只讀或可更新?tīng)顟B(tài))
如果只是需要在多個(gè)線程間共享數(shù)據(jù),并且主要是讀取,或者更新操作不沖突,使用高性能的并發(fā)集合是最佳選擇。
// 場(chǎng)景1:讀多寫極少,可使用 CopyOnWriteArrayList private final List<String> readOnlyList = new CopyOnWriteArrayList<>(); // 寫入時(shí)會(huì)復(fù)制整個(gè)底層數(shù)組,所以寫性能差,讀性能極佳且無(wú)鎖。 // 場(chǎng)景2:通用的高并發(fā) KV 存儲(chǔ),使用 ConcurrentHashMap private final ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>(); // 分段鎖/CAS 機(jī)制,保證了高并發(fā)下的性能。它本身就是一個(gè)“復(fù)用”的容器。 // 場(chǎng)景3:如果需要一個(gè)全局復(fù)用的、可更新的列表,但又不想加鎖影響性能,可考慮使用不可變對(duì)象。 // 每次更新都創(chuàng)建一個(gè)新的 List,然后原子性地替換引用(適用于更新不頻繁的場(chǎng)景)。 private final AtomicReference<List<String>> atomicListRef = new AtomicReference<>(Collections.unmodifiableList(new ArrayList<>()));
4. 顯式緩存與清理(適用于特定生命周期的對(duì)象)
在明確的業(yè)務(wù)周期內(nèi)復(fù)用對(duì)象,并在周期結(jié)束時(shí)統(tǒng)一清理。
public class ReportGenerator {
// 在生成一份大型報(bào)告期間復(fù)用這個(gè)列表
private List<ReportItem> reportItems;
public void generateReport() {
this.reportItems = new ArrayList<>(5000); // 在方法開始時(shí)創(chuàng)建
try {
// ... 填充 reportItems
// ... 多次使用 reportItems 進(jìn)行計(jì)算和渲染
} finally {
// 報(bào)告生成完畢,生命周期結(jié)束,可以置為 null 輔助 GC
// 或者保留,如果下一次 generateReport 能復(fù)用其容量(但通常不建議,邏輯易混淆)
this.reportItems = null;
}
}
}總結(jié)對(duì)比
方法 | 適用場(chǎng)景 | 優(yōu)點(diǎn) | 缺點(diǎn) | 注意事項(xiàng) |
|---|---|---|---|---|
錯(cuò)誤:方法內(nèi)創(chuàng)建? | 任何場(chǎng)景 | 簡(jiǎn)單 | 性能極差,GC 壓力大 | 絕對(duì)避免在循環(huán)或高頻方法中使用 |
錯(cuò)誤:Static 字段? | 幾乎無(wú) | 看似簡(jiǎn)單 | 線程不安全,極易內(nèi)存泄漏 | 禁止使用? |
錯(cuò)誤:ThreadLocal 不 remove? | 任何場(chǎng)景 | 無(wú) | 確定性內(nèi)存泄漏 | 嚴(yán)禁不清理就結(jié)束使用 |
正確:對(duì)象池? | 創(chuàng)建成本極高的對(duì)象 | 節(jié)省創(chuàng)建成本,控制總量 | 復(fù)雜,有額外開銷 | 謹(jǐn)慎選擇,配置得當(dāng) |
正確:ThreadLocal + remove? | 線程內(nèi)復(fù)用,生命周期清晰(如請(qǐng)求) | 線程安全,無(wú)鎖,性能好 | 濫用易導(dǎo)致內(nèi)存泄漏 | 必須在 finally 塊中清理? |
正確:并發(fā)集合? | 多線程共享數(shù)據(jù) | API 簡(jiǎn)單,性能經(jīng)過(guò)高度優(yōu)化 | 并非所有場(chǎng)景都適用(如需要深拷貝) | 根據(jù)場(chǎng)景選擇 |
正確:顯式緩存與清理? | 對(duì)象生命周期與業(yè)務(wù)流程綁定 | 邏輯清晰,易于管理 | 復(fù)用范圍有限 | 確保在生命周期結(jié)束時(shí)清理 |
核心心法:復(fù)用是為了效率和穩(wěn)定,但不能以犧牲線程安全和引入內(nèi)存泄漏為代價(jià)。? 在選擇方法時(shí),始終問(wèn)自己三個(gè)問(wèn)題:
是否線程安全?
是否會(huì)內(nèi)存泄漏?
性能是否符合預(yù)期?
總結(jié)
到此這篇關(guān)于Java大對(duì)象(如 List、Map)如何復(fù)用以及錯(cuò)誤和正確的方法講解的文章就介紹到這了,更多相關(guān)Java大對(duì)象List、Map復(fù)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java不用算數(shù)運(yùn)算符來(lái)實(shí)現(xiàn)求和方法
我們都知道,Java的運(yùn)算符除了具有優(yōu)先級(jí)之外,還有一個(gè)結(jié)合性的特點(diǎn)。當(dāng)一個(gè)表達(dá)式中出現(xiàn)多種運(yùn)算符時(shí),執(zhí)行的先后順序不僅要遵守運(yùn)算符優(yōu)先級(jí)別的規(guī)定,還要受運(yùn)算符結(jié)合性的約束,以便確定是自左向右進(jìn)行運(yùn)算還是自右向左進(jìn)行運(yùn)算,但是如果不用運(yùn)算符怎么求和呢2022-04-04
Java利用Apache PDFBox實(shí)現(xiàn)將PDF保存為圖片
將 PDF 文件轉(zhuǎn)換為圖片是常見(jiàn)的需求之一,特別是在需要將 PDF 內(nèi)容以圖像形式展示或處理時(shí),其中最常用的是 Apache PDFBox,下面我們就來(lái)看看具體實(shí)現(xiàn)方法吧2026-02-02
Java實(shí)現(xiàn)redis分布式鎖的三種方式
本文主要介紹了Java實(shí)現(xiàn)redis分布式鎖的三種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
mybatis的動(dòng)態(tài)SQL以及連接池詳解
這篇文章主要介紹了mybatis的動(dòng)態(tài)SQL以及連接池詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot無(wú)法識(shí)別bootstrap.yml小綠葉問(wèn)題的解決辦法
一般單獨(dú)使用?Spring?Boot?時(shí),bootstrap.yml?文件一般是不會(huì)生效的,也就是沒(méi)有小綠葉圖標(biāo),本文給大家介紹了SpringBoot無(wú)法識(shí)別bootstrap.yml小綠葉問(wèn)題的解決辦法,文中給出了兩種解決方案,需要的朋友可以參考下2024-07-07
SpringBoot為何可以使用Jar包啟動(dòng)詳解
springboot jar包啟動(dòng)腳本,適用于快速啟動(dòng),刪除,重啟,以及查看狀態(tài),下面這篇文章主要給大家介紹了關(guān)于SpringBoot為何可以使用Jar包啟動(dòng)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03
JavaWeb監(jiān)聽(tīng)器Listener實(shí)例解析
這篇文章主要為大家詳細(xì)介紹了JavaWeb監(jiān)聽(tīng)器Listener實(shí)例,針對(duì)監(jiān)聽(tīng)器進(jìn)行進(jìn)行細(xì)致分析,感興趣的小伙伴們可以參考一下2016-08-08
java中javamail收發(fā)郵件實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了java中javamail收發(fā)郵件實(shí)現(xiàn)方法,實(shí)例分析了javamail的使用方法與相關(guān)注意事項(xiàng),非常具有實(shí)用價(jià)值,感興趣的小伙伴們可以參考一下2016-02-02

