Java對象和它的內存管理


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()方法
@Override
public 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 修飾。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM