• JDK对String操作优化


    前言

    近些天,无意间看到一篇博文说,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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    前两个构造器还算正常,但最后一个构造器居然直接使用入参的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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:

    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个结果有没有超出想象?这是同一个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的优化,对使用者无感知。
  • 相关阅读:
    OAuth2基础概念篇
    MutationObserver对象
    如何让脚本在任意地方可执行
    MPEG-NTA-NI 甲氧基聚乙二醇-氮川三乙酸-镍
    【JVM】JVM垃圾回收机制GC
    LyScript 验证PE程序开启的保护
    最新版微信如何打开青少年模式?
    SQL11 高级操作符练习(1)
    2.let 和 const 命令
    基于机器学习LightGBM进行海洋轨迹预测 代码+数据
  • 原文地址:https://blog.csdn.net/Evan_L/article/details/126005529