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