本文源自https://www.zhihu.com/question/29884421,由原作者回答整理而成,感谢。
String类
String的定义String类是一个final类(不可被继承),内部通过一个final字符数组表示该字符串内容。一旦String对象被创建后,就不能再修改此对象中存储的字符串内容。String对象的所有编辑功能都是通过创建一个新的对象来实现的,而不是对原有对象进行修改。
它的equals()方法是通过比较字符数组的内容是否相等来判断字符串对象是否相等。
这里顺带说一下toString()方法:System.out.println()方法会调用打印对象的toString()方法。Object基类默认的toString()方法返回的是getClass().getName() +"@"+ hashcode(),而String类重写了Object基类的toString()方法,来返回String的字面量。
String赋值方式
String有两种赋值方式,第一种是通过"字面量"赋值,比如String str = "Hello"。第二种是通过new关键字创建新对象,比如String str = new String("Hello")。
这两种方式到底有什么不同?程序执行的时候,内存里到底有几个"实例"?"实例"存在了内存的哪里?"字面量"又存在了哪里?"变量"又存在了哪里?
内存回顾
讲之前,先回顾一下内存。
虚拟机结构图我们主要看运行时数据区,一般讲起来虚拟机內存最主要的就是三块:堆和栈和方法区。
将这三块放大并分析:
(下图中的非堆(Non Heap)可以粗略理解为代表着与堆区分开来的一块区域,包括代码缓冲和永生带,永生带中包括了驻留字符串区(即我们要重点讲的字符串常量池)和方法区,也就是说这里我们将方法区归在非堆中。)
堆与栈与非堆堆
上图中,首先堆分成新生代和老年代,先不用管它,这是GC垃圾回收时候的事。
栈
再说栈,重要的是栈里的局部变量表(Local Variables)和操作数栈(Operand Stack)。栈是线程私有的,每个线程里的每个方法被执行的时候都会创建一个栈帧(Stack Frame),而每个栈帧里对应的都维护着一个局部变量表和操作数栈。我们总说基本型和对象引用存在栈里,其实就是存在局部变量表里。而操作数栈是线程实际的操作台。看下面这张图,做个加法100+98,局部变量表就是存数据的地方,一直不变,到加法做完再把和加进去。操作数栈就很忙了,先把两个数字压进去,再求和,算出来以后再弹出去。
局部变量表与操作数栈非堆中的方法区
我们再说方法区。"类"被加载后的信息、常量、静态变量存放在方法区,方法区全局共享。在Hotspot里方法区也叫"永生代",但两者不能等同。
上面说了,每个类加载完之后,数据都存在方法区里。和String最相关的是方法区中的类数据中的运行时常量池(Run-Time Constant pool),它是每个类私有的。后面会说到,每个class文件里的class文件常量池在类被加载器加载之后,就映射存放在这个地方。另外一个是字符串常量池,它和运行时常量池不是—个概念,字符串常量池是全局共享的。位置就在第二张图里Interned Strings(驻留字符串区)的位置,可以理解为在永生代里,方法区外面。后面会讲到,String.intern()方法,字符串驻留之后,字符串的引用就放在这个String Pool。
String创建分析
示例编译成Test.class文件之后,如下图,除了版本、字段、方法、接口等描述信息外,还有一个也叫常量池的东西(淡绿色区块,即前面提到的class文件常量池),但这个常量池和內存里的常量池不是一个东西。class文件里的常量池主要存两个东西:"字面量"和"符号引用量"。其中字面量就包括类中定义的一些常量,因为String是不可变的(由final关键字修饰过了),所以代码里的"Hello"字符串,就是作为字面量(常量)写在class文件常量池里。
.class文件运行程序用到Test类的时候,Test.class会被类加载器加载,而Test.class文件的信息就会被解析到内存的方法区里。class文件常量池里大部分数据会被加载到运行时常量池。但String不是,例子中的"Hello"的一个引用会被存到同样在Non Heap区的字符串常量池里,而"Hello"本体还是和所有对象一样,被创建在堆区。测试的结果是在新生代的Eden区,但因为一直有一个引用驻留在字符串常量池,所以不会被GC清理掉。这个 Hello对象会生存到整个线程结束。如下图所示,字符串常量池的具体位置是在过去说的永生代里,方法区的外面。
加载器加载类文件后的内存状况注意:这只是在Test类被类加载器加载时候的情形。主线程中的str变量这时候都还没有被创建,但Hello的实例已经在堆里了,对它的引用也已经在字符串常量池里了。等主线程开始创建str变量的时候,虛拟机就会到字符串常量池里找,看有没有equals("Hello")为true的String。如果找到了,就在栈区当前栈帧的局部变量表里创建str变量,然后把字符串常量池里对Hello对象的引用复制给str变量。找不到的话,才会在堆重新创建一个对象,然后把引用驻留到字符串常量区,然后再把引用复制到栈帧的局部变量表。
主线程创建str变量后如果我们定义了很多个值为"Hello"的字符串,比如String str1 = "Hello";String str2 = "Hello";String str3 = "Hello";,有三个变量,也不会在堆上增加String实例,局部变量表里三个变量统一指向同一个堆内存地址。
3个变量的情况但如果是下面的情况:String str1 = "Hello";String str2 = "Hello";String str3 = new String("Hello");,str3会指向一块由new关键字创建的的新对象。虽然字面还是"Hello",但是是完全不同的对象,有不同的内存地址。
有new关键字的情况intern()方法
intern()方法让我们能手动检查字符串常量池,把有新字面值的字符串地址驻留到常量池中。
最后补充一下,JDK7开始HotSpot把Interned String从PermGen移到堆,JDK8又彻底取消了PermGen。但不管怎样,基本原来还是不变的。
网友评论