字符串常量池

字符串常量池

  • 字符串常量池
    • 1、创建字符串的方式
    • 2、 Java 内存模型 与 常量池
    • ​3、字符串常量池

1、创建字符串的方式

在Java中有两种方法可以创建一个字符串对象:

  • 使用new运算符。例如:
    java String str = new String("Hello");
  • 使用字符串常量或者是常量表达式
 
  

2、 Java 内存模型 与 常量池

1、Java 内存模型
字符串常量池_第1张图片

  • 程序计数器

      多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此,每个线程要有一个独立的程序计数器,记录下一条要运行的指令,其为线程私有的内存区域如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

  • 虚拟机栈

      线程私有的,与线程在同一时间创建,是管理JAVA方法执行的内存模型。栈中主要存放一些基本类型的变量数据(int, short, long, byte, float, double, boolean, char)和对象引用。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:
      字符串常量池_第2张图片

  • 本地方法区

      和虚拟机栈功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用 C 实现的。

  • JAVA堆

      线程共享的,存放所有对象实例和数组,是垃圾回收的主要区域堆是一个运行时数据区,类的对象从中分配空间,这些对象通过new、newarray、 anewarray 和 multianewarray等指令建立,它们不需要程序代码来显式的释放。堆可以分为新生代和老年代(tenured)。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,新生代对象就会被移入老年代。新生代又可进一步细分为eden(伊甸园)、survivorSpace0(s0,from space)、survivorSpace1(s1,tospace)。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。
    字符串常量池_第3张图片

    默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

    因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

    在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

​下面这个小故事来生动的理解一下对象的一生:

一个对象的这一辈子
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

  • 方法区

      线程共享的,用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即时编译器编译后的代码,也成为永久代如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收。
      


2、常量池

  常量池属于类信息的一部分,而类信息反映到 JVM 内存模型中对应于方法区,也就是说,常量池位于方法区。常量池主要存放两大常量:字面量(Literal) 和 符号引用(Symbolic References)。其中,字面量主要包括字符串字面量,整型字面量 和 声明为final的常量值等;而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

​3、字符串常量池

  字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价。JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并放到池中。Java能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突进行共享。例如:

public class Program
{
    public static void main(String[] args)
    {
       String str1 = "Hello";  
       String str2 = "Hello"; 
       System.out.print(str1 == str2);
    }
}

其结果是 :true,不幸的是,当使用的是:
String a=new String("Hello");

一个字符串对象在字符串常量池外创建,即使池里存在相同的字符串。考虑到这些,要避免new一个字符串除非你明确的知道需要这么做!例如:

public class Program
{
    public static void main(String[] args)
    {
       String str1 = "Hello";  
       String str2 = new String("Hello");
       System.out.print(str1 == str2 + " ");
       System.out.print(str1.equals(str2));
    }
}

结果是:false true
JVM中有一个常量池,任何字符串至多维护一个对象。字符串常量总是指向字符串池中的一个对象。**通过new操作符创建的字符串对象不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。**java.lang.String.intern()返回一个保留池字符串,就是一个在全局字符串池中有了一个入口。如果以前没有在全局字符串池中,那么它就会被添加到里面。例如:

public class Program
{
    public static void main(String[] args)
    {
        // Create three strings in three different ways.
        String s1 = "Hello";
        String s2 = new StringBuffer("He").append("llo").toString();
        String s3 = s2.intern();

        // Determine which strings are equivalent using the ==
        // operator
        System.out.println("s1 == s2? " + (s1 == s2));
        System.out.println("s1 == s3? " + (s1 == s3));
    }
}

输出是:

s1 == s2? false
s1 == s3? true

为了优化空间,运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用。这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。
下面是一个综合的例子:

public class DemoStringCreation {
    public static void main(String args[]) {
        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1 and str2 are created by using string literal.");
        System.out.println("    str1 == str2 is " + (str1 == str2));
        System.out.println("    str1.equals(str2) is " + str1.equals(str2));  
        String str3 = new String("Hello");
        String str4 = new String("Hello");
        System.out.println("str3 and str4 are created by using new operator.");
        System.out.println("    str3 == str4 is " + (str3 == str4));
        System.out.println("    str3.equals(str4) is " + str3.equals(str4));  
        String str5 = "Hel" + "lo";
        String str6 = "He" + "llo";
        System.out.println("str5 and str6 are created by using string constant expression.");
        System.out.println("    str5 == str6 is " + (str5 == str6));
        System.out.println("    str5.equals(str6) is " + str5.equals(str6));  
        String s = "lo";
        String str7 = "Hel" + s;
        String str8 = "He" + "llo";
        System.out.println("str7 is computed at runtime.");
        System.out.println("str8 is created by using string constant expression.");
        System.out.println("    str7 == str8 is " + (str7 == str8));
        System.out.println("    str7.equals(str8) is " + str7.equals(str8));  
    }
}

输出结果为:

str1 and str2 are created by using string literal.
    str1 == str2 is true
    str1.equals(str2) is true
str3 and str4 are created by using new operator.
    str3 == str4 is false
    str3.equals(str4) is true
str5 and str6 are created by using string constant expression.
    str5 == str6 is true
    str5.equals(str6) is true
str7 is computed at runtime.
str8 is created by using string constant expression.
    str7 == str8 is false
    str7.equals(str8) is true
  • 同一个包下同一个类中的字符串常量的引用指向同一个字符串对象;
  • 同一个包下不同的类中的字符串常量的引用指向同一个字符串对象;
  • 不同的包下不同的类中的字符串常量的引用仍然指向同一个字符串对象;
  • 由常量表达式计算出的字符串在编译时进行计算,然后被当作常量;
  • 在运行时通过连接计算出的字符串是新创建的,因此是不同的;
  • 通过计算生成的字符串显示调用intern方法后产生的结果与原来存在的同样内容的字符串常量是一样的。

你可能感兴趣的:(java)