Java對象和它的記憶體管理

來源:互聯網
上載者:User

標籤:靜態   class   main   rri   sub()   nts   ext   current   ble   

java中的記憶體管理分為兩個方面:

記憶體配置:指建立java對象時JVM為該對象在堆空間中所分配的記憶體空間。

記憶體回收:指java 對象失去引用,變成垃圾時,JVM的記憶體回收機制自動清理該對象,並回收該對象所佔用的記憶體。

雖然JVM 內建了記憶體回收機制,但仍可能導致記憶體泄露、資源流失等,所以我們不能肆無忌憚的建立對象。此外,記憶體回收機制是由一個後台線程完成,也是很消耗效能的。

1.執行個體變數和類變數

java程式中的變數,大體可以分為 成員變數 和 局部變數 。其中局部變數可分為如下三類:

形參 :在方法名中定義的變數,有方法調用者負責為其賦值,隨著方法的結束而消亡。 
方法內局部變數 :在方法內定義的變數,必須在方法內對其進行初始化。它從初始化完成後開始生效,隨著方法結束而消亡。 
代碼塊內局部變數 :在代碼塊內定義的變數,必須在代碼塊內對其顯示初始化。從初始化完成後生效,隨著代碼塊的結束而消亡。 
局部變數的作用時間很短暫,他們被存在棧記憶體中。

類體內定義的變數為成員變數。如果使用 static 修飾,則為靜態變數或者類變數,否則成為非靜態變數或者執行個體變數。

static:

他的作用是將執行個體成員編程類成員。只能修飾在類裡定義的成員部分,包括變數、方法、內部內(枚舉與介面)、初始化塊。不能用於修飾外部類、局部變數、局部內部類。

使用static修飾的成員變數是類類型,屬於類本身,沒有修飾的屬於執行個體變數,屬於該類的執行個體。在同一個JVM中,每個類可以建立多個java對象。同一個JVM中每個類只對應一個Class對象,類變數只佔一塊記憶體空間,但是執行個體變數,每次建立便會分配一塊記憶體空間。

class Person{String name;int age;static int eyeNum;public void info(){ System.out.println("我的名字是:" + name  + ", 我的年齡是:" + age);}}public class FieldTest {public static void main(String[] args) {// 類變數屬於該類本身,只要該類初始化完成,    // 程式即可使用類變數。Person.eyeNum=2;System.out.println("Person的eyeNum屬性:"            +Person.eyeNum);// 通過Person類訪問eyeNum類變數//建立第一個Person對象// 通過p訪問Person類的eyeNum類變數Person p=new Person();p.name="hoo";p.age=33;System.out.println("通過p變數訪問eyeNum類變數:"            +p.eyeNum);p.info();// 建立第二個Person對象Person p2 = new Person();    p2.name = "po";    p2.age = 50;    p2.info();     p2.eyeNum=4;// 通過p2修改Person類的eyeNum類變數 // 分別通過p、p2和Person訪問Person類的eyeNum類變數    System.out.println("通過p變數訪問eyeNum類變數:"            + p.eyeNum);        System.out.println("通過p2變數訪問eyeNum類變數:"            + p2.eyeNum);        System.out.println("通過Person類訪問eyeNum類變數:"            + Person.eyeNum);}}

  代碼中的記憶體配置如下:

當Person類初始化完成,類變數也隨之初始化完成,不管再建立多少個Person對象,系統都不再為 eyeNum 分配記憶體,但會為 name 和age 分配記憶體並初始化。當eyeNum值改變後,通過每個Person對象訪問eyeNum的值都隨之改變。

1).執行個體變數的初始化

對於執行個體變數,它屬於java對象本身,每次程式建立java對象時都會為其分配記憶體空間,並初始化。

執行個體變數初始化地方:

定義執行個體化變數時; 
非靜態初始化塊中; 
構造器中。 
其中前兩種比第三種更早執行,而前兩種的執行順序與他們在程式中的排列順序相同。它們三種作用完全類似,經過編譯後都會提取到構造器中執行,且位於所有語句之前,定義變數賦值和初始化塊賦值的順序與他們在原始碼中一致。

可以使用 javap 命令查看java編譯器的機制:

用法: javap <options> <classes> 其中, 可能的選項包括: -help  --help  -?        輸出此用法訊息-version        
版本資訊-v  -verbose          
  輸出附加資訊-l                  
    輸出行號和本地變數表-public      
           僅顯示公用類和成員-protected    
          顯示受保護的/公用類和成員-package          
      顯示程式包/受保護的/公用類              
        和成員 (預設)-p  -private        
    顯示所有類和成員-c          
      對代碼進行反組譯碼-s                
      輸出內部類型簽名-sysinfo          
      顯示正在處理的類的                  
            系統資訊 (路徑, 大小, 日期, MD5 散列)-constants            
  顯示最終常量-classpath <path>  
     指定尋找使用者類檔案的位置-cp <path>          
    指定尋找使用者類檔案的位置-bootclasspath <path>  
 覆蓋引導類檔案的位置

2).類變數的初始化

類變數屬於java 類本身,每次運行時才會初始化。

類變數的初始化地方:

定義類變數時初始化; 
靜態代碼塊中初始化 
如下代碼,表面上看輸出的是:17.2,17.2;但是實際上輸出的是:-2.8,17.2

class Price{// 類成員是Price執行個體final static Price INSTANCE=new Price(2.8);// 再定義一個類變數。static double initPrice=20;// 定義該Price的currentPrice執行個體變數double currenPrice;public Price(double discount){// 根據靜態變數計算執行個體變數currenPrice=initPrice-discount;}}public class FieldTest {public static void main(String[] args) {// 通過Price的INSTANCE訪問currentPrice執行個體變數System.out.println(Price.INSTANCE.currenPrice);Price p=new Price(2.8);// 顯式建立Price執行個體System.out.println(p.currenPrice);// 通過先前建立的Price執行個體訪問currentPrice執行個體變數}}

  

第一次使用Price 時,程式對其進行初始化,可分為兩個階段:

(1)系統為類變數分配記憶體空間;

(2)按初始化代碼順序對變數進行初始化。

這裡的運行結果為:-2.8,17.2

說明:初始化第一階段,系統先為 INSTANCE,initPrice兩個類變數分配記憶體空間,他們的預設值為null和0.0,接著第二階段依次為他們賦值。對 INSTANCE 賦值時要調用 Price(2.8),建立Price執行個體,為currentPrice賦值,此時,還未對 initPrice 賦值,就是用他的預設值0,則 currentPrice 值為-2.8,接著程式再次將 initPrice 賦值為20,但對於 currentPrice 執行個體變數已經不起作用了。

2.父類構造器

java中,建立對象時,首先會依次調用每個父類的非靜態初始化塊、構造器(總是先從Object開始),然後再使用本類的非靜態初始化塊和構造器進行初始化。在調用父類時可以用 super 進行 顯示調用 ,也可以 隱式調用 。

在子類調用父類構造器時,有以下幾種情境:

子類構造器第一行代碼是用 super() 進行顯示調用父類構造器,則根據super傳入的參數調用相應的構造器; 
子類構造器第一行代碼是用 this() 進行顯示調用本類中重載的構造器,則根據傳入this的參數調用相應的構造器; 
子類構造器中沒有this和super,則在執行子類構造器前,隱式調用父類無參構造器。 
註:super和this都是顯示調用構造器,只能在構造器中使用,且必須在第一行,只能使用它們其中之一,最多隻能調用一次。

一般情況下,子類對象可以訪問父類的執行個體變數,但父類不能訪問子類的,因為父類不知道它會被哪個子類繼承,子類又會添加怎樣的方法。但在極端的情況下,父類可以訪問子類變數的情況,如下執行個體代碼:

class Base{private int i=2;public Base() {//this:運行時是Driver類型,編譯時間是Base 類型,這裡是Driver對象this.display();}public void display(){System.out.println(i);}}//繼承Base的Derived子類class Derived extends Base{private int i=22;public Derived() {i=222;}public void display(){System.out.println(i);}}public class FieldTest {public static void main(String[] args) {// 建立Derived的構造器建立執行個體new Derived();}}

  

上面的代碼執行後,輸出的並不是2、22或者222,而是 0 。在調用Derived 的構造器前會隱式調用Base的無參構造器,初始化 i= 2,此時如果輸出 this.i 則為2,它訪問的是Base 類中的執行個體變數,但是當調用 this.display() 時,表現的為Driver對象的行為,對於driver對象,它的變數i還未賦初始值,僅僅是為其開闢了記憶體空間,其值為0。

在java 中,構造器負責執行個體變數的初始化(即,賦初始值),在執行構造器前,該對象記憶體空間已經被分配了,他們在記憶體中存的事其類型所對應的預設值。

在上面的代碼中,出現了變數的編譯時間類型與運行時類型不同。通過該變數訪問他所引用的對象的執行個體變數時,該執行個體變數的值由申明該變數的類型決定的,當通過該變數調用它所引用的執行個體對象的執行個體方法時,該方法將由它實際所引用的對象來決定

當子類重寫父類方法時,也會出現父類調用之類方法的情形,如下具體代碼,通過上面的則很容易理解。

class Animal{private String desc;public Animal() {this.desc=getDesc();}public String getDesc() {return "Animal";}public String toString() {return  desc ;}}public class Wolf extends Animal{private String name;private double weight;public Wolf(String name, double weight) {this.name = name;this.weight = weight;}//重寫父類的getDesc()方法@Overridepublic String getDesc() {return "Wolf[name=" + name + " , weight="            + weight + "]";  //輸出:Wolf[name=null , weight=0.0]}public static void main(String[] args) {System.out.println(new Wolf("狼", 2.9));}}

  

3.父子執行個體的記憶體控制

java中的繼承,在處理成員變數和方法時是不同的。如果子類重寫了父類的方法,則完全覆蓋父類的方法,並將其其移到子類中,但如果是完全同名的執行個體變數,則不會覆蓋,不會從父類中移到子類中。所以,對於一個參考型別的變數,如果訪問他所引用對象的執行個體變數時,該執行個體變數的值取決於申明該變數的類型,而調用方法時,則取決於它實際引用對象的類型。

在繼承中,記憶體中子類執行個體儲存有父類的變數的執行個體。

class Base{int count=2;}class Mid extends Base{int count=20;}public class Sub extends Mid{int count = 200; public static void main(String[] args) {    // 建立一個Sub對象Sub s=new Sub();// 將Sub對象向上轉型後賦為Mid、Base類型的變數Mid s2m=s;Base s2b=s;// 分別通過3個變數來訪問count執行個體變數System.out.println(s.count);    //輸出:200        System.out.println(s2m.count);    //輸出:20        System.out.println(s2b.count);    //輸出:2}}

  記憶體中的:

在記憶體中只有一個Sub對象,並沒有Mid和Base對象,但存在3個count的執行個體變數。

子類中會隱藏父類的變數可以通過super來擷取,對於類變數,也可以通過super來訪問。

4.final 修飾符

final 的修飾範圍:

修飾變數,被賦初始值後不可重新賦值; 
修飾方法 ,不能被重寫; 
修飾類,不能派生出子類。 
對於final 類型的變數,初始化可以在:定義時、非靜態代碼塊和構造器中;對於final 類型的類變數,初始化可以在:定義時和靜態代碼塊中。

當final類型的變數定義時就指定初始值,那麼該該變數本質上是一個“宏變數”,編譯器會把用到該變數的地方直接用其值替換。

如果在內部內中使用局部變數,必須將其指定為final類型的。普通的變數範圍就是該方法,隨著方法的執行結束,局部變數也隨之消失,但內部類可能產生隱式的“閉包”,使局部變數脫離它所在的方法繼續存在。內部內可能擴大局部變數的範圍,如果內部內中訪問的局部變數沒有適用final修飾,則可以隨意修改它的值,這樣將會引起混亂,所以編譯器要求被內部訪問的局部變數必須使用final 修飾。

 

Java對象和它的記憶體管理

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.