本问题正式讨论见于帖子 http://bbs.csdn.net/topics/391819221 :
例子1:
public class MemoryTest {
int[] a = new int[0] ;
public void set(){
a = new int[10] ;
for(int i =0;i<10;i++)
a[i] = i ;
}
/**
* @param args
*/
public static void main(String[] args) {
MemoryTest mt = new MemoryTest() ;
mt.set();
for(int v:mt.a)
System.out.println(v); //这里a数组的对象应该是分配在堆上还是栈上? 如果是在堆上,会否因为set函数执行完被回收么? 还是说只要mt对象存在,该数组里面的对象就存在?
System.gc(); //这里垃圾回收似乎没有起到作用
System.out.println("-------------");
for(int v:mt.a)
System.out.println(v);
}
}
其内存分配正确理解如下
内存可以简单划分为:
1、栈:局部变量、对象的引用名、数组的引用名
2、堆:new出来的东西(如:对象的实体,数组的实体),因此含成员变量,什么是new出来的东西?就是引用数据类型
3、方法区:包含常量池
4、静态域:声明为static的变量
详细的参见下图
通过给出的类
a是MemoryTest类的成员变量
main方法是程序的入口开始
new MemoryTest(),内存将在堆中开辟空间,这个空间包含了成员变量a,a指向了在堆中的一个空间,存放new出来的数组
mt是对象的引用名,存储在栈;
mt.set(),调用方法set(),在堆空间存放new int[10],给数组成员赋值
垃圾回收先可以理解为 栈空间的用完就会释放,就是程序运行到变量的作用范围之外就会自动释放
堆空间由JVM虚拟机回收,实例对象没有任何引用指向它,垃圾回收就会回收它
因此,a数组的对象应该是分配在堆上,其回收根据MemoryTest对象的回收进行回收
gc的实现现在基本使用的是gc root判定方法,即从几个根引用区域(调用栈,常量区)出发,所能引用到的对象都不会被回收。
另,引用计数方法存在一个弱点,不能回收循环引用(即仅有对象a, b之间相互保留引用)的对象,故现在gc不再单独使用这种方法。
a作为数组引用,是mt的成员变量,被分配在堆上,set方法执行时,为a分配了10个对象,都在堆上,当然set方法是在栈上执行的,执行完后set部分被回收,但是a作为引用仍然存在,并指向刚刚分配的 10个基本类型,在mt引用未被释放前,a一直存在,因此a指向的数组对象也存在,不会被占用或者回收。
几点关键概念理解:
1.a指向的是一个数组对象,分配的10个是基本int类型而非Integer对象,数组对象有其元数据信息,同时没有发生装/拆箱操作。
1) 数组分配时作为一个对象进行分配,有其元数据信息,详细参考这里:http://www.ibm.com/developerworks/cn/java/j-codetoheap/
Java 数组对象详解
数组对象(例如一个 int 值数组)的形状和结构与标准 Java 对象相似。主要差别在于数组对象包含说明数组大小的额外元数据。因此,数据对象的元数据包括:
类:一个指向类信息的指针,描述了对象类型。举例来说,对于 int 字段数组,这是 int[] 类的一个指针。
标记:一组标记,描述了对象的状态,包括对象的散列码(如果有),以及对象的形状(也就是说,对象是否是数组)。
锁:对象的同步信息,也就是说,对象目前是否正在同步。
大小:数组的大小。
。。。
按照里面的描述,图3的int数组应该有5个元素,共占用320bit内存, 160位用于保存元信息,剩下160位存储5个int元素
2) 分配过程中没有发生装/拆箱操作。
自动装箱自动拆箱是Java新特性,主要是减少了编译时基本类型与包装器类型之间的转换操作,是个偷懒行为,其实编译器还是要做转换操作。
比如 向一个容器list中添加元素,必须要添加引用数据类型 如果调用方法list.add(3);,编译器不会报错,会将3自动装箱成Integer对象;如果你从容器中取出这个元素,进行算术运算的话 int j = list.get(0) + 1; 由于运算只能在基本类型之间进行(个别除外),这个时候编译器会自动拆箱为基本类型进行运算。
所以说,在你声明的类型与代码要操作的所需要的类型有冲突的时候,才发生自动装箱拆箱,在这个例子中你声明的是int[],自然这个数组的元素都是int类型了。
说到底,Java是个强类型语言,声明的类型必须和操作所需的类型一致,否则要么编译器替你做了,要么编译的时候就会报错。
总结: 在你声明的类型与代码要操作的所需要的类型有冲突的时候,才发生自动装箱拆箱.
ps:前阵遇到同步加锁的问题,当时使用inti作为被锁的对象,同步条件是i增加或者减少,当时很奇怪为何不起作用,后来发现是因为i加减过程中由于被当成wait和notify的目标,必须被当成对象来处理,因此被初始化成了Integer对象,自然i值变化后就i作为一个引用就会指向其他对象,加锁的目标就不是同一个对象了,因而起不到作用,这里例子1中数组作为一个对象,分配时为基本类型,不需要用到对象操作,因而没有装箱拆箱!
3)关于int[] a = new int[0];
int[] a = newint[0]; 是在堆空间开辟了一个空间,存放数组数据,这个数组的长度为0;在栈空间开辟空间存储一个int[]类型的变量a, a的值是new出来的堆空间的首地址值(a指向了一个对象实例)。
int[] b = null;在栈空间开辟空间存储一个int[]类型的变量b,b没有值(b没有指向了一个对象实例)。
至于具体大小,看1)中的解释即可。
另外还有一个问题:
包装规范,介于-128~127的short和int类型,包装时会包装到一个固定对象中,char<=127,boolean和byte类型也是.
== 操作符 比较的是值
基本数据类型的值是其字面量值
引用数据类型(Integer,Double)的值是其引用的对象实例的首地址值,既然引用同一个对象,自然用==比较就是true了
int a = 100 ;
Integer b = 100 ;
System.out.println(a==b?1:0);
System.out.println(b==a?1:0);
结果都是1,至于编译器会怎么编译,我也不知道,我觉得应该是转成基本数据类型比较,毕竟操作符主要操作基本类型.
对其进行了测试,代码:
Integer a = 100 ;
Integer b = 100 ;
System.out.println(a.intValue()==b.intValue()?1:0);
System.out.println(a==b?1:0);
Double d1 = 1.2366666666666666 ;
Double d2 = 1.2366666666666666 ;
System.out.println(d1.doubleValue()==d2.doubleValue()?1:0);
System.out.println(d1==d2?1:0);
确实是这样,即对于引用对象比较,较短的如Integer会指向同一个100对象,而Double并非如此!
看下面的例子:
public class MemoryTest2 {
public void set(Integer test){
test = 5 ; //这里test重新指向一个5对象,是否传入的tt引用也指向5这个对象,set执行完后,tt应该是空指针么? 但是tt之前已经被初始化了... 不明白内存分配过程
}
public void set1(Integer[] test){
for(int i =0;i<10;i++)
test[i] = i ;
}
public void set2(Integer[] test){
test = new Integer[10] ; //1.set方法中传递的是值还是地址? 如果是地址,set中执行此行会否导致tt2也指向这块内存,所以导致后面的Integer出现空指针问题?
//2.哪位能把这行的流程仔细描述下? 这里的test和tt2是何关系?
//3.我理解这里应该类似C++中的传地址,因而test引用的修改等同于tt2引用的修改。
for(int i =0;i<10;i++)
test[i] = i ;
}
/**
* @param args
*/
public static void main(String[] args) {
MemoryTest2 mt = new MemoryTest2() ;
Integer tt = 0 ;
mt.set(tt);
System.out.println(tt);
Integer[] tt1 = new Integer[10] ;
mt.set1(tt1);
for(int t:tt1)
System.out.println(t);
Integer[] tt2 = new Integer[10] ;
mt.set2(tt2);
for(int t:tt2) //这里即会报空指针,会否因为tt2指向的数组对象没有被初始化?
System.out.println(t);
}
}
for(int t:tt2)
这行会强制将Integer拆箱,此时其值为null,拆箱自然会引发空指针。
ava的参数传递被称为值传递,如果参数类型为类,那传递的值实际上是类实例引用。所以set方法的test参数传递的是引用堆中对象实例的引用(可以理解成地址,或者指针)。set(tt)相当于把tt复制了一份给test,两个引用值相同,都引用同一个Integer对象,值是0。test=5因为右值是int类型,所以引起一次装箱,相当于test=new Integer(5),这里只有test发生了变化(指向了另一个对象),tt保持不变,所以输出0。
同理,tt1也被复制了一份给test,对test[i]的操作并不影响test本身的值,所以set1执行完毕后test和tt1依然引用同一个对象new Integer[10]。所以输出tt1能够正常输出0-9。
同理,tt2也被复制了一份给test。test=new Integer[10]将test引用了一个新的数组对象,这个操作没有影响tt2的值,因此tt2的元素未被初始化,用foreach循环试图对其中的元素进行拆箱会引起空指针异常。
补充一下关于空指针的原因,之所以会报空指针是因为常使用int类型的变量对tt2进行迭代,这会引起一次拆箱,而对null引用进行拆箱这一行为引发了异常。可以试试把循环改成for(Integer t : tt2)会发现输出了10个null。