JAVA9 String新特性,说说你不知道的东西

前言

字符串是java绕不去的路,于是乎这两天准备搞明白java字符串的内部的一些底层机制,JDK9,在各种书上都学不到的东西,去国外网站偷学了一波 哈哈

OpenJDK

两字节char

有一个概念深入人心,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 带来了字符串两大改善,更小的内存空间和改善表现

Compact

JDK8的字符串存储在char类型的数组里面,不难想象在大多数情况下,char类型只需要一个字节就能表示出来了,比如各种字母什么的,两个字节存储势必会浪费空间,JDK9的一个优化就在这,内存的优化

    /** The value is used for character storage. */
    private final char value[];

对比JDK9:

    private final byte[] value;

我们可以看到JDK9是由byte类型的数组来进行存储的

故事就从这开始了~

Sting-JDK9

    /**
     * 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.

JAVA9
《JAVA9 String新特性,说说你不知道的东西》

JAVA8
《JAVA9 String新特性,说说你不知道的东西》
实例证明

        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后的连锁反应

由于byte数组的使用方式,引申出了两个类StringLatin1和StringUTF16两个类,分担String类的操作,包括StingBuilder等,跟String有关的都得到了这方面的优化

getBytes

关于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 的最下面看见

Indify String Concatenation

实例

由于一堆理论对于我这个新手来讲也看不懂,了解一些浅层次的大概就挺好,所以我们这里从实例开始讲起,如果你和我一样学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."<init>":()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."<init>"
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

先简要的分析一下这个流程把,

  • ldc 将常量池中的String入栈
  • astore_1 将栈顶ref对象保存至局部变量1
  • aload_1 将局部变量1入栈
  • invokedynamic动态调用
  • astore 保存最终的值到一个位置
  • return

对应于Code里面的8是一个索引,它的前两个操作数字#4指向常量池,后两个为0,看看常量池里面的内容

#4 = InvokeDynamic      #0:#22

第一位 表示index,第二位引导方法的索引,第三位对应的方法名和类型的索引
《JAVA9 String新特性,说说你不知道的东西》

  • 常量池22
#22 = NameAndType        #28:#29

解析:
#28 代表name index
#29 代表描述符的索引

  #28 = Utf8               makeConcatWithConstants
  #29 = Utf8               (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
()里面的是参数类型,后面的是返回值
  • 引导方法#0
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
#20 = MethodHandle 6:#26

解析:
#20 常量池index
6:代表kind,代表REF_invokeStatic
#26常量池index

  • 常量池26
  #26 = Methodref          #30.#31

方法引用,前一个代表类,后一个代表方法中间用.

  • 常量池30
#30 = Class #32
#32 = Utf8 java/lang/invoke/StringConcatFactory

代表了这个类

  • 常量池31
#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

makeConcatWithConstants

设计师为我们定义了两种这样的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方法->实际方法,这个顺序

    原文作者:NoobIn江湖
    原文地址: https://blog.csdn.net/qq_41376740/article/details/80143215
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞