【Java】栈内存、堆内存、String常量池

栈内存


在函数中定义的基本数据类型的变量对象的引用变量,都存储在栈内存中,当变量退出作用域后,栈中的数据会被自动释放供接下来的使用。

堆内存


堆内存中储存new关键字创建的对象和数组,在该区域中分配的内存由Java垃圾回收机制进行管理。

当栈内存中的引用变量因为退出作用域而被释放之后,堆内存中相应的数据便会因为没有引用指向变得无法有效访问而变成数据垃圾,这个时候便会被垃圾回收机制所回收。

String常量池


  1. jdk1.7.0版本之前,String 常量池位于Java虚拟机内存分配区域中的方法区(Method Area)
  2. jdk1.7.0版本之后,String 常量池被移到了堆内存中。

对于一个String字符串类型来说,它的引用都是保存在栈中的,但是如果是在编译期就能确定值得话(比如双引号赋值),这个值就会保存在String 常量池中,如果是通过new关键词创建的话,就会保存在堆中,new几个就保存几个。

对于使用new关键词创建字符串时,会先去String 常量池中检查是否存在相同内容的字符串,如果不存在,便会先在String 常量池中也创建一份,然后才会在堆中创建一份;如果存在,便只会在堆中创建一份。

所以,对于String str = new String("Hello");这一行代码,如果常量池中不存在"Hello"这个字符串,那么实际上这行代码创建了两个对象。

既然对于同样的字符串"Hello"new一次,堆中就会多一个相同内容的对象,这样有可能在某些情况下造成资源浪费,那么有没有可以在new操作的时候去String常量池中先查找有没有相同内容的字符串,如果有那就直接引用的办法呢?

这个时候我们可以使用intern()方法,在new操作的时候,调用这个方法,就可以实现上面的想法。

public class StringTest {

    public static void main(String[] args) {
            String a = new String("Hello");
            String b = new String("Hello").intern();
            System.out.println(b == a);
    }   
}
true

String常量池问题的例子


在编译期,由常量拼接起来的字符串被解析为一个字符串常量:

public class StringTest {

    public static void main(String[] args) {
            String a = "a1";
            String b = "a" + 1;
            String c = "a" + "1";
            System.out.println("a == b: " + (a == b));
            System.out.println("a == c: " + (a == c));
    }   
}
a == b:true
a == c:true

在字符串的"+"连接中,如果存在字符串常量值的引用,那么在编译期并不能确定,只能在运行期在堆中分配内存空间,但是可以通过final修饰符来修饰这个字符串常量值的引用,使之在编译期被解析为一个常量值:

public class StringTest {

    public static void main(String[] args) {
            String a = "abcd";

            String b = "ab";
            final String bf = "ab";

            String d = b + "cd";
            String df = bf + "cd";

            System.out.println("a == d: " + (a == d));
            System.out.println("a == df: " + (a == df));
    }   
}
a == d: false
a == df: true

在编译期无法确定的字符串将于运行期在堆内存中分配内存空间,并且引用的也是堆中的对象,如果使用了intern()方法,则会引用String常量池中的对象:

public class StringTest {

    public static void main(String[] args) {
        String a = "a1";
        String b = new String("a1");
        String c = new String("a1").intern();
        System.out.println("a == b: " + (a == b));
        System.out.println("a == c: " + (a == c));
        System.out.println("b == c: " + (b == c));
    }   
}
a == b: false
a == c: true
b == c: false

String类型的不可变性


看一下这段代码:

public class Test {

    public static void main(String[] args) {
        String a = "ab";
        String b = "cd";
        String c = "ef";
        String str = a + b + c;
    }   
}

然后我们来看一下编译出来的字节码文件内容:

D:\>javap -c Test.class
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String ab
       2: astore_1
       3: ldc           #3                  // String cd
       5: astore_2
       6: ldc           #4                  // String ef
       8: astore_3
       9: new           #5                  // class java/lang/StringBuilder
      12: dup
      13: invokespecial #6                  // Method java/lang/StringBuilder."":()V
      16: aload_1
      17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      20: aload_2
      21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: aload_3
      25: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      28: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      31: astore        4
      33: return
}

就会发现其实就相当于这段代码:

public class Test {

    public static void main(String[] args) {
        String a = "ab";
        String b = "cd";
        String c = "ef";
        StringBuilder builder = new StringBuilder();
        builder.append(a);
        builder.append(b);
        builder.append(c);
        String str = builder.toString();
    }   
}

然后我们再来看一下StringBuilder类中的toString()方法:

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

就会发现每当我们使用"+"来拼接字符串时,都会生成一个StringBuilder对象,然后这个对象用完就被废弃,可能一次两次这样做的话资源浪费的不是很明显,那么当我们使用循环拼接的时候呢?比如循环"+="100次,那就会有100个StringBuilder对象被创建出来,这是很严重的资源浪费。

因此我们为何不尝试使用一下可变长度的StringBuffer类呢。

你可能感兴趣的:(Java)