版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处连接和本声明。
本文连接:https://blog.csdn.net/qq_34490018/article/details/82110578
目录html
JVM相关知识java
String源码分析数组
Srtring在JVM层解析缓存
String典型案例安全
String被设计成不可变和不能被继承的缘由性能优化
JVM相关知识多线程
下面这张图是JVM的体系结构图:app
下面咱们了解下Java栈、Java堆、方法区和常量池:函数
Java栈(线程私有数据区):工具
每一个Java虚拟机线程都有本身的Java虚拟机栈,Java虚拟机栈用来存放栈帧,每一个方法被执行的时候都会同时建立一个栈帧(Stack Frame)用于存储局部变量表、操做栈、动态连接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
Java堆(线程共享数据区):
在虚拟机启动时建立,此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配。
方法区(线程共享数据区):
方法区在虚拟机启动的时候被建立,它存储了每个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。在JDK8以前永久代是方法区的一种实现,而JDK8元空间替代了永久代,永久代被移除,也能够理解为元空间是方法区的一种实现。
常量池(线程共享数据区):
常量池常被分为两大类:静态常量池和运行时常量池。
静态常量池也就是Class文件中的常量池,存在于Class文件中。
运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。
下面重点了解的是字符串常量池:
字符串常量池存在运行时常量池之中(在JDK7以前存在运行时常量池之中,在JDK7已经将其转移到堆中)。
字符串常量池的存在使JVM提升了性能和减小了内存开销。
使用字符串常量池,每当咱们使用字面量(String s=”1”;)建立字符串常量时,JVM会首先检查字符串常量池,若是该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。若是字符串不存在常量池中,就会实例化该字符串而且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
使用字符串常量池,每当咱们使用关键字new(String s=new String(”1”);)建立字符串常量时,JVM会首先检查字符串常量池,若是该字符串已经存在常量池中,那么再也不在字符串常量池建立该字符串对象,而直接堆中复制该对象的副本,而后将堆中对象的地址赋值给引用s,若是字符串不存在常量池中,就会实例化该字符串而且将其放到常量池中,而后在堆中复制该对象的副本,而后将堆中对象的地址赋值给引用s。
下图是API说明:
翻译为:“初始化一个新建立的字符串对象,以便它表示与参数相同的字符序列;换句话说,新建立的字符串是参数字符串的副本。除非须要显式的原始副本,不然使用此构造函数是没必要要的,由于字符串是不可变的。”
因为String字符串的不可变性咱们能够十分确定常量池中必定不存在两个相同的字符串。
鉴于String.intern()在API上的说明和new String(“a”)建立字符串(建立了两个对象,若是字符串常量池存在则是一个对象)在官方API上的说明,我我的认为字符串常量池存的是字符串对象,固然在JKD7以后,常量池中存储的多是堆对象的引用,后面会讲到。(可用javap -c反编译便可获得JVM执行的字节码内容,javap -verbose 反编译查看常量池内容)
关于常量池,我会在后面的一篇相关文章中进行解析。。。。
String源码分析
下面是String类的部分源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
首先咱们来看看String类,String类是用final修饰的,这意味着String不能被继承,并且全部的成员方法都默认为final方法。
接下来看看String类实现的接口:
java.io.Serializable:这个序列化接口仅用于标识序列化的语意。
Comparable<String>:这个compareTo(T 0)接口用于对两个实例化对象比较大小。
CharSequence:这个接口是一个只读的字符序列。包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。
最后看看String的成员属性:
value[] :char数组用于储存String的内容。
offset :存储的第一个索引。
count :字符串中的字符数。
hash :String实例化的hashcode的一个缓存,String的哈希码被频繁使用,将其缓存起来,每次使用就不必再次去计算,这也是一种性能优化的手段。这也是String被设计为不可变的缘由之一,后面会讲到。
下面是一个String类的一个方法实现:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
能够发现,最初传入的String并无改变,其返回的是一个new String(),即新建立的String对象。其实String类的其余方法也是如此,并不会改变原字符串。这也是String的不可变性,后面会讲到。
Srtring在JVM层解析
建立字符串形式
首先形如声明为S ss是一个类S的引用变量ss(咱们经常称之为句柄,后面JVM相关内容会讲到),而对象通常经过new建立。因此这里的ss仅仅是引用变量,并非对象。
建立字符串的两种基本形式:
String s1=”1”;
String s2=new String(“1”);
从图中能够看出,s1使用””引号(也是平时所说的字面量)建立字符串,在编译期的时候就对常量池进行判断是否存在该字符串,若是存在则不建立直接返回对象的引用;若是不存在,则先在常量池中建立该字符串实例再返回实例的引用给s1。注意:编译期的常量池是静态常量池,之后和会讲。。。。
再来看看s2,s2使用关键词new建立字符串,JVM会首先检查字符串常量池,若是该字符串已经存在常量池中,那么再也不在字符串常量池建立该字符串对象,而直接堆中复制该对象的副本,而后将堆中对象的地址赋值给引用s2,若是字符串不存在常量池中,就会实例化该字符串而且将其放到常量池中,而后在堆中复制该对象的副本,而后将堆中对象的地址赋值给引用s2。注意:此时是运行期,那么字符串常量池是在运行时常量池中的。。。。
“+”链接形式建立字符串(更多能够查看API):
(1)String s1=”1”+”2”+”3”;
使用包含常量的字符串链接建立是也是常量,编译期就能肯定了,直接入字符串常量池,固然一样须要判断是否已经存在该字符串。
(2)String s2=”1”+”3”+new String(“1”)+”4”;
当使用“+”链接字符串中含有变量时,也是在运行期才能肯定的。首先链接操做最开始时若是都是字符串常量,编译后将尽量多的字符串常量链接在一块儿,造成新的字符串常量参与后续的链接(可经过反编译工具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”);
这个过程跟(2)相似。。。。。。
String.intern()解析
String.intern()是一个Native方法,底层调用C++的 StringTable::intern 方法,源码注释:当调用 intern 方法时,若是常量池中已经该字符串,则返回池中的字符串;不然将此字符串添加到常量池中,并返回字符串的引用。
下面咱们来看个案例:
public class StringTest {
public static void main(String[] args) {
// TODO 自动生成的方法存根
String s3 = new String("1") + new String("1");
System.out.println(s3 == s3.intern());
}
}
JDK6的执行结果为:false
JDK7和JDK8的执行结果为:true
JDK6的内存模型以下:
咱们都知道JDK6中的常量池是放在永久代的,永久代和Java堆是两个彻底分开的区域。而存在变量使用“+”链接而来的的对象存在Java堆中,且并未将对象存于常量池中,当调用 intern 方法时,若是常量池中已经该字符串,则返回池中的字符串;不然将此字符串添加到常量池中,并返回字符串的引用。因此结果为false。
JDK7JDK8的内存模型以下:
JDK7中,字符串常量池已经被转移至Java堆中,开发人员也对intern 方法作了一些修改。由于字符串常量池和new的对象都存于Java堆中,为了优化性能和减小内存开销,当调用 intern 方法时,若是常量池中已经存在该字符串,则返回池中字符串;不然直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。因此结果为true。
String典型案例
关于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方法进行了重写用来比较指向的对象所存储的内容是否相等。
public class StringTest {
public static void main(String[] args) {
// TODO 自动生成的方法存根
/**
* 情景一:字符串池
* 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为常量变量,编译期会被优化
//↑------------------------------------------------------over
}
}
运行结果:
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被设计成不可变和不能被继承的缘由
String是不可变和不能被继承的(final修饰),这样设计的缘由主要是为了设计考虑、效率和安全性。
字符串常量池的须要:
只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现能够在运行时节约不少heap空间,由于不一样的字符串变量都指向池中的同一个字符串。倘若字符串对象容许改变,那么将会致使各类逻辑错误,好比改变一个对象会影响到另外一个独立对象. 严格来讲,这种常量池的思想,是一种优化手段。
String对象缓存HashCode:
上面解析String类的源码的时候已经提到了HashCode。Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的惟一性。
安全性
首先String被许多Java类用来当参数,若是字符串可变,那么会引发各类严重错误和安全漏洞。
再者String做为核心类,不少的内部方法的实现都是本地调用的,即调用操做系统本地API,其和操做系统交流频繁,假如这个类被继承重写的话,不免会是操做系统形成巨大的隐患。
最后字符串的不可变性使得同一字符串实例被多个线程共享,因此保障了多线程的安全性。并且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
学习参考资料:
https://www.cnblogs.com/xiaoxi/p/6036701.html
https://tech.meituan.com/in_depth_understanding_string_intern.html ———————————————— 版权声明:本文为CSDN博主「个人书包哪里去了」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处连接及本声明。原文连接:https://blog.csdn.net/qq_34490018/article/details/82110578