Java新特性梳理——Java13

26 阅读11分钟

概述

2019 年 9 月 17 日,国际知名的 OpenJDK 开源社区发布了 Java 编程语言环境的最新版本 OpenJDK 13。

Features:总共有 5 个新的 JEP(JDK Enhancement Proposals):openjdk.java.net/projects/jd…

语法层面变化

switch表达式

在 Java 12 中引入了 switch 表达式作为预览特性。Java 13 提出了第二个 switch 表达式预览。JEP 354 修改了这个特性,它引入了 yield 语句,用于返回值。这意味着,switch 表达式(返回值)应该使用 yield, switch语句(不返回值)应该使用 break

在以前,我们想要在 switch 中返回内容,还是比较麻烦的,一般语法如下:

String x = "3";
int i;
switch (x) {
    case "1":
        i = 1;
        break;
    case "2":
        i = 2;
        break;
    default:
        i = x.length();
        break;
}
System.out.println(i);

在 Java 13 中使用以下语法:

String x = "3";
int i = switch (x) {
    case "1" -> 1;
    case "2" -> 2;
    default -> {
        yield 3;
    }
};
System.out.println(i);

// 或者这样写
String x = "3";
int i = switch (x) {
    case "1":
        yield 1;
    case "2":
        yield 2;
    default:
        yield 3;
};
System.out.println(i);

在这之后,switch 中就多了一个关键字用于跳出 switch 块了,那就是yield,他用于返回一个值。和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

文本块

在 Java 中,通常需要使用 String 类型表达 HTML、XML、SQL 或 JSON 等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。文本块就是指多行字符串,例如一段格式化后的xml、json等。而有了文本块以后,用户不需要转义,Java能自动搞定。因此,文本块将提高Java程序的可读性和可写性。

比如说定义一段 HTML 代码:

<html>
    <body>
        <a href="http://www.mashibing.com">波波烤鸭</a>
    </body>
</html>

将这段代码放入 Java 的 String 对象中,会出现如下效果:

String words =
    "<html>\n" +
    "\t<body>\n" +
    "\t\t<a href=\"http://www.hao123.com\">Hao123</a>\n" +
    "\t</body>\n" +
    "</html>";

自动将空格换行缩进和特殊符号进行了转义,但是在 Java 13 中可以使用这样的语法了:

String words = """
    <html>
        <body>
            <a href="http://www.hao123.com">Hao123</a>
        </body>
    </html>
""";

使用 """ 作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。看起来就十分清爽了。

如常见的 SQL 语句:

select empno,ename,sal,deptno
from emp
where deptno in (40, 50, 60)
order by deptno asc

原来的方式:

String query = "select empno,ename,sal,deptno\n" +
"from emp\n" +
"where deptno in (40, 50, 60)\n" +
"order by deptno asc";

现在的方式:

String newQuery = """
select empno, ename, sal, deptno
from emp
where deptno in (40, 50, 60)
order by deptno asc
""";

小细节:

  • 文本块是 Java 语言中的一种新文字。它可以用来表示任何字符串,并且提供更大的表现力和更少的复杂性。
  • 文本块由零个或多个字符组成,由开始和结束分隔符括起来。
  • 开始分隔符是由三个双引号字符 """,后面可以跟零个或多个空格,最终以行终止符结束。文本块内容以开始分隔符的行终止符后的第一个字符开始。
  • 结束分隔符也是由三个双引号字符 """ 表示,文本块内容以结束分隔符的第一个双引号之前的最后一个字符结束。
  • 文本块中的内容可以直接使用 ,,但不是必需的。
  • 文本块中的内容可以直接包括行终止符。允许在文本块中使用 \n,但不是必需的。
  • 编译时会删除多余的空格:

下面这段代码中,我们用 . 来表示我们代码中的的空格,而这些位置的空格就是多余的:

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

多余的空格还会出现在每一行的结尾,特别是当你从其他地方复制过来时,更容易出现这种情况,比如下面的代码:

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

每行文字后面的空格,编译器会自动帮助我们去掉,但是开头部分的空格和结束的 """ 与前面的空格数有关。"""; 前面有几个空格,编译器就会自动帮助我们去掉每一行前面的几个空格。

  • 允许开发人员使用 \n \f \r 来进行字符串的垂直格式化,使用 \b\t 进行水平格式化。比如下面的代码是合法的:
String html = """
<html>\n
<body>\n
<p>Hello, world</p>\n
</body>\n
</html>\n
""";
  • 允许文本块连接

可以在任何可以使用字符串的地方使用文本块。例如,文本块和字符串可以相互连接:

String code = "public void print(Object o) {" +
"""
System.out.println(Objects.toString(o));
}
""";

但是,涉及文本块的连接可能变得相当笨重。以下面文本块为基础:

String code = """
public void print(Object o) {
System.out.println(Objects.toString(o));
}
""";

假设我们想把上面的 Object 改为来自某一变量,我们可能会这么写:

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

可以发现这种写法可读性是非常差的,更简洁的替代方法是使用 String.replaceString.format 等方法。

API层面变化

重构Socket API

重新实现了古老的 Socket 接口。现在已有的 java.net.Socket 和 java.net.ServerSocket 以及它们的实现类,都可以回溯到 Java 1.0 时代了。

  • 它们的实现是混合了 Java 和 C 的代码的,维护和调试都很痛苦。
  • 实现类还使用了线程栈作为 I/O 的缓冲,导致在某些情况下还需要增加线程栈的大小。
  • 支持异步关闭,此操作是通过使用一个本地的数据结构来实现的,这种方式这些年也带来了潜在的不稳定性和跨平台移植问题。该实现还存在几个并发问题,需要彻底解决。

在未来的网络世界,要快速响应,不能阻塞本地方法线程,当前的实现不适合使用了。

全新实现的 NioSocketImpl 来替换 Java 1.0 的 PlainSocketImpl。此实现与 NIO 实现共享相同的内部基础结构,并且与现有的缓冲区高速缓存机制集成在一起,因此不需要使用线程堆栈。除此之外,他还有一些其他更改,例如使用 java.lang.ref.Cleaner 机制关闭套接字,实现在尚未关闭的套接字上进行了垃圾收集,以及在轮训时套接字处于非阻塞模式时处理超时操作等方法。

  • 它便于维护和调试,与 Non-I/O (NIO) 使用相同的 JDK 内部结构,因此不需要使用系统本地代码。
  • 它与现有的缓冲区缓存机制集成在一起,这样就不需要为 I/O 使用线程栈。
  • 它使用 java.util.concurrent 锁,而不是 synchronized 同步方法,增强了并发能力。
  • 新的实现是 Java 13 中的默认实现,但是旧的实现还没有删除,可以通过设置系统属性jdk.net.usePlainSocketImpl 来切换到旧版本。

其他变化

ZGC取消未使用的内存

G1 和 Shenandoah:

JVM 的 GC 释放的内存会还给操作系统吗? GC 后的内存如何处置,其实是取决于不同的垃圾回收器。因为把内存还给 OS,意味着要调整 JVM 的堆大小,这个过程是比较耗费资源的。

  • Java 12 的 346: Promptly Return Unused Committed Memory from G1 新增了两个参数分别是 G1PeriodicGCIntervalG1PeriodicGCSystemLoadThreshold 用于 GC 之后重新调整 Java heap size,然后将多余的内存归还给操作系统。
  • Java 12 的 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental) 拥有参数
    -XX:ShenandoahUncommitDelay= 来指定 ZPage 的 page cache 的失效时间,然后归还内存。

HotSpot 的 G1 和 Shenandoah 这两个 GC 已经提供了这种能力,并且对某些用户来说,非常有用。因此,Java 13则给 ZGC 新增归还 unused heap memory 给操作系统的特性。

在 Java 11 中,Java 引入了 ZGC,这是一款可伸缩的低延迟垃圾收集器,但是当时只是实验性的。号称不管你开了多大的堆内存,它都能保证在 10ms 内释放 JVM,不让它停顿在那。但是,当时的设计是它不能把内存归还给操作系统。对于比较关心内存占用的应用来说,肯定希望进程不要占用过多的内存空间了,所以这次增加了这个特性。

在 Java 13 中,JEP 351 再次对 ZGC 做了增强,将没有使用的堆内存归还给操作系统。ZGC 当前不能把内存归还给操作系统,即使是那些很久都没有使用的内存,也只进不出。这种行为并不是对任何应用和环境都是友好的,尤其是那些内存占用敏感的服务,例如:

  • 按需付费使用的容器环境。
  • 应用程序可能长时间闲置,并且和很多其他应用共享和竞争资源的环境。
  • 应用程序在执行期间有非常不同的堆空间需求,例如,可能在启动的时候所需的堆比稳定运行的时候需要更多的堆内存。

ZGC 的堆内存由若干个 Region 组成,每个 Region 被称之为 ZPage。每个 Zpage 与数量可变的已提交内存相关联。当ZGC 压缩堆的时候,ZPage 就会释放,然后进入 page cache,即 ZPageCache。这些在 page cache 中的ZPage 集合就表示没有使用部分的堆,这部分内存应该被归还给操作系统。回收内存可以简单的通过从 page cache 中逐出若干个选好的 ZPage 来实现,由于 page cache 是以 LRU(Least recently used: 最近最少使用)顺序保存ZPage 的,并且按照尺寸(小,中,大)进行隔离,因此逐出 ZPage 机制和回收内存相对简单了很多,主要挑战是设计关于何时从 page cache 中逐出 ZPage 的策略。

一个简单的策略就是设定一个超时或者延迟值,表示 ZPage 被驱逐前,能在 page cache 中驻留多长时间。这个超时时间会有一个合理的默认值,也可以通过 JVM 参数覆盖它。Shenandoah GC 用了一个类型的策略,默认超时时间是 5分钟,可以通过参数 -XX:ShenandoahUncommitDelay=milliseconds 覆盖默认值。

像上面这样的策略可能会运作得相当好。但是,用户还可以设想更复杂的策略:不需要添加任何新的命令行选项。例如,基于 GC 频率或某些其他数据找到合适超时值的启发式算法。Java 13 将使用哪种具体策略目前尚未确定。可能最初只提供一个简单的超时策略,使用 -XX:ZUncommitDelay=seconds 选项,以后的版本会添加更复杂、更智能的策略(如果可以的话)。

uncommit 能力默认是开启的,但是无论指定何种策略,ZGC 都不能把堆内存降到低于 Xms。这就意味着,如果 Xmx 和 Xms 相等的话,这个能力就失效了。-XX:-ZUncommit 这个参数也能让这个内存管理能力失效。

动态CDS提案

在 Java 应用程序在程序执行结束时动态归档类. 归档的类将包括默认基层 CDS 归档中不存在的所有已加载应用程序类和类库。

CDS,是 Java 12 的特性了,可以让不同 Java 进程之间共享一份类元数据,减少内存占用,它还能加快应用的启动速度。而 Java 13 的这个特性支持在 Java application 执行之后进行动态 archive。存档类将包括默认的基础层CDS 存档中不存在的所有已加载的应用程序和库类。也就是说,在 Java 13 中再使用 AppCDS 的时候,就不再需要这么复杂了。该提案处于目标阶段,旨在提高 AppCDS 的可用性,并消除用户进行运行时创建每个应用程序的类列表的需要。

# JVM退出时动态创建共享归档文件:导出jsa
java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello
# 用动态创建的共享归档文件运行应用:使用jsa
java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello

Java 13 这次对 CDS 增强的目的:

  • 改善 APPCDS 的可用性,减少用户每次都要创建一个类列表的需要。
  • 通过开启 -Xshare:dump 选项来开启静态归档,使用类列表仍然行得通,包含内置的类加载信息和用户定义的类加载信息。

在 Java 13 中做的增强,可以只开启命令行选项完成上述过程,在程序运行的时候,动态评估哪些类需要归档,同时支持内置的类加载器和用户定义的类加载器。

在第一次程序执行完成后,会自动的将类进行归档,后续启动项目的时候也无需指定要使用哪些归档,整个过程看起来更加透明。