柚子快報(bào)激活碼778899分享:jvm 內(nèi)存泄漏 與 內(nèi)存溢出
柚子快報(bào)激活碼778899分享:jvm 內(nèi)存泄漏 與 內(nèi)存溢出
1.內(nèi)存溢出(Memory Overflow)
生活樣例: ? ? ? ??內(nèi)存容量就像一個(gè)桶,內(nèi)存就是水,水 溢出 就是水滿了。定義: ????????內(nèi)存溢出是指程序試圖使用超過其可用內(nèi)存限制的內(nèi)存。這種情況通常會(huì)導(dǎo)致程序崩潰或異常。內(nèi)存溢出一般是由于分配了過多內(nèi)存或者在使用數(shù)據(jù)結(jié)構(gòu)時(shí)超出了其限制。例子:堆內(nèi)存溢出 ? ? ? ? 堆內(nèi)存用于動(dòng)態(tài)分配對象。當(dāng)程序嘗試分配超過堆內(nèi)存限制的內(nèi)存時(shí),就會(huì)發(fā)生堆內(nèi)存溢出。
public class HeapMemoryoverflowExample {
public static void main(string[] args) {
List
while (true) {
list.add(new int[10000001);//不斷分配大塊內(nèi)存
}
}
}
常見內(nèi)存溢出情況及解決方案:
堆內(nèi)存溢出(Java Heap Space):
原因:長時(shí)間運(yùn)行的應(yīng)用可能會(huì)持續(xù)創(chuàng)建對象,如果這些對象沒有被及時(shí)回收,就可能導(dǎo)致堆內(nèi)存耗盡。解決:增加JVM堆內(nèi)存大小(通過-Xms和-Xmx參數(shù)設(shè)置);優(yōu)化代碼以減少內(nèi)存使用,比如使用對象池來減少對象創(chuàng)建;分析內(nèi)存泄漏并修復(fù)。 棧溢出(StackOverflowError):
原因:通常是由于遞歸調(diào)用太深或循環(huán)創(chuàng)建了大量局部變量。解決:優(yōu)化遞歸邏輯,確保有正確的終止條件;減少方法調(diào)用深度;優(yōu)化循環(huán)邏輯,避免創(chuàng)建大量局部變量。 元空間溢出(Metaspace):
原因:Java 8 以后的版本使用元空間代替了永久代,用于存儲(chǔ)類的元數(shù)據(jù)。如果類的元數(shù)據(jù)消耗過多內(nèi)存,可能會(huì)觸發(fā)元空間溢出。解決:增加元空間大?。ㄍㄟ^-XX:MetaspaceSize和-XX:MaxMetaspaceSize參數(shù)設(shè)置);優(yōu)化代碼以減少類加載。 大對象處理不當(dāng):
原因:處理大型對象或集合時(shí),可能會(huì)占用大量內(nèi)存。解決:優(yōu)化大對象的處理邏輯,比如分批處理、使用流式處理等。 線程資源管理不當(dāng):
原因:線程創(chuàng)建過多,每個(gè)線程都有自己的??臻g,可能導(dǎo)致內(nèi)存溢出。解決:合理管理線程資源,避免創(chuàng)建過多線程;使用線程池來復(fù)用線程。
2.內(nèi)存泄露(Memory Leak)
生活樣例: 桶破了,水漏出去了。桶中的水就相當(dāng)于內(nèi)存,慢慢的流失了定義: ????????內(nèi)存泄露是指程序在運(yùn)行過程中動(dòng)態(tài)分配內(nèi)存后,沒有正確地釋放不再使用的內(nèi)存,導(dǎo)致這些內(nèi)存無法被再次分配和使用。長時(shí)間運(yùn)行的程序如果存在內(nèi)存泄露,會(huì)導(dǎo)致內(nèi)存逐漸耗盡,最終可能導(dǎo)致系統(tǒng)性能下降或者程序崩潰。例子: ? ? ? ? 在 Java 中,雖然有垃圾回收機(jī)制,但也可能出現(xiàn)內(nèi)存泄露。例如,當(dāng)某個(gè)對象不再需要但仍然被引用時(shí),垃圾回收器無法回收該對象的內(nèi)存。 ?
public class MemoryLeakExamplef
public static void main(string[l args) {
List
while (true) {
list.add(new 0bject());// 對象不斷增加,但沒有被釋放
}
}
}
2.1 靜態(tài)屬性導(dǎo)致內(nèi)存泄露
????????會(huì)導(dǎo)致內(nèi)存泄露的一種情況就是大量使用static靜態(tài)變量。在Java中,靜態(tài)屬性的生命周期通常伴隨著應(yīng)用整個(gè)生命周期(除非ClassLoader符合垃圾回收的條件)。
public class StaticTest {
public static List
public void populateList() {
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
Log.info("Debug Point 2");
}
public static void main(String[] args) {
Log.info("Debug Point 1");
new StaticTest().populateList();
Log.info("Debug Point 3");
}
}
如果監(jiān)控內(nèi)存堆內(nèi)存的變化,會(huì)發(fā)現(xiàn)在打印Point1和Point2之間,堆內(nèi)存會(huì)有一個(gè)明顯的增長趨勢圖。
但當(dāng)執(zhí)行完populateList方法之后,對堆內(nèi)存并沒有被垃圾回收器進(jìn)行回收。
針對上述程序,如果將定義list的變量前的static關(guān)鍵字去掉,再次執(zhí)行程序,會(huì)發(fā)現(xiàn)內(nèi)存發(fā)生了具體的變化。VisualVM監(jiān)控信息如下圖:
對比兩個(gè)圖可以看出,程序執(zhí)行的前半部分內(nèi)存使用情況都一樣,但當(dāng)執(zhí)行完populateList方法之后,后者不再有引用指向?qū)?yīng)的數(shù)據(jù),垃圾回收器便進(jìn)行了回收操作。
因此,我們要十分留意static的變量,如果集合或大量的對象定義為static的,它們會(huì)停留在整個(gè)應(yīng)用程序的生命周期當(dāng)中。而它們所占用的內(nèi)存空間,本可以用于其他地方。
那么如何優(yōu)化呢?第一,進(jìn)來減少靜態(tài)變量;第二,如果使用單例,盡量采用懶加載。
2.2 未關(guān)閉的資源
無論什么時(shí)候當(dāng)我們創(chuàng)建一個(gè)連接或打開一個(gè)流,JVM都會(huì)分配內(nèi)存給這些資源。比如,數(shù)據(jù)庫鏈接、輸入流和session對象。
忘記關(guān)閉這些資源,會(huì)阻塞內(nèi)存,從而導(dǎo)致GC無法進(jìn)行清理。特別是當(dāng)程序發(fā)生異常時(shí),沒有在finally中進(jìn)行資源關(guān)閉的情況。
這些未正常關(guān)閉的連接,如果不進(jìn)行處理,輕則影響程序性能,重則導(dǎo)致OutOfMemoryError異常發(fā)生。
如果進(jìn)行處理呢?
第一,始終記得在finally中進(jìn)行資源的關(guān)閉;第二,關(guān)閉連接的自身代碼不能發(fā)生異常;第三,Java7以上版本可使用try-with-resources代碼方式進(jìn)行資源關(guān)閉。
2.3 不當(dāng)?shù)膃quals方法和hashCode方法實(shí)現(xiàn)
當(dāng)我們定義個(gè)新的類時(shí),往往需要重寫equals方法和hashCode方法。在HashSet和HashMap中的很多操作都用到了這兩個(gè)方法。如果重寫不得當(dāng),會(huì)造成內(nèi)存泄露的問題。
下面來看一個(gè)具體的實(shí)例:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
}
現(xiàn)在將重復(fù)的Person對象插入到Map當(dāng)中。我們知道Map的key是不能重復(fù)的。
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map
for(int i=0; i<100; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertFalse(map.size() == 1);
}
上述代碼中將Person對象作為key,存入Map當(dāng)中。理論上當(dāng)重復(fù)的key存入Map時(shí),會(huì)進(jìn)行對象的覆蓋,不會(huì)導(dǎo)致內(nèi)存的增長。
但由于上述代碼的Person類并沒有重寫equals方法,因此在執(zhí)行put操作時(shí),Map會(huì)認(rèn)為每次創(chuàng)建的對象都是新的對象,從而導(dǎo)致內(nèi)存不斷的增長。
VisualVM中顯示信息如下圖:?
當(dāng)重寫equals方法和hashCode方法之后,Map當(dāng)中便只會(huì)存儲(chǔ)一個(gè)對象了。方法的實(shí)現(xiàn)如下:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
return result;
}
}
經(jīng)過上述修改之后,Assert中判斷Map的size便會(huì)返回true。
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map
for(int i=0; i<2; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertTrue(map.size() == 1);
}
重寫equals方法和hashCode方法之后,堆內(nèi)存的變化如下圖:
另外的例子就是當(dāng)使用ORM框架,如Hibernate時(shí),會(huì)使用equals方法和hashCode方法進(jìn)行對象的的分析和緩存操作。
如果不重寫這些方法,則發(fā)生內(nèi)存泄漏的可能性非常高,因?yàn)镠ibernate將無法比較對象(每次都是新對象),然后不停的更新緩存。
如何進(jìn)行處理?
第一,如果創(chuàng)建一個(gè)實(shí)體類,總是重寫equals方法和hashCode方法;第二,不僅要覆蓋默認(rèn)的方法實(shí)現(xiàn),而且還要考慮最優(yōu)的實(shí)現(xiàn)方式;
2.4 外部類引用內(nèi)部類
這種情況發(fā)生在非靜態(tài)內(nèi)部類(匿名類)中,在類初始化時(shí),內(nèi)部類總是需要外部類的一個(gè)實(shí)例。
每個(gè)非靜態(tài)內(nèi)部類默認(rèn)都持有外部類的隱式引用。如果在應(yīng)用程序中使用該內(nèi)部類的對象,即使外部類使用完畢,也不會(huì)對其進(jìn)行垃圾回收。
public class OuterClass {
private String importantData;
public OuterClass(String importantData) {
this.importantData = importantData;
}
public void doSomething() {
// 創(chuàng)建并啟動(dòng)線程,使用靜態(tài)匿名內(nèi)部類
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程運(yùn)行中..." + importantData);
}
});
thread.start();
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass("重要數(shù)據(jù)");
outerClass.doSomething();
//嘗試釋放outerClass對象
outerClass = null;
//...其他業(yè)務(wù)代碼
}
}
? ? ? ? ?這段代碼中main方法執(zhí)行 outerClass = null; 如果匿名內(nèi)部類開啟的線程沒有執(zhí)行結(jié)束,outerClass由于還被引用,不會(huì)被垃圾回收!
?????????在這個(gè)例子中,內(nèi)存泄漏的原因在于非靜態(tài)匿名內(nèi)部類(實(shí)現(xiàn)了Runnable接口的類)隱式地持有對其外部類實(shí)例OuterClass的引用。這個(gè)引用是通過importantData字段訪問外部類的成員變量時(shí)建立的。即使在main方法中將outer變量設(shè)置為null,外部類實(shí)例OuterClass也不能被垃圾回收,因?yàn)槟涿麅?nèi)部類中的線程仍然持有對它的引用。也就是說如果這個(gè)線程沒有結(jié)束,引用就一直存在。
這里我們只需要拷貝一份局部變量,就可以解除這個(gè)引用,從而避免內(nèi)存泄漏的問題。
public class OuterClass {
private String importantData;
public OuterClass(String importantData) {
this.importantData = importantData;
}
public void doSomething() {
// 創(chuàng)建并啟動(dòng)線程,使用靜態(tài)匿名內(nèi)部類
String data = this.importantData; //定義個(gè)局部的final變量
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程運(yùn)行中..." + data);
}
});
thread.start();
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass("重要數(shù)據(jù)");
outerClass.doSomething();
}
}
這樣的話,匿名內(nèi)部類中就不存在對外部類實(shí)例的引用。線程就不再直接引用OuterClass實(shí)例的成員變量,而是引用了一個(gè)局部變量的副本。因此,即使線程還在運(yùn)行,一旦main方法中將outerClass變量設(shè)置為null,OuterClass的實(shí)例就可以被垃圾回收了。
2.5 ThreadLocal導(dǎo)致的內(nèi)存泄漏
為什么源碼要用弱引用?
????????我們知道ThreadLocal是線程變量,每個(gè)線程都有一個(gè)配對的ThreadLocal對象(后面統(tǒng)稱為tl,ThreadLocal tl = new ThreadLocal();)。tl在線程中被銷毀時(shí),該線程指向的ThreadLocal對象應(yīng)該也被垃圾回收。ThreadLocalMap中的Entry中的key用的就是ThreadLocal的引用。如果這個(gè)引用為強(qiáng)引用。那么即使線程被銷毀,如圖指向ThreadLocal的引用tl也沒有了。但是如果是強(qiáng)引用那么ThreadLocalMap中還有指向ThreadLocal的引用,導(dǎo)致對象無法被垃圾回收!因此要用弱引用。
????????當(dāng)function01方法執(zhí)行完畢后,棧幀銷毀,強(qiáng)引用tl也就沒有了,但此時(shí)線程的ThreadLocalMap里某個(gè)entry的key引用還指向這個(gè)對象(因?yàn)榧词箞?zhí)行function01方法的線程銷毀了,ThreadLocal是另一個(gè)類,是兩個(gè)不同的類,具有獨(dú)立性)
舉個(gè)例子:之前也說了這兩個(gè)類之間的關(guān)系就像自然人 (Thread) 和 身份證 (ThreadLocal) 一樣,人是人,身份證是身份證,是兩樣不同的東西。如果這個(gè)人去世了,這個(gè)身份證也應(yīng)該作廢銷毀。一句話,身份證應(yīng)該跟著人走。如果是強(qiáng)引用,就會(huì)出現(xiàn)人沒了,但身份證信息還存在,這樣信息就會(huì)越來越多導(dǎo)致內(nèi)存溢出。
若這個(gè)Key是強(qiáng)引用,就會(huì)導(dǎo)致Key指向的ThreadLocal對象即V指向的對象不能被gc回收,造成內(nèi)存泄露。
若這個(gè)引用時(shí)弱引用就大概率會(huì)減少內(nèi)存泄漏的問題(當(dāng)然,還得考慮key為null這個(gè)坑),使用弱引用就可以使ThreadLocal對象在方法執(zhí)行完畢后順利被回收且entry的key引用指向?yàn)閚ull。
現(xiàn)在我們已經(jīng)知道了,Entry中使用弱引用就是為了避免內(nèi)存泄漏。
?弱引用就萬事大吉了嗎?
????????使用弱引用就可以使ThreadLocal對象在方法執(zhí)行完畢后順利被回收且entry的key引用指向?yàn)閚ull。此后我們調(diào)用get、set或remove方法時(shí),就會(huì)嘗試刪除key為null的entry,可以釋放value對象所占用的內(nèi)存
key為null的情況:
? ? ? ? 1.當(dāng)我們?yōu)閠hreadLocal變量賦值,實(shí)際上就是當(dāng)前的Entry(threadLocal實(shí)例為key,值為value)往這個(gè)threadLocalMap中存放。Entry中的key是弱引用,當(dāng)threadLocal外部強(qiáng)引用(就是在指向當(dāng)前ThreadLocal的線程中引用被置為null)被置為null (如 tl?= null ) ,那么系統(tǒng) GC 的時(shí)候,根據(jù)可達(dá)性分析,這個(gè)threadLocal實(shí)例就沒有任何一條鏈路能夠引用到它,這個(gè)ThreadLocal勢必會(huì)被回收,這樣一來,ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話(這個(gè)t1就不會(huì)被干掉),這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread的引用?-> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收,造成內(nèi)存泄漏。
我們知道hashmap中允許key為null,可以看到這里存儲(chǔ)了key為null的value,造成了內(nèi)存泄漏
? ? ? ? 2.當(dāng)然,如果當(dāng)前thread運(yùn)行結(jié)束,threadLocal,threadLocalMap,Entry沒有引用鏈可達(dá),在垃圾回收的時(shí)候都會(huì)被系統(tǒng)進(jìn)行回收。
? ? ? ? 3.但在實(shí)際使用中我們有時(shí)候會(huì)用線程池去維護(hù)我們的線程,比如在Executors.newFixedThreadPool()時(shí)創(chuàng)建線程的時(shí)候,為了復(fù)用線程是不會(huì)結(jié)束的,所以threadLocal內(nèi)存泄漏就值得我們小心。
總結(jié):我們知道,一個(gè)Thread對應(yīng)一個(gè)ThreadLocalMap,但可以在一個(gè)線程下創(chuàng)建多個(gè)ThreadLocal對象,每個(gè)ThreadLocal對象都可以作為ThreadLocalMap中的key。這時(shí)候,如果多個(gè)ThreadLocal對象使用完畢外部強(qiáng)引用賦值為null希望被垃圾回收?(new一個(gè)ThreadLocal對象的時(shí)候就是強(qiáng)引用,只是在ThreadLocalMap中的key是弱引用) 。這時(shí)候就出現(xiàn)了key為null但value還存在著的情況。如果這時(shí)候由于線程復(fù)用,Thread遲遲不結(jié)束,就會(huì)可能會(huì)導(dǎo)致越來越多的key為null (比如說現(xiàn)在ThreadLocalMap中有十個(gè)鍵值對,key從t1到t10,但這些ThreadLocal對象其實(shí)都已經(jīng)被銷毀了全部為null,下一次線程池復(fù)用該線程,這十個(gè)key對應(yīng)的value是無法被內(nèi)存釋放的)
解決方法:set、get方法會(huì)去檢查所有鍵為null的Entry對象
這些方法都對key== null 也就是臟Entry進(jìn)行了處理,防止內(nèi)存泄漏
set() 方法
get()方法
remove()方法
ThreadLocal tl1 = new ThreadLocal();
tl1.set("name");
ThreadLocal tl2 = new ThreadLocal();
tl2.set("滴滴滴");
//在當(dāng)前線程中,把強(qiáng)引用tl2設(shè)置為null,
//當(dāng)垃圾回收時(shí),ThreadLocalMap中的弱引用也沒有了,就沒人指向tl2
//這樣的話當(dāng)前Thread指向tl2就會(huì)被垃圾回收
//此時(shí)就會(huì)出現(xiàn)ThreadLocalMap中的key為空,但value仍然存在的情況,導(dǎo)致內(nèi)存泄漏
//現(xiàn)在我們使用線程,一般都會(huì)和使用線程池,這就導(dǎo)致線程一直不會(huì)結(jié)束,一直存在內(nèi)存泄漏!
tl2 = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println(tl1.get() + " " + tl2.get());
//為了避免內(nèi)存泄漏,我們在使用完ThreadLocal之后,應(yīng)該手動(dòng)調(diào)用remove方法
tl1.remove();
tl2.remove();
????????簡而言之,如果一個(gè)ThreadLocal調(diào)用以上三種方法,都會(huì)在底層做個(gè)檢查。如果當(dāng)前ThreadLocal已經(jīng)為null,就會(huì)去ThreadLocalMap中把對應(yīng)的value賦值為null,等垃圾回收的時(shí)候就會(huì)自動(dòng)釋放內(nèi)存了。
柚子快報(bào)激活碼778899分享:jvm 內(nèi)存泄漏 與 內(nèi)存溢出
推薦鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。