String源码阅读笔记,string源码阅读
String源码阅读笔记,string源码阅读
-
为什么设置final
-
为什么安全性不如char[]
-
new String("")的过程
-
怎么比较大小?
-
怎么比较相等?
-
怎么计算hashcode?
-
intern的作用?
-
说说字符串常量池
-
substring方法做了什么
-
包私有构造器
-
大小写转换的原理
1.final类以及字段
被final修饰的类不能被继承,意味着在类似于JDK常量池之类的底层实现的时候,不需要考虑由于有子类带来的其他问题。内部的字符数组也被定义为final,因此string内部的char[]是不能修改的。由于这些特性,Java中的String具有不可变的特性,因此JVM可以优化字符串的内存使用:只存储一份字面量字符串在常量池中,这个过程称为interning(翻译为内化?)。
在内存信息敏感的情况下,char[]比String安全,因为String内部的char[]是final类型的,不能用过后立刻擦除,意味着敏感信息一直会在内存中存在直到垃圾回收。
3.字符串构造的过程
假设使用字符串构造一个String对象:
public static void main(String[] args) { new String("hello"); }
将上述代码反编译得到如下结果:
public static void main(java.lang.String[]); Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String hello 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: pop 10: return
0:新建String对象
4:ldc命令,从字符串常量池加载"hello"
6:调用String的构造方法
String构造方法:
/** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 public String(String original) { this.value = original.value; this.hash = original.hash; }
由此可以推出结论,new String("")方法得到的字符串对象,共享的同一个字符数组char[],达到节约内存的目的,这也是char[]被声明为final的原因,为了在多个String对象之间共享,必须声明为不可变。
如果不想要共享同一个字符数组,则可以使用入参为char[]的构造方法,该方法复制了一份char[]:
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
我们来看看上面两个构造方法有没有满足String的不可变特性。
-
入参是String,由于String的value是不可变的,因此赋值后的value也是不可变的
-
入参是char[],拷贝一份新的字符数组再赋值,赋值后的value是拷贝后的数组的唯一一份引用,没有其他人能改变它,因此也是不可变的
4.包私有构造器
入参是字符数组char[]的构造器,每次都要重新拷贝一份数组,浪费空间,但是为了安全又不得不这么做。
如果是自己人调用,明确不会修改作为入参的字符数组,没有必要每次都重新复制一份字符数组,那能不能提供一个性能更好的构造方法呢,答案是有的:
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
这个构造方法只允许同一个包下面的类调用,也就是自己人调用。新增的参数share只是为了重载用的。
5.intern方法
该方法能够从将字符串缓存到常量池中,并返回常量池中的引用,使得下面语句成立:
Assert.assertTrue("abc" == new String("abc").intern())
根据.equals()方法判断字符串是否存在常量池中,如果已经存在,直接返回常量池中的引用,否则缓存入常量池再返回常量池中的引用。
根据实验,intern出来的字符串如果没有被引用,会被垃圾回收。
for (int i = 0; i < Integer.MAX_VALUE; i++) { System.out.println(String.valueOf(i).intern()); }
6.substring方法做了什么
返回一份字符数组的拷贝:
public String(char value[], int offset, int count) { this.value = Arrays.copyOfRange(value, offset, offset+count); }
为什么这里不使用第四点提到的包私有构造方法来节约内存呢?JDK7之前实际上是使用的,JDK7之后才替换成这个,因为使用共享字节数组会造成内存泄漏,如下所示:
String longString = "...a very long string..."; String part = longString.substring(20, 40); return part;
假设longString是一个很长的字符串,但是我们只需要对part进行解析,如果内部数组是从longString那里共享的,虽然longString对象可以被回收,但是它的内部数组不能被回收。表面上看part的长度只有20,实际上内部数组的长度却很大,反而浪费了更多的内存。
7.比较大小
compareTo()方法
对于每一位char,如果不一致,两者做减法并直接返回相差的值,如果都一样,长度更长的则更大。
规律总结如下:
-
abc>ab
-
abc<d
-
abc>aaaa
8.比较相等
contentEqulas与equals方法
-
contentEqulas比较的是实现了CharSequence方法的类的每个char
-
equlas不仅仅比较char还比较是否是String对象
9.hashCode
hashCode的实现使用了一下数学公式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
选择31有两个原因,第一是素数,可以减少散列冲突的情况,尽可能散列均匀;第二是2的幂,有利于高性能运算,在CPU层面执行时直接左移5位再减1,避免了更耗时的乘法运算,节约了很多个CPU的时间片。
10.字符串常量池
10.1.原理
10.1.1.常量池中的字符串
当我们创建字符串变量并通过双引号赋值字面量给它的时候,JVM会搜索字符串常量池中的对象,通过equals方法比较是否相等,如果找到,则直接返回该对象的地址的引用,不需要再额外分配内存;如果没找到,则将当前字面量的字符串添加到常量池中并且返回常量池中的引用。例子:
String constantString1 = "Baeldung"; String constantString2 = "Baeldung"; assertThat(constantString1).isSameAs(constantString2);
10.1.2.构造器创建的字符串
如果是通过new操作符创建的字符串,JVM会创建一个新的对象并把它存储到堆空间上。
像这样创建出来的字符串,都会指向一个不同的内存地址。例子:
String constantString = "Baeldung"; String newString = new String("Baeldung"); assertThat(constantString).isNotSameAs(newString);
10.1.3.字面量字符串vs字符串对象
如果使用双引号创建的字符串,则返回常量池中的字符串;如果是new出来的字符串,则总是会在堆上创建一个新的对象。
证明字符串常量池的例子:
String first = "Baeldung"; String second = "Baeldung"; System.out.println(first == second); // True
证明创建新对象的例子:
String third = new String("Baeldung"); String fourth = new String("Baeldung"); System.out.println(third == fourth); // False
两种方式的比较:
String fifth = "Baeldung"; String sixth = new String("Baeldung"); System.out.println(fifth == sixth); // False
10.2.常量池垃圾回收
根据 https://www.baeldung.com/java-string-pool 的资料显示,Java7之前,JVM将字符串常量池放在PermGen空间,拥有固定大小,这使得常量池不能够在运行时拓展,也不能被垃圾收集器回收。如果内化了太多的字符串,就会导致OOM。
Java7之后,字符串常量池存储在堆空间,因此能够被垃圾收集器回收。这个方法的优势是减少了OOM的风险,因为不再被引用的字符串会被移出字符串常量池,以释放内存。
如果是字面量的intern,由于会被隐式调用,因此不会被垃圾回收。
10.3.性能和优化
Java 6的增大字符串常量池的方法是增大永久代的空间:
-XX:MaxPermSize=1G
Java 7之后有更多的选项,例如:
-XX:StringTableSize=
`4901
这个值需要在1009 和 2305843009213693951之间
查看常量池大小的方法:
-XX:+PrintFlagsFinal
-XX:+PrintStringTableStatistics
11.大小写转换
先扫描到第一个大写的字符,拷贝前面的到新的字符串数组,再对之后的每个char作判断,如果大写则转小写,再赋值给数组 。
12.valueOf
调用各个包装类型的toString()方法。
13.重写String类
https://stackoverflow.com/questions/22094111/how-to-print-the-whole-string-pool
参考资料
https://www.baeldung.com/java-string-pool
https://stackoverflow.com/questions/18406703/when-will-a-string-be-garbage-collected-in-java
http://xmlandmore.blogspot.com/2013/05/understanding-string-table-size-in.html
https://www.hollischuang.com/archives/99
相关文章
- 暂无相关文章
用户点评