編程易筋經:內存解析之實例講解

一、引言


        作爲一名優秀的編程人員我們不能會敲代碼,我們還必須要懂得那些代碼在計算機中是如何一步步執行的。只有這樣我們才能寫出經久不衰,經得起考驗,更優秀的代碼,開發出更好的軟件。我們都知道任何代碼的執行都是在內存中進行的,其實也就是對內存單元的操作。不論任何語言,對內存的操作都是它們的核心,最根本的東西,所以這是我們學通任何一門語言必須掌握的東西。曾有人對內存解析在編程語言學習中的重要性,做過這樣一個比喻:內存解析就好比武功祕籍中的易筋經,只要學會了它,再去學習任何武功,都會變的輕而易舉。由此可見內存解析在編程語言中的重要性。下面我們來詳細的解析一下。


       要想學懂內存解析,我們得先來看看計算機中的內存結構佈局。一張圖勝過千言萬語,我們先來一張圖Look,Look。





           這是一個Java程序代碼在計算機內存中的完整的執行過程。從圖中我們可以看到,計算機的內存分爲四個區域,分別是堆(heap)區棧(stack)區數據區(data segment)代碼區(code segment)。具體在Java語言中它們都是用來幹嘛的呢?請仔細看這幅圖。



二、內存塊劃分


1. 堆


         用來存放new出來的東西,如 new出來的類對象,數組,會放到這裏。在堆中產生了一個數組或對象後,還可以在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。引用變量就相當於是爲數組或對象起的一個別名,以後就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象。引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外後被釋放。而數組和對象本身在堆中分配,即使程序運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身佔據的內存不會被釋放。數組和對象在沒有引用變量指向它的時候,就會變爲垃圾,不能再被使用,但仍然佔據內存空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。這也是 Java 比較佔內存的原因。


2. 棧 


       用來存放基本類型的數據和對象的引用,但對象本身不存放在棧中,而是存放在堆中。比如,存放局部變量,方法的形參,臨時的返回值等。當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當該變量退出該作用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間可以立即被另作他用。 


3.數據區


     用來存放靜態變量,常量,字符串等。


4. 代碼區


    當然用來存放程序代碼的了,比如調用對象的方法體時,必須得到這個區域來調用。


       分析到這裏相信大家,都已經明白了。而對於這張圖的程序執行過程相信大家都能看得懂,在這裏就不贅述了。下面我們通過舉幾個具體的實例來詳細解析。



三、舉例說明


1. 簡單變量初始化


public class Test {
	public static void main(String []are) {

			int a = 5;
			double b = 5.0;
			String c = "內存分析";
						
			System.out.println(a + b);
			System.out.println(c);
		
							
	}
}

   我們來詳細的分析一下這個小程序在內存中的執行情況。分析任何程序,我們必須先從Main方法開始分析。詳細過程見下圖 :


程序執行內存解析圖 :



解析:先從Main方法開始分析)

    首先main方法在棧中建立一int類型內存單元 a 存放5,再建立一double型的內存單元b存放5.0,再建立一引用型內存單元c,在data segment區建立一字符串對象存放“內存分析”,然後c指向該字符串對象,再然後在棧中建立一臨時內存單元來存放a+b的和,最後分別輸出到屏幕。mian方法執行完,java的垃圾回收器把內存清理乾淨。


2. 類的實例化過程


class Point {
	double x;
	double y;
	double z;
	
	public Point(double _x, double _y, double _z) {
		x = _x;
		y = _y;
		z = _z;
	}
	
	public void setX(double _x) {
		x = _x;
	}
	
	public void setY(double _y) {
		y = _y;
	}
	
	public void setZ(double _z) {
		z = _z;
	}
	
	public double getDistance(Point p) {
		return (p.x-x)*(p.x-x) + (p.y-y)*(p.y-y) + (p.z-z)*(p.z-z);
	}
}

public class TestPoint {
	public static void main(String []arge) {
		Point p = new Point(1.0,2.0,3.0);
		Point p1 = new Point(0.0,0.0,0.0);
		System.out.println(p.getDistance(p1));
		
	}
}



程序執行內存解析圖 :



解析:同樣先從Main方法開始分析


1. 在棧中創建一Point類型引用變量 P 

2. 在堆中創建一Point類型的對象(創建對象的過程實質是調用類的構造方法的過程)

a. 在棧中分別創建Point構造方法的形參變量_x,_y,_z 並把實參傳過來的值放入相應的形參內存單元中。

b. 在堆中創建一Point類型對象框架。

c. 初始化對象,把_x,_y,_z 的值分別拷貝到相應的對象成員變量x,y,z 的內存單元中。(構造方法調用完成_x,_y,_z這三個變量的內存    單元被釋放。)

3. 引用變量P指向該對象。(當main方法執行完畢,p變量的內存單元被釋放,指向的對象就會變爲垃圾,由Java垃圾回收器回收。)


 創建P1對象同理.......



3. 方法調用


abstract class Person {
	private String name;
	
	Person(String name) {
		this.name = name;
	}
	abstract public void sleep();
	public String getName() {return name;}
}

class Student extends Person {
	private int sid;
	
	Student(String name,int sid) {
		super(name);
		this.sid = sid;
	}
	public void sleep() {
		System.out.println(this.getName() + " is sleeping");
	}
	public void study() {
		System.out.println(this.getName() + " is studying");
	}
	public void sing() {
		System.out.println(this.getName() + " is singing");
	}
	
}

public class Test {
	public static void main(String []are) {
		Person p = new Student("小美",123456);
		p.sleep();
		Student s =(Student)p;
		s.sing(); 
		s.study();
		
	}
}



程序執行內存解析圖 :



解析:同樣先從Main方法開始分析


1. 在棧中創建一Person類型引用變量 P 。


2. 在堆中創建一Student類型的對象(創建對象的過程實質是調用類的構造方法的過程,在這裏Student類繼承了Person類,經過了2次調用構造方法)

a.在棧中分別創建Student類構造方法的形參變量namesid並把實參傳過來的值放入相應的形參內存單元中。

  因爲形參name是一個字符串類型,給它賦的值是一字符串常量,和基本類型變量賦值有點不同,不能把小美這個字符串常量直接放到name內存單元中。而是把小美這個常量放到數據區(datasegment)中,讓name變量指向它,即name變量中存放的是小美這個字符串常量的地址,這時name就成了"小美"這個字符串常量的引用。

b.在堆中創建一Student類型對象框架。

  因爲Student類繼承了Person類,所以在Student類實例化的對象中包含了一個Person類的對象,如上圖所示。

c.初始化對象,首先把形參name指向的"小美"字符串常量的地址拷貝給Student對象的成員變量name(實質是拷貝給Person類對象的成員變量name,即指向"小美"字符串常量,然後把形參sid的值拷貝到Student類對象成員變量sid的內存單元中。(對象初始化完畢,形參name和sid的內存單元被釋放)


3. 引用變量P指向Student類對象(實質是指向Student類對象中的Person對象)。

因爲變量PPerson類型的引用,所以它能看到的只是Student類對象中的Person類對象部分,其他部分它是看不到的,無權限操作。所以,Student子類所特有的方法sing()和study(),P變量是無權訪問的,必須把P對象向下轉型爲Student類型,纔可以訪問sing()和study()方法。


4. 調用sleep()方法。
因爲sleep()是Person類中的放法,所以P對象可以直接到代碼區(Codesegment)中調用sleep()的方法體執行。(方法的方法體是被存儲在代碼區的,只有被調用是執行,如上圖)
5. 對象轉型。
a.先在棧中創建一引用變量S,用來存儲p對象轉型後的對象地址。
b.把P對象由Person類型向下轉爲Student類型,其實就是擴大p對象的訪問權限,使它指向整個Student類型的對象而非只Person類型。
c.讓引用變量S指向Student類型對象。
6. 通過S引用變量到堆中找到創建的Student類對象,通過Student類對象再到代碼區中找到sing()方法及study()方法順序執行。

 


發佈了48 篇原創文章 · 獲贊 10 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章