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定義如下:

  1. publicclassRectangle{
  2. double width;
  3. double height;
  4. publicRectangle(double w,double h){
  5. w = width;
  6. h = height;
  7. }
  8. }

(1)宣告物件時的記憶體模型
用Rectangle rect;宣告一個物件rect時,將在棧記憶體為物件的引用變數rect分配記憶體空間,但Rectangle的值為空,稱rect是一個空物件。空物件不能使用,因為它還沒有引用任何”實體”。
(2)物件例項化時的記憶體模型
當執行rect=new Rectangle(3,5);時,會做兩件事:在堆記憶體中為類的成員變數width,height分配記憶體,並將其初始化為各資料型別的預設值;接著進行顯式初始化(類定義時的初始化值);最後呼叫構造方法,為成員變數賦值。返回堆記憶體中物件的引用(相當於首地址)給引用變數rect,以後就可以通過rect來引用堆記憶體中的物件了。

c)建立多個不同的物件例項

一個類通過使用new運算子可以建立多個不同的物件例項,這些物件例項將在堆中被分配不同的記憶體空間,改變其中一個物件的狀態不會影響其他物件的狀態。例如:

  1. Rectangle r1=newRectangle(3,5);
  2. Rectangle r2=newRectangle(4,6);

此時,將在堆記憶體中分別為兩個物件的成員變數 width 、 height 分配記憶體空間,兩個物件在堆記憶體中佔據的空間是互不相同的。如果有:

  1. Rectangle r1=newRectangle(3,5);
  2. 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”;這種
合下,其字串值卻是儲存了一個指向存在棧中資料的引用。
為了更好地說明這個問題,我們可以通過以下的幾個程式碼進行驗證。

  1. String str1="abc"
  2. String str2="abc"
  3. System.out.println(s1==s2);//true

注意,這裡並不用 str1.equals(str2);的方式,因為這將比較兩個字串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個物件時才返回真值。而我們在這裡要看的是,str1與str2是否都指向了同一個物件。
我們再接著看以下的程式碼。

  1. String str1=newString("abc");
  2. String str2="abc"
  3. 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屬性在類載入,也就是第一次用到這個類的時候初始化,對於後來的例項的建立,不再次進行初始化。

我們常可看到類似以下的例子來說明這個問題:

  1. classStudent{
  2. staticint numberOfStudents =0;
  3. Student()
  4. {
  5. numberOfStudents ++;
  6. }
  7. }

每一次建立一個新的Student例項時,成員numberOfStudents都會不斷的遞增,並且所有的Student例項都訪問同一個numberOfStudents變數,實際上intnumberOfStudents變數在記憶體中只儲存在一個位置上。

5.java的記憶體管理例項

Java程式的多個部分(方法,變數,物件)駐留在記憶體中以下兩個位置:即堆和棧,現在我們只關心三類事物:例項變數,區域性變數和物件:
例項變數和物件駐留在堆上
區域性變數駐留在棧上
讓我們檢視一個 java 程式,看看他的各部分如何建立並且對映到棧和堆中:

  1. publicclassDog{
  2. Collar c;
  3. String name;
  4. //1.main()方法位於棧上
  5. publicstaticvoid main(String[] args){
  6. //2.在棧上建立引用變數d,但Dog物件尚未存在
  7. Dog d;
  8. //3.建立新的Dog物件,並將其賦予d引用變數
  9. d =newDog();
  10. //4.將引用變數的一個副本傳遞給go()方法
  11. d.go(d);
  12. }
  13. //5.將go()方法置於棧上,並將dog引數作為區域性變數
  14. void go(Dog dog){
  15. //6.在堆上建立新的Collar物件,並將其賦予Dog的例項變數
  16. c =newCollar();
  17. }
  18. //7.將setName()新增到棧上,並將dogName引數作為其區域性變數
  19. void setName(String dogName){
  20. //8.name的例項物件也引用String物件
  21. name =dogName;
  22. }
  23. //9.程式執行完成後,setName()將會完成並從棧中清除,此時,區域性變數dogName也會消失,儘管它所引用的String仍在堆上
  24. }

6. 垃圾回收機制

問題一:什麼叫垃圾回收機制?
垃圾回收是一種動態儲存管理技術,它自動地釋放不再被程式引用的物件,按照特定的垃圾收集演算法來實現資源自動回收的功能。當一個物件不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新物件使用,以免造成記憶體洩露。

問題二:java的垃圾回收有什麼特點?
jAVA語言不允許程式設計師直接控制記憶體空間的使用。記憶體空間的分配和回收都是由JRE負責在後臺自動進行的,尤其是無用記憶體空間的回收操作(garbagecollection,也稱垃圾回收),只能由執行環境提供的一個超級執行緒進行監測和控制。

問題三:垃圾回收器什麼時候會執行?
一般是在CPU空閒或空間不足時自動進行垃圾回收,而程式設計師無法精確控制垃圾回收的時機和順序等。、

問題四:什麼樣的物件符合垃圾回收條件?
當沒有任何獲得執行緒能訪問一個物件時,該物件就符合垃圾回收條件。

問題五:垃圾回收器是怎樣工作的?
垃圾回收器如發現一個物件不能被任何活執行緒訪問時,他將認為該物件符合刪除條件,就將其加入回收佇列,但不是立即銷燬物件,何時銷燬並釋放記憶體是無法預知的。垃圾回收不能強制執行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執行垃圾回收,而不是要求,虛擬機器會盡其所能滿足請求,但是不能保證JVM從記憶體中刪除所有不用的物件。

問題六:一個java程式能夠耗盡記憶體嗎?
可以。垃圾收集系統嘗試在物件不被使用時把他們從記憶體中刪除。然而,如果保持太多活的物件,系統則可能會耗盡記憶體。垃圾回收器不能保證有足夠的記憶體,只能保證可用記憶體儘可能的得到高效的管理。

問題七:如何顯示的使物件符合垃圾回收條件?
(1)空引用:當物件沒有對他可到達引用時,他就符合垃圾回收的條件。也就是說如果沒有對他的引用,刪除物件的引用就可以達到目的,因此我們可以把引用變數設定為null,來符合垃圾回收的條件。

  1. StringBuffer sb =newStringBuffer("hello");
  2. System.out.println(sb);
  3. sb=null;

(2)重新為引用變數賦值:可以通過設定引用變數引用另一個物件來解除該引用變數與一個物件間的引用關係。
StringBuffer sb1 = new StringBuffer(“hello”);
StringBuffer sb2 = new StringBuffer(“goodbye”);
System.out.println(sb1);
sb1=sb2;//此時”hello”符合回收條件
(3)方法內建立的物件:所建立的區域性變數僅在該方法的作用期間記憶體在。一旦該方法返回,在這個方法內建立的物件就符合垃圾收集條件。有一種明顯的例外情況,就是方法的返回物件。

  1. publicstaticvoid main(String[] args){
  2. Date d = getDate();
  3. System.out.println("d="+d);
  4. }
  5. privatestaticDate getDate(){
  6. Date d2 =newDate();
  7. StringBuffer now =newStringBuffer(d2.toString());
  8. System.out.println(now);
  9. return d2;
  10. }

(4)隔離引用:這種情況中,被回收的物件仍具有引用,這種情況稱作隔離島。若存在這兩個例項,他們互相引用,並且這兩個物件的所有其他引用都刪除,其他任何執行緒無法訪問這兩個物件中的任意一個。也可以符合垃圾回收條件。

  1. publicclassIsland{
  2. Island i;
  3. publicstaticvoid main(String[] args){
  4. Island i2 =newIsland();
  5. Island i3 =newIsland();
  6. Island i4 =newIsland();
  7. i2. i =i3;
  8. i3. i =i4;
  9. i4. i =i2;
  10. i2=null;
  11. i3=null;
  12. i4=null;
  13. }
  14. }

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

7.final問題

final使得被修飾的變數”不變”,但是由於物件型變數的本質是”引用”,使得”不變”也有了兩種含義:引用本身的不變和引用指向的物件不變。
引用本身的不變:

  1. finalStringBuffer a=newStringBuffer("immutable");
  2. finalStringBuffer b=newStringBuffer("not immutable");
  3. a=b;//編譯期錯誤
  4. finalStringBuffer a=newStringBuffer("immutable");
  5. finalStringBuffer b=newStringBuffer("not immutable");

a=b;//編譯期錯誤

引用指向的物件不變:

  1. finalStringBuffer a=newStringBuffer("immutable");
  2. a.append"broken!");//編譯通過
  3. finalStringBuffer a=newStringBuffer("immutable");
  4. a.append("broken!");//編譯通過

可見,final只對引用的”值”(也即它所指向的那個物件的記憶體地址)有效,它迫使引用只能指向初始指向的那個物件,改變它的指向會導致編譯期錯誤。至於它所指向的物件的變化,final是不負責的。這很類似==操作符:==操作符只負責引用的”值”相等,至於這個地址所指向的物件內容是否相等,==操作符是不管的。在舉一個例子:

  1. publicclassName{
  2. privateString firstname;
  3. privateString lastname;
  4. publicString getFirstname(){
  5. return firstname;
  6. }
  7. publicvoid setFirstname(String firstname){
  8. this.firstname = firstname;
  9. }
  10. publicString getLastname(){
  11. return lastname;
  12. }
  13. publicvoid setLastname(String lastname){
  14. this.lastname = lastname;
  15. }
  16. }
  17. publicclassName{
  18. privateString firstname;
  19. privateString lastname;
  20. publicString getFirstname(){
  21. 相關文章