Java中內(nèi)存溢出和內(nèi)存泄漏如何解決
內(nèi)存溢出
內(nèi)存溢出(OutOfMemoryError)是指程序在運行時嘗試分配內(nèi)存,但由于沒有足夠的內(nèi)存可用,Java 虛擬機(JVM)拋出了 OutOfMemoryError
錯誤。常見的內(nèi)存溢出區(qū)域包括堆內(nèi)存和永久代(在 Java 8 之后被元空間取代)。
導(dǎo)致的原因
導(dǎo)致內(nèi)存溢出主要有以下幾個原因:1. 2. 堆內(nèi)存溢出:創(chuàng)建大量對象,導(dǎo)致堆內(nèi)存耗盡。2. 棧內(nèi)存溢出:遞歸調(diào)用過深,導(dǎo)致棧內(nèi)存耗盡。3. 永久代/元空間溢出:類加載過多,導(dǎo)致永久代/元空間耗盡。
下面我們用三個示例,分別展示了堆內(nèi)存溢出、棧內(nèi)存溢出和永久代/元空間溢出的情況:
堆內(nèi)存溢出
如下示例代碼,通過不斷向 ArrayList
添加對象來耗盡堆內(nèi)存。
import java.util.ArrayList; import java.util.List; public class HeapMemoryOverflow { public static void main(String[] args) { List<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); } } }
在運行上述 HeapMemoryOverflow
示例時,可能需要調(diào)整 JVM 參數(shù)以較小的堆大小運行,例如 -Xmx10m
,以更快地觀察到 OutOfMemoryError
。
棧內(nèi)存溢出
如下示例代碼,通過遞歸調(diào)用一個沒有終止條件的方法,導(dǎo)致棧內(nèi)存溢出。
public class StackMemoryOverflow { public static void main(String[] args) { recursiveMethod(); } public static void recursiveMethod() { // 沒有終止條件的遞歸調(diào)用 recursiveMethod(); } }
運行StackOverflowError
代碼,通常會很快發(fā)生棧內(nèi)存溢出,因為默認的棧大小不大。
永久代/元空間溢出
在 Java 8 之前,永久代溢出可以通過動態(tài)生成大量類來模擬,Java 8 之后,永久代被元空間取代,以下是一個使用 CGLIB 動態(tài)生成類的示例,可能導(dǎo)致元空間溢出,需要添加 CGLIB 庫依賴。
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class MetaspaceOverflow { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(DummyClass.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class DummyClass { } }
運行 MetaspaceOverflow
示例時,可以使用 JVM 參數(shù) -XX:MaxMetaspaceSize=10m
來限制元空間大小,以更快地觀察到溢出。
解決方法
在這里,我們只是給了一個大的思路,關(guān)于內(nèi)存溢出的排查工作也是一個很重要的知識點,我們會在后面的文章中去詳細介紹。
增加內(nèi)存:調(diào)整 JVM 參數(shù)增加堆內(nèi)存大小,如
-Xmx
。優(yōu)化代碼:減少不必要的對象創(chuàng)建,優(yōu)化數(shù)據(jù)結(jié)構(gòu)。
檢查遞歸:避免過深的遞歸調(diào)用。
監(jiān)控和分析:使用工具如 JVisualVM、JProfiler 分析內(nèi)存使用情況。
內(nèi)存泄漏
內(nèi)存泄漏(Memory Leak)是指程序中存在一些對象,它們不再被使用,但由于仍然被引用,垃圾回收器無法回收這些對象。因此,隨著時間的推移,內(nèi)存泄漏會導(dǎo)致可用內(nèi)存逐漸減少,最終可能導(dǎo)致內(nèi)存溢出。
導(dǎo)致的原因
導(dǎo)致內(nèi)存泄漏主要有以下幾個原因:
靜態(tài)集合類:使用
static
修飾的集合類持有對象引用,因為靜態(tài)集合的生命周期和 JVM 一致,所以靜態(tài)集合引用的對象不能被釋放。監(jiān)聽器和回調(diào):注冊的監(jiān)聽器或回調(diào)未被移除。
長生命周期對象持有短生命周期對象:長生命周期對象不當(dāng)持有短生命周期對象的引用。
下面我們用三個示例,分別展示了內(nèi)存泄漏可能發(fā)生的場景:
靜態(tài)集合類導(dǎo)致的內(nèi)存泄漏
靜態(tài)集合類持有對象引用,導(dǎo)致這些對象無法被垃圾回收。
import java.util.ArrayList; import java.util.List; public class StaticCollectionLeak { // 靜態(tài)集合持有對象引用 private static List<Object> objectList = new ArrayList<>(); public static void main(String[] args) { for (int i = 0; i < 10000; i++) { // 每次創(chuàng)建一個新對象并添加到靜態(tài)集合中 objectList.add(new Object()); } // 即使在這里試圖清理掉一些其他的引用 System.gc(); // 這些對象仍然無法被回收,因為它們被靜態(tài)集合引用 } }
監(jiān)聽器和回調(diào)未被移除
注冊的監(jiān)聽器或回調(diào)未被移除,導(dǎo)致內(nèi)存泄漏。
import java.util.ArrayList; import java.util.List; public class ListenerLeak { private List<EventListener> listeners = new ArrayList<>(); public void addListener(EventListener listener) { listeners.add(listener); } public void triggerEvent() { for (EventListener listener : listeners) { listener.onEvent(); } } public static void main(String[] args) { ListenerLeak leakExample = new ListenerLeak(); // 匿名類創(chuàng)建的監(jiān)聽器對象 leakExample.addListener(new EventListener() { @Override public void onEvent() { System.out.println("Event triggered"); } }); // 假設(shè)在某個時候不再需要監(jiān)聽器,但未移除 // listeners.remove(listener); // 應(yīng)該移除不需要的監(jiān)聽器 } } interface EventListener { void onEvent(); }
長生命周期對象持有短生命周期對象
長生命周期對象不當(dāng)持有短生命周期對象的引用,導(dǎo)致短生命周期對象無法被回收。
import java.util.HashMap; import java.util.Map; public class LongLifeCycleLeak { private static Map<String, byte[]> cache = new HashMap<>(); public static void main(String[] args) { while (true) { // 短生命周期對象 byte[] data = new byte[1024 * 1024]; // 1MB // 長生命周期對象持有短生命周期對象的引用 cache.put(String.valueOf(System.nanoTime()), data); // 需要定期移除不再需要的數(shù)據(jù),否則會導(dǎo)致內(nèi)存泄漏 // cache.clear(); // 應(yīng)該在適當(dāng)時機清理緩存 } } }
解決方法
在這里,我們只是給了一個大的思路,關(guān)于內(nèi)存泄漏的排查工作也是一個很重要的知識點,我們會在后面的文章中去詳細介紹。
及時釋放引用:確保不再使用的對象引用被清除。
使用弱引用:對緩存或非關(guān)鍵對象使用
WeakReference
。比如 ThreadLocal 的弱引用會導(dǎo)致內(nèi)存泄漏,因此使用完 ThreadLocal 一定要記得使用 remove 方法來進行清除。正確管理生命周期:特別是監(jiān)聽器和回調(diào),確保在不需要時移除。
示例代碼
下面示例代碼,用于測試內(nèi)存泄漏。
import java.util.HashMap; import java.util.Map; public class MemoryLeakExample { private static Map<Integer, String> map = new HashMap<>(); public static void main(String[] args) { for (int i = 0; i < 100000; i++) { map.put(i, "value" + i); } } }
在上面的代碼中,如果 map
是一個長期存在的靜態(tài)變量,并且沒有及時清理,則可能導(dǎo)致內(nèi)存泄漏。
對比
關(guān)于內(nèi)存溢出和內(nèi)存泄漏的比較如下:
觸發(fā)時機:內(nèi)存溢出通常在內(nèi)存耗盡時立即觸發(fā),而內(nèi)存泄漏可能在一段時間后逐漸顯現(xiàn)。
影響范圍:內(nèi)存溢出會立即影響程序的可用性,而內(nèi)存泄漏通常是一個逐步積累的問題。
檢測難度:內(nèi)存溢出較容易檢測,而內(nèi)存泄漏往往需要深入分析和調(diào)試。
解決復(fù)雜度:內(nèi)存溢出的解決相對簡單,通常通過優(yōu)化內(nèi)存使用或增加內(nèi)存即可。而內(nèi)存泄漏的解決需要識別并清理不必要的引用,可能涉及更復(fù)雜的代碼重構(gòu)。
到此這篇關(guān)于Java中內(nèi)存溢出和內(nèi)存泄漏如何解決的文章就介紹到這了,更多相關(guān)Java 內(nèi)存溢出和內(nèi)存泄漏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA2020 1.1中Plugins加載不出來的問題及解決方法
這篇文章主要介紹了IDEA2020 1.1中Plugins加載不出來的問題,本文還給大家提到了IDEA 2020.1.1 找不到程序包和符號的問題,感興趣的朋友跟隨小編一起看看吧2020-06-06JAVAEE中用Session簡單實現(xiàn)購物車功能示例代碼
本篇文章主要介紹了JAVAEE中用Session簡單實現(xiàn)購物車功能示例代碼,非常具有實用價值,需要的朋友可以參考下。2017-03-03SpringBoot+Mybatis使用Mapper接口注冊的幾種方式
本篇博文中主要介紹是Mapper接口與對應(yīng)的xml文件如何關(guān)聯(lián)的幾種姿勢,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07Java面試題-實現(xiàn)復(fù)雜鏈表的復(fù)制代碼分享
這篇文章主要介紹了Java面試題-實現(xiàn)復(fù)雜鏈表的復(fù)制代碼分享,小編覺得還是挺不錯的,具有參考價值,需要的朋友可以了解下。2017-10-10Maven的porfile與SpringBoot的profile結(jié)合使用案例詳解
這篇文章主要介紹了Maven的porfile與SpringBoot的profile結(jié)合使用,通過maven的profile功能,在打包的時候,通過-P指定maven激活某個pofile,這個profile里面配置了一個參數(shù)activatedProperties,不同的profile里面的這個參數(shù)的值不同,需要的朋友可以參考下吧2021-12-12