java 管理內存

轉載自 ---- http://yangzhiyong77.iteye.com/blog/1468175


1. java 是如何管理內存的

 Java 的內存管理就是對象的分配和釋放問題。(兩部分)

分配 :內存的分配是由程序完成的,程序員需要通過關鍵字new 爲每個對象申請內存空間 ( 基本類型除外) ,所有的對象都在堆 (Heap) 中分配空間。 
釋放:對象的釋放是由垃圾回收機制決定和執行的,這樣做確實簡化了程序員的工作。但同時,它也加重了JVM的工作。因爲,GC 爲了能夠正確釋放對象,GC 必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都需要進行監控。

2. 什麼叫java 的內存泄露

     在Java 中, 內存泄漏就是存在一些被分配的對象,華夏名網怎麼樣這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連(也就是說仍存 在該內存對象的引用);其次,這些對象是無用的,即程序以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定爲Java 中的內存泄漏,這些對象不會被GC 所回收,然而它卻佔用內存。

3. JVM 的內存區域組成

java 把內存分兩種:一種是棧內存,另一種是堆內存1 。在函數中定義的基本類型變量和對象的引用變量都在函數的棧內存中分配;2 。堆內存用來存放由new 創建的對象和數組以及對象的實例變量在函數(代碼塊)中定義一個變量時,java 就在棧中爲這個變量分配內存空間,當超過變量的作用域後,java 會自動釋放掉爲該變量所分配的內存空間;在堆中分配的內存由java 虛擬機的自動垃圾回收器來管理 
堆和棧的優缺點   

  堆的優勢是可以動態分配內存大小,生存期也不必事先告訴編譯器,因爲它是在運行時動態分配內存的。

缺點就是要在運行時動態分配內存,存取速度較慢; 棧的優勢是,存取速度比堆要快,僅次於直接位於CPU 中的寄存器。

另外,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。

4. Java 中數據在內存中是如何存儲的

 

a) 基本數據類型

  Java 的基本數據類型共有8 種,即int, short, long, byte, float, double, boolean, char( 注意,並沒有string 的基本類型) 。這種類型的定義是通過諸如int a = 3 ; long b = 255L ;的形式來定義的。如int a = 3 ;這裏的a 是一個指向int 類型的引用,指向3 這個字面值。這些字面值的數據,由於大小可知,生存期可知( 這些字面值定義在某個程序塊裏面,程序塊退出後,字段值就消失了) ,出於追求速度的原因,就存在於棧中。 
另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。比如:我們同時定義: 
int a=3; 
int b =3; 
    編譯器先處理int a = 3 ;首先它會在棧中創建一個變量爲a 的引用,然後查找有沒有字面值爲3 的地址,沒找到,就開闢一個存放3 這個字面值的地址,然後將a 指向3 的地址。接着處理int b = 3 ;在創建完b 這個引用變量後,由於在棧中已經有3 這個字面值,便將b 直接指向3 的地址。這樣,就出現了a 與b 同時均指向3 的情況。   定義完a 與b 的值後,再令a = 4 ;那麼,b 不會等於4 ,還是等於3 。在編譯器內部,遇到時,它就會重新搜索棧中是否有4 的字面值,如果沒有,重新開闢地址存放4 的值;如果已經有了,則直接將a 指向這個地址。因此a 值的改變不會影響到b 的值。

 

b)  對象

 

在Java 中,創建一個對象包括對象的聲明和實例化兩步,下面用一個例題來說明對象的內存模型。  

假設有類Rectangle 定義如下: 
public class Rectangle { 
 double width; 
 double height; 
public Rectangle(double w,double h)

 { 
 w = width; 
 h = height; 
 } 
}


(1) 聲明對象時的內存模型 
 用Rectangle rect ;聲明一個對象rect 時,將在棧內存爲對象的引用變量rect 分配內存空間,但Rectangle 的值爲空,稱rect 是一個空對象。空對象不能使用,因爲它還沒有引用任何" 實體" 。 
(2) 對象實例化時的內存模型 
 當執行rect=new Rectangle(3,5) ;時,會做兩件事: 在堆內存中爲類的成員變量width,height 分配內存,並將其初始化爲各數據類型的默認值;接着進行顯式初始化(類定義時的初始化值);最後調用構造方法,爲成員變量賦值。  返回堆內存中對象的引用(相當於首地址)給引用變量rect, 以後就可以通過rect 來引用堆內存中的對象了。

c)  創建多個不同的對象實例

        一個類通過使用new 運算符可以創建多個不同的對象實例,這些對象實例將在堆中被分配不同的內存空間,改變其中一個對象的狀態不會影響其他對象的狀態。例如: 
Rectangle r1= new Rectangle(3,5); 
Rectangle r2= new Rectangle(4,6); 
 此時,將在堆內存中分別爲兩個對象的成員變量width 、height 分配內存空間,兩個對象在堆內存中佔據的空間是互不相同的。如果有: 
Rectangle r1= new Rectangle(3,5); 
Rectangle r2=r1; 
則在堆內存中只創建了一個對象實例,在棧內存中創建了兩個對象引用,兩個對象引用同時指向一個對象實例。 
 

d)   包裝類

         基本型別都有對應的包裝類:如int 對應Integer 類,double 對應Double 類等,基本類型的定義都是直接在棧中,如果用包裝類來創建對象,就和普通對象一樣了。例如:int i=0 ;i 直接存儲在棧中。 Integer i (i 此時是對象) = new Integer(5) ;這樣,i 對象數據存儲在堆中,i 的引用存儲在棧中,通過棧中的引用來操作對象。 
 

e)  String

 String 是一個特殊的包裝類數據。可以用用以下兩種方式創建:String str = new String("abc") ;String str = "abc"; 
第一種創建方式,和普通對象的的創建過程一樣; 
第二種創建方式,Java 內部將此語句轉化爲以下幾個步驟: 
(1) 先定義一個名爲str 的對String 類的對象引用變量:String str ; 
(2) 在棧中查找有沒有存放值爲"abc" 的地址,如果沒有,則開闢一個存放字面值爲"abc" 
地址,接着創建一個新的String 類的對象o ,並將o 的字符串值指向這個地址,而且在棧 
這個地址旁邊記下這個引用的對象o 。如果已經有了值爲"abc" 的地址,則查找對象o ,並 
回o 的地址。 
(3) 將str 指向對象o 的地址。 
值得注意的是,一般String 類中字符串值都是直接存值的。但像String str = "abc" ;這種 
合下,其字符串值卻是保存了一個指向存在棧中數據的引用。 
爲了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。 
String str1="abc" ; 
String str2="abc" ; 
System.out.println(s1==s2) ;//true 
注意,這裏並不用str1.equals(str2) ;的方式,因爲這將比較兩個字符串的值是否相等。== 號,根據JDK 的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這裏要看的是,str1 與str2 是否都指向了同一個對象。 
我們再接着看以下的代碼。 
String str1= new String("abc") ; 
String str2="abc" ; 
System.out.println(str1==str2) ;//false 
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。     以上兩段代碼說明,只要是用new() 來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。

f)   數組

         當定義一個數組,int x[] ;或int []x ;時,在棧內存中創建一個數組引用,通過該引用(即數組名)來引用數組。x=new int[3] ;將在堆內存中分配3 個保存int 型數據的空間,堆內存的首地址放到棧內存中,每個數組元素被初始化爲0 。 
 

g)   靜態變量

         用static 的修飾的變量和方法,實際上是指定了這些變量和方法在內存中的" 固定位置" -static storage ,可以理解爲所有實例對象共有的內存空間。static 變量有點類似於C 中的全局變量的概念;靜態表示的是內存的共享,就是它的每一個實例都指向同一個內存地址。把static 拿來,就是告訴JVM 它是靜態的,它的引用(含間接引用)都是指向同一個位置,在那個地方,你把它改了,它就不會變成原樣,你把它清理了,它就不會回來了。         那靜態變量與方法是在什麼時候初始化的呢?對於兩種不同的類屬性,static 屬性與instance 屬性,初始化的時機是不同的。instance 屬性在創建實例的時候初始化,static 屬性在類加載,也就是第一次用到這個類的時候初始化,對於後來的實例的創建,不再次進行初始化。         我們常可看到類似以下的例子來說明這個問題: 
class Student{ 
static int numberOfStudents=0; 
Student() 

numberOfStudents++; 


每一次創建一個新的Student 實例時, 成員numberOfStudents 都會不斷的遞增, 並且所有的Student 實例都訪問同一個numberOfStudents 變量, 實際上int numberOfStudents 變量在內存中只存儲在一個位置上。

5. Java 的內存管理實例

 Java 程序的多個部分( 方法,變量,對象) 駐留在內存中以下兩個位置:即堆和棧,現在我們只關心3 類事物:實例變量,局部變量和對象: 
實例變量和對象駐留在堆上 
局部變量駐留在棧上


   讓我們查看一個java 程序,看看他的各部分如何創建並且映射到棧和堆中:

  1. public class Dog {   
  2. Collar c;   
  3. String name;   
  4. //1. main() 方法位於棧上   
  5. public static void main(String[] args) {   
  6. //2. 在棧上創建引用變量d, 但Dog 對象尚未存在   
  7. Dog d;   
  8. //3. 創建新的Dog 對象,並將其賦予d 引用變量   
  9. d = new Dog();   
  10. //4. 將引用變量的一個副本傳遞給go() 方法   
  11. d.go(d);   
  12. }   
  13. //5. 將go() 方法置於棧上,並將dog 參數作爲局部變量   
  14. void go(Dog dog){   
  15. //6. 在堆上創建新的Collar 對象,並將其賦予Dog 的實例變量   
  16. c =new Collar();   
  17. }   
  18. //7. 將setName() 添加到棧上,並將dogName 參數作爲其局部變量   
  19. void setName(String dogName){   
  20. //8. name 的實例對象也引用String 對象   
  21. name=dogName;   
  22. }   

//9. 程序執行完成後,setName() 將會完成並從棧中清除,此時,局部變量dogName 也會消失,儘管它所引用的String 仍在堆上 
}

6. 垃圾回收機制:

(問題一:什麼叫垃圾回收機制?)

垃圾回收是一種動態存儲管理技術,它自動地釋放不再被程序引用的對象,按照特定的垃圾收集算法來實現資源自動回收的功能。當一個對象不再被引用的時候,內存回收它佔領的空間,以便空間被後來的新對象使用,以免造成內存泄露。

(問題二:java 的垃圾回收有什麼特點?) 

JAVA 語言不允許程序員直接控制內存空間的使用。內存空間的分配和回收都是由JRE 負責在後臺自動進行的,尤其是無用內存空間的回收操作(garbagecollection, 也稱垃圾回收) ,只能由運行環境提供的一個超級線程進行監測和控制。

(問題三:垃圾回收器什麼時候會運行?) 

一般是在CPU 空 閒或空間不足時自動進行垃圾回收,而程序員無法精確控制垃圾回收的時機和順序等。

(問題四:什麼樣的對象符合垃圾回收條件?)

當沒有任何獲得線程能訪問一 個對象時,該對象就符合垃圾回收條件。

(問題五:垃圾回收器是怎樣工作的?)

垃圾回收器如發現一個對象不能被任何活線程訪問時,他將認爲該對象符合刪除條 件,就將其加入回收隊列,但不是立即銷燬對象,何時銷燬並釋放內存是無法預知的。垃圾回收不能強制執行,然而Java 提供了一些方法(如:System.gc() 方法),允許你請求JVM執行垃圾回收,而不是要求,虛擬機會盡其所能滿足請求,但是不能保證JVM 從內存中刪除所有不用的對象。

(問題六:一個java 程序能夠耗盡內存嗎?)

可以。垃圾收集系統嘗試在對象不被使用時把他們從內存中刪除。然而,如果保持太多活的對象,系統則可能會耗盡內存。垃圾回收器不能保證有足夠的內存,只能保證可用內存儘可能的得到高效的管理。

(問題七:如何顯示的使對象符合垃圾回收條件?)


(1 ) 空引用:當對象沒有對他可到達引用時,他就符合垃圾回收的條件。也就是說如果沒有對他的引用,刪除對象的引用就可以達到目的,因此我們可以把引用變量設置爲null ,來符合垃圾回收的條件。 
StringBuffer sb = new StringBuffer("hello"); 
System.out.println(sb); 
sb=null; 


(2 )重新爲引用變量賦值:可以通過設置引用變量引用另一個對象來解除該引用變量與一個對象間的引用關係。
StringBuffer sb1 = new StringBuffer("hello"); 
StringBuffer sb2 = new StringBuffer("goodbye"); 
System.out.println(sb1); 
sb1=sb2;// 此時"hello" 符合回收條件  


(3 )方法內創建的對象:所創建的局部變量僅在該方法的作用期間內存在。一旦該方法返回,在這個方法內創建的對象就符合垃圾收集條件。有一種明顯的例外情況,就是方法的返回對象。 
public static void main(String[] args) { 
Date d = getDate(); 
System.out.println("d = " + d); 

private static Date getDate() { 
Date d2 = new Date(); 
StringBuffer now = new StringBuffer(d2.toString()); 
System.out.println(now); 
return d2; 


(4 )隔離引用:這種情況中,被回收的對象仍具有引用,這種情況稱作隔離島。若存在這兩個實例,他們互相引用,並且這兩個對象的所有其他引用都刪除,其他任何線程無法訪問這兩個對象中的任意一個。也可以符合垃圾回收條件。 
public class Island { 
Island i; 
public static void main(String[] args) { 
Island i2 = new Island(); 
Island i3 = new Island(); 
Island i4 = new Island(); 
i2.i=i3; 
i3.i=i4; 
i4.i=i2; 
i2=null; 
i3=null; 
i4=null; 


(問題八:垃圾收集前進行清理------finalize() 方法) java 提供了一種機制,使你能夠在對象剛要被垃圾回收之前運行一些代碼。這段代碼位於名爲finalize() 的方法內,所有類從Object 類繼承這個方法。由於不能保證垃圾回收器會刪除某個對象。因此放在finalize() 中的代碼無法保證運行。因此建議不要重寫finalize();


7. final 問題: 
     final 使得被修飾的變量" 不變" ,但是由於對象型變量的本質是" 引用" ,使得" 不變" 也有了兩種含義:引用本身的不變? ,和引用指向的對象不變。?         引用本身的不變: 
final StringBuffer a=new StringBuffer("immutable"); 
final StringBuffer b=new StringBuffer("not immutable"); 
a=b;// 編譯期錯誤
引用指向的對象不變: 
final StringBuffer a=new StringBuffer("immutable"); 
a.append(" broken!"); // 編譯通過
可見,final 只對引用的" 值"( 也即它所指向的那個對象的內存地址) 有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final 是不負責的。這很類似== 操作符:== 操作符只負責引用的" 值" 相等,至於這個地址所指向的對象內容是否相等,== 操作符是不管的。在舉一個例子: 
public class Name { 
private String firstname; 
private String lastname; 
public String getFirstname() { 
return firstname; 

public void setFirstname(String firstname) { 
this.firstname = firstname; 

public String getLastname() { 
return lastname; 

public void setLastname(String lastname) { 
this.lastname = lastname; 


  

  
         編寫測試方法: 
public static void main(String[] args) { 
final Name name = new Name(); 
name.setFirstname("JIM"); 
name.setLastname("Green"); 
System.out.println(name.getFirstname()+" "+name.getLastname()); 
}
  
         理解final 問題有很重要的含義。許多程序漏洞都基於此----final 只能保證引用永遠指向固定對象,不能保證那個對象的狀態不變。在多線程的操作中, 一個對象會被多個線程共享或修改,一個線程對對象無意識的修改可能會導致另一個使用此對象的線程崩潰。一個錯誤的解決方法就是在此對象新建的時候把它聲明爲final ,意圖使得它" 永遠不變" 。其實那是徒勞的。        Final 還有一個值得注意的地方:         先看以下示例程序: 
class Something { 
final int i; 
public void doSomething() { 
System.out.println("i = " + i); 

}
         對於類變量,Java 虛擬機會自動進行初始化。如果給出了初始值,則初始化爲該初始值。如果沒有給出,則把它初始化爲該類型變量的默認初始值。但是對於用final 修飾的類變量,虛擬機不會爲其賦予初值,必須在constructor ( 構造器) 結束之前被賦予一個明確的值。可以修改爲"final int i = 0;" 。 
  
8.    如何把程序寫得更健壯: 
    1 、儘早釋放無用對象的引用。好的辦法是使用臨時變量的時候,讓引用變量在退出活動域後,自動設置爲null,暗示垃圾收集器來收集該對象,防止發生內存泄露。對於仍然有指針指向的實例,jvm 就不會回收該資源, 因爲垃圾回收會將值爲null 的對象作爲垃圾,提高GC 回收機制效率; 
    2 、定義字符串應該儘量使用 String str="hello"; 的形式,避免使用String str = new String("hello"); 的形式。因爲要使用內容相同的字符串,不必每次都new 一個String 。例如我們要在構造器中對一個名叫s 的String 引用變量進行初始化,把它設置爲初始值,應當這樣做: 
public class Demo { 
private String s; 
public Demo() { 
s = "Initial Value"; 


 
... 

  而非 
s = new String("Initial Value"); 
     後者每次都會調用構造器,生成新對象,性能低下且內存開銷大,並且沒有意義,因爲String 對象不可改變,所以對於內容相同的字符串,只要一個String 對象來表示就可以了。也就說,多次調用上面的構造器創建多個對象,他們的String 類型屬性s 都指向同一個對象。   

3 、我們的程序裏不可避免大量使用字符串處理,避免使用String ,應大量使用StringBuffer ,因爲String 被設計成不可變(immutable) 類,所以它的所有對象都是不可變對象,請看下列代碼; 
String s = "Hello";   
s = s + " world!";  
String s = "Hello"; 
s = s + " world!"; 
       在這段代碼中,s 原先指向一個String 對象,內容是 "Hello" ,然後我們對s 進行了+ 操作,那麼s 所指向的那個對象是否發生了改變呢?答案是沒有。這時,s 不指向原來那個對象了,而指向了另一個 String 對象,內容爲"Hello world!" ,原來那個對象還存在於內存之中,只是s 這個引用變量不再指向它了。         通過上面的說明,我們很容易導出另一個結論,如果經常對字符串進行各種各樣的修改,或者說,不可預見的修改,那麼使用String來代表字符串的話會引起很大的內存開銷。因爲 String 對象建立之後不能再改變,所以對於每一個不同的字符串,都需要一個String 對象來表示。這時,應該考慮使用StringBuffer 類,它允許修改,而不是每個不同的字符串都要生成一個新的對象。並且,這兩種類的對象轉換十分容易。 
    4 、儘量少用靜態變量,因爲靜態變量是全局的,GC 不會回收的; 
    5 、儘量避免在類的構造函數裏創建、初始化大量的對象,防止在調用其自身類的構造器時造成不必要的內存資源浪費,尤其是大對象,JVM 會突然需要大量內存,這時必然會觸發GC 優化系統內存環境;顯示的聲明數組空間,而且申請數量還極大。         以下是初始化不同類型的對象需要消耗的時間:

運算操作   
  示例    
  標準化時間 
  
本地賦值    
 i = n 
 1.0 
  
實例賦值    
 this.i = n 
 1.2 
  
方法調用    
 Funct() 
 5.9 
  
新建對象    
 New Object() 
 980 
  
新建數組    
 New int[10] 
 3100 
 

        
從表1 可以看出,新建一個對象需要980 個單位的時間,是本地賦值時間的980 倍,是方法調用時間的166 倍,而新建一個數組所花費的時間就更多了。 
    6 、儘量在合適的場景下使用對象池技術以提高系統性能,縮減縮減開銷,但是要注意對象池的尺寸不宜過大,及時清除無效對象釋放內存資源,綜合考慮應用運行環境的內存資源限制,避免過高估計運行環境所提供內存資源的數量。 
    7 、大集合對象擁有大數據量的業務對象的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。 
    8 、不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用hashtable ,vector 創建一組對象容器,然後從容器中去取那些對象,而不用每次new 之後又丟棄。 
    9 、一般都是發生在開啓大型文件或跟數據庫一次拿了太多的數據,造成 Out Of Memory Error 的狀況,這時就大概要計算一下數據量的最大值是多少,並且設定所需最小及最大的內存空間值。 
    10 、儘量少用finalize 函數 ,因爲finalize() 會加大GC 的工作量,而GC 相當於耗費系統的計算能力。 
   11 、不要過濫使用哈希表,有一定開發經驗的開發人員經常會使用hash 表(hash 表在JDK 中的一個實現就是HashMap )來緩存一些數據,從而提高系統的運行速度。比如使用HashMap 緩 存一些物料信息、人員信息等基礎資料,這在提高系統速度的同時也加大了系統的內存佔用,特別是當緩存的資料比較多的時候。其實我們可以使用操作系統中的緩 存的概念來解決這個問題,也就是給被緩存的分配一個一定大小的緩存容器,按照一定的算法淘汰不需要繼續緩存的對象,這樣一方面會因爲進行了對象緩存而提高 了系統的運行效率,同時由於緩存容器不是無限制擴大,從而也減少了系統的內存佔用。現在有很多開源的緩存實現項目,比如ehcache、oscache等,這些項目都實現了FIFO、MRU等常見的緩存算法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章