1.常见字符串编码常见的字符串编码有:
latin1 只能保存ascii字符,又称iso-8859-1。
utf-8是一种变长字节编码,使用1个、2个或3个字节表示一个字符。由于中文通常需要3个字节表示,中文场景utf-8编码通常需要更多的空间,替代的方案是gbk/gb2312/gb18030。
utf-16 2个字节,一个字符需要使用2个byte表示,又称ucs-2 (2-byte universal character set)。根据大小端的区分,utf-16有两种形式,utf-16be和utf-16le,缺省utf-16指utf-16be。java语言中的char是utf-16le编码。
gb18030采用变长字节编码方式,每个字符使用1个、2个或3个字节来表示。与utf8类似,用2个字符表示中文可以节省字节数,但这种方法在国际上不具通用性。
为了计算方便,内存中字符串通常使用等宽字符,java语言中char和.net中的char都是使用utf-16。早期windows-nt只支持utf-16。
2.编码转换性能utf-16和utf-8之间转换比较复杂,通常性能较差。
如下是一个将utf-16转换为utf-8编码的实现,可以看出算法比较复杂,所以性能较差,这个操作也无法使用vector api做优化。
static int encodeutf8(char[] utf16, int off, int len, byte[] dest, int dp) { int sl = off + len, last_offset = sl - 1; while (off < sl) { char c = utf16[off++]; if (c < 0x80) { // have at most seven bits dest[dp++] = (byte) c; } else if (c < 0x800) { // 2 dest, 11 bits dest[dp++] = (byte) (0xc0 | (c >> 6)); dest[dp++] = (byte) (0x80 | (c & 0x3f)); } else if (c >= '\ud800' && c < '\ue000') { int uc; if (c < '\udc00') { if (off > last_offset) { dest[dp++] = (byte) '?'; return dp; } char d = utf16[off]; if (d >= '\udc00' && d < '\ue000') { uc = (c << 10) + d + 0xfca02400; } else { throw new runtimeexception("encodeutf8 error", new malformedinputexception(1)); } } else { uc = c; } dest[dp++] = (byte) (0xf0 | ((uc >> 18))); dest[dp++] = (byte) (0x80 | ((uc >> 12) & 0x3f)); dest[dp++] = (byte) (0x80 | ((uc >> 6) & 0x3f)); dest[dp++] = (byte) (0x80 | (uc & 0x3f)); off++; // 2 utf16 } else { // 3 dest, 16 bits dest[dp++] = (byte) (0xe0 | ((c >> 12))); dest[dp++] = (byte) (0x80 | ((c >> 6) & 0x3f)); dest[dp++] = (byte) (0x80 | (c & 0x3f)); } } return dp;}
由于java中char是utf-16le编码,如果需要将char[]转换为utf-16le编码的byte[]时,可以使用sun.misc.unsafe#copymemory方法快速拷贝。比如:
static int writeutf16le(char[] chars, int off, int len, byte[] dest, final int dp) { unsafe.copymemory(chars , char_array_base_offset + off * 2 , dest , byte_array_base_offset + dp , len * 2 ); dp += len * 2; return dp;}
3.java string的编码由于不同版本的jdk实现的字符串处理方法不同,因此会出现不同的性能表现。在jdk 9之后,尽管char仍使用utf-16编码,但string内部也可以使用latin1编码。
3.1. jdk 6之前的string实现static class string { final char[] value; final int offset; final int count;}
在java 6之前,string.substring方法产生的string对象和原来string对象共用一个char[] value,这会导致substring方法返回的string的char[]被引用而无法被gc回收。很多库会避免使用substring方法,以防止在jdk 6及以下版本中出现问题。
3.2. jdk 7/8的string实现static class string { final char[] value;}
jdk 7之后,字符串去掉了offset和count字段,value.length就是原来的count。这避免了substring引用大char[]的问题,优化也更容易,从而jdk7/8中的string操作性能比java 6有较大提升。
3.3. jdk 9/10/11的实现static class string { final byte code; final byte[] value; static final byte latin1 = 0; static final byte utf16 = 1;}
jdk 9之后,value类型从char[]变成byte[],增加了一个字段code,如果字符全部是ascii字符,使用value使用latin编码;如果存在任何一个非ascii字符,则用utf16编码。这种混合编码的方式,使得英文场景占更少的内存。缺点是导致java 9的string api性能可能不如jdk 8,特别是传入char[]构造字符串,会被做压缩为latin编码的byte[],有些场景会下降10%。
4.快速构造字符串的方法为了实现字符串是不可变特性,构造字符串的时候,会有拷贝的过程,如果要提升构造字符串的开销,就要避免这样的拷贝。
比如如下是jdk8的string的一个构造函数的实现
public final class string { public string(char value[]) { this.value = arrays.copyof(value, value.length); }}
在jdk8中,有一个构造函数是不做拷贝的,但这个方法不是public,需要用一个技巧实现methodhandles.lookup & lambdametafactory绑定反射来调用,文章后面有介绍这个技巧的代码。
public final class string { string(char[] value, boolean share) { // assert share : "unshared not supported"; this.value = value; }}
快速构造字符的方法有三种:
使用methodhandles.lookup和lambdametafactory来绑定反射
使用javalangaccess的相关方法
使用unsafe直接构造
1和2的性能相近,3稍微慢一些,但与直接使用new字符串相比,它们都更快。jdk8使用jmh测试的数据如下:
benchmark mode cnt score error units
stringcreatebenchmark.invoke thrpt 5 784869.350 ± 1936.754 ops/ms
stringcreatebenchmark.langaccess thrpt 5 784029.186 ± 2734.300 ops/ms
stringcreatebenchmark.unsafe thrpt 5 761176.319 ± 11914.549 ops/ms
stringcreatebenchmark.newstring thrpt 5 140883.533 ± 2217.773 ops/ms
在jdk 9之后,对全部是ascii字符的场景,直接构造能达到更好的效果。
4.1 基于methodhandles.lookup & lambdametafactory绑定反射的快速构造字符串的方法4.1.1 jdk8快速构造字符串
public static bifunction<char[], boolean, string> getstringcreatorjdk8() throws throwable { constructor<methodhandles.lookup> constructor = methodhandles.lookup.class.getdeclaredconstructor(class.class, int.class); constructor.setaccessible(true); methodhandles lookup = constructor.newinstance( string.class , -1 // lookup.trusted ); methodhandles.lookup caller = lookup.in(string.class); methodhandle handle = caller.findconstructor( string.class, methodtype.methodtype(void.class, char[].class, boolean.class) ); callsite callsite = lambdametafactory.metafactory( caller , "apply" , methodtype.methodtype(bifunction.class) , handle.type().generic() , handle , handle.type() ); return (bifunction) callsite.gettarget().invokeexact();}
4.1.2 jdk 11快速构造字符串的方法
public static tointfunction<string> getstringcode11() throws throwable { constructor<methodhandles.lookup> constructor = methodhandles.lookup.class.getdeclaredconstructor(class.class, int.class); constructor.setaccessible(true); methodhandles.lookup lookup = constructor.newinstance( string.class , -1 // lookup.trusted ); methodhandles.lookup caller = lookup.in(string.class); methodhandle handle = caller.findvirtual( string.class, "coder", methodtype.methodtype(byte.class) ); callsite callsite = lambdametafactory.metafactory( caller , "applyasint" , methodtype.methodtype(tointfunction.class) , methodtype.methodtype(int.class, object.class) , handle , handle.type() ); return (tointfunction<string>) callsite.gettarget().invokeexact();}
if (jdkutils.jvm_version == 11) { function<byte[], string> stringcreator = jdkutils.getstringcreatorjdk11(); byte[] bytes = new byte[]{'a', 'b', 'c'}; string apply = stringcreator.apply(bytes); assertequals("abc", apply);}
4.1.3 jdk 17快速构造字符串的方法
在jdk 17中,methodhandles.lookup使用reflection.registerfieldstofilter对lookupclass和allowedmodes做了保护,网上搜索到的通过修改allowedmodes的办法是不可用的。
在jdk 17中,要通过配置jvm启动参数才能使用methodhandlers。如下:
--add-opens java.base/java.lang.invoke=all-unnamed
public static bifunction<byte[], charset, string> getstringcreatorjdk17() throws throwable { constructor<methodhandles.lookup> constructor = methodhandles.lookup.class.getdeclaredconstructor(class.class, class.class, int.class); constructor.setaccessible(true); methodhandles.lookup lookup = constructor.newinstance( string.class , null , -1 // lookup.trusted ); methodhandles.lookup caller = lookup.in(string.class); methodhandle handle = caller.findstatic( string.class, "newstringnorepl1", methodtype.methodtype(string.class, byte[].class, charset.class) ); callsite callsite = lambdametafactory.metafactory( caller , "apply" , methodtype.methodtype(bifunction.class) , handle.type().generic() , handle , handle.type() ); return (bifunction<byte[], charset, string>) callsite.gettarget().invokeexact();}
if (jdkutils.jvm_version == 17) { bifunction<byte[], charset, string> stringcreator = jdkutils.getstringcreatorjdk17(); byte[] bytes = new byte[]{'a', 'b', 'c'}; string apply = stringcreator.apply(bytes, standardcharsets.us_ascii); assertequals("abc", apply);}
4.2 基于javalangaccess快速构造通过sharedsecrets提供的javalangaccess,也可以不拷贝构造字符串,但是这个比较麻烦,jdk 8/11/17的api都不一样,对一套代码兼容不同的jdk版本不方便,不建议使用。
javalangaccess javalangaccess = sharedsecrets.getjavalangaccess();javalangaccess.newstringnorepl(b, standardcharsets.us_ascii);
4.3 基于unsafe实现快速构造字符串public static final unsafe unsafe;static { unsafe unsafe = null; try { field theunsafefield = unsafe.class.getdeclaredfield("theunsafe"); theunsafefield.setaccessible(true); unsafe = (unsafe) theunsafefield.get(null); } catch (throwable ignored) {} unsafe = unsafe;}////////////////////////////////////////////object str = unsafe.allocateinstance(string.class);unsafe.putobject(str, valueoffset, chars);
注意:在jdk 9之后,实现是不同,比如:
object str = unsafe.allocateinstance(string.class);unsafe.putbyte(str, coderoffset, (byte) 0);unsafe.putobject(str, valueoffset, (byte[]) bytes);
4.4 快速构建字符串的技巧应用:如下的方法格式化日期为字符串,性能就会非常好。
public string formatyyyymmdd(calendar calendar) throws throwable { int year = calendar.get(calendar.year); int month = calendar.get(calendar.month) + 1; int dayofmonth = calendar.get(calendar.day_of_month); byte y0 = (byte) (year / 1000 + '0'); byte y1 = (byte) ((year / 100) % 10 + '0'); byte y2 = (byte) ((year / 10) % 10 + '0'); byte y3 = (byte) (year % 10 + '0'); byte m0 = (byte) (month / 10 + '0'); byte m1 = (byte) (month % 10 + '0'); byte d0 = (byte) (dayofmonth / 10 + '0'); byte d1 = (byte) (dayofmonth % 10 + '0'); if (jdkutils.jvm_version >= 9) { byte[] bytes = new byte[] {y0, y1, y2, y3, m0, m1, d0, d1}; if (jdkutils.jvm_version == 17) { return jdkutils.getstringcreatorjdk17().apply(bytes, standardcharsets.us_ascii); } if (jdkutils.jvm_version <= 11) { return jdkutils.getstringcreatorjdk11().apply(bytes); } return new string(bytes, standardcharsets.us_ascii); } char[] chars = new char[]{ (char) y0, (char) y1, (char) y2, (char) y3, (char) m0, (char) m1, (char) d0, (char) d1 }; if (jdkutils.jvm_version == 8) { return jdkutils.getstringcreatorjdk8().apply(chars, true); } return new string(chars);}
5.快速遍历字符串的办法无论jdk什么版本,string.charat都是一个较大的开销,jit的优化效果并不好,无法消除参数index范围检测的开销,不如直接操作string里面的value数组。
public final class string { private final char value[]; public char charat(int index) { if ((index < 0) || (index >= value.length)) { throw new stringindexoutofboundsexception(index); } return value[index]; }}
在jdk 9之后的版本,charat开销更大
public final class string { private final byte[] value; private final byte coder; public char charat(int index) { if (islatin1()) { return stringlatin1.charat(value, index); } else { return stringutf16.charat(value, index); } }}
5.1 获取string.value的方法获取string.value的方法有如下:
使用field反射
使用unsafe
unsafe和field反射在jdk 8 jmh的比较数据如下:
benchmark mode cnt score error units
stringgetvaluebenchmark.reflect thrpt 5 438374.685 ± 1032.028 ops/ms
stringgetvaluebenchmark.unsafe thrpt 5 1302654.150 ± 59169.706 ops/ms
5.1.1 使用反射获取string.value
static field valuefield;static { try { valuefield = string.class.getdeclaredfield("value"); valuefield.setaccessible(true); } catch (nosuchfieldexception ignored) {}}////////////////////////////////////////////char[] chars = (char[]) valuefield.get(str);
5.1.2 使用unsafe获取string.value
static long valuefieldoffset;static { try { field valuefield = string.class.getdeclaredfield("value"); valuefieldoffset = unsafe.objectfieldoffset(valuefield); } catch (nosuchfieldexception ignored) {}}////////////////////////////////////////////char[] chars = (char[]) unsafe.getobject(str, valuefieldoffset);
static long valuefieldoffset;static long coderfieldoffset;static { try { field valuefield = string.class.getdeclaredfield("value"); valuefieldoffset = unsafe.objectfieldoffset(valuefield); field coderfield = string.class.getdeclaredfield("coder"); coderfieldoffset = unsafe.objectfieldoffset(coderfield); } catch (nosuchfieldexception ignored) {}}////////////////////////////////////////////byte coder = unsafe.getobject(str, coderfieldoffset);byte[] bytes = (byte[]) unsafe.getobject(str, valuefieldoffset);
6.更快的encodeutf8方法当能直接获取到string.value时,就可以直接对其做encodeutf8操作,会比string.getbytes(standardcharsets.utf_8)性能好很多。
6.1 jdk8高性能encodeutf8的方法public static int encodeutf8(char[] src, int offset, int len, byte[] dst, int dp) { int sl = offset + len; int dlascii = dp + math.min(len, dst.length); // ascii only optimized loop while (dp < dlascii && src[offset] < '\u0080') { dst[dp++] = (byte) src[offset++]; } while (offset < sl) { char c = src[offset++]; if (c < 0x80) { // have at most seven bits dst[dp++] = (byte) c; } else if (c < 0x800) { // 2 bytes, 11 bits dst[dp++] = (byte) (0xc0 | (c >> 6)); dst[dp++] = (byte) (0x80 | (c & 0x3f)); } else if (c >= '\ud800' && c < ('\udfff' + 1)) { //character.issurrogate(c) but 1.7 final int uc; int ip = offset - 1; if (c >= '\ud800' && c < ('\udbff' + 1)) { // character.ishighsurrogate(c) if (sl - ip < 2) { uc = -1; } else { char d = src[ip + 1]; // d >= '\udc00' && d < ('\udfff' + 1) if (d >= '\udc00' && d < ('\udfff' + 1)) { // character.islowsurrogate(d) uc = ((c << 10) + d) + (0x010000 - ('\ud800' << 10) - '\udc00'); // character.tocodepoint(c, d) } else { dst[dp++] = (byte) '?'; continue; } } } else { // if (c >= '\udc00' && c < ('\udfff' + 1)) { // character.islowsurrogate(c) dst[dp++] = (byte) '?'; continue; } else { uc = c; } } if (uc < 0) { dst[dp++] = (byte) '?'; } else { dst[dp++] = (byte) (0xf0 | ((uc >> 18))); dst[dp++] = (byte) (0x80 | ((uc >> 12) & 0x3f)); dst[dp++] = (byte) (0x80 | ((uc >> 6) & 0x3f)); dst[dp++] = (byte) (0x80 | (uc & 0x3f)); offset++; // 2 chars } } else { // 3 bytes, 16 bits dst[dp++] = (byte) (0xe0 | ((c >> 12))); dst[dp++] = (byte) (0x80 | ((c >> 6) & 0x3f)); dst[dp++] = (byte) (0x80 | (c & 0x3f)); } } return dp;}
使用encodeutf8方法举例
char[] chars = unsafe.getobject(str, valuefieldoffset);// ensurecapacity(chars.length * 3)byte[] bytes = ...; // int byteslength = ioutils.encodeutf8(chars, 0, chars.length, bytes, bytesoffset);
这样encodeutf8操作,不会有多余的arraycopy操作,性能会得到提升。
6.1.1 性能测试比较
测试代码
public class encodeutf8benchmark { static string str = "01234567890abcdefghijklmnopqrstuvwzyzabcdefghijklmnopqrstuvwzyz一二三四五六七八九十"; static byte[] out; static long valuefieldoffset; static { out = new byte[str.length() * 3]; try { field valuefield = string.class.getdeclaredfield("value"); valuefieldoffset = unsafeutils.unsafe.objectfieldoffset(valuefield); } catch (nosuchfieldexception e) { e.printstacktrace(); } } @benchmark public void unsafeencodeutf8() throws exception { char[] chars = (char[]) unsafeutils.unsafe.getobject(str, valuefieldoffset); int len = ioutils.encodeutf8(chars, 0, chars.length, out, 0); } @benchmark public void getbytesutf8() throws exception { byte[] bytes = str.getbytes(standardcharsets.utf_8); system.arraycopy(bytes, 0, out, 0, bytes.length); } public static void main(string[] args) throws runnerexception { options options = new optionsbuilder() .include(encodeutf8benchmark.class.getname()) .mode(mode.throughput) .timeunit(timeunit.milliseconds) .forks(1) .build(); new runner(options).run(); }}
测试结果
encodeutf8benchmark.getbytesutf8 thrpt 5 20690.960 ± 5431.442 ops/ms
encodeutf8benchmark.unsafeencodeutf8 thrpt 5 34508.606 ± 55.510 ops/ms
从结果来看,通过unsafe + 直接调用encodeutf8方法, 编码的所需要开销是newstringutf8的58%。
6.2 jdk9/11/17高性能encodeutf8的方法public static int encodeutf8(byte[] src, int offset, int len, byte[] dst, int dp) { int sl = offset + len; while (offset < sl) { byte b0 = src[offset++]; byte b1 = src[offset++]; if (b1 == 0 && b0 >= 0) { dst[dp++] = b0; } else { char c = (char)(((b0 & 0xff) << 0) | ((b1 & 0xff) << 8)); if (c < 0x800) { // 2 bytes, 11 bits dst[dp++] = (byte) (0xc0 | (c >> 6)); dst[dp++] = (byte) (0x80 | (c & 0x3f)); } else if (c >= '\ud800' && c < ('\udfff' + 1)) { //character.issurrogate(c) but 1.7 final int uc; int ip = offset - 1; if (c >= '\ud800' && c < ('\udbff' + 1)) { // character.ishighsurrogate(c) if (sl - ip < 2) { uc = -1; } else { b0 = src[ip + 1]; b1 = src[ip + 2]; char d = (char) (((b0 & 0xff) << 0) | ((b1 & 0xff) << 8)); // d >= '\udc00' && d < ('\udfff' + 1) if (d >= '\udc00' && d < ('\udfff' + 1)) { // character.islowsurrogate(d) uc = ((c << 10) + d) + (0x010000 - ('\ud800' << 10) - '\udc00'); // character.tocodepoint(c, d) } else { return -1; } } } else { // if (c >= '\udc00' && c < ('\udfff' + 1)) { // character.islowsurrogate(c) return -1; } else { uc = c; } } if (uc < 0) { dst[dp++] = (byte) '?'; } else { dst[dp++] = (byte) (0xf0 | ((uc >> 18))); dst[dp++] = (byte) (0x80 | ((uc >> 12) & 0x3f)); dst[dp++] = (byte) (0x80 | ((uc >> 6) & 0x3f)); dst[dp++] = (byte) (0x80 | (uc & 0x3f)); offset++; // 2 chars } } else { // 3 bytes, 16 bits dst[dp++] = (byte) (0xe0 | ((c >> 12))); dst[dp++] = (byte) (0x80 | ((c >> 6) & 0x3f)); dst[dp++] = (byte) (0x80 | (c & 0x3f)); } } } return dp;}
使用encodeutf8方法举例
byte coder = unsafe.getobject(str, coderfieldoffset);byte[] value = unsafe.getobject(str, coderfieldoffset);if (coder == 0) { // ascii arraycopy} else { // ensurecapacity(chars.length * 3) byte[] bytes = ...; // int byteslength = ioutils.encodeutf8(value, 0, value.length, bytes, bytesoffset);}
这样encodeutf8操作,不会有多余的arraycopy操作,性能会得到提升。
以上就是java字符串编码解码性能怎么提升的详细内容。