乐趣区

JAVA13新鲜特性简述

本文是 oracle 最新发布的 java13 新特性一览, 不包含被 deprecated 的特性, 以及与安全, 代码集等有关的内容.

1.nio 新 api:

类:FileSystems

newFileSystem(Path)
newFileSystem(Path, Map<String, ?>)
newFileSystem(Path, Map<String, ?>, ClassLoader)

粗略看了一下描述, 其实直接看这个参数最全的方法:

public static FileSystem newFileSystem​(Path path, Map<String,​?> env, ClassLoader loader) throws IOException

官方的描述是尝试去构建一个 FileSystem, 而它会按一个文件系统的方式去访问指定的文件.

该方法会使用一些掀起定的 provider, 这些 provider 会创建一些假的文件, 并用一至多个文件的内容表示一个文件系统.

方法自身会迭代所有安装的 provider(看起来慒逼的读者稍等), 并依次执行每个 provider 的 newFileSystem(Path,Map)方法, 如果有一个 provider 返回了一个文件系统, 那么迭储藏室终止并返回该文件系统. 如果没有任何一个安装的 provider 返回 FileSystem, 那么将尝试使用参数中的 class loader 去定位 provider, 如果成功, 则停止迭代并返回 file system.

所谓 ” 安装的 provider”, 它其实是 FileSystemProvider 抽象类, 字面意思很明显, 从 jdk7 就已经出现了, 而所谓 ” 安装 ”, 其实是依赖 spi 机制, 就是正常的 spi 用法, 没什么可解释的.

很巧作者之前没了解过 FileSystemProvider 这个鬼, 正好借机了解了, 看注释就是将若干文件的内容按照文件系统解释, 仿佛自己实现了一套文件系统, 有一点虚拟化的味道. 是不是想到了 zookeeper?

参数 path 是文件路径,map 则是配置文件系统的属性集合, 可空.

其实这个 api 就是相当于一个封装, 我们使用 spi 机制安装了若干个 FileSystemProvider, 然后使用 FileSystems 的 newFileSystem 就可以获取到 provider 实现的 FileSystem.

2.nio 新 api:

类 ByteBuffer, 新增若干对于 buffer 的批量的数据 get/put, 而且不影响 buffer 位.

不了解 nio 的同学可能有些难以理解, 其实要理解也容易, 打开 jdk8 的 ByteBuffer 源码, 直接找到 get 或者 put 方法, 要参数最长的那个, 把注释看一遍就理解了.

jdk8 中现有的示例:

public ByteBuffer put(byte[] src, int offset, int length);
public ByteBuffer get(byte[] dst, int offset, int length);

注意这两个方法,get 方法的 get 操作其实是一个复合操作, 它会将当前 buffer 的数据从 position 开始, 拷贝指定的长度 length 到目标 dst, 同时更新自身的 position 增加 length, 当然, 超长的情况下会抛出异常并且不执行拷贝和更新 position.

put 操作同样, 也会将 src 中从 offset 开始的 length 个元素放置入 buffer, 并更新 position 增加 length.

在 java8 中, 多线程进行批量的读写处理要如何进行呢?

通过 slice 方法获取一个从当前 position 开始的 buffer 切面, 共享这一段数据, 但 postion,mark 和 limit 独立; 或使用 duplicate 方法, 共享完整的数据, 同样保持 position,mark 和 limit 的独立, 各线程各玩个的, 而 position,mark 和 limit 等信息是不对外暴露的.

从某种角度理解, 在 8 中多线程操作同一个 buffer 的这种使用场景, 虽然是共同操作了同一段底层数据, 实际上是多个 buffer 对象, 互相对对方的读写没有和 mark,limit,position 有关的影响, 所以各个线程可以依旧从自己持有的新 buffer 引用 (共同底层数据, 不同的 buffer 对象) 的 position 位置去读写. 所以其实并非完全是同一个 buffer 在多线程下共享.

因此, 从 jdk13 开始支持了这样的场景: 多个线程同时对一个 buffer 进行批量读写, 且不创建新的 buffer(slice 或者 duplicate), 并且这个操作必然能并发, 那么这些可并发的操作就不能去更新相应的 position, 若有其他操作依托于 position 进行, 在保持了可见性的前提下, 依旧能从 buffer 的 position 位进行读写.

所以, 你会在 java13 的 ByteBuffer api 上看见四个这样的方法:

public ByteBuffer get(int index, byte[] dst);
public ByteBuffer get(int index, byte[] dst, int offset, int length);
public ByteBuffer put(int index, byte[] src);
public ByteBuffer put(int index, byte[] src, int offset, int length);

注意第一个参数 index, 代表从 buffer 的数据的哪个索引开始, 它绝对不会改变 position, 也没有创建新的 buffer.

在 ByteBuffer 的子类 (官方 8 提供) 中,MappedByteBuffer 是个抽象类, 没有实现 slice 或者 duplicate 方法, 如果自己去实现, 很可能会保持大量的原 buffer 的引用. 可想而知,13 中推出的四个新 api 可以令我们在多线程环境下减少无用引用的创建.

3.unicode12.1 支持.

最近每个版本都会扩大字符集支持, 略.

4.zgc 更新, 支持配置返还无用内存, 最大堆内存支持 16T, 软最大内存设置.

在前面的 java9-12 一文中, 曾介绍 G1 为了云友好, 支持在空闲时返还无用内存, 从而帮助用户错峰节省了内存资源占用.zgc 在 11 出现,12 支持了并发类卸载, 但并不支持内存的无用返还, 目前在 13 中已经支持. 这一特性可以使用 -XX:-ZUncommit 来禁用, 默认是开启的, 同时, 如果内存占用已达到配置的最低内存, 那么即使内存空闲也不会返还, 所以若显式将初始内存和最大内存配置一致, 等于隐式禁用了这一特性.

zgc 可以使用 -XX:ZUncommitDelay=<seconds> 决定多少秒算空闲, 默认为 300 秒.

此外, 在 jdk13 中, 官方为 hotspot 虚拟机新增了一个选项:-XXSoftMaxHeapSize. 但目前只有 zgc 实现了对它的支持, 其他 gc 并不生效, 即开启 -XX:+UseZGC.

当我们使用该选项设置了一个软的最大堆内存大小后,gc 将争取堆内存不超过指定的大小, 除非 gc 最终认定这样做是避免 oom 必须的. 软最大堆内存大小不可超过 -Xms 指定的值, 如果我们没有在命令行中设置它, 默认的值就是最大堆内存大小.

该值是动态可调的, 在运行时, 可以使用 jcmd VM.set_flag SoftMaxHeapSize <bytes> 或者通过 HotSpot MXBean 设置.

在一些特定的场景下我们需要这个新的特性, 如非常在意资源使用, 希望保持低堆内存占用, 但同时又想同时能应付临时的, 偶发的内存空间突增, 尤其是在并发场景下无法预知的对象分配速率的突增. 设置软最大堆内存将鼓励 gc 维护一个小堆, 这样 gc 会更积极的进行垃圾回收, 对于应用程序突发地增加对象分配速度也能更加从容应对.

5. 动态 cds 归档.

详情参考 JEP350, 这一特性支持了在 java 进程退出时动态的进行类信息的归档, 从而避免了用户需要烦琐地对每个进程手动归档. 在前面 java9-12 一文中, 已经介绍了近几个版本对于类数据共享的新支持, 这一个算是续集吧.

6. 语法糖.(预览版)

在前面 JAVA9-12 一文中, 已经介绍过 switch 支持了新的 case a -> 表达式的语法糖.

在 jdk13, 支持了更加丰富的扩展, 说起来费劲, 总之官方的两段示例代码很好理解:

String formatted = 
    switch (obj) {case Integer i -> String.format("int %d", i)
        case Byte b    -> String.format("byte %d", b);
        case Long l    -> String.format("long %d", l); 
        case Double d  -> String.format("double %f", d); 
        case String s  -> String.format("String %s", s); 
        default        -> obj.toString();};
int eval(Node n) {switch(n) {case IntNode(int i): return i;
        case NegNode(Node n): return -eval(n);
        case AddNode(Node left, Node right): return eval(left) + eval(right);
        case MulNode(Node left, Node right): return eval(left) * eval(right);
        default: throw new IllegalStateException(n);
    };
}

同时,jdk13 提出了一个 ” 文本块 ” 的概念,

其实就是支持了字符串 (文档) 的多行语法, 一样说起来费力, 看起来挺像 python 的.

从前:

String html = "<html>\n" +
              "<body>\n" +
              "<p>Hello, world</p>\n" +
              "</body>\n" +
              "</html>\n";

现在:

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

现在:

String query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;
               """;

使用该表达式的字符串对象兼容老的表达方式, 如两者混合使用, 进行相加等操作, 调用字符串 String 的方法等.

String code = String.format("""
              public void print(%s o) {System.out.println(Objects.toString(o));
              }
              """, type);

Stirng 新增的有关方法:

String::stripIndent(): 从文本块语法的表达中去除空格.
String::translateEscapes(): 转义退出字符序列.
String::formatted(Object... args): 进行文本块中某些值的格式化.

7. 与 dom sax 解析有关的新 api, 我也不怎么熟悉, 简单说一下吧.

官方的说法是为初始化 dom sax 工厂时提供了创建带有默认命名空间的 api, 特点是方法名上带有 NS, 三个方法分别是:

newDefaultNSInstance()
newNSInstance()
newNSInstance(String factoryClassName, ClassLoader classLoader)

相应的, 使用 jdk13, 这段代码:

DocumentBuilder db = DocumentBuilderFactory.newDefaultNSInstance().newDocumentBuilder();

等效于:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance(); 
dbf.setNamespaceAware(true); 
DocumentBuilder db = dbf.newDocumentBuilder();

8.StringBuilder 和 StringBuffer 参数为负长度数组时抛出 NegativeArraySizeException .

9.linux 系统下, 进程的默认开启机制使用 posix_spawn, 这一块需要参考 linux 创建进程的四种方式,fork(), fork()-exec(), posix_spawn()是异步进程, 多进程可以并行执行, 和 system 这种同步方式, 多进程不能同时执行. 从官方给的链接来看, 原来用的似乎是 fork/vfork?

10.jli 包限定使用方法句柄对于静态常量字段的设值权限. 即使使用了 Field.setAccessibe(true)设置了权限, 那么对相应的字段进行 java.lang.invoke.MethodHandles.Lookup::unreflectSetter 时, 也会抛出 IllegalAccessException . 在前面 java9-12 一文中提到过一些关于两个句柄的介绍, 作者个人建议了解, 个人觉得它们有未来在各框架中大量使用的可能性.

10.SocketImpl 一些默认实现的变更.

此处影响的三个方法是 jdk9 才出现的方法, 在 13 中, 它的默认返回值会是一个空集,getOption 和 setOption 将默认抛出 UnsupportedOperationException, 代码中或继承了 SocketImpl 或 DatagramSocketImpl 则必须重写这两个方法.

11.NewDirectByteBuffer 开辟的直接内存将固定为大字节序.

  1. Files.isHidden 在 windows 中使用隐藏目录时返回 true.

13.Base64.Encoder 在编码解码时若不能申请内存成功, 将抛出 oom 而不是 NegativeArraySizeException.

14. 对于压缩文件 (zip 或 jar) 的 api 进行内容更新时的注意事项, 若使用了 zip file system 去更新其中包含未压缩项的压缩包, 则会令损坏该文件, 若该包中不包含未压缩的项, 则不会有问题. 当要对这种含有未压缩条目的压缩包更新时, 应使用 jar 工具或者 java.util.zip 包下的工具解决.

15. 对于 x86_64 无压缩引用的场景, 将有更多可用寄存器资源.

在 x86_64 上有压缩引用的场景下运行时, 会消耗一个 cpu 寄存器去存放 base 指针, 它将用来处理引用的编解码, 那么这个寄存器就不能用来分配了. 在 13 之前, 即使没用到压缩引用, 这个寄存器也是无用了, 在本版本, 该寄存器会被交还给寄存器池, 使用了超大堆和有 XX:-UseCompressedOops 配置的场景将因此获益.

16. 提升稀疏 prt 条目工效.

prt 即 per region table, 与垃圾收集器中的位图有关, 在 g1 中, 记忆集的存储便是稀疏 prt, 官方的简述很好理解, 原来 prt 中能存放的条目是随着 region 的增大而线性增长的, 现在变为指数增长.

这意味着 G1 对于 1/2/4/8/16/32 MB 的 region 将能每 prt 分别使用 4/8/16/32/62/128 个条目, 以前则分别是每 prt 使用 4 /8/12/16/20/24 条目.

17.jrt 协议只能编码 jrt:/modules 树下的路径. 前面说过, 从 jdk9 开始, 官方提供了一个新的 url pattern jrt, 并使用 jrt:/module/resource 具体定位一个资源(类或配置等). 在 /packages 树中的文件, 使用 jrt 文件系统进行 toUri 操作时, 可能会抛出 IOError, 阻止下一步操作.

退出移动版