標籤:
java記憶體管理分為兩個方面:記憶體配置和記憶體回收
不能隨意揮霍java的記憶體配置,會造成java程式的運行效率低下:
不斷分配記憶體使得系統中可用記憶體減少,從而降低程式運行效能。
大量已經分配記憶體的回收使得記憶體回收的負擔加重,降低程式的運行效能。
一、. 執行個體變數和類變數的記憶體配置
java程式的變數大致分為成員變數和局部變數。局部變數分為3類:
形參:在方法中定義的局部變數,由方法調用者負責為其賦值,隨方法的結束而消失
方法內的局部變數: 在方法內定義的局部變數,必須在方法內對其進行顯示初始化。這種變數從初始化完成後開始生效,隨方法的結束而消失。
代碼塊內的局部變數:在代碼塊中定義的局部變數,必須在代碼塊內對其進行顯示初始化。這種類型的局部變數從初始化完成後開始生效,隨代碼塊的結束而消失。
局部變數的作用時間很短,它們都被儲存在方法的棧記憶體中。
類體內定義的變數是成員變數,如果定義成員變數沒有使用static修飾,該成員變數又被稱為執行個體變數,非靜態變數;加了static的成員變數是類變數,靜態變數。
static只能修飾類裡定義的成員部分,包括成員變數、方法、內部類、初始化快、內部枚舉類。
int num1=num2-1;
int num2=3;
這會出現非法前向引用,因為第一行中需要用到num2的值來對num1進行賦值,但num2在第二行才進行賦值。
int num1=num2-1;
static int num2=3;
這樣就不會出錯,因為num1是執行個體變數,而num2是類變數,
類變數的初始化時機總是處於執行個體變數的初始化時機之前,所以正確。
使用static修飾的成員變數是類變數,屬於該類本身;沒有使用static的變數是執行個體變數,屬於該類的執行個體。由於同一個JVM內每個類只對應一個class對象,因此同一個JVM內的一個類的類變數只需一塊記憶體空間;但對於執行個體變數而言,該類每創一次執行個體,就需要為執行個體變數分配一塊記憶體空間。也就是說,程式中有幾個執行個體,執行個體變數就需要幾塊記憶體空間。
class person
{
string name;
int age;
static int eyenum;//類變數
}
public class fieldtest
{
public static void main (string [ ] args)
{
person.eyenum=2; //對類變數初始化,通過person
person p=new person();
p.name="豬八戒“;
p.age=300;
person p2=new person();
p2.name="孫悟空”;
p2.age=500;
p2.eyenum=3; }
}
java分配記憶體的情況如下:
當程式建立person的對象p時,系統不再為eyenum類變數分配記憶體空間,而只為person對象的執行個體變數執行初始化,因為執行個體變數才是屬於person執行個體,而類變數是屬於person類本身的。
當person類的eyenum類變數被修改之後,程式通過p,p2,person類訪問eyenum類變數都將輸出3.
2.執行個體變數的初始化時機
程式可以在3個地方對執行個體變數執行初始化:
定義執行個體變數時指定初始值;
非靜態初始化塊中對執行個體變數指定初始值;
構造器中對執行個體變數指定初始化;
前兩種比第三種更早執行。但第一、二種的執行順序與它們在來源程式中的排列順序相同。
class cat
{
string name;
int age;
public cat (string name, int age)
{ this.name=name;
this.age=age;
}
{ weight=2.0;}
double weight=2.3;
}
public class fieldtest
{
public static void main (string [ ] args)
{
cat cat1=new cat("kitty",2);
cat cat2=new cat("niko",3);
}
}
程式執行的時候會先執行cat中的非靜態初始化塊(因為該類中沒有類變數),再調用該cat類的構造器來初始化該cat的執行個體,weight的值是2.3而不是2。這是因為,初始化中指定初始值,定義weight時指定初始值,都屬於對該執行個體變數執行的初始化操作,它們的執行順序與它們在原始碼中的排列順序相同。所以,初始化塊中對weight所指定的初始化的值每次都將被2.3所覆蓋。
總結:定義執行個體變數時定義的初始值、初始化塊中為執行個體變數指定的初始值、構造器中為執行個體變數指定的初始值,三者的作用完全類似,都用於對執行個體變數指定初始值。經過編譯器處理之後,它們對應的賦值語句都被合并到構造器中,在合并過程中,構造器中的語句都在前面那些語句的後面,而前兩種情況的執行順序,和它們原始碼中的順序一樣。
3. 類變數的初始化時機
程式可以在2個地方對類變數進行初始化:
定義類變數時指定初始值;
靜態初始化塊中對類變數指定初始值;
程式執行的時候,首先為所有類變數分配記憶體空間,再按原始碼中的排列順序執行靜態初始化塊中所指定的初始值和定義類變數時所指定的初始值。
二、父類構造器
當建立任何java對象時,程式總是會先依次調用每個父類非靜態初始化塊、父類構造器執行初始化,最後才調用本類的非靜態初始化塊、構造器執行初始化。
class creature
{
{
System.out.println("cresature的非靜態初始化塊");
}
public creature()
{
System.out.println("cresature的無參構造器");
}
public creature(String name)
{
this();
System.out.println("cresature的帶有name參數的構造器,name參數:"+name);
}
}
class animal extends creature
{
{
System.out.println("animal的非靜態初始化塊");
}
public animal(String name)
{
super(name);
System.out.println("animal的帶參數nmae的構造器");
}
public animal(String name, int age)
{
this(name);
System.out.println("animale的帶兩個參數的構造器");
}
}
class wolf extends animal
{
{
System.out.println("wolf的非靜態初始化塊");
}
public wolf()
{
super("灰太狼",3);
System.out.println("wolf的無參的構造器");
}
public wolf(double weight)
{
this();
System.out.println("wolf的帶weight參數的構造器"+weight);
}
}
public class inittest {
public static void main(String[] args)
{
new wolf(5.6);
}
}
上面程式定義了三個類,都包含非靜態初始化、構造器成分。其執行結果如下
cresature的非靜態初始化塊
cresature的無參構造器
cresature的帶有name參數的構造器,name參數:灰太狼
animal的非靜態初始化塊
animal的帶參數nmae的構造器
animale的帶兩個參數的構造器
wolf的非靜態初始化塊
wolf的無參的構造器
wolf的帶weight參數的構造器5.6
只要在程式建立java對象的時候,系統總是先調用最頂層父類的初始化操作,包括初始化塊和構造器,然後依次向下調用所以父類的初始化操作,最後執行本類的初始化操作返回本類的執行個體。至於調用父類的那兒一個構造器初始化,分為幾種情況:
子類構造器執行體的第一行代碼使用super()顯示調用父類構造器
子類構造器執行體的第一行代碼使用this()顯示調用本類的重載構造器,系統根據this調用裡傳入的實參列表來確定本類的另一個構造器
子類中既沒有super也沒有this,則隱式調用父類中無參數構造器
super和this都只能在構造器中使用,且都必須在構造器中的第一行,一個構造器中只能使用其中一個,最多使用一次。
2.2 訪問子類對象的執行個體變數
子類的方法可以訪問父類的執行個體變數,這是因為子類繼承了父類。父類的方法不能訪問子類的執行個體變數,因為父類無法知道會被哪兒個子類繼承。但是在一些極端的情況下,可能出現父類訪問子類變數的情況。
構造器只是負責對java對象執行個體變數執行初始化,即賦值,在執行構造器代碼之前,該對象所佔的記憶體已經被分配下來,這些記憶體裡值都預設是空值。當this在構造器中時,this代表正在初始化的java對象,
當變數的編譯時間類型和運行時類型不同時,通過該變數訪問它引用的對象的執行個體變數時,該執行個體變數的值由聲明該變數的類型決定;但通過該變數調用它引用的對象的執行個體方法,該方法行為將由它實際所引用的對象來決定。
2.3調用被子類重新的方法
一般情況下,父類不能調用子類的方法,但是有一種特殊情況,但子類方法重新父類的方法之後,父類表面上只是調用自己的方法,但實際是調用了子類的方法。
class animal
{
private String desc;
public animal()
{
this.desc=getDesc();
}
public String getDesc()
{
return"animal"; //2
}
public String toString()
{
return desc;
}
}
class wolf extends animal
{
private String name;
private double weight;
public wolf(String name, double weight)
{
this.name=name; //3
this.weight=weight;
}
@Override
public String getDesc()
{
return "wolf[name="+name+",weight="+weight+"]";
}
}
public class inittest {
public static void main(String[] args)
{
System.out.println(new wolf("灰太狼",32.3));//1
}
}
上面的程式輸出的結果是wolf[name=null,weight=0.0]
子類重寫了父類的getDesc()函數,程式執行的時候,首先是執行1部分程式,也就是調用構造器來初始化wolf對象,但是會先調用父類的無參數構造器先初始化,也就是第二部分的程式,先於3被執行,父類的無參構造器中的函數被子類重新,於是調用子類的方法,於是輸出這樣的結果。執行完2部分的程式之後,會執行3部分的程式,wolf對象會對name, weight進行賦值,但是desc執行個體變數的值是wolf[name=null,weight=0.0]
為了避免這種不希望看到的結果,應該避免在animal類 的構造器中調用被子類重寫的方法,因此改為
class animal
{
private String desc;
public String getDesc()
{
return"animal"; //2
}
public String toString()
{
return getDesc();
}
}
結果就會是你期待的wolf[name=灰太狼,weight=32.3]
2.3 父子執行個體的記憶體控制
class base
{
int count=2;
public void display()
{
System.out.println(this.count);
}
}
class derived extends base
{
int count=20;
@Override
public void display()
{
System.out.println(this.count);
}
}
public class inittest {
public static void main(String[] args)
{
base b=new base();//1
System.out.println(b.count);
b.display();
derived d= new derived();
System.out.println(d.count);
d.display();
base bd=new derived();
System.out.println(bd.count);
bd.display();
base db2=d;
System.out.println(db2.count);
}
}
輸出的結果為
2
2 // 輸出結果毫無疑問,聲明一個base變數b
20
20// 也是沒有問題的
2
20// 聲明了一個base變數,但是卻將derived對象賦給變數。此時系統會自動進行向上轉型來保證程式正確。直接通過db訪問count執行個體變數,輸出的將是base(聲明時的類型)對象的count執行個體變數的值;如果通過db來調用display()方法,該方法將表現出derived(運行時的類型)對象的行為方法。
2// 此時db2和d都指向同一個對象,但是db2是base變數,所以通過它訪問執行個體變數時,顯示的是宣告類型的行為,所以是2而不是20
但不管是d 變數,還是db,db2,只要它們實際指向一個derived對象,不管聲明它們時用什麼類型,當通過這些變數調用方法時,方法總是表現出derived對象的行為,而訪問執行個體變數時,表現的是聲明變數類型的行為
如果在子類重寫了父類方法,就意味著子類裡定義的方法徹底覆蓋了父類裡的同名方法,系統將不可能把父類裡的方法轉移到子類中。對於執行個體變數則不存在這樣的現象,即使子類中有與父類相同的執行個體變數,這個執行個體變數也不會覆蓋父類中的。因此繼承成員變數和繼承方法之間存在這樣的差別。
2.3 父子類的類變數
由於類變數屬於類本身,而執行個體變數屬於對象,所以類變數不會那麼的複雜。java允許通過對象來訪問類變數,如果在子類中直接存取父類中定義的count類變數,可以直接使用父類名.count或者super.count,建議用前一種。
2.4 final修飾符
final可以修飾變數,被final修飾的變數被賦值初始值之後,不能對它重新賦值
final可以修飾方法,被final修飾的方法不能被重寫
final可以修飾類,被修飾的類不能派生子類
對於普通執行個體變數,java程式可以對它執行預設的初始化,也就是將執行個體 變數的值指定為預設的初始值0或null;但對於final執行個體變數,則必須由程式員顯示指定初始值。
只能在3個地方對final變數指定初始值;
定義final執行個體變數時指定初始值;
在非靜態初始化塊中為final執行個體變數指定初始值;
在構造器中為final執行個體變數指定的初始值。
對於final類變數而言,同樣必須顯示指定初始值,而且只能在2處初始化:
定義final類變數時
在靜態初始化塊中為final類變數賦值
對於一個final變數,不管它是類變數、執行個體變數,還是局部變數,只要定義該變數時使用了final修飾符修飾,並在定義該final類變數時指定了初始值,而且該初始值在編譯時間就確定下來,那麼這個final變數本質上已經不再是變數,而是一個直接量。
final修飾符的一個重要用途就是定義“宏變數"。如果被賦的運算式只是基本的算術運算式或字串串連運算,沒有訪問普通變數,調用方法,會將其看成宏變數處理。
但對於final執行個體變數而言只有在定義該變數時指定初始值才會有”宏變數“的效果。
public class inittest {
public static void main(String[] args)
{
String s1="賴yuppies";
String s2="賴"+"yuppies";
System.out.println(s1==s2);
String str1="賴";
String str2="yuppies";
String s3=str1+str2;
System.out.println(s1==s3);
}
}
輸出的結果為true false
s1是一個直接量,s2的值是兩個字串直接量進行串連運算,由於編譯器可以在編譯階段就確定s2的值,所以系統會讓s2直接指向字串池中緩衝中的”賴yuppies"字串。str1,str2是兩個變數,在編譯的時候,s3的值不能確定,所以是false.
可以將str1,str2修飾為final類型。
如果程式需要在匿名內部類中使用局部變數,那麼這個局部變數必須使用final修飾符修飾。
java要求所有被內部類訪問的局部變數都使用final修飾的原因:對於局部變數而言,它的範圍停留在該方法中,當執行方法結束時,該局部變數也隨之消失;但內部類則可能產生隱式的“閉包”,閉包將使得變數脫離它所在的方法繼續存在。
java 對象與記憶體