一、常量池(线程共享数据区):

常量池常被分为两大类:静态常量池和运行时常量池。

静态常量池也就是Class文件中的常量池,存在于Class文件中。

运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。

二、重点了解字符串常量池:

字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。

字符串常量池的存在使JVM提高了性能和减少了内存开销。

常量赋值:

        使用字符串常量池,每当我们使用字面量(String s=1;)创建字符串常量(创建了一个对象)时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)

new 对象赋值:

        使用字符串常量池,每当我们使用关键字new(String s=new String(1);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s

        由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串

intern :

        鉴于String.intern()在API上的说明和new String(a)创建字符串(创建了两个对象【常量池创建一个字符串对象a,堆中创建一个字符串对象b并把a的地址引用指向b】,如果字符串常量池存在则是一个对象)在官方API上的说明,我个人认为字符串常量池存的是字符串对象,当然在JKD7之后,常量池中存储的可能是堆对象的引用,后面会讲到。(可用javap -c反编译即可得到JVM执行的字节码内容,javap -verbose 反编译查看常量池内容)

三、String JVM层解析

两种创建方式比较

  1. String s1=”1”;
  2. String s2=new String(“1”);

从图中可以看出,s1使用””引号(也是平时所说的字面量)创建字符串,在编译期的时候就对常量池进行判断是否存在该字符串,如果存在则不创建直接返回对象的引用;如果不存在,则先在常量池中创建该字符串实例再返回实例的引用给s1。注意:编译期的常量池是静态常量池。

再来看看s2,s2使用关键词new创建字符串,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s2。注意:此时是运行期,那么字符串常量池是在运行时常量池中的。。。。(这段话是转载作者的原文,这里我的认为是编译的时候s1的1和s2的1字符串常量被放在静态常量池中,而运行时虚拟机加载class类的时候把静态常量池中的常量加载到了运行时的常量池中,也就是说其实在类的加载完成时候这个字符串已经存在)

对于new String("")这种 方式是否会在常量池添加字符串对象,之前也有过迷惑,后来看了源码,传进来的是一个字符串对象,这个就是常量池的对象,然后复制该对象的副本

又或者在jdk7及以上的版本下测试以下代码返回结果false,这是因为先添加了123的字符串对象到常量池里了,str1返回的是堆的对象,str1.intern()返回的是常量池的引用。因为在7版本以上的常量池在堆中,intern方法优化会返回首次遇到的对象引用(下面会解析这个方法)所以如果不是先添加到常量池,这里应该的结果是true才对。反向证明测试了

StringBuilder 的注意

new StringBuilder().append("").toString() =>编译优化 new String("");

StringBuilder的toString()不会把append后的结果字符串对象添加到常量池,下面是toString调用实例化String的构造器

同样这里也可以用下面例子测试(JDK7及以上),结果:都是true,这样说明了不会添加到常量池,所以intern方法让常量池直接存储堆中的引用,并且返回。str1 == str2 => true 就可以说明这个常量池里存储的是str1的引用

+= 拼接

String s4 = "a";

s4 +="b";

此过程产生了5个对象 ,在底层调用了StringBuilder的append()方法

底层实现 s4 = new StringBuilder("a").append("b").toString();

其中toString()方法底层 是 new String(), 执行完成之后s4指向了一个新的地址。

“+”连接形式创建字符串

这里注意这种方式底层都是使用StringBuilder的append拼接

(1)String s1=”1”+”2”+”3”; =》编译优化后 String s1 = "123";

使用包含常量的字符串连接创建是也是常量,编译期就能确定了,直接入字符串常量池,当然同样需要判断是否已经存在该字符串。

(2)String s2=”1”+”3”+new String(“1”)+”4”;

编译后=》 String s2 = new StringBuilder("12").append(new String("1")).append("4").toString();

当使用“+”连接字符串中含有变量时,也是在运行期才能确定的。首先连接操作最开始时如果都是字符串常量,编译后将尽可能多的字符串常量连接在一起,形成新的字符串常量参与后续的连接(可通过反编译工具jd-gui进行查看)。

接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象(可变字符串对象),然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。

实际上的实现过程为:String s2=new StringBuilder(“13”).append(new String(“1”)).append(“4”).toString();

当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

(3)String s3=new String(“1”)+new String(“1”);

编译后:String s3 = new StringBuilder().append("1").append("1").toString();

String.intern()解析

String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。

public class StringTest {public static void main(String[] args) {String s3 = new String("1") + new String("1");System.out.println(s3 == s3.intern());}
}

JDK6的执行结果为:false
JDK7和JDK8的执行结果为:true

出现这个现象的原因:主要是jdk6与7以上的字符串常量池的不一样,6的常量池在方法区,7的常量池在堆

JDK6的内存模型如下:

JDK6中的常量池是放在永久代的,永久代和Java堆是两个完全分开的区域。而存在变量使用“+”连接而来的的对象存在Java堆中,且并未将对象存于常量池中。当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。所以结果为false。

JDK7JDK8的内存模型如下:

JDK7中,字符串常量池已经被转移至Java堆中,开发人员也对intern 方法做了一些修改。因为字符串常量池和new的对象都存于Java堆中,为了优化性能和减少内存开销,当调用 intern 方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。所以结果为true。

关于equals和== :

(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。

(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等。

(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。

例子:

/*** 情景一:字符串池* JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象;* 并且可以被共享使用,因此它提高了效率。* 由于String类是final的,它的值一经创建就不可改变。* 字符串池由String类维护,我们可以调用intern()方法来访问字符串池。  */
String s1 = "abc";
//↑ 在字符串池创建了一个对象
String s2 = "abc";
//↑ 字符串pool已经存在对象“abc”(共享),所以创建0个对象,累计创建一个对象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一个对象,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true  值相等  //↑------------------------------------------------------over /*** 情景二:关于new String("")*  */
String s3 = new String("abc");
//↑ 创建了两个对象,一个存放在字符串池中,一个存在与堆区中;
//↑ 还有一个对象引用s3存放在栈中
String s4 = new String("abc");
//↑ 字符串池中已经存在“abc”对象,所以只在堆中创建了一个对象
System.out.println("s3 == s4 : "+(s3==s4));
//↑false   s3和s4栈区的地址不同,指向堆区的不同地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true  s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地区多不同,一个栈区,一个堆区
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true  值相同  //↑------------------------------------------------------over /*** 情景三:  * 由于常量的值在编译的时候就被确定(优化)了。* 在这里,"ab"和"cd"都是常量,因此变量str3的值在编译时就可以确定。* 这行代码编译后的效果等同于: String str3 = "abcd";*/
String str1 = "ab" + "cd";  //1个对象
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11));  //↑------------------------------------------------------over  /*** 情景四:  * 局部变量str2,str3存储的是存储两个拘留字符串对象(intern字符串对象)的地址。*  * 第三行代码原理(str2+str3):* 运行期JVM首先会在堆中创建一个StringBuilder类,* 同时用str2指向的拘留字符串对象完成初始化,* 然后调用append方法完成对str3所指向的拘留字符串的合并,* 接着调用StringBuilder的toString()方法在堆中创建一个String对象,* 最后将刚生成的String对象的堆地址存放在局部变量str4中。*  * 而str5存储的是字符串池中"abcd"所对应的拘留字符串对象的地址。* str4与str5地址当然不一样了。*  * 内存中实际上有五个字符串对象:*       三个拘留字符串对象、一个String对象和一个StringBuilder对象。*/
String str2 = "ab";  //1个对象
String str3 = "cd";  //1个对象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false  //↑------------------------------------------------------over  /*** 情景五:*  JAVA编译器对string + 基本类型/常量 是当成常量表达式直接求值来优化的。*  运行期的两个string相加,会产生新的对象的,存储在堆(heap)中*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67));
//↑str6为变量,在运行期才会被解析。
final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 : "+ (str9 == str89));
//↑str8为常量变量,编译期会被优化

运行结果:

s1 == s2 : true

s1.equals(s2) : true

s3 == s4 : false

s3.equals(s4) : true

s1 == s3 : false

s1.equals(s3) : true

str1 = str11 : true

str4 = str5 : false

str7 = str67 : false

str9 = str89 : true

String是不可变和不能被继承的(final修饰),这样设计的原因主要是为了设计考虑、效率和安全性。

字符串常量池的需要:

只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段。

String对象缓存HashCode:

上面解析String类的源码的时候已经提到了HashCode。Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的唯一性。

安全性

首先String被许多Java类用来当参数,如果字符串可变,那么会引起各种严重错误和安全漏洞。

再者String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。

最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

转:https://blog.csdn.net/qq_34490018/article/details/82110578

String 深入理解相关推荐

  1. str在java中什么意思_Java中String的理解

    Java中String的理解 最近在读String的源码,看了些String的文章,自己对String作了下总结记录下来. 1.String为什么是不可变的? String是final类,不可继承,其 ...

  2. 从C# String类理解Unicode(UTF8/UTF16)

    上一篇博客:从字节理解Unicode(UTF8/UTF16).这次我将从C# code 中再一次阐述上篇博客的内容. C# 代码看UTF8 代码如下: string test = "UTF- ...

  3. 从自定义string类型理解右值引用

    理解右值引用 前言 问题复现 自定义string(CMyString) 遇到问题 图示理解 右值引用 什么是右值 添加右值引用参数的成员方法 结果对比 解决遗留问题 前言 在之前,我写过一篇: 通过自 ...

  4. string java getbytes_从 String.getBytes 理解 Java 编码和解码

    原码,补码,反码 因为原码,补码,反码比较简单,我这里粘贴一个例子进行展示. Unicode 和 UTF-8 的关系 Uincode 是一个字符集.它规定了我们使用到的字或符号的码点(code poi ...

  5. Java常量字符串String理解

    Java常量字符串String理解 以前关于String的理解仅限于三点: 1.String 是final类,不可继承 2.String 类比较字符串相等时时不能用" == ",只 ...

  6. java string string_深入理解Java:String

    在讲解String之前,我们先了解一下Java的内存结构. 一.Java内存模型 按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配. JVM主要管理两 ...

  7. Java基础提升篇:理解String 及 String.intern() 在实际中的应用

    点击上方"好好学java",选择"置顶公众号" 优秀学习资源.干货第一时间送达! 好好学java java知识分享/学习资源免费分享 关注 精彩内容 你所需要的 ...

  8. Java提升篇:理解String 及 String.intern() 在实际中的应用

    String的深入解析 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null:但它又是一种特殊的对象,有其它对象没有的一些 ...

  9. java stringbuffer原理_深入理解Java:String

    在讲解String之前,我们先了解一下Java的内存结构. 一.Java内存模型 按照官方的说法:Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配. JVM主要管理两 ...

最新文章

  1. Windows Server 2008 Server Core - 小脚印,大安全
  2. Optiver Career Fair
  3. 润乾ajax,通过异步请求实现报表组功能
  4. 前端学习(2783):封装myrequest并绑定到全局
  5. 震惊,用了这么多年的 CPU 利用率,其实是错的
  6. TQ2440实现触摸屏和qt图形 解决segmentation fault
  7. modelsim安装_Vivado联合ModelSim
  8. 2021最新手机号正则
  9. 实用小软件实现Mac读写ntfs U盘 移动硬盘
  10. pygame编写井字棋游戏
  11. matlab 函数中引用文件,Matlab如何调用其他m文件中的函数
  12. valgrind和Kcachegrind性能分析工具详解
  13. 【乐理学习】音程 升降调 力度标记
  14. python黑马教程ppt_,python基础教程 PPT
  15. 基于zookeeper的瞬时节点实现分布式锁
  16. 什么是灰度发布,以及灰度发布A/B测试
  17. 接口测试 Pytest的简单示例
  18. Intel VMD技术和SPDK VMD驱动模块介绍及使用
  19. InnoDB和MyISAM区别?
  20. 【论文阅读笔记】Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference

热门文章

  1. 合数python_python输出100以内的质数与合数
  2. Qml自定义等待指示器
  3. poco c++感性认识
  4. 判断用户使用的浏览器是不是IE8
  5. 解决电脑无法运行 Mscomctl.ocx控件 常见的4个问题
  6. [Kali Linux]入门:内网穿透的教程和实战(很适合入门|附图)
  7. allegro17.4的brd文件用AD打开
  8. 掘金总点赞量前 5000 排行发布 | 掘金总关注量前 5000 排行
  9. 2018省赛第九届蓝桥杯真题C语言B组第九题题解 全球变暖
  10. 苹果手机如何远程控制华为安卓平板电脑