天天看點

Java 記憶體管理

java 記憶體管理機制

JAVA 記憶體管理總結

  1. java 是如何管理記憶體的

    Java 的記憶體管理就是對象的配置設定和釋放問題。(兩部分)

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

  1. 什麼叫 java 的記憶體洩露

在 Java 中,記憶體洩漏就是存在一些被配置設定的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連(也就是說仍存在該記憶體對象的引用);其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為 Java 中的記憶體洩漏,這些對象不會被 GC 所回收,然而它卻占用記憶體。

  1. JVM 的記憶體區域組成

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

堆和棧的優缺點

堆的優勢是可以動态配置設定記憶體大小,生存期也不必事先告訴編譯器,因為它是在運作時動态配置設定記憶體的。

缺點就是要在運作時動态配置設定記憶體,存取速度較慢; 棧的優勢是,存取速度比堆要快,僅次于直接位于 CPU 中的寄存器。

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

  1. 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變量在記憶體中隻存儲在一個位置上。

  1. Java 的記憶體管理執行個體

Java 程式的多個部分(方法,變量,對象)駐留在記憶體中以下兩個位置:即堆和棧,現在我們隻關心3類事物:執行個體變量,局部變量和對象: 執行個體變量和對象駐留在堆上 局部變量駐留在棧上 讓我們檢視一個 java 程式,看看他的各部分如何建立并且映射到棧和堆中:

public class Dog {
Collar c;
String name;
//1. main()方法位于棧上
public static void main(String[] args) {
//2. 在棧上建立引用變量d,但Dog對象尚未存在
Dog d;
//3. 建立新的Dog對象,并将其賦予d引用變量
d = new Dog();
//4. 将引用變量的一個副本傳遞給go()方法
d.go(d);
}
//5. 将go()方法置于棧上,并将dog參數作為局部變量
void go(Dog dog){
//6. 在堆上建立新的Collar對象,并将其賦予Dog的執行個體變量
c =new Collar();
}
//7.将setName()添加到棧上,并将dogName參數作為其局部變量
void setName(String dogName){
//8. name的執行個體對象也引用String對象
name=dogName;
}
//9. 程式執行完成後,setName()将會完成并從棧中清除,此時,局部變量dogName也會消失,盡管它所引用的String仍在堆上
}      
  1. 垃圾回收機制:

(問題一:什麼叫垃圾回收機制?) 垃圾回收是一種動态存儲管理技術,它自動地釋放不再被程式引用的對象,按照特定的垃圾收集算法來實作資源自動回收的功能。當一個對象不再被引用的時候,記憶體回收它占領的空間,以便空間被後來的新對象使用,以免造成記憶體洩露。 (問題二: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();

  1. 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;"。      
  1. 如何把程式寫得更健壯: 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 優化系統記憶體環境;顯示的聲明數組空間,而且申請數量還極大。 以下是初始化不同類型的對象需要消耗的時間:

Java 記憶體管理

從表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 等常見的緩存算法

熬夜不易,點選請老王喝杯烈酒!!!!!!!