Java 内存与堆栈

原文地址:为了备忘,转载自http://www.j2megame.org/index.php/content/view/1989/125.html

 

1. java中堆与栈 

在Java程序运行时,有6个地方可以用于保存数据: 
(1) 寄存器。最快的保存区域,位于处理器内部,数量十分有限,它是根据需要由编译器分配。我们对此没有直接的控制权. 

(2) 栈(stack)。驻留于常规RAM(随机访问存储器)区域,这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准 确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这失去了一定的灵活性,因此对象句柄是存放在栈中,但Java对象并不放到其中。 

(3) 堆(heap)。保存了Java对象。和栈不同,它最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的 时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保 存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! 

(4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。 

(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。 

(6) 非RAM存储。数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。 

2.堆和栈的区别 

栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。 

Java 的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建 立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分 配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 

栈的优势是, 存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量 (,int, short, long, byte, float, double, boolean, char)和对象句柄。 

栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义: 
int a = 3; 
int b = 3; 
编 译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。 

这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。 

要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。 

String是一个特殊的包装类数据。可以用: 
String str = new String("abc"); 
String str = "abc"; 
两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。 
而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。 

比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。 
String str1 = "abc"; 
String str2 = "abc"; 
System.out.println(str1==str2); //true 
可以看出str1和str2是指向同一个对象的。 

String str1 =new String ("abc"); 
String str2 =new String ("abc"); 
System.out.println(str1==str2); // false 
用new的方式是生成不同的对象。每一次生成一个。 


因 此用第二种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已. 这种写法有利与节省内存空间. 同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。 

另 一方面, 要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的 对象。只有通过new()方法才能保证每次都创建一个新的对象。 
由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。 

参考文献: 
关于Java栈与堆的思考 
http://www.duduwolf.com/post/3.asp 

第2章 一切都是对象 
http://52cg.com/Training/KaiFa/java/200510/31900.html 


3.关于java中数据占用内存空间的大小 

――测试环境 
j2me midp1.0 
――测试方法: 
使用下面的语句,获取对象创建前后的内存值,它们的差值就为所创建对象占用的内存大小: 
1.i = Runtime.getRuntime().freeMemory(); 
2.control = new Control(Control.HEAD_MOVE, 1, 1); 
3.j = Runtime.getRuntime().freeMemory(); 
4.System.out.println(i-j); 
这个方法并不准确,因为JVM会不定时在后台运行一些东西,比如在gc,后台运行东西会占用一定的内存,如果这个后台程序的运行发生在1、3句之间的时候就会造成不准确。 

为了尽可能保持正确性,将语句变为如下,先gc,再取内存,让那些由于后台运行而占用的内存释放出来。 
Runtime.getRuntime().gc(); 
Thread.yield(); 
i = Runtime.getRuntime().freeMemory(); 
test t = new test(); 
Runtime.getRuntime().gc(); 
Thread.yield(); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); 
但是这种方法还是不一定能够保证gc会被调用,因此每一个测试得到的数据都是多次测试得出相同的那个数据,取当堆大小稳定以后显示的数据,追求尽量准确。 


(1) i = Runtime.getRuntime().freeMemory(); 
int t = 0; 
char a ='a'; 
long l =23410389; 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); // 0 
【结论】因为基本数据类型存放在栈中,并不占用堆的内存,可见freeMemory返回的数值是堆中的内存,因此值为0。 

(2) 
i = Runtime.getRuntime().freeMemory(); 
String s2="1234567"; 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); // 0 
――――――――――――――――――――――――――――――― 
i = Runtime.getRuntime().freeMemory(); 
String s2=new String("1234567"); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); // 56 

【结论】以String s2="1234567"方式创建的字符串是存放在栈中,它不占用堆空间;而用String s2=new String("1234567");方式创建的字符串是存放在堆中,它占用了56byte的堆空间。 

(3) 
Runtime.getRuntime().gc(); 
Thread.yield(); 
i = Runtime.getRuntime().freeMemory(); 
test t = new test(); //test为一个空的Class 
Runtime.getRuntime().gc(); 
Thread.yield(); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); // 12 

―――――――――――――――――――――――――――――――― 
Runtime.getRuntime().gc(); 
Thread.yield(); 
i = Runtime.getRuntime().freeMemory(); 
Contorl c = new Control(Control.HEAD_MOVE, 1, 1); 
Runtime.getRuntime().gc(); 
Thread.yield(); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); //112 

【结论1】:一个空的对象需要占用12byte的堆空间,将其理解为对象头。 
【结论2】:一个zhanguo/Control的一个“HEAD_MOVE”对象占用112byte堆空间。 

为何是112byte? 
Control中有25个非static的基本数据类型和引用变量,而int,short,byte等基本数据类型和引用变量占用的内存大小都是4byte(详见4),因此大小为: 
25*4 + 12 (对象头)==112 


(4)在上面的Class test中加入: private int a = 0; 
得出结果:16byte , 
由(3)可以知空object占12byte,所以一个int占用4byte的空间。 
将int改为其他基本数据类型,用同样方法进行测试,得出如下结论: 
【结论1】:float, boolean,byte, short, int, char占用4个byte的空间, 
long,double占用8byte空间 (midp1.0不支持float和double) 
引用变量,比如 Control c = null, 占用4byte空间。 

【结论2】:在java中类型决定行为,而不是大小;用byte起到限制数据的作用,但是并不能节约内存,在内存中byte和int一样是占用4byte的空间。 

【结论3】:虽然a为基本的数据类型,但是它是对象t的一个属性,是在运行时才动态创建的,因此它也是存放在堆中,占用4byte的堆空间。 


(5)在Class test中加入下面语句:并去掉(4)中加入的变量 int a。 
private final static int j = 1; 
private final static int k = 4; 
private static void testFunction(){ 

得出结果:12 
【结论1】一个object的占用堆空间的多少只与类中的非static的基本数据类型和引用变量有关,而与static的变量和function的多少并没有关系。Static是存放在“静态存储空间”,不占用堆空间。 

【结 论2】对于需要生成多个object的类,比如control等,类中的成员变量尽量用static,因为object中的非staitc变量在堆中需要 占用一定的空间,当object数量比较多时,占用的堆空间会增大很多。从而容易出现内存不足(主要是堆空间不足)造成“应用程序错误”的情况。 

(6) 
i = Runtime.getRuntime().freeMemory(); 
Strin s=new String(""); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); //40 
【结论】一个空的String就要占用40字节的堆空间,理解为String头信息,在代码中要避免反复得new一个相同的string,而是应该利用上面(2)中所讲的那种方式: 
String s2="1234567"来生成String。 


(7) 
Runtime.getRuntime().gc(); 
Thread.yield(); 
i = Runtime.getRuntime().freeMemory(); 
int a[][] = new int[0][0]; 
Runtime.getRuntime().gc(); 
Thread.yield(); 
j = Runtime.getRuntime().freeMemory(); 
System.out.println(i-j); //16 
---------------------------------------------------------------------------------- 
int a[][] = new int[1][0]; //36 
int a[][] = new int[2][0]; //56 
int a[][] = new int[3][0]; //76 
int a[][] = new int[200][0]; //4016 
int a[][] = new int[200][1]; //4816 ,因为比上面多了200个int,所以多了800 

【结论1】每一个嵌套的int[dim2]都是一个对象,一个int占用4byte空间,每一个对象都有一个16字节的数组对象头。 
【结论2】二维数组的数组头的内存消耗很大,对于对内存较小的手机,比如nokia旧40系列,最好将二维数组改为用一维数组。 

参考文章: 
Discover how much memory an object consumes 
http://www.mywelt.net/?q=node/577 

你知道数据大小吗?-不要花太多的功夫来隐藏类的成员 
http://istudy.com.ru/Article/Software/Java/20051025/54,81897,0.html 


TODO: 
1. 关于类,变量,函数等与jar大小的关系,考虑混淆器的作用,对于混淆器可以做到的就不必在程序中去改变。 
2. 关于j2me程序与内存的关系进一步深化,比如线程,gc等等。 

??? 
1.Java程序编译后编程*.class文件,那么在运行时这些文件是一次性加载进内存的?如此程序代码很长的话,光是存放这些代码就要消耗大量的内存?它们是存放在内存的什么地方?上面提到的几个地方明显都不是存放代码的地方。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值