变量在Java栈、堆内存中的运用管理分析

2017-04-18 18:08陈益杨晓艳
软件导刊 2017年2期

陈益 杨晓艳

摘要 初次接触面向对象程序设计,不易弄清楚各种类型变量在内存中是如何分配和管理的。以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.

(责任编辑:杜能钢)