字符串是java绕不去的路,于是乎这两天准备搞明白java字符串的内部的一些底层机制,JDK9,在各种书上都学不到的东西,去国外网站偷学了一波 哈哈
OpenJDK
有一个概念深入人心,char占几个字节?两个~异口同声~对吧?对,没错根据UTF-16,JAVA的内部编码格式,编译成了Class文件,然后送给JVM执行,一个UTF-16 字符占2个字节,不管是什么样的char都是2个字节,但是!这一切在JDK 9中发生了变化
看下面老外的一句话说的很好
Java 9 comes with 2 major changes on how String behaves to lower memory usage and improve performance.
它说JDK9 带来了字符串两大改善,更小的内存空间和改善表现
JDK8的字符串存储在char类型的数组里面,不难想象在大多数情况下,char类型只需要一个字节就能表示出来了,比如各种字母什么的,两个字节存储势必会浪费空间,JDK9的一个优化就在这,内存的优化
/** The value is used for character storage. */
private final char value[];
对比JDK9:
private final byte[] value;
我们可以看到JDK9是由byte类型的数组来进行存储的
故事就从这开始了~
/**
* The identifier of the encoding used to encode the bytes in
* {@code value}. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;
在JDK9的String类中,维护了这样的一个属性coder,它是一个编码格式的标识,使用LATIN1还是UTF-16,这个是在String生成的时候自动的,如果字符串中都是能用LATIN1就能表示的就是0,否则就是UTF-16.
String a = "中";
String b = "国";
String c = "1中";
String d = "abEF23";
我们可以明显看到JDK9在这方面的优化,在较多情况下不包含那些奇奇怪怪的字符的时候,足以应付,而这个空间却小了1byte,实现了String空间的压缩。
JDK8
public int length() {
return value.length;
}
直接返回的char数组的长度
JDK9
public int length() {
return value.length >> coder();
}
将byte数组的长度向右位移coder()
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;
}
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
我们可以看到coder()返回的值,如果是LATIN-1就是右移0位,如果是UTF-16就右移1位,这样就能返回正确的字符串长度,默认的compact_string是开启的,因为在静态域里面优先于类加载。
由于byte数组的使用方式,引申出了两个类StringLatin1和StringUTF16两个类,分担String类的操作,包括StingBuilder等,跟String有关的都得到了这方面的优化
关于getBytes这个方法的坑,我之前想通过这个方法来获取字符串所占的字节数,结果中文返回了3,英文返回了1
例子:
System.out.println("1".getBytes().length);
System.out.println("中".getBytes().length);
----结果----
1
2
本以为说好的UTF-16不应该都是2嘛0.0,这里需要科普一下,这个方法的介绍里面有如下这一句话
Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array.
它说,用的是平台默认的编码,什么是默认的编码呢- -
System.out.println(System.getProperty("file.encoding"));
文件的编码用的就是系统的默认编码,当然也可以在eclipse的File->properties->Resource 的最下面看见
由于一堆理论对于我这个新手来讲也看不懂,了解一些浅层次的大概就挺好,所以我们这里从实例开始讲起,如果你和我一样学java1个月,因为好奇来研究,下面的内容足够了。PS 这方面的资料不多,有也是全英文的,比较难懂
public class Test
{
public static void main(String[] args)
{
String hello = "hello";
String world = "world";
String message = hello + world;
}
}
你可能探究过JDK8,对字符串连接的操作,虚拟机没错使用的是StringBuilder进行的优化,但是JAVA 9 却用到了,InvokeDynamic。
javap -v deep_in_string/Test.class
反编译一看真相
public class deep_in_string.Test {
public deep_in_string.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: aload_1
7: aload_2
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: return
}
BootstrapMethods:
0: #19 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#20 \u0001\u0001
盗用一张人家的JDK8的图:
6: new #4 // class StringBuilder
9: dup
10: invokespecial #5 // Method StringBuilder.""
13: aload_1 // String Hello
14: invokevirtual #6 // Method StringBuilder.append:(LString;)LStringBuilder;
17: aload_2 // String world!
18: invokevirtual #6 // Method StringBuilder.append:(LString;)LStringBuilder;
21: invokevirtual #7 // Method StringBuilder.toString:()LString;
我们可以看到,JAVA 9用的是动态调用
PS:知识小普及
invokestatic 调用类方法(静态绑定,速度快)
invokevirtual 调用实例方法(动态绑定)
invokespecial 调用实例方法(静态绑定,速度快)
invokeinterface 调用引用类型为interface的实例方法(动态绑定)
invokedynamicJDK 7引入的,主要是为了支持动态语言的方法调用
关于JVM的各种调用,暂时不看,我们就先看看这个动态调用
下面的内容翻译的人家的,以我的浅薄经验来看,值得一试,先看我分析的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: aload_1
7: aload_2
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: ldc #5 // String 123
16: astore 4
18: return
先简要的分析一下这个流程把,
对应于Code里面的8是一个索引,它的前两个操作数字#4指向常量池,后两个为0,看看常量池里面的内容
#4 = InvokeDynamic #0:#22
第一位 表示index,第二位引导方法的索引,第三位对应的方法名和类型的索引
#22 = NameAndType #28:#29
解析:
#28 代表name index
#29 代表描述符的索引
#28 = Utf8 makeConcatWithConstants
#29 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
()里面的是参数类型,后面的是返回值
BootstrapMethods:
0: #20 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#21 \u0001\u0001new
解析:
index 0
bootstrap_method_ref是常量池#20
方法参数 常量池#21 \u0001\u0001new
#20 = MethodHandle 6:#26
解析:
#20 常量池index
6:代表kind,代表REF_invokeStatic
#26常量池index
#26 = Methodref #30.#31
方法引用,前一个代表类,后一个代表方法中间用.
#30 = Class #32
#32 = Utf8 java/lang/invoke/StringConcatFactory
代表了这个类
#31 = NameAndType #28:#36
#28 = Utf8 makeConcatWithConstants
#36 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
NameAndType包含了方法的名字和参数类型,返回类型,看完了差不多的字节码之后,我们不难推断出流程:
1、编译器在你的方法体里面放置一个动态调用指令来表明,这个动态调用的地方就叫dynamic call site(动态调用点)
2、这个动态调用点里面有一个call site specifier,调用点描述符号保存在常量池里面
3、JVM通过这个常量池就得到了几个信息
(1)动态调用的引导方法bootstrap
(2)NameAndType方法名和类型
4、启动bootstrap方法,里面包含了这样几个信息
(1)MethodHandle,MethodHandle里面包含了一个MethoRef方法引用,这个方法引用指向真正工作的引导方法,顺便(2)的方法参数一起传递进给这个方法,最终这个真正工作的引导方法,返回一个CallSite对象,和这个动态调用点关联在一起,到此引导方法的任务完成
(2)别处得来的方法参数
5、调用这个和CallSite关联的新MethodHandle指向的方法
动态调用,为了更好的实现字符串的连接+,在运行的时候确定它的方法的调用,而不需要人为开发者去选择。在这里,它的实际调用就是BootstrapMethods:里面的StringConcatFactory.makeConcatWithConstants,后面跟的是一堆参数,我们着重看以下两个
MethodType:
编译器推断出具体的连接方法,然后JVM在运行时将这个方法对象传递给bootstrap引导方法,这使得人可以通过javap反编译indy 指令,StringConcatFactory使用这些信息生成包含字符串连接方法的CallSite
recipe:
前三个参数都是JVM自动填充的,但是第四个参数是怎么来的呢?也就是这里的recipe,recipe使用两个标记字符也就是下面的\u0001和\u0002 来表示是否使用栈中或者常量中的参数,比如这里的hello,和world都是从栈中读的数据,但是如果我们修改成这样呢?
String message = hello + world + "new";
在反编译中,我们会得到这样的结果:
Method arguments:
#20 \u0001\u0001new
我们可以看到,它并不是用的栈,也不是用的常量,而是直接放在了recipe里面,免去了加载的过程,这是一个优化的地方
再尝试一下:
String message2 = "1"+"2"+"3";
在上面的做法中,我们不使用new 来初始化,直接进行相加,结果吃了一惊,里面居然都没有使用动态调用,而是直接把它们仨拼接起来了。由于没有用到变量就不存在栈了。
14: ldc #5 // String 123
设计师为我们定义了两种这样的bootsrap引导方法,都在StringConcatFactory中
public static CallSite makeConcatWithConstants(MethodHandles.Lookup lookup,
String name,
MethodType concatType,
String recipe,
Object... constants) throws StringConcatException {
if (DEBUG) {
System.out.println("StringConcatFactory " + STRATEGY + " is here for " + concatType + ", {" + recipe + "}, " + Arrays.toString(constants));
}
return doStringConcat(lookup, name, concatType, false, recipe, constants);
}
private enum Strategy {
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder}.
*/
BC_SB,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but trying to estimate the required storage.预测所需要的存储空间
*/
BC_SB_SIZED,
/**
* Bytecode generator, calling into {@link java.lang.StringBuilder};
* but computing the required storage exactly.准确的计算出存储空间
*/
BC_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also tries to estimate the required storage.
*/
MH_SB_SIZED,
/**
* MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
* This strategy also estimate the required storage exactly.
*/
MH_SB_SIZED_EXACT,
/**
* MethodHandle-based generator, that constructs its own byte[] array from
* the arguments. It computes the required storage exactly.
*/
MH_INLINE_SIZED_EXACT
}
很遗憾 到这里便戛然而止了,虽然我还是对它充满了好奇,但是由于能力的限制,我无法进一步去看它的这种策略是如何实现的。洋洋洒洒 这么多字,还是提升挺多的。至少我知道了动态调用和+号的实现JAVA9 对它再次优化了
- - - - - - - - - - - - - - - - - - - - - - - -分割线- - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - -5.11补充- - - - - - - - - - - - - - - - - - - - - - - -
最近JVM学到了动态指令部分,书上有两个字节码模拟,于是我自己操作了一下,熟悉了流程,下面是我的代码
package methodHandle;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleTest {
static class ClassA{
public void println(){
System.out.println("this is A");
}
}
static class ClassB extends ClassA{
public void println() {
System.out.println("this B");
}
}
public static void test2() throws Throwable{
Object obj = new ClassA();
MethodType mt = MethodType.methodType(void.class);
MethodHandle method = MethodHandles.lookup().findVirtual(obj.getClass(), "println", mt).bindTo(obj);
method.invokeExact();
}
public static void main(String[] args) throws Throwable
{
test2();
}
}
package methodHandle;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class DynamicTest {
public static void test() {
System.out.println("成功");
}
public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable{
return new ConstantCallSite(lookup.findStatic(DynamicTest.class, name, mt));
}
public static MethodType getMethodType() {
return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
}
public static MethodHandle getMethodHandle() throws Throwable{
return MethodHandles.lookup().findStatic(DynamicTest.class, "BootstrapMethod", getMethodType());
}
private static MethodHandle INDY_BootstrapMethod() throws Throwable{
CallSite cs=(CallSite) getMethodHandle().invokeWithArguments(MethodHandles.lookup(),"test", MethodType.fromMethodDescriptorString("()V",null));
return cs.dynamicInvoker();
}
public static void main(String[] args) throws Throwable{
MethodHandle mh = INDY_BootstrapMethod();
mh.invokeExact();
}
}
代码分析:
这两段代码分别是invokeVIrtual和动态调用的模拟,代码不长却能说明流程,之前一直是理论,现在实践了一下。直接说动态调用的那部分把。这里我们是模拟的动态调用静态方法。
先解释一下各个方法的作用
Bootstrap方法返回调用点,指明了最终实际调用方法
getMethodType从描述符中读取bootstrap方法的返回类型,参数
getMethodHandle 用来找到Bootstrap方法
INDY_BootstrapMethodHandle 其实是上述所有结果的整合
对比起字节码指令的解析,就能知道了,InvokeDynamic指令->Bootstrap方法->实际方法,这个顺序