Java例項化物件過程中的記憶體分配
問題引入
這裡先定義一個很不標準
的“書”類,這裡為了方便演示就不對類的屬性進行封裝了。
class Book{
String name; //書名
double price; //價格
public void getInfo(){
System.out.println("name:"+name+";price:"+price);
}
}
在這個類中定義了兩個屬性和一個方法,當然也是可以定義多和類和多個方法的。 類現在雖然已經定義好了,但是一個類要使用它必須要例項化物件,那麼物件的定義格式有一下兩種格式:
//宣告並例項化物件: 類名稱 物件名稱 = new 類名稱()
Book book = new Book();
//分步完成宣告和例項操作:
// |- 宣告物件: 類名稱 物件名稱 = null;
Book book = null;
// |- 例項化物件: 物件名稱 = new 類名稱();
book = new Book();
物件屬於引用資料型別,其和基本資料型別最大的不同在於引用資料型別需要進行記憶體分配,而關鍵字new
主要的功能就是開闢記憶體空間,也就是說只要是使用引用資料型別就必須使用關鍵字new
來開闢空間。有些時候我們需要對物件屬性進行操作,那麼其中的堆疊記憶體空間又是如何分配的呢?接下來我們來分析一下其中的過程。
堆記憶體與棧記憶體
如果想對物件操作的過程進行記憶體分析,首先要了解兩塊記憶體空間的概念:
- 堆記憶體:儲存每一個物件的屬性內容,堆記憶體需要用關鍵字new才能開闢。
- 棧記憶體:儲存的是一塊堆記憶體的地址。
堆記憶體很好理解,可能有人會有疑問為什麼會有棧記憶體,舉個例子,好比學校有很多教室,每個教室有一個門牌號,教室內放了很多的桌椅等等,這個編號就好比地址,老師叫小明去一個教室拿東西,老師必須把房間號告訴小明才能拿到,也就是為什麼地址必須存放在一個地方,而這個地方在計算機中就是棧記憶體。
物件空屬性
我們先例項化一個物件,並對其的屬性不設定任何值
public class Test{
public static void main(String args[]){
Book book = new Book();
book.getInfo();
}
}
執行結果如下:
name:null;price:0.0
其記憶體變化圖如下:
使用關鍵字new就在棧記憶體中開闢一個空間存放book物件,並且指向堆記憶體的一個空間,此時並未對其賦值,所以始終指向預設的堆記憶體空間。
操作物件屬性
我們先宣告並例項化Book類,並對例項出的book物件操作其屬性內容。
public class Test{
public static void main(String args[]){
Book book = new Book();
book.name = "深入理解JVM";
book.price = 99.8;
book.getInfo();
}
}
編譯執行後的結果如下:
name:深入理解JVM;price:99.8
記憶體變化圖如下:
分步例項化物件
示例程式碼如下:
public class Test{
public static void main(String args[]){
Book book = null; //宣告物件
book = new Book(); //例項化物件
book.name = "深入理解JVM";
book.price = 99.8;
book.getInfo();
}
}
很明顯結果肯定和前面一樣
name:深入理解JVM;price:99.8
表面沒什麼區別,但是記憶體分配過程卻不一樣,接下來我們來分析一下
任何情況下只要使用了new就一定要開闢新的堆記憶體空間,一旦堆記憶體空間開闢了,裡面就一定會所有類中定義的屬性內容,此時所有的屬性內容都是其對應資料型別的預設值。
直觀的說就是棧記憶體先要指向一個null,然後等待開闢新的棧記憶體空間後才能指向其屬性內容。
NullPointerException的出現
那麼如果使用了沒有例項化的物件,就會出現最常見也是最讓人頭疼的一個異常NullPointerException
,像下面的程式碼
public class Test{
public static void main(String args[]){
Book book = null;
// book = new Book(); //例項化的這一步被註釋
book.name = "深入理解JVM";
book.price = 99.8;
book.getInfo();
}
}
在編譯的過程是不會出錯的,因為只有語法錯誤才會在編譯時中斷,而這種邏輯性錯誤能成功編譯,但是執行的時候卻會丟擲NullPointerException異常。 執行結果:
Exception in thread "main" java.lang.NullPointerException at language.Test.main(Test.java:19)
空指標異常是平時遇到最多的一類異常,只要是引用資料型別都有可能出現它。這種異常的出現也是很容易理解的,猶如你說今天被一隻恐龍追著跑,恐龍早就在幾個世紀前就滅絕了,現實生活中不可能存在,當然人們就會認為你說的這句話是謊言。在程式中也一樣,沒有被例項化的物件直接呼叫其中的屬性或者方法,肯定會報錯。
引用資料分析
引用是整個java中的核心精髓,引用類似於C++中的指標概念,但是又比指標的概念更加簡單。
舉個簡單的例子,比如李華的小名叫小華,一天李華因為生病向老師請假了,老師問今天誰請假了,說李華請假了和小華請假了都是一個意思,小華是李華的別名,他們兩個都是對應一個個體。
如果程式碼裡面宣告兩個物件,並且使用了關鍵字new
為兩個物件分別進行了物件的例項化操作,那麼一定是各自佔用各自的堆記憶體空間,並且不會互相影響。
例如:宣告兩個物件
public class Test{
public static void main(String args[]){
Book bookA = new Book();
Book bookB = new Book();
bookA.name = "深入理解JVM";
bookA.price = 99.8;
bookA.getInfo();
bookB.name = "Java多執行緒";
bookB.price = 69.8;
bookB.getInfo();
}
}
執行結果如下:
name:深入理解JVM;price:99.8
name:Java多執行緒;price:69.8
我們來分析一下記憶體的變化
接下來我們看看那物件引用傳遞
例如:物件引用傳遞
public class Test{
public static void main(String args[]){
Book bookA = new Book(); //宣告並例項化物件
Book bookB = null; //宣告物件
bookA.name = "深入理解JVM";
bookA.price = 99.8;
bookB = bookA; //引用傳遞
bookB.price = 69.8;
bookA.getInfo();
}
}
執行結果如下:
name:深入理解JVM;price:69.8
嚴格來講bookA和bookB裡面儲存的是物件的地址資訊,所以以上的引用過程就屬於將bookA的地址賦給了bookB,此時兩個物件指向的是同一塊堆記憶體空間,因此任何一個物件修改了堆記憶體之後都會影響其他物件。
一塊堆記憶體可以同時被多個棧記憶體所指向,但是反過來,一塊棧記憶體只能儲存一塊堆記憶體空間的地址。
垃圾的產生
先看如下程式碼:
public class Test{
public static void main(String args[]){
Book bookA = new Book(); //宣告並例項化物件
Book bookB = new Book(); //宣告並例項化物件
bookA.name = "深入理解JVM";
bookA.price = 99.8;
bookB.name = "Java多執行緒";
bookB.price = 69.8;
bookB = bookA; //引用關係
bookB.price = 120.8;
bookA.getInfo();
}
}
執行結果如下:
name:深入理解JVM;price:120.8
整個過程記憶體又發生了什麼變化呢?我們來看一下 在此過程中原來bookB所指向的堆記憶體無棧記憶體指向,一塊沒有任何棧記憶體指向的堆記憶體空間就將成為垃圾,等待被java中的回收機制回收,回收之後會釋放掉其佔用的空間。
雖然在java中支援了自動的垃圾收集處理,但是在程式碼的編寫過程中應該儘量減少垃圾空間的產生。