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 修飾。
