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