1.栈(Stack):存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new出来的对象)或者常量池中(字符串常量对象存放在常量池中)。
2.堆(heap):存放所有new出来的对象。
3.常量池(constant pool):在堆中分配出来的一块存储区域,存储显式的String常量和基本类型常量(float、int等)。另外,也可以存储不经常改变的东西(public static final)。常量池中的数据可以共享。
4.静态存储:存放静态成员(static定义的)。
示例分析
1).
String a = "abc";①
String b = "abc";②
分析:
①代码执行后在常量池(constant pool)中创建了一个值为abc的String对象
②执行时,因为常量池中存在"abc"所以就不再创建新的String对象了。
2).
String c = new String("xyz");①
String d = new String("xyz");②
分析:
①Class被加载时,"xyz"被作为常量读入,在常量池(constant pool)里创建了一个共享的值为"xyz"的String对象;然后当调用到new String("xyz")的时候,会先去常量池中查找是否有"xyz"对象,如果没有则在常量池中创建一个此字符串对象,然后再在堆中创建一个此常量池中"xyz"对象的拷贝,最后在堆(heap)里创建这个new String("xyz")对象;
②由于常量池(constant pool)中存在"xyz"对象,所以不用再在常量池中创建"xyz",直接在堆里创建新的new String("xyz")对象。
3).
String s1 = new String("xyz"); //创建二个对象(常量池中"xyz"常量对象和堆中new String("xyz")对象),一个引用(栈中s1引用堆中new String("xyz")对象)
String s2 = new String("xyz"); //创建一个对象(堆中),并且以后每执行一次同样赋值操作均创建一个对象,一个引用
String s3 = "xyz";//创建一个对象(常量池中"xyz"常量对象),一个引用
String s4 = "xyz";//不创建对象(共享上次常量池中的数据),只是创建一个新的引用
4). intern()
java.lang.String的intern()方法"abc".intern()方法的返回值还是字符串"abc",表面上看起来好像这个方法没什么用处。但实际上,它做了个小动作:检查字符串池里是否存在"abc"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"abc"添加到字符串池中,然后再返回它的引用。
String s1 = "Monday";//在常量池中创建"Monday"对象
String s2 = new String("Monday").intern();//在堆中创建new String("Monday")对象后,并未使用该对象,而是继续检查常量池中是否存在"Monday"对象,并返回常量池中的"Monday"对象给s2,此时s1,s2指向同一对象。
5). 而对于基础类型的变量和常量,变量和引用存储在栈中,常量存储在常量池中
int a1 = 1, a2 = 2, a3 = 3;
public static final int INT1 = 1;
public static final int INT2 = 1;
public static final int INT3 = 1;
6). Java中的装箱和拆箱。
在JDK1.5之前,我们要实现基本类型和包装类之间的转换,大多是通过包装类提供的方法,Integer i = Integer.valueOf(5)或者int j = i.intValue()来做互相转换的。JDK1.5之后,编译器会在我们做赋值操作(这里所说的赋值操作不包括构造函数)的时候帮我们自动完成基本类型和包装类之间的相互转换。包装类是类,是对象,而基本类型是有值的“变量”,包装类的实例(对象)创建在堆上,而基本类型创建在栈上。包装类作为类,可以容纳更多的信息。包装类都实现了Comparable接口,可以实现对象之间的比较,所以包装类之间的比较尽量用compareTo,而不是><=这些运算符。上面我们所说的赋值操作,基本上可以分为两种情况,一种是显式赋值,另一种是隐式赋值。
显式赋值,我们可以理解为有赋值符号出现的情况,比如,Integer i = 11;这里编译器会自动的帮你完成把11这个int的基本类型装箱成包装类实例的工作,这样就不用我们再手动的转换了。
隐式赋值,就是没有赋值符号出现的情况,比如方法调用时候的参数传递,比如我们有一个接受Integer类型参数的方法(void method(Integer i)),我们在调用的时候可以直接传递一个基本类型的实参进去(method(5)),这时候编译器会自动的将我们传递进去的实参装箱成包装类,从而完成对方法的调用。在方法调用的时候,在内存中的栈上,首先会为方法的形参创建内存区域,然后将我们传递进去的实参赋值给形参,就是在这时,发生了赋值操作,才会让编译器完成装箱操作。当然,方法执行完后,栈上有关形参的内存区域会被回收。
还有我们要记住一点,就是编译器的装箱/拆箱原则,就是基本类型可以先加宽(比如int转换为long),再转变成宽类型的包装类(Long),但是不能直接转变成宽类型的包装类型。比如我们有个方法 void method(longl),我们通过method(5)是可以调用该方法的,因为int类型的5被加宽成了long类型;但是如果这个方法变成void method(Long l),我们通过method(5)就不能调用它了,因为int类型的5是不能直接转换成Long包装类的,切记。
最后还有一个值得注意的地方,就是上面我们提到的显示赋值的情况下,比如Integer i = 11的情况,实际上在源码中是调用到了Integer的静态方法valueOf(),在这一块,java的设计者采用了cache pool的设计模式,在Integer这个类里面有一个长度为256的静态Integer数组,里面存储了从-128到127的Integer对象,当我们传递进去的参数在这个范围之内时,这个方法会返回该数组中实际保存的Integer对象实例,只有超过该范围时,才会返回new出来的Integer对象实例。
7).综合示例