12、JVM实战:从jvm角度看String

1. String的特性

1、 定义方式;

1、 Stringstr=“hello”;;
2、 Stringstr=“hello”;;
2、 String声明为final,不可被继承;
3、 String实现了Serializable接口和Comparable接口;
4、 String的底层存储结构在jdk1.8之后从char[]变为了byte[];
5、 String不可变,String一旦定义,不可修改,修改=重新定义;
6、 String常量池Stringpool中不会出现相同的内容,Stringpool本质上是一个hashtable,jdk6中长度未1009,jdk7以后为60013(解决hash冲突),该长度可以通过-XX:StringTableSize指定,jdk8之后此值最小1009;

2. 内存分配

使用字面量的方式创建的String对象直接存放在常量池中
String hello = “hello”;存放在常量池中

jdk6: 常量池位于永久代中
jdk7: 常量池挪到了堆
jdk8: 堆

为什么挪到了堆?

1、 永久代空间较小;
2、 垃圾回收不易,fullGC频率极低;

3. 从内存角度看字符串拼接

String hello = "hel"+"lo";

1、 常量与常量的结果放在常量池,原理是编译期优化;

这是常量+常量

1、 如果其中一个是变量,则结果就在堆中(此堆区别于常量池所在堆),原理是StringBuilder拼接;
2、 如果变量+常量的拼接结果调用了intern()方法,如果此结果在常量池中不存在,就会将该结果放入常量池中;

常见问题:

String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2);// true 内存地址均为常量池中的hello

String s3 = s1 + "world";
String s4 = s2 + "world";
System.out.println(s3==s4); // false 每个对象的内存地址都是不一样的

String s5 = (s1 + "world").intern(); 
System.out.println(s4==s5); // true 将结果放入到了常量池,所以是true

3.1 字符串变量拼接

从字节码看字符串拼接的本质

/**
 * @Author: Zy
 * @Date: 2021/12/7 18:50
 * 测试字符串拼接底层内存结构
 */
public class StringConcatTest {
   
     
    public static void main(String[] args) {
   
     
        String s1 = "hello";
        String s2 = "world";
        String s3 = s1 + s2;
    }
}

*
可以看到两个变量拼接本质上就是StringBuilder的append;

3.2 字符串变量拼接测试

既然说拼接底层使用的是StringBuilder,为什么还说他比较慢
代码测试如下:

package com.zy.study12;

/**
 * @Author: Zy
 * @Date: 2021/12/7 21:39
 * 测试字符串拼接和使用StringBuilder.append方法的时间
 */
public class StringBuilderTest {
   
     

    /**
     * 使用拼接的方式拼接100000个字符串
     * @author Zy
     * @date 2021/12/7
     * @param
     * @return void
     */
    public static void stringConcat(){
   
     
        long start = System.currentTimeMillis();
        String result = "";
        for (int i = 0; i < 100000; i++) {
   
     
            result += "test";
        }
        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }

    /**
     * 创建一个StringBuilder对象,进行append
     * @author Zy
     * @date 2021/12/7
     * @param
     * @return void
     */
    public static void stringBuilderAppend(){
   
     
        long start = System.currentTimeMillis();
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 100000; i++) {
   
     
            builder.append("test");
        }

        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }

    public static void main(String[] args) {
   
     
        // 字符串拼接
        StringBuilderTest.stringConcat();

        // append拼接
        StringBuilderTest.stringBuilderAppend();
    }

}

结果差距巨大:
*

这个结果应当是显而易见的,但是我们要思考的是为什么?
原因:

1、 字符串拼接每次拼接都会创建一个StringBuilder对象,而且在返回的时候会再次新建一个String对象(StringBuilder.toString()方法),每次都创建了两个对象,耗时严重;
2、 每次循环创建两个对象占用内存巨大;
3、 内存占用过大,GC的次数也会增加,GC是最耗时的操作;

即使是StringBuilder单一对象append,也可以进行优化:
如果确定了StringBuilder的次数,那么就可以在创建StringBuilder的时候指定他的容量,从而减少StringBuilder的扩容操作,扩容操作也是非常耗时的.

/**
 * 指定StringBuilder容量
 */
public static void stringBuilderAppend(){
   
     
        long start = System.currentTimeMillis();
        StringBuilder builder = new StringBuilder(100000);
        for (int i = 0; i < 100000; i++) {
   
     
            builder.append("test");
        }

        long end = System.currentTimeMillis();
        System.out.println(end-start);
    }

4. String的intern方法

此方法在上文用过,如果调用此方法的字符串不在常量池中,就将此字符串放到常量池中,如果此字符串在常量池中,那么就返回常量池中的字符串
该方法为native方法,即本地方法
源码注释:

Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java* Language Specification.
Returns:
a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
public native String intern();

翻译:

返回字符串对象的规范表示。
字符串池最初是空的,由 String 类私下维护。
当调用 intern 方法时,如果池中已经包含一个等于该 String 对象的字符串(由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中并返回对此 String 对象的引用。
因此,对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为真时,s.intern() == t.intern() 为真。
所有文字字符串和字符串值常量表达式都被实习。字符串文字在 Java* 语言规范的第 3.10.5 节中定义。
返回:
与此字符串具有相同内容的字符串,但保证来自唯一字符串池。

例子:
(“a”+“b”+“c”).intern() == “abc”;

intern() 方法在不同的jdk版本有着不一样的表现
jdk6: 调用intern()方法,如果该字符串不在常量池中存在,就直接在永久代的字符串常量池中创建该字符串对象
jdk7/8及以后: 调用intern()方法,如果该字符串不在常量池中,那么不会再元空间常量池中创建一个新的对象,而是直接使用堆空间中的String对象的地址作为字符串常量池的地址.

String test = new String("hello") + new String("world");
String intern = test.intern();

String test1 = "helloworld";
System.out.println(intern == test); // jdk6中为false  jdk7/8中为true

4.1 intern()的用处

如上文所说,intern()方法可以获取常量池中的对象,如果对象不存在会新建一个,那么当系统中有大量重复字符串存储的时候就可以使用intern()方法
好处:
避免在堆中创建大量对象,占用堆空间; 因为一个字符串在常量池中维护过一次以后,下次再使用intern()方法就会获取常量池中对象的引用,而不是再在堆中创建一个新对象

但是要酌情使用,非频繁的字符串对象不要放入常量池,因为常量池位于方法区,不易gc

5. 题目理解String底层

5.1 new String(“ab”)创建了几个对象

答案:两个

1、 new创建的String对象;
2、 “ab”字面量创建的字符串常量池对象;

证明:通过字节码指令看
*

5.2 字符串对象拼接创建了多少对象

// 创建了多少个对象
String test = new String("hello") + new String("world");

*从字节码上看应该是创建了五个对象
但是如果再往深层看, StringBuilder的toString()方法也是创建了一个新的String对象的.

6. StringTable的垃圾回收

String也是有垃圾回收的
证明:

package com.zy.study12;

/**
 * @Author: Zy
 * @Date: 2021/12/13 21:37
 * 测试String的垃圾回收
 * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
 */
public class StringGc {
   
     
    public static void main(String[] args) {
   
     
        for (int i = 0; i < 100000; i++) {
   
     
            String.valueOf(i).intern();
        }
    }
}
-Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails
堆内存15m 打印StringTable的统计数据 打印gc详细信息

输出:
gc信息,发生了一次YGC,回收了一次eden区
*
StringTable统计信息,可以看到到6w多后,就不再增大,等待垃圾回收后再次创建.
*

6.1 扩展: G1中对String的去重操作

jdk8中默认的垃圾回收期G1在回收的时候会对String进行去重操作

1、 去重去的是堆中的对象,而不是常量池;
2、 jdk1.8中String底层的存储是char[],去重也是针对此char[];
3、 去重的操作:分析每一个可能存在重复的char[],放入一个不重复的hashtable中,之后有另外一个队列会查询此hashtable,如果存在一个一样的char[],那么就会对两个char[]进行去重,释放其中一个char[]的引用,如果此hashtable中不存在该char[],那么就将此char[]放入hasttable中.;

jvm参数开启去重(默认不开启):

UseStringDeduplication 开启去重 true/false
PrintStringDeduplicationStatistics 打印去重统计信息 true/false
StringDeduplicationAgeThreshold 设置去重候选对象的年龄,如果达到此值,被认为可能是重复对象

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: