2019-03-23 继续了解了一下字符串常量池…
Java 源码分析 — String 的设计
这篇讲的太好了,源码方面的分析直接看这篇吧…
JDK1.8关于运行时常量池, 字符串常量池的要点
Java 永久代去哪儿了
JAVA中String.intern的理解
几张图轻松理解String.intern()
深入解析String#intern
在线查看 jdk8 源码 这里
讨论若未作说明,皆基于 jdk 1.8 +
这一块太抽象了,很多地方要有图才说明得清楚…我只是稍作记录,便于回顾,就不弄那么详细了…
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}
能看到,String 类由 final 修饰,这代表 String 初始化后就不能改变.这里的改变,指的是创建的对象不可变.
String,StringBuffer 和 StringBuilder 都是 final 类,String 不可变是由于里面真正存储数据的属性 :
/** The value is used for character storage. */
private final char value[];
value 数组为 final…
比如:
String str = "hello";
str = "world";
看起来似乎是改变了,实际上只是引用的指向变了,第一次创建的那个对象 “hello” 并没有变化.
而 String 为 final,效果是 String 不能被继承,避免被其他人继承后破坏.
其次,String 实现了三个接口.
看源码,底层由一个 char 数组实现
private final char value[];
String 不可变,String 类中每一个看起来会修改 String 值的方法,实际上都是新建了一个对象.
以 substring() 举例.
public String substring(int beginIndex, int endIndex) {
...
int subLen = endIndex - beginIndex;
...
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
实际上返回了一个新对象.
PS : 本来想用字符串的拼接举例的,比如
String str = "hell" + "o";
记得在 C++ 中可以重载 “+” 运算符,可是 JAVA 怎么做呢…
[java 编程思想]
java 并不允许程序员重载任何操作符,用于 String 的 “+” 和 “+=” 是 java 中仅有的两个重载过的操作符.
jdk 1.8 文档
字符串连接是通过
StringBuilder
(或StringBuffer
)类及其append
方法实现的.
[java 编程思想]
编译期做了一定程度的优化,自动引入了 StringBuilder,调用它的 append 和 toString 方法实现拼接.
不能依赖编译器的优化,频繁拼接应选用 StringBuilder.
注意,equals() 与 == 在默认情况下都是比较变量指向的首地址.
查看源码可以发现 String 重写了 equals(),使其目的变成比较字符串对象的内容是否一致…
String 创建对象有两种方式.
1. String str1 = "hello";
2. String str2 = new String("hello");
首先第一种,看一段代码.
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
输出 : true
通过双引号指定的字符串对象,在编译期间确定,通过 equals() 查找常量池有无该字符串,若有,返回.若无,创建再返回.
PS : 用 equals() 来比较,比较的到底是字符串对象还是字符串对象的引用都无妨了…
s1,s2 都指向字符串常量池中的同一个地址.
第二种.
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
输出 : false
s1,s2 是堆中两个不同对象的引用.
看看 jdk1.8 的源码 :
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
这种方式实际上创建了两个对象.
in other words, the newly created string is a copy of the argument string.
参数实际传的是一个 String 对象的引用(也就是上面提到的用 双引号 指定字符串对象,此时会有与字符串常量池中数据的判断).这里生成了一个对象,这个对象在编译期间确定.
第二次又用 new 关键字创建了一个对象.这个对象实际是参数对象的副本.这一块和普通对象的创建一致.
查看 对象的创建
字符串常量池是干吗用的??
因为字符串作为一个基础的数据类型,使用非常频繁.
而我们已经知道,String 对象是不可变的.所以当我们需要多个 String 对象时,它们实际上指向的同一个对象还是不同的对象,都没什么关系.
基于此,为了减少频繁创建字符串所带来的性能影响,JVM 为字符串开辟了一个类似于缓存区的空间,这个空间就是字符串常量池.在字符串常量池中,每个字符串只保存一份.(通过 equals() 判断 String 的值)
在 第一种创建方式 中,之所以打印"true",就是因为对于同一个字符串对象,字符串常量池中只保存一份.实际比较的其实是字符串常量池中的同一个地址.
jdk 1.7 以前,字符串常量池在运行时常量池里,而运行时常量池放在方法区.
不过字符串常量池放在方法区 会导致内存溢出错误.(与 JVM 内存分配联系)
于是在 jdk 1.7 中,把字符串常量池从方法区移动到堆中.(运行时常量池中的其他东西不变动)
方法区是 JVM 的概念模型.这一块联系 JVM 的内存模型.
永久代和元空间是方法区的两种不同具体实现.
- MetaSpace(元空间) --> 使用本地内存
- PermGen(永久代) --> 使用 JVM 的内存
字符串常量池存在永久代中,更容易出现性能问题和内存溢出.
jdk 1.7 中,把字符串常量池从方法区移动到堆中.
- 类和方法的信息大小难以确定,给永久代的大小指定带来困难.
- 永久代会为 GC 带来不必要的复杂度.
jdk 1.8 中,HotSpot 虚拟机用元空间取代了永久代.
好处 : java.lang.OutOfMemoryError : PermGen 这个异常不复存在了
String str = "hello";
1. System.out.println(str);
2. System.out.println(str.intern());
从输出结果来看,1,2 没区别…
hello
hello
intern() 返回的是字符串对象的规范表示.
它返回的内容取自具有唯一字符串的值.
其实也就是说它返回的值来自字符串常量池.
看网上的介绍,对于字符串常量池中到底保存的是字符串对象,还是字符串对象的引用,众说纷纭…
看 jdk1.8 对 intern() 的说明.
When the intern method is invoked, if the pool already contains a string equal to this
String
object as determined by theequals(Object)
method, then the string from the pool is returned. Otherwise, thisString
object is added to the pool and a reference to thisString
object is returned.
这个说明对于理解原理貌似没有啥参考价值…
jdk6 和 jdk6+ 的描述是一样的,但是 jdk6 和 jdk6+ 的实现并不一致.
尝试跟踪源码.
public native String intern();
是个本地方法,继续深入 :
在线查看
\openjdk7\jdk\src\share\native\java\lang\String.c
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
日…到这步跟不下去了.还是搜索一下看看别人有没有关于这玩意儿的资料吧…
果然找到!!
深入解析String#intern
还是直接给结果吧…
看段代码:
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
jdk6 输出 : false false
jdk7 输出 : false true
why?
jdk6
上面已经提到过,String s = new String("1");
这种方式创建字符串实际生成了两个字符串对象.
首先,构造器中传入了一个字符串对象 “1”,它就被放在字符串常量池中.
然后在堆中创建一个字符串对象,s 指向这个字符串对象的首地址.
调用 s.intern()
,字符串常量池已经存在该对象,直接返回.
s2 指向的就是字符串常量池中 “1” 的首地址.
而 == 判断的是地址是否相同.显然不同.
s3 与 s4 不同之处在于,执行 s3.intern()
时,字符串常量池中还没有字符串对象,所以需要在字符串常量池中创建一个对象.其余的与上相同.
jdk 7
s 与 s2 情况与 jdk6 一样.
但是执行 s3.intern()
时,字符串常量池中还没有字符串对象,此时先堆中有没有该字符串对象,如果有,就把堆中字符串对象的引用保存到字符串常量池中.
所以 s4 指向的和 s3 一样,都是堆中 String 对象的引用.
所以判断为 true.
总结 :
jdk6 : 当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用.否则,将该字符串对象添加到字符串常量池中,并且返回该字符串对象的引用.
jdk6+ : 当调用 intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用.否则,如果该字符串对象已经存在于 Java堆 中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用.
jdk6 字符串只存在于 字符串常量池.
jdk6+ 字符串存在于字符串常量池和 Java堆.
那么回到上面那个问题,字符串常量池中到底保存的是字符串对象,还是字符串对象的引用?
答案就是,jdk6 保存的是字符串对象,jdk6+ 既保存了字符串对象,又保存了字符串对象的引用.
https://hqweay.cn/2018/10/09/jvm/#内存管理机制
[JAVA 编程思想中文版] P284
尝试了一下拼接字符串,然后调用 jdk 自带的反编译工具查看…
拼接字符串被 JVM 优化,操作过于频繁时会使用 StringBuilder.
首先对 java 文件进行编译.如 Test.java
javac Test.java
生成了 Test.class
再执行 javap -c Test.calss
或者 javap -c Test
即可查看反编译的代码.
String str4 = "haha" + "hehe" + 112 + "shsh";
当只有这么一行的时候,反编译效果.
ldc #5 // String hahahehe112shsh
astore 4
后面并未注释使用了 StringBuilder.
但是执行
for(int i = 0; i < 15; i++){
str4 += "llo";
}
反编译发现:
30: new #6 // class java/lang/StringBuilder
33: dup
34: invokespecial #7 // Method java/lang/StringBuilder."":()V
37: aload 4
39: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
42: ldc #9 // String llo
44: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
50: astore 4
可以大概看出使用了 StringBuilder 的append().