必須先要了解的
1。c/c++是程序員自己管理內存,Java內存是由GC自動回收的。
我雖然不是很熟悉C++,不過這個應該沒有犯常識性錯誤吧。
2。什么是內存泄露?
內存泄露是指系統中存在無法回收的內存,有時候會造成內存不足或系統崩潰。
在C/C++中分配了內存不釋放的情況就是內存泄露。
3。Java存在內存泄露
我們必須先承認這個,才可以接著討論。雖然Java存在內存泄露,但是基本上不用很關心它,特別是那些對代碼本身就不講究的就更不要去關心這個了。
Java中的內存泄露當然是指:存在無用但是垃圾回收器無法回收的對象。
而且即使有內存泄露問題存在,也不一定會表現出來。
4。Java中參數都是傳值的。
對于基本類型,大家基本上沒有異議,但是對于引用類型我們也不能有異議。
Java內存泄露情況
1、堆內存溢出(outOfMemoryError:java heap space)
在jvm規范中,堆中的內存是用來生成對象實例和數組的。
如果細分,堆內存還可以分為年輕代和年老代,年輕代包括一個eden區和兩個survivor區。
當生成新對象時,內存的申請過程如下:
a、jvm先嘗試在eden區分配新建對象所需的內存;
b、如果內存大小足夠,申請結束,否則下一步;
c、jvm啟動youngGC,試圖將eden區中不活躍的對象釋放掉,釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區;
d、Survivor區被用來作為Eden及old的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區;
e、 當OLD區空間不夠時,JVM會在OLD區進行full GC;
f、full GC后,若Survivor及OLD區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現”out of memory錯誤”:
1
|
outOfMemoryError:java heap space |
2、方法區內存溢出(outOfMemoryError:permgem space)
在jvm規范中,方法區主要存放的是類信息、常量、靜態變量等。
所以如果程序加載的類過多,或者使用反射、gclib等這種動態代理生成類的技術,就可能導致該區發生內存溢出,一般該區發生內存溢出時的錯誤信息為:
1
|
outOfMemoryError:permgem space |
3、線程棧溢出(java。lang。StackOverflowError)
線程棧時線程獨有的一塊內存結構,所以線程棧發生問題必定是某個線程運行時產生的錯誤。
一般線程棧溢出是由于遞歸太深或方法調用層級過多導致的。
發生棧溢出的錯誤信息為:
1
|
java。lang。StackOverflowError |
內存泄露的幾種場景:
1、長生命周期的對象持有短生命周期對象的引用
這是內存泄露最常見的場景,也是代碼設計中經常出現的問題。
例如:在全局靜態map中緩存局部變量,且沒有清空操作,隨著時間的推移,這個map會越來越大,造成內存泄露。
2、修改hashset中對象的參數值,且參數是計算哈希值的字段
當一個對象被存儲進HashSet集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段,否則對象修改后的哈希值與最初存儲進HashSet集合中時的哈希值就不同了,在這種情況下,即使在contains方法使用該對象的當前引用作為參數去HashSet集合中檢索對象,也將返回找不到對象的結果,這也會導致無法從HashSet集合中刪除當前對象,造成內存泄露。
3、機器的連接數和關閉時間設置
長時間開啟非常耗費資源的連接,也會造成內存泄露。
來看個內存泄露的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class Stack { private Object[] elements= new Object[ 10 ]; private int size = 0 ; public void push(Object e){ ensureCapacity(); elements[size++] = e; } public Object pop(){ if ( size == 0 ) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity(){ if (elements。length == size){ Object[] oldElements = elements; elements = new Object[ 2 * elements。length+ 1 ]; System。arraycopy(oldElements, 0 , elements, 0 , size); } } } |
上面的原理應該很簡單,假如堆棧加了10個元素,然后全部彈出來,雖然堆棧
是空的,沒有我們要的東西,但是這是個對象是無法回收的,這個才符合了內存
泄露的兩個條件:無用,無法回收。
但是就是存在這樣的東西也不一定會導致什么樣的后果,如果這個堆棧用的比較少,
也就浪費了幾個K內存而已,反正我們的內存都上G了,哪里會有什么影響,再說
這個東西很快就會被回收的,有什么關系。下面看兩個例子。
例子1
1
2
3
4
5
6
7
8
|
public class Bad{ public static Stack s=Stack(); static { s。push( new Object()); s。pop(); //這里有一個對象發生內存泄露 s。push( new Object()); //上面的對象可以被回收了,等于是自愈了 } } |
因為是static,就一直存在到程序退出,但是我們也可以看到它有自愈功能,
就是說如果你的Stack最多有100個對象,那么最多也就只有100個對象無法被回收
其實這個應該很容易理解,Stack內部持有100個引用,最壞的情況就是他們都是
無用的,因為我們一旦放新的進取,以前的引用自然消失!
例子2
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class NotTooBad{ public void doSomething(){ Stack s= new Stack(); s。push( new Object()); //other code s。pop(); //這里同樣導致對象無法回收,內存泄露。 } //退出方法,s自動無效,s可以被回收,Stack內部的引用自然沒了,所以 //這里也可以自愈,而且可以說這個方法不存在內存泄露問題,不過是晚一點 //交給GC而已,因為它是封閉的,對外不開放,可以說上面的代碼99。9999%的 //情況是不會造成任何影響的,當然你寫這樣的代碼不會有什么壞的影響,但是 //絕對可以說是垃圾代碼!沒有矛盾吧,我在里面加一個空的for循環也不會有 //什么太大的影響吧,你會這么做嗎? } |
上面兩個例子都不過是小打小鬧,但是C/C++中的內存泄露就不是Bad了,而是Worst了。
他們如果一處沒有回收就永遠無法回收,頻繁的調用這個方法內存不就用光了!
因為Java還有自愈功能(我自己起的名字,還沒申請專利),所以Java的內存泄露問題
幾乎可以忽略了,但是知道的人就不要犯了。
為了避免內存泄露,在編寫代碼的過程中可以參考下面的建議:
1、盡早釋放無用對象的引用;
2、使用字符串處理,避免使用String,應大量使用StringBuffer,每一個String對象都得獨立占用內存一塊區域;
3、盡量少用靜態變量,因為靜態變量存放在永久代(方法區),永久代基本不參與垃圾回收;
4、避免在循環中創建對象;
5、開啟大型文件或從數據庫一次拿了太多的數據很容易造成內存溢出,所以在這些地方要大概計算一下數據量的最大值是多少,并且設定所需最小及最大的內存空間值。