Java中的記憶體管理
要了解Java中的記憶體洩漏,首先就得知道Java中的記憶體是如何管理的。
在Java程式中,我們通常使用 new 為物件分配記憶體,而這些記憶體空間都在堆上。
Java判斷物件是否可以回收使用的而是可達性分析演算法。
這個演算法的基本思路就是通過一系列名為 "GC Roots" 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的,下圖物件 object5, object6, object7 雖然有互相判斷,但它們到 GC Roots 是不可達的,所以它們將會判定為是可回收物件。
在 Java 語言中,可作為 GC Roots 物件的包括如下幾種:
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件
- 本地方法棧(Native方法)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
什麼是Java中的記憶體洩漏
Java 中的記憶體洩漏,廣義並通俗的說,就是:不再會被使用的物件的記憶體不能被回收,就是記憶體洩漏。
Java 中的記憶體洩漏與 C++ 中的表現有所不同。
在 C++ 中,所有被分配了記憶體的物件,不再使用之後,都必須程式設計師手動的去釋放他們。但是在 Java 中,我們不用自己釋放記憶體,無用的記憶體由 GC 自動清理,這也極大的簡化了我們的程式設計工作。但實際有時候一些不再會被使用的物件在 GC 看來不能被釋放就會造成記憶體洩漏。
物件都是有生命週期的,有的長,有的短。如果長生命週期的物件持有短生命週期的引用,就很可能會出現記憶體洩漏。例如:
public class Test {
Object object;
public void method() {
object = new Object();
// ...
}
}
這裡的 object 例項,其實我們期望它只作用於 method() 方法中,且其他地方也不會再用到它,但是當 method() 方法執行完之後,object物件所分配的記憶體不會馬上被認為是可以被釋放的物件。只有在 Test 類建立的物件被釋放後才會被釋放。嚴格地說,這就是一種記憶體洩漏。解決辦法就是將 object 作為 method() 方法中的區域性變數。當然也可以在使用完 object 之後 將其置為 null。
public class Test {
Object object;
public void method() {
object = new Object();
// ...
object = null;
}
}
這樣,之前 new Object() 分配的記憶體就可以被 GC 回收。
Java中記憶體洩漏的例子
- 靜態集合類
如HashMap、LinkedList等等。如果這些容器為靜態的,那麼它們的生命週期與程式一致,則容器中的物件在程式結束之前將不能被釋放,從而造成記憶體洩漏。簡單而言,長生命週期的物件持有短生命週期物件的引用,儘管短生命週期的物件不再使用,但是因為長生命週期物件持有它的引用而導致不能被回收。
static Vector v = new Vector();
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中,程式碼棧中存在 Vector 物件的引用 v 和 Object 物件的引用 o 。在 For 迴圈,我們不斷的生成新的物件,然後將其新增到 Vector 物件中,之後將 o 引用置空。問題是當 o 引用被置空後,如果發生 GC,我們建立的 Object 物件是否能夠被 GC 回收呢?答案是否定的。因為, GC 在跟蹤程式碼棧中的引用時,會發現 v 引用,而繼續往下跟蹤,就會發現 v 引用指向的記憶體空間中又存在指向 Object 物件的引用。也就是說盡管o 引用已經被置空,但是 Object 物件仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此迴圈之後, Object 物件對程式已經沒有任何作用,那麼我們就認為此 Java 程式發生了記憶體洩漏。
- 各種連線,如資料庫連線、網路連線和IO連線等
在對資料庫進行操作的過程中,首先需要建立與資料庫的連線,當不再使用時,需要呼叫close方法來釋放與資料庫的連線。只有連線被關閉後,垃圾回收器才會回收對應的物件。否則,如果在訪問資料庫的過程中,對Connection、Statement或ResultSet不顯性地關閉,將會造成大量的物件無法被回收,從而引起記憶體洩漏。
- 變數不合理的作用域
一般而言,一個變數的定義的作用範圍大於其使用範圍,很有可能會造成記憶體洩漏。另一方面,如果沒有及時地把物件設定為null,很有可能導致記憶體洩漏的發生。
public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();// 從網路中接受資料儲存到msg中
saveDB();// 把msg儲存到資料庫中
}
}
如上面這個虛擬碼,通過 readFromNet() 方法把接受的訊息儲存在變數 msg 中,然後呼叫 saveDB() 方法把 msg 的內容儲存到資料庫中,此時 msg 已經就沒用了,由於 msg 的生命週期與物件的生命週期相同,此時 msg 還不能回收,因此造成了記憶體洩漏。
實際上這個 msg 變數可以放在 receiveMsg() 方法內部,當方法使用完,那麼 msg 的生命週期也就結束,此時就可以回收了。還有一種方法,在使用完 msg 後,把 msg 設定為 null,這樣垃圾回收器也會回收 msg 的記憶體空間。
- 內部類持有外部類
如果一個外部類的例項物件的方法返回了一個內部類的例項物件,這個內部類物件被長期引用了,即使那個外部類例項物件不再被使用,但由於內部類持有外部類的例項物件,這個外部類物件將不會被垃圾回收,這也會造成記憶體洩露。
- 改變雜湊值
當一個物件被儲存進 HashSet 集合中以後,就不能修改這個物件中的那些參與計算雜湊值的欄位了,否則,物件修改後的雜湊值與最初儲存進 HashSet 集合中時的雜湊值就不同了,在這種情況下,即使在 contains 方法使用該物件的當前引用作為的引數去 HashSet 集合中檢索物件,也將返回找不到物件的結果,這也會導致無法從 HashSet 集合中單獨刪除當前物件,造成記憶體洩露。
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變
set.remove(p3); //此時remove不掉,造成記憶體洩漏
set.add(p3); //重新新增,居然新增成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
- 單例物件在被初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部物件的引用,那麼這個外部物件將不能被jvm正常回收,導致記憶體洩露
- 快取洩漏
記憶體洩漏的另一個常見來源是快取,一旦你把物件引用放入到快取中,他就很容易遺忘,對於這個問題,可以使用 WeakHashMap 代表快取,此種 Map 的特點是,當除了自身有對 key 的引用外,此 key 沒有其他引用那麼此 map 會自動丟棄此值
- 監聽器和回撥
記憶體洩漏第三個常見來源是監聽器和其他回撥,如果客戶端在你實現的 API 中註冊回撥,卻沒有顯示的取消,那麼就會積聚。需要確保回撥立即被當作垃圾回收的最佳方法是隻儲存他的弱引用,例如將他們儲存成為 WeakHashMap 中的鍵。
記憶體洩露解決的原則
1.儘量減少使用靜態變數,類的靜態變數的生命週期和類同步的。
2.宣告物件引用之前,明確記憶體物件的有效作用域,儘量減小物件的作用域,將類的成員變數改寫為方法內的區域性變數;
3.減少長生命週期的物件持有短生命週期的引用;
4.使用StringBuilder和StringBuffer進行字串連線,Sting和StringBuilder以及StringBuffer等都可以代表字串,其中String字串代表的是不可變的字串,後兩者表示可變的字串。如果使用多個String物件進行字串連線運算,在執行時可能產生大量臨時字串,這些字串會儲存在記憶體中從而導致程式效能下降。
5.對於不需要使用的物件手動設定null值,不管GC何時會開始清理,我們都應及時的將無用的物件標記為可被清理的物件;
6.各種連線(資料庫連線,網路連線,IO連線)操作,務必顯示呼叫close關閉。