陳益 楊曉艷
摘要 初次接觸面向對象程序設計,不易弄清楚各種類型變量在內存中是如何分配和管理的。以Java為例,主要介紹基本數據類型一維數組內存模型、引用數據類型數組內存模型、方法調用時變量的內存模型、內部類的內存模型的活動空間。了解對象的屬性和行為在內存中的位置和彼此間的關系,有助于更好地理解程序的編譯原理和運行機制。
關鍵詞 JVM;內存模型;基本類型;引用類型
DOI DOI: 10.11907/rjdk.162172
中圖分類號: TP301
文獻標識碼: A 文章編號 文章編號: 16727800(2017)002002903
0 引言
對于不同的平臺,內存模型通常有所差異。Java虛擬機、Java Virtual Machine(簡稱JVM)的內存模型規范是統一的。Java內存分配時涉及到的區域有:①棧內存(簡稱棧):一般用來存放基本類型的數據和對象的引用,不包括對象自身;②堆內存(簡稱堆):用來存放由new關鍵字創建的對象;③常量池:用來存放常量;④靜態域:用來存放靜態成員;⑤非RAM存儲:一般指硬盤等永久存儲空間;⑥寄存器:由編譯器根據實際需要分配內存區域,用戶在編程中無法控制它。
Java中的變量包括基本類型和引用類型變量兩大類,當用戶在一個類中定義了一個變量,JVM在棧內存中為此變量分配空間大小,對象調用完變量后,Java虛擬機將釋放掉先前為該變量分配的空間[1]。方法中的基本類型變量和對象引用變量,都在方法的棧中被分配空間。棧中主要存放一些基本類型的變量數據和對象引用。也即當用戶在Java中聲明一個變量時,則在棧中為其分配了一塊空間,用來存放變量的值,變量的值可以是數值、false、null等。直接被賦值為數值或false的變量,即是通常Java中規定的8種基本數據類型變量,包括:boolean、byte、char、int、short、long、float、double,它們的值則放在棧中提供給用戶使用;值為null的變量是引用類型的變量,在Java中凡是聲明為數組、類(對象)、字符串、接口等類型的變量都是引用類型變量,在聲明時其默認值也存放在棧中。
由于引用類型變量在聲明時默認值為null,所以要和存放在堆中的對象產生一定聯系,以獲取該對象的首地址信息來替換聲明時的null,這種聯系被形象地稱為指向。建立了堆棧之間的指向,棧中的變量才有了真正的實體,才能被使用。Java中堆棧之間的指向類似于C語音中的指針。Java中的堆是一個運行時的數據區,用來存放由new關鍵字創建的對象和數組,它不需要程序代碼來顯式地釋放內存空間,而是由堆動態地分配內存大小,由JVM虛擬機的自動垃圾回收器來管理那些不再被引用的內存。本文主要介紹引用數據類型變量在內存中的分配狀況。
1 基本數據類型一維數組內存模型
聲明一個一維整形數組變量n,比如int []n,則n的值默認為null,存放在棧中,和其它內存沒有任何交集,內存模型如圖1所示。要想使用n數組變量,則應該創建對象給數組分配大小,給n變量賦予實際意義的值,所賦的值是該數組首元素的地址。在Java中聲明一維整型數組后,由new關鍵字創建對象并為其分配空間,聲明變量和創建對象通常由一步完成。如int []n=new int[3];n的值為數組首元素地址,即n指向數組的首元素,具體做法是將數組首元素的地址放入n變量中,替換n的初始值null。n中有3個能操作的變量,可以給3個變量作賦值操作,如同給任意一個簡單變量賦值操作一樣。如果用戶沒有給3個數組元素初始化,因為3個元素的類型都是整型(數值型),在堆內存中會默認3個元素的值都為0。
內存模型如圖2所示。如n不再指向數組的首元素,即當賦值號兩邊的操作對象之間沒有任何聯系時,n的值再次為null,內存模型如圖3所示。沒有被任何引用類型變量指向的對象是一個匿名對象。
2 引用數據類型數組內存模型
2.1 對象數組類型
聲明一個引用類型的數組變量stu,數組的類型為對象引用型,如Student []stu;與基本整型的一維數組相比,雖然都是引用類型,但數組的類型是對象型,不過值都是一樣為null,對象數組值為null的內存模型和基本類型數組的模型一樣,如圖1所示。用new分配3個長度的空間,Student []stu = new Student[3],因為3個數組元素都為對象型的變量,初始默認值為null,還不能使用,內存模型如圖4所示。要想使用數組中的每個數組元素,需要分別給各個數組元素補充完整信息,如圖5所示。給數組的首元素賦值,有兩個屬性分為姓名“lisi”和年齡“18”,即可使用首元素。
2.2 字符串類型
圖6為字符串對象模型,當用戶聲明一個字符串類型引用變量時,如String s1,變量s在棧內存中被分配空間,其值為null,系統只為變量分配了引用空間,還沒有創建具體對象。當new關鍵字調用構造方法后才創建對象,并將對象的引用賦值給字符串引用類型的變量s1,兩步工作可以合并為一步完成,如s2字符串引用類型變量聲明所示[2]。代碼段如下所示:
class StringDemo
{
public static void main(String args[])
{ Stirng s1;
s1=new Stiring(“def”);
String s2=new String(“def”);
if(s1==s2) System.out.println(“s1==s2”);
else System.out.println(“s1!=s2”);
if(s1.equals(s2)) System.out.println(“s1 equals s2”);
else System.out.println(“s1 not equals s2”);
}
}
先分析源程序的運行結果,程序中兩個if語句描述了字符串等號“= =”和equals方法的功能。String s1=new String("def"); String s2=new String("def"),由字符串String類產生了兩個字符串對象,分別為s1和s2。第一個if語句if(s1= =s2)的結果是s1!=s2,第二個if語句if(s1.equals(s2))的結果是s1 equals s2。分析結果產生的根源要從內存模型來解釋。s1和s2是兩個引用(字符串)類型的變量,當兩個字符串引用類型的變量作“= =”比較時,表面上是比較兩個變量的值,但該值不同于簡單變量的數值,引用類型變量的值實質上指的是地址,它們分別為兩個不同的字符串常量,因此其地址肯定不相等。但兩個變量表示的字符串內容相等,都是“def”,采用equals方法的作用是比較兩個變量代表的內容,如圖6所示。
另外,程序中用來給變量賦值的常量(如數值、字符串等)都位于常量池中。常量池是由編譯器確定被保存在.class文件中的數據信息,里面除了基本數據類型和引用類型的常量外,還包含一些文本形式的符號引用,比如:類、變量、方法及接口的名稱和描述符。對于String類型的常量,它的值存放在常量池中。在JVM中,常量池在內存中是以表的形式存在。對于String類型,有一張固定長度的常量字符串信息表,專門負責存儲文字字符串值,而不存儲符號引用。位于.class字節碼文件中的常量,在運行期間由JVM自行裝載,還具有擴充功能。String類中的intern即是擴充常量池的一個方法。
3 方法調用時變量內存模型
圖7為方法調用模型1,圖8為方法調用模型2,自定義方法中有無參數(方法中的參數稱為形式參數,簡稱形參)都可以。方法中若有參數將帶來程序的靈活性,參數類型由用戶根據具體需要設定。基本數據類型變量之間的值傳遞是簡單的單向傳遞。JVM中傳遞各種類型變量值的方法主要分為3種:①方法的參數為簡單變量,進行單向值傳遞;②方法的參數為數組變量,進行地址傳遞;③方法的參數為對象變量,進行地址傳遞。在如下代碼段中,fn1方法有3個重載的方法,參數分別為整型變量、數組變量和對象變量,試分析程序的運行結果。
class MethodDemo
{ public static void fn1(int x,int y)
{ x=x+y; y=x-y; x=x-y; }
public static void fn2(int[] n)
{ n[0]=n[0]+n[1]; n[1]=n [0]-n[1]; n[0]=n[0]-n[1]; }
public static void fn3(Test p)
{ p.x=p.x+p.y; p.y=p.x-p.y; p.x=p.x-p.y; }
public static void main(String[] args)
{ int x=5,y=7; fn1(x,y); System.out.println("x="+x+”,”+"y="+y);
int[] n=new int[]{5,7}; fn2(n);
System.out.println("x="+n[0]+","+"y="+n[1]);
Test p=new Test(); p.x=5; p.y=7; fn3(p);
System.out.println("x="+x+”,”+"y="+y);?}
}
class Test
{ int x,y;
public String toString()
{ return "x="+x+","+"y="+y;? }
}
由運行結果可知,在Java中,對于方法fn1,簡單變量的值傳遞與其它語言中簡單變量的值傳遞原理一致,都是單向傳遞,傳遞的是值本身,方法調用過程中不會改變原有值,并且JVM中所有的簡單變量都保存在棧內存中,與堆內存沒有聯系,具體如圖7所示;對于方法fn2和fn3,當數組或對象作為方法的參數時,因為傳遞的是地址,地址改變時,原來的值也跟隨改變,具體如圖8所示。
4 內部類內存模型
類的成員除變量和方法外,還可以有另一個成員內部類。內部類(也稱Inner Class)指在一個類中定義的另外一個類。內部類和外部類的定義方式一樣,其擁有自己獨立的屬性和方法,并將它們封裝在一個類中。內部類擁有和方法一樣的訪問權限,可以聲明為public、protected、default和private。內部類將邏輯上相關的一組類組織起來,由外部類(OuterDemo Class)來控制其可見性[3]。代碼如下所示:
class OuerDemo
{
private int n=50;
class Inner
{ void fn2()
{ System.out.println(n); }
}
void fn2()
{ Inner in=new Inner();
in.fn2();
}
}
class Test
{
public static void main(String args[])
{
OuterDemo out=new OuterDemo();
out.fn2();
}
}
編譯后將產生3個.class文件,一個是含有main方法的Test類的Test.class,一個是外部類的OuterDemo.class文件,還有一個是內部類的OuterDemo$ Inner.class文件,運行結果打印輸出為50。程序中將成員變量n的訪問權限設置為私有的(private),檢驗外部類的私有變量n能夠被內部類Inner中的fn2( )方法所訪問。如果私有變量都能被訪問,外部類中其它的訪問權限,共有的、受保護的和友好的則也能被內部類的成員訪問。主要當創建一個內部類對象時,它與外部類對象之間便產生了一種聯系,這種聯系是通過一個特殊變量this搭建的,從而使內部類對象能隨意訪問外部類中的所有成員。具體內存模型如圖9所示。
5 結語
JVM定義了各種變量的內存模型狀態,每個變量都在自己的內存空間中活動,同時又與其它內存建立聯系。Java自動管理棧內存和堆內存,程序員不能直接設置棧內存或堆內存,棧內存中放置基本類型、局部變量和引用變量的值。引用變量存放在棧內存中,對象內容根據創建方式而定,由編譯器事先創建好并存放在常量池中,程序運行時由new調用構造方法創建的對象,存放在堆內存中。 棧的優點是數據共享、存取速度快;缺點是數據大小與生存期必須是確定的,缺少靈活性。
堆內存放置new調用構造方法創建的對象。一旦在堆中產生數組或對象后,可以在棧中聲明一個特殊變量,變量的值等于數組或對象在堆內存中的首地址,棧里聲明的特殊變量則成了數組或對象的引用變量。其實,棧內存中的變量指向堆內存中的對象,可以被理解為JVM中的指針。由于堆內存分配是在程序運行時動態進行的,所以堆內存的存取速度相對于棧內存而言較慢。
String類表示一個字符串,在Java中所有的文字串,例如“abc”都是作為String類的實例來實現的。String類是Java中一個特殊的封裝類,它被聲明為final,用戶不能從String類派生出其它類,一個String類的對象是一個常量,創建之后值不能被改變。
參考文獻:
[1] 耿祥義.Java2實用教程[M].第4版.北京:清華大學出版社,2012.
[2] 張桂珠.JVM面向對象程序設計[M].第3版.北京:北京郵電大學出版社,2010.
[3] 孫鑫.Java無難事[M].北京:電子工業出版社,2004.
[4] 林樹澤.Java完全自學手冊[M].北京:機械工業出版社,2009.
[5] 周志明.深入理解Java虛擬機[M].北京:機械工業出版社,2011.
[6] [美]BRUCE ECKEL.Java編程思想[M].第4版.陳昊鵬,譯.北京:機械工業出版社,2007.
[7] 聶芬.Java中堆與棧的內存分配[J].電腦學習,2010(6):123124.
(責任編輯:杜能鋼)