天天看點

JAVA 記憶體管理總結

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

  配置設定 :記憶體的配置設定是由程式完成的,程式員需要通過關鍵字new 為每個對象申請記憶體空間 (基本類型除外),所有的對象都在堆 (heap)中配置設定空間。

  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 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");

  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程式,看看他的各部分如何建立并且映射到棧和堆中:

  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仍在堆上

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;

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;//編譯期錯誤

  引用指向的對象不變:

  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;

  編寫測試方法:

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";

...

public demo {

  而非

  s = new string("initial value");

  後者每次都會調用構造器,生成新對象,性能低下且記憶體開銷大,并且沒有意義,因為string對象不可改變,是以對于内容相同的字元串,隻要一個string對象來表示就可以了。也就說,多次調用上面的構造器建立多個對象,他們的string類型屬性s都指向同一個對象。

  3、我們的程式裡不可避免大量使用字元串處理,避免使用string,應大量使用stringbuffer ,因為string被設計成不可變(immutable)類,是以它的所有對象都是不可變對象,請看下列代碼;

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

最新内容請見作者的github頁:http://qaseven.github.io/