美文网首页java高级开发群青春校园开发技巧
阿里从业十年的架构师浅谈JVM系列之:String.intern

阿里从业十年的架构师浅谈JVM系列之:String.intern

作者: Java斗帝之路 | 来源:发表于2020-07-27 13:43 被阅读0次

    简介

    StringTable是什么?它和String.intern有什么关系呢?在字符串对象的创建过程中,StringTable起到了什么作用呢?

    一切的答案都在本文中,快来看看吧。

    intern简介

    intern是String类中的一个native方法,所以它底层是用c++来实现的。感兴趣的同学可以去查看下JVM的源码了解更多的内容。

    这里我们主要谈一下intern的作用。

    intern返回的是这个String所代表的对象,怎么理解呢?

    String class维护了一个私有的String pool, 这个String pool也叫StringTable,中文名字叫做字符串常量池。

    当我们调用intern方法的时候,如果这个StringTable中已经包含了一个相同的String对象(根据equals(Object)方法来判断两个String对象是否相等),那么将会直接返回保存在这个StringTable中的String。

    如果StringTable中没有相同的对象,那么这个String对象将会被加入StringTable,并返回这个String对象的引用。

    所以,当且仅当 s.equals(t) 的时候s.intern() == t.intern()。

    intern和字符串字面量常量

    我们知道在类文件被编译成class文件时,每个class文件都有一个常量池,常量池中存了些什么东西呢?

    字符串常量,类和接口名字,字段名,和其他一些在class中引用的常量。

    看一个非常简单的java类:

    publicclassSimpleString{

    publicString site="www.flydean.com";}

    然后看一下编译出来的class文件中的Constant Pool:

    Constant pool:

    #1 = Methodref

    #2.

    #3          // java/lang/Object."":()V  #2 = Class

    #4            // java/lang/Object  #3 = NameAndType

    #5:

    #6          // "":()V  #4 = Utf8              java/lang/Object  #5 = Utf8#6 = Utf8              ()V

    #7 = String

    #8            // www.flydean.com  #8 = Utf8              www.flydean.com

    #9 = Fieldref

    #10.

    #11        // com/flydean/SimpleString.site:Ljava/lang/String;  #10 = Class

    #12            // com/flydean/SimpleString  #11 = NameAndType

    #13:

    #14        // site:Ljava/lang/String;  #12 = Utf8              com/flydean/SimpleString  #13 = Utf8              site  #14 = Utf8              Ljava/lang/String;

    #15 = Utf8              Code

    #16 = Utf8              LineNumberTable

    #17 = Utf8              LocalVariableTable

    #18 = Utf8              this

    #19 = Utf8              Lcom/flydean/SimpleString;

    #20 = Utf8              SourceFile

    #21 = Utf8              SimpleString.java

    上面的结果,我们可以看到class常量池中的index 7存放了一个字符串,这个字符串的实际内容存放在index 8中,是一个变种的Utf8的编码。

    #7=String#8// www.flydean.com 

      #8 = Utf8              www.flydean.com

    好了,现在问题来了,class文件中的常量池在运行时需要转换成为JVM能够识别的运行时常量池,这个运行时的常量池和StringTable和intern有什么关系呢?

    在java对象的实例化过程中,所有的字符串字面量都会在实例化的时候自动调用intern方法。

    如果是第一次调用,则会创建新的String对象,存放在String Table中,并返回该String对象的引用。

    分析intern返回的String对象

    从上面的图中,我们也可以出来String Table中存储的是一个String对象,它和普通的String对象没有什么区别,也分为对象头,底层的byte数组引用,int hash值等。

    如果你不相信,可以使用JOL来进行分析:

    log.info("{}",ClassLayout.parseInstance("www.flydean.com".intern()).toPrintable());

    看下输出结果:

    INFO com.flydean.StringInternJOL - java.lang.String object internals:

    OFFSETSIZETYPEDESCRIPTIONVALUE04(objectheader)05000000(00000101000000000000000000000000)(5)

    44(objectheader)00000000(00000000000000000000000000000000)(0)

    84(objectheader)771a0600(01110111000110100000011000000000)(399991)

    124byte[]String.value[119,119,119,46,102,108,121,100,101,97,110,46,99,111,109]

    164intString.hash0

    201byteString.coder0

    211booleanString.hashIsZerofalse

    222(lossduetothenextobjectalignment)

    Instance size: 24 bytes   

    Space losses:0bytes

    internal+2bytesexternal=2bytestotal

    分析实际的问题

    有了上面的知识,让我们分析一下下面的实际问题吧:

    Stringa =newString(newchar[]{'a','b','c'});

    Stringb = a.intern();

    System.out.println(a == b);

    Stringx =newString("def");

    Stringy = x.intern();

    System.out.println(x == y);

    两个很简单的例子,答案是什么呢?答案是true和false。

    第一个例子按照上面的原理很好理解,在构建String a的时候,String table中并没有”abc“这个字符串实例。所以intern方法会将该对象添加到String table中,并返回该对象的引用。

    所以a和b其实是一个对象,返回true。

    那么第二个例子呢?初始化String的时候,不是也没有”def“这个字符串吗?为什么回返回false呢?

    还记得我们上面一个小节分析的吗?所有的字符串字面量在初始化的时候会默认调用intern方法。

    也就是说”def“在初始化的时候,已经调用了一次intern了,这个时候String table中已经有”def“这个String了。

    所以x和y是两个不同的对象,返回的是false。

    注意,上面的例子是在JDK7+之后运行的,如果你是在JDK6中运行,那么得到的结果都是false。

    JDK6和JDK7有什么不同呢?

    在JDK6中,StringTable是存放在方法区中的,而方法区是放在永久代中的。每次调用intern方法,如果String Table中不存在该String对象,则会将该String对象进行一次拷贝,并返回拷贝后String对象的引用。

    因为做了一次拷贝,所以引用的不是同一个对象了。结果为false。

    在JDK7之后,StringTable已经被转移到了java Heap中了,调用intern方法的时候,StringTable可以直接将该String对象加入StringTable,从而指向的是同一个对象。

    G1中的去重功能

    如果频繁的进行String的复制,实际上是非常消耗内存空间的。所以在G1垃圾回收器中,可以使用下面的:

    -XX:+UseStringDeduplication

    来开启String的去重功能。

    我们还记得String对象的底层结构吧,就是一个byte[]数组,String去重的原理就是让多个字符串对象底层的byte数组指向同一个地方。从而节省内存。

    我们可以通过使用:

    -XX:+PrintStringTableStatistics

    参数来查看StringTable的大小。并通过:

    -XX:StringTableSizen=n

    来指定StringTable的大小。

    最后

    《深入理解java虚拟机》第二版 57页

    对String.intern()返回引用的测试代码如下:

    结果是 :

    true

    false

    可能很多人觉得这个结果很奇怪,在这里我们进行深入地探究。

    -------------------------------转发+关注后私信“666”免费获取这本书籍的PDF版-----------------------------------

    书中写道,如果JDK1.6会返回两个false,JDK1.7运行则会返回一个true一个false。

    因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

    在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。

    str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。

    那么就有疑问了,这个“java”字符串在哪里出现过呢?显然并不是直接出现在这个类里面。

    我们分别打开String 、StringBuilder和System类的源码看看有啥发现,其中在System类里发现

    根据注释可以看出来,System是由虚拟机自动调用的。

    在initializeSystemClass 方法中发现调用了Version对象的init静态方法

    而Version类里 laucher_name是私有静态字符串常量

    因此sun.misc.Version 类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

    因此我们修改一下代码:

    String str2 =newStringBuilder("Java(TM) SE ").append("Runtime Environment").toString();

    System.out.println(str2.intern() == str2)

    发现结果还是false

    从而更加证实了我们的猜测。

    再遇到类似问题的时候,希望大家可以多从源码角度去追本溯源,能够多分享出来。

    转发+关注后私信“666”免费获取这本书籍的PDF版,第2版第3版打包参考;

    相关文章

      网友评论

        本文标题:阿里从业十年的架构师浅谈JVM系列之:String.intern

        本文链接:https://www.haomeiwen.com/subject/vzdzlktx.html