java中的內存管理分爲兩個方面:
雖然JVM 內置了垃圾回收機制,但仍可能導致內存泄露、資源泄露等,所以我們不能肆無忌憚的創建對象。此外,垃圾回收機制是由一個後臺線程完成,也是很消耗性能的。
1.實例變量和類變量
java程序中的變量,大體可以分爲成員變量和局部變量。其中局部變量可分爲如下三類:
- 形參:在方法名中定義的變量,有方法調用者負責爲其賦值,隨着方法的結束而消亡。
- 方法內局部變量:在方法內定義的變量,必須在方法內對其進行初始化。它從初始化完成後開始生效,隨着方法結束而消亡。
- 代碼塊內局部變量:在代碼塊內定義的變量,必須在代碼塊內對其顯示初始化。從初始化完成後生效,隨着代碼塊的結束而消亡。
局部變量的作用時間很短暫,他們被存在棧內存中。
類體內定義的變量爲成員變量。如果使用static
修飾,則爲靜態變量或者類變量,否則成爲非靜態變量或者實例變量。
static:
他的作用是將實例成員編程類成員。只能修飾在類裏定義的成員部分,包括變量、方法、內部內(枚舉與接口)、初始化塊。不能用於修飾外部類、局部變量、局部內部類。
使用static修飾的成員變量是類類型,屬於類本身,沒有修飾的屬於實例變量,屬於該類的實例。在同一個JVM中,每個類可以創建多個java對象。同一個JVM中每個類只對應一個Class對象,機類變量只佔一塊內存空間,但是實例變量,每次創建便會分配一塊內存空間。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
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; //① // 通過Person類訪問eyeNum類變量 System.out.println("Person的eyeNum屬性:" + Person.eyeNum); // 創建第一個Person對象 Person p = new Person(); p.name = "豬八戒"; p.age = 300; // 通過p訪問Person類的eyeNum類變量 System.out.println("通過p變量訪問eyeNum類變量:" + p.eyeNum); //② p.info(); // 創建第二個Person對象 Person p2 = new Person(); p2.name = "孫悟空"; p2.age = 500; p2.info(); // 通過p2修改Person類的eyeNum類變量 p2.eyeNum = 3; //③ // 分別通過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的值都隨之改變。
a.實例變量的初始化
對於實例變量,它屬於java對象本身,每次程序創建java對象時都會爲其分配內存空間,並初始化。
實例變量初始化地方:
- 定義實例化變量時;
- 非靜態初始化塊中;
- 構造器中。
其中前兩種比第三種更早執行,而前兩種的執行順序與他們在程序中的排列順序相同。它們三種作用完全類似,經過編譯後都會提取到構造器中執行,且位於所有語句之前,定義變量賦值和初始化塊賦值的順序與他們在源代碼中一致。
可以使用 javap
命令查看java編譯器的機制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
用法: 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> 覆蓋引導類文件的位置
|
b.類變量的初始化
類變量屬於java 類本身,每次運行時纔會初始化。
類變量的初始化地方:
- 定義類變量時初始化;
- 靜態代碼塊中初始化
如下代碼,表面上看輸出的是:17.2,17.2;但是實際上輸出的是:-2.8,17.2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
class Price { // 類成員是Price實例 final static Price INSTANCE = new Price(2.8); // 在定義一個類變量。 static double initPrice = 20; // 定義該Price的currentPrice實例變量 double currentPrice; public Price(double discount) { // 根據靜態變量計算實例變量 currentPrice = initPrice - discount; } } public class PriceTest { public static void main(String[] args) { // 通過Price的INSTANCE訪問currentPrice實例變量 System.out.println(Price.INSTANCE.currentPrice);//輸出:-2.8 // 顯式創建Price實例 Price p = new Price(2.8); // 通過先是創建的Price實例訪問currentPrice實例變量 System.out.println(p.currentPrice); //輸出:17.2 } }
|
第一次使用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 實例變量已經不起作用了。
以下爲在ide中的debug結果截圖:
2.父類構造器
java中,創建對象時,首先會依次調用每個父類的非靜態初始化塊、構造器(總是先從Object開始),然後再使用本類的非靜態初始化塊和構造器進行初始化。在調用父類時可以用super
進行顯示調用,也可以隱式調用。
在子類調用父類構造器時,有以下幾種場景:
- 子類構造器第一行代碼是用super()進行顯示調用父類構造器,則根據super傳入的參數調用相應的構造器;
- 子類構造器第一行代碼是用this()進行顯示調用本類中重載的構造器,則根據傳入this的參數調用相應的構造器;
- 之類構造器中沒有this和super,則在執行子類構造器前,隱式調用父類無參構造器。
注:super和this都是顯示調用構造器,只能在構造器中使用,且必須在第一行,只能使用它們其中之一,最多隻能調用一次。
一般情況下,子類對象可以訪問父類的實例變量,但父類不能訪問子類的,因爲父類不知道它會被哪個子類繼承,子類又會添加怎樣的方法。但在極端的情況下,父類可以訪問子類變量的情況,如下實例代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
package cn.imtianx.p02; class Base { private int i = 2; public Base() { this.display();//this:運行時是Driver類型,編譯時是Base 類型,這裏是Driver對象 } 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 Test { 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 中,構造器負責實例變量的初始化(即,賦初始值),在執行構造器前,該對象內存空間已經被分配了,他們在內存中存的事其類型所對應的默認值。
在上面的代碼中,出現了變量的編譯時類型與運行時類型不同。通過該變量訪問他所引用的對象的實例變量時,該實例變量的值由申明該變量的類型決定的,當通過該變量調用它所引用的實例對象的實例方法時,該方法將由它實際所引用的對象來決定
當子類重寫父類方法時,也會出現父類調用之類方法的情形,如下具體代碼,通過上面的則很容易理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
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("灰太狼" , 32.3)); } }
|
3.父子實例的內存控制
java中的繼承,在處理成員變量和方法時是不同的。如果之類重寫了父類的方法,則完全覆蓋父類的方法,並將其其移到子類中,但如果是完全同名的實例變量,則不會覆蓋,不會從父類中移到子類中。所以,對於一個引用類型的變量,如果訪問他所引用對象的實例變量時,該實例變量的值取決於申明該變量的類型,而調用方法時,則取決於它實際引用對象的類型。
在繼承中,內存中子類實例保存有父類的變量的實例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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 修飾。