前言

近些天,无意间看到一篇博文说,String在jdk9进行了不少优化,其中为了提高字符串拼接效率的优化引起了我的注意。

关于String

String就是不可变的值对象,一旦创建就不可更改。这也是我们不管是replace方法,还是substring方法,都是返回一个新的对象的重要原因。

JDK9的新变化

新增3个包私有构造器,这样说可能没感觉,我先上源码!

/*
     * Package private constructor. Trailing Void argument is there for
     * disambiguating it against other (public) constructors.
     *
     * Stores the char[] value into a byte[] that each byte represents
     * the8 low-order bits of the corresponding character, if the char[]
     * contains only latin1 character. Or a byte[] that stores all
     * characters in their byte sequences defined by the {@code StringUTF16}.
     */
    String(char[] value, int off, int len, Void sig) {
        if (len == 0) {
            this.value = "".value;
            this.coder = "".coder;
            return;
        }
        if (COMPACT_STRINGS) {
            byte[] val = StringUTF16.compress(value, off, len);
            if (val != null) {
                this.value = val;
                this.coder = LATIN1;
                return;
            }
        }
        this.coder = UTF16;
        this.value = StringUTF16.toBytes(value, off, len);
    }

    /*
     * Package private constructor. Trailing Void argument is there for
     * disambiguating it against other (public) constructors.
     */
    String(AbstractStringBuilder asb, Void sig) {
        byte[] val = asb.getValue();
        int length = asb.length();
        if (asb.isLatin1()) {
            this.coder = LATIN1;
            this.value = Arrays.copyOfRange(val, 0, length);
        } else {
            if (COMPACT_STRINGS) {
                byte[] buf = StringUTF16.compress(val, 0, length);
                if (buf != null) {
                    this.coder = LATIN1;
                    this.value = buf;
                    return;
                }
            }
            this.coder = UTF16;
            this.value = Arrays.copyOfRange(val, 0, length << 1);
        }
    }

   /*
    * Package private constructor which shares value array for speed.
    */
    String(byte[] value, byte coder) {
        this.value = value;
        this.coder = coder;
    }

前两个构造器还算正常,但最后一个构造器居然直接使用入参的byte[]作为String的value。这意味着,String有可能是可变的!这可是个大问题。
由于是包私有的,所以这里用反射操作一波,毕竟也就只有setAccessible这个大流氓能干这些鸡鸣狗盗之事了(手动滑稽)

@Test
    public void testString1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<?> aClass = Class.forName("java.lang.String");
        Constructor<?> constructor = aClass.getDeclaredConstructor(byte[].class, byte.class);
        constructor.setAccessible(true);
        byte[] bytes = new byte[6];
        String str = (String) constructor.newInstance(bytes, (byte)0);
        bytes[0] = 'h';
        bytes[1] = 'e';
        bytes[2] = 'l';
        bytes[3] = 'l';
        logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
        bytes[4] = 'o';
        bytes[5] = '.';
        logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
        bytes[5] = '!';
        logger.info("identicalHashCode:{}, str:{}", System.identityHashCode(str), str);
    }

输出结果:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.evan.datastructure.LinkedList06 (file:/D:/InfoTechHome/workspace/datastructure/target/test-classes/) to constructor java.lang.String(byte[],byte)
WARNING: Please consider reporting this to the maintainers of com.evan.datastructure.LinkedList06
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hell  
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hello.
00:33:11.963 [main] INFO com.evan.datastructure.LinkedList06 - identicalHashCode:660017404, str:hello!

Process finished with exit code 0

这个结果有没有超出想象?这是同一个String对象,值却不停地在变化!与此同时,反射调用告警:非法的反射调用,将在未来版本中被拒绝!
但是不必惊讶,并非只有新的这个构造器才能达到这种效果。以反射大流氓的能力,直接操作String中的value属性,也能达到修改String内容的效果!
因此强调一下,Java的封装,大家还是尽量不要破坏它,这可能导致一些不安全问题。例如上面的String骚操作修改了原字符串后,如果直接修改的字面量的对象内容,那么将导致原字面量地址跟新内容的字符地址是一样的,不管是使用==还是equals都无法区分!

jdk9的优化

为了提高String的操作效率,jdk9进行了一些优化:

  1. 底层使用byte[],而不是char[]。并且通过判断是否全部为拉丁字母,对字符串进行压缩。如果是,则使用1个byte对每个字符进行存储,否则使用2个byte存储。
  2. 字符串拼接优化。invokedynamic + StringConcatHelper。我猜测,上面String构造器的新增,也是为了这里得以利用。

字符串拼接

很多时候我们的代码都会进行字符串拼接操作,

String a = “A” + 1 + “h”;
String b = a + ‘d’ + “h”;
对于第一种情况,可以在编译期就优化成字面量,从而不需要拼接。但是对于第二种情况,每拼接一次就会创建一个新的String对象。

  1. jdk8使用StringBuilder进行了优化。StringBuilder的操作也就是先操作char[],然后在toString时调用new String(char[])。只会产生一个String对象,char[]需要拷贝一次(创建String对象时)。
  2. jdk9优化思路:有没有办法直接操作String内部的value(jdk9为byte[])数组!如此,只需要创建一个String对象,也不存在大对象的数组扩容问题,同时还能减少垃圾对象!

jdk9的字符串拼接

到这里总算是把事情衔接上了。为了实现上述的优化思路,首当其冲的就是String本身,这也是新增构造器的意义。然后,为了方便操作value数组,引入了StringConcatHelper,专门干拼接的活。但是这还没完,工具算是搞定了,但是怎么知道要申请多大的value数组呢?这必须得计算一下。
思前想后,如果可以把这些+拼接的变量全部拿到,那不就可以了。但是要用什么方式呢?直接调用StringConcatHelper?这可不行,因为为了封装安全,只有java.lang包下的类才能直接访问。而业务类肯定都不在此列。使用反射?那更不行,本来就是为了提高效率做优化的,使用反射不是倒退了?于是,java7中引入的invokedynamic上场了!

invokedynamic

相比于invokeinterface和invokevirtual,invokedynamic直接由应用程序来选择目标方法。invokedynamic的参数指向的是启动方法BootstrapMethod,该方法会生成一个CallSite调用点。在调用点中封装了目标方法的方法句柄。
方法句柄-MethodHandle,作为invokedynamic的基石,跟反射有什么区别?

  1. 权限校验。方法句柄只在其创建时进行权限校验,而在后续的调用都不会再校验了。但反射每次都会校验权限。
  2. 权限范围。对于反射,如果要范围私有成员,需要setAccessible(true)。而方法句柄则取决于MethodHandle.Lookup的创建位置。MethodHandle.Lookup可以理解为检索目标的上下文。如果在目标类的内部创建,则对于该类的成员的访问都类似于this访问。

再次回到前文:jdk9的字符串拼接:
现在整个调用就可以这样串起来了:
封装MethodHandle,调用时通过invokestatic调用StringConcatHelper。
通过BootstrapMethod,将封装好的MethodHandle再次封装为CallSite。
最后是invokedynamic指令调用。

总结

  1. 反射是个大流氓,没事不要乱用,容易造成不可预知问题。
  2. String的操作优化:
    2.1 jdk8在字符串拼接时使用了StringBuilder
    2.2 jdk9使用了invokedynamic + StringConcatHelper,StringConcatHelper通过改变value数组实现对String对象的修改。invokedynamic则解决StringConcatHelper访问问题。
    2.3 jdk9还修改value数组为byte[],同时拉丁字母进行压缩,减少空间占用
  3. String的操作优化都是jdk的优化,对使用者无感知。