前言
近些天,无意间看到一篇博文说,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进行了一些优化:
- 底层使用byte[],而不是char[]。并且通过判断是否全部为拉丁字母,对字符串进行压缩。如果是,则使用1个byte对每个字符进行存储,否则使用2个byte存储。
- 字符串拼接优化。invokedynamic + StringConcatHelper。我猜测,上面String构造器的新增,也是为了这里得以利用。
字符串拼接
很多时候我们的代码都会进行字符串拼接操作,
String a = “A” + 1 + “h”;
String b = a + ‘d’ + “h”;
对于第一种情况,可以在编译期就优化成字面量,从而不需要拼接。但是对于第二种情况,每拼接一次就会创建一个新的String对象。
- jdk8使用StringBuilder进行了优化。StringBuilder的操作也就是先操作char[],然后在toString时调用new String(char[])。只会产生一个String对象,char[]需要拷贝一次(创建String对象时)。
- 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的基石,跟反射有什么区别?
- 权限校验。方法句柄只在其创建时进行权限校验,而在后续的调用都不会再校验了。但反射每次都会校验权限。
- 权限范围。对于反射,如果要范围私有成员,需要setAccessible(true)。而方法句柄则取决于MethodHandle.Lookup的创建位置。MethodHandle.Lookup可以理解为检索目标的上下文。如果在目标类的内部创建,则对于该类的成员的访问都类似于this访问。
再次回到前文:jdk9的字符串拼接:
现在整个调用就可以这样串起来了:
封装MethodHandle,调用时通过invokestatic调用StringConcatHelper。
通过BootstrapMethod,将封装好的MethodHandle再次封装为CallSite。
最后是invokedynamic指令调用。
总结
- 反射是个大流氓,没事不要乱用,容易造成不可预知问题。
- String的操作优化:
2.1 jdk8在字符串拼接时使用了StringBuilder
2.2 jdk9使用了invokedynamic + StringConcatHelper,StringConcatHelper通过改变value数组实现对String对象的修改。invokedynamic则解决StringConcatHelper访问问题。
2.3 jdk9还修改value数组为byte[],同时拉丁字母进行压缩,减少空间占用 - String的操作优化都是jdk的优化,对使用者无感知。