java 版本

ddatsh

dev #java change

Go、Rust 等一些语言的新版本特性,云原生的一些基础设施(Docker,Kubernetes,ServiceMesh,Dapr,Serverless)

启动速度快、依赖少、内存占用少、Goroutine 并发等击中 Java 的软肋

Java 曾经一些优秀的设计在今天看来似乎并不那么重要甚至过时了

Write Once, Run Everywhere,现在这种部署的便利性已经完全可以交由容器提供了,而且做得更好,可以将整个运行环境进行打包。Docker 的口号也是:“Build Once, Run Anywhere”

Java 总体面向大规模、长时间运行的服务端应用而设计

微服务化甚至 Serverless 化的部署形态下,有了高可用的服务集群,也无须追求单个服务要 7×24 小时不可间断地运行,它们随时可以中断和更新,Java 的这一优势无形中被削弱了

Java 资源占用包含两方面:静态程序大小和动态内存占用

JRE +各种复杂 Jar 包依赖,Java 应用容器镜像大小轻松上 G

应用运行期内存占用居高不下,天生缺陷很难克服

Serverless 应用或函数,冷启动速度至关重要,之前 AWS Lambda 函数允许最多运行 5 分钟,很难想象还要花一分钟时间先启动

命运当然要靠自我奋斗,另一方面也要考虑历史的进程,队友们给不给力

Java 9:难产的模块化

Java 9 开始,除去能够肉眼感知的语法和 API 的变动(Productivity)之外,也在性能(Performance)上一直努力

Java 平台模块系统(JPMS),项目代号 Jigsaw

之前,Java 以 package 对代码进行组织,再将 package 和资源打成 Jar 包

模块将多个逻辑、功能上相关的包以及相关的资源文件封装

官方介绍文档:Understanding Java 9 Modules

Java Runtime 庞大臃肿一直为人诟病(rt.jar 60 多 M),此外还有 Jar Hell、安全性等等问题

模块化隐藏着陷阱:

compact strings 对底层存储的优化来减少 String 的内存占用

jaotc 一大应用便是编译 java.base module

JVMCI API 提供了访问 JVM 结构、安装编译代码和插入 JVM 编译系统的机制,Graal 基于 JVMCI,Java 虚拟机与 Graal 解耦合

语法

在interface中定义私有方法

改善接口内部的代码可重用性,接口吸收了抽象类的优点

例,需要两个默认方法来共享代码,私有接口方法允许它们共享代码,但不将该私有方法暴露给它的实现类调用

分别计算奇数与偶数的和

import java.util.function.IntPredicate;
import java.util.stream.IntStream;

public interface CustomCalculator {

    default int addEvenNumbers(int... nums) { //非抽象,java8 开始可以定义default方法
        return add(n -> n % 2 == 0, nums);   //过滤偶数并求和,调用private私有方法
    }

    default int addOddNumbers(int... nums) { //非抽象,java8 开始可以定义default方法
        return add(n -> n % 2 != 0, nums);  //过滤奇数并求和,调用private私有方法
    }

    //按照过滤条件过滤奇数或偶数并sum求和:java9开始可以定义private私有方法
    private int add(IntPredicate predicate, int... nums) {

        return IntStream.of(nums)   //java8 Stream流
                .filter(predicate)   //java8 predicate及过滤器
                .sum();  //sum求和
    }

}
public class MainCalculator implements CustomCalculator {

    public static void main(String[] args) {

        CustomCalculator demo = new MainCalculator();

        int sumOfEvens = demo.addEvenNumbers(1, 2, 3, 4, 5, 6, 7, 8, 9);
        System.out.println(sumOfEvens);   //过滤所有偶数并求和,结果是20

        int sumOfOdds = demo.addOddNumbers(1, 2, 3, 4, 5, 6, 7, 8, 9);
        System.out.println(sumOfOdds);   //过滤所有奇数并求和,结果是25
    }

}

资源自动关闭的语法增强

java 9 try-with-resources 语法的 try()可以包含变量,多个变量用分号隔开

改进目的是让语义更加明确,将资源创建代码与尝试资源回收的语法分离

void testJava9Try() throws IOException {
  String fileName = "testJava9Try.txt";

  FileOutputStream fos = new FileOutputStream(fileName);
  OutputStreamWriter osw = new OutputStreamWriter(fos);
  BufferedWriter bw = new BufferedWriter(osw);

  try(bw;osw;fos){
    bw.write("Java9-可以被自动调用close()方法");
    bw.flush();
  }

同一个Jar支持多JDK版本运行

MANIFEST.MF:

Multi-Release: true
jar root
  - A.class
  - B.class
  - META-INF
     - versions
        - 9
           - A.class
        - 10
           - A.class

Reactive Streams

JDK 8主要通过NIO.2(java.nio.channels)提供非阻塞 I/O 能力

Reactive Streams规范是JDK 9中通过Flow API引入

public class MySubscriber implements Flow.Subscriber<String> {

    private Flow.Subscription subscription;  //订阅令牌

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        System.out.println("订阅关系建立onSubscribe: " + subscription);
        this.subscription = subscription;
        subscription.request(2);
    }

    @Override
    public void onNext(String item) {
        System.out.println("item: " + item);
        // 一个消息处理完成之后,可以继续调用subscription.request(n);向发布者要求数据发送
        //subscription.request(n);
    }

    @Override
    public void onError(Throwable throwable) {
        System.out.println("onError: " + throwable);
    }

    @Override
    public void onComplete() {
        System.out.println("onComplete");
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;

public class SubmissionPublisherExample {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(1);
        SubmissionPublisher<String> sb = new SubmissionPublisher<>(executor, Flow.defaultBufferSize());
        sb.subscribe(new MySubscriber());   //建立订阅关系,可以有多个订阅者
        sb.submit("数据 1");  //发送消息1
        sb.submit("数据 2"); //发送消息2
        sb.submit("数据 3"); //发送消息3

        executor.shutdown();
    }

}

即使发布者 submit 了 3 条数据,MySubscriber也仅收到了 2 条数据进行了处理

因为在 MySubscriber#onSubscribe()方法中使用了 subscription.request(2)

这就是 “背压” 的响应式编程效果,我有能力处理多少数据,就会通知消息发布者给多少数据

Collection集合类的增强与优化

Of()方法创建集合

Set<Integer> integers = Set.of(2, 6, 7, 10);
List<Integer> integers = List.of(2, 6, 7, 10);
Map<Integer, String> map = Map.of(2, "two", 6, "six");
Map<Integer, String> map = Map.ofEntries(Map.entry(2, "two"), Map.entry(4, "four"));

Arrays.mismatch()

查找两个数组之间的第一个不匹配索引

int[] ints1 = {1, 3, 5, 7, 9};
int[] ints2 = {1, 3, 5, 6, 7};
int i = Arrays.mismatch(ints1, ints2);
System.out.println(i); //3

Arrays.equals()

新增两个被比较的数组 fromIndex和toIndex参数

根据两个数组的相对索引位置检查两个数组的相等性

 String[] sa = {"d", "e", "f", "g", "h"};
 String[] sb = {"a", "b", "c", "d", "e", "f"};
 boolean b = Arrays.equals(sa, 0, 2, sb, 3, 5);
 System.out.println(b);  //true

从sa数组的索引0-2,与sb数组的索引3-5的元素进行比对。结果为true相等

Java 10

G1 多线程并发 mark-sweep-compact:Java9 使用单线程做 mark-sweep-compact

Application Class-Data Sharing,不同 Java 进程间共享应用类的元数据来降低启动时间和内存占用,算是对 Java 5 引入的 CDS 的扩展,之前只支持 Bootstrap Classloader 加载的系统类

Docker 支持更好,能认出 Docker 环境了

Java 11

ZGC

Epsilon 不会做 GC 的垃圾回收器,对于一些不需要长时间运行、小规模的程序来说,会更关注启动时间、内存占用等指标,很典型的就比如 Serverless 函数。只要 JVM 能正确分配内存,然后在堆耗尽之前退出,那显然运行负载极小、没有任何回收行为的 Epsilon 便是很恰当的选择

Java 12

OpenJDK Shenandoah

G1 能自动将未使用的堆内存返还 OS

垃圾对象会持续占用内存,直到下一次 GC 为止

GC 算法也决定了更多的内存占用(标记-复制需要有两块内存区域,Survivor);标记-清除同样需要更大的内存区域,GC 结束时有大量的空间碎片

CMS/G1 在垃圾收集阶段用户线程还需要持续运行,需要预留足够内存空间提供给用户线程使用

CMS 在老年代达到指定的占用率后(Java 6 默认 92%)开始 GC,-XX:CMSInitiatingOccupancyFraction 调高这个值,但调得太高又容易碰到 Concurrent Mode Failure

G1 为每个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,Region 一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上,并且默认不回收在这个地址以上的对象

一般 JVM 启动时一次性申请大块内存(Reserved Heap),倾向于在运行期保留这些内存。虽然一次 GC 结束后可能会空出很多内存,但 JVM 在内存返还策略上有时会左右为难,这些内存有可能很快就需要被拿来分配对象,如果频繁进行归还,再而触发 page fault 反而带来性能下降。折中的策略是动态地根据负载来决定是否返还

此前 G1 只有在 Full-GC 或并发周期期间才能返还内存,而 G1 的目标之一是避免 Full-GC,并且仅根据 Java 堆占用和分配活动触发并发循环,多数场景下,除非强制触发,并不会有内存返回行为

Java 12 后 G1 会在应用不活动的空闲期间定期尝试继续或触发并发循环以确定整体 Java 堆使用情况,并自动将 Java 堆中未使用的部分返回给 OS

Java 13

ZGC 同 G1 和 Shenandoah 一样,可以将未使用的内存返还给 OS

AppCDS 增强:在 Java10 的 AppCDS 基础上支持动态归档,可以在程序退出时自动创建

Java 14

ZGC 支持 Mac 和 Windows

G1 支持 Numa-Aware 内存分配

G1 之前的收集器就只有针对吞吐量设计的 Parallel Scavenge 支持 NUMA 内存分配

Java 15(2020/09/15)

ZGC 和 Shenandoah 转正 Production 了

Java 16(2021/03/16)

ID JEP Feature
1 394 Pattern Matching for instanceof
2 395 Records
3 392 Packaging Tool
4 387 Elastic Metaspace
5 376 ZGC: Concurrent Thread-Stack Processing
6 380 UNIX-Domain Socket Channels
7 396 Strongly Encapsulate JDK Internals by Default
8 390 Warnings for Value-Based Classes
9 338 Vector API (Incubator)
10 389 Foreign Linker API (Incubator)
11 393 Foreign-Memory Access API (Third Incubator)
12 397 Sealed Classes (Second Preview)
13 347 Enable C++14 Language Features (in the JDK source code)
14 357 Migrate from Mercurial to Git
15 369 Migrate to GitHub
16 386 Alpine Linux Port
17 388 Windows/Aarch64 Port

394:Pattern Matching for instanceof

模式匹配 for instanceof,相当于是增强的 instanceof,JDK 14 首次成为预览特性,JDK 16 转正

模式匹配的到来使 instanceof 变得更简洁、更安全

if (object instanceof Kid) {
  Kid kid = (Kid) object;
  // ...
} else if (object instanceof Kiddle) {
  Kid kid = (Kid) object;
  // ...
}
  1. 判断 object 是否是 Kid 的实例

  2. 把 object 强制转换为 Kid 类型

  3. 创建了一个局部变量:kid

判断完之后为什么还要进行一次类型强转?这应该不是必需的,而且强转的时候可能类型出错

模式匹配的 instanceof 写法

if (object instanceof Kid kid) {
  // ...
} else if (object instanceof Kiddle kiddle) {
  // ...
}

判断、赋值一步到位

395:Records

新语法糖,简化代码,JDK 14 首次成为预览特性,JDK 16 式转正

一定程度上避免低级冗余的代码,比如:constructors, getters, equals(), hashCode(), toString() 方法等,相当于 Lombok 的 @Data 注解,但又不能完全替代

public record Student(String name, int id, int age) {}

387:Elastic Metaspace

帮助 HotSpot 虚拟机,将元空间中未使用的 class 元数据内存更及时地返回给 OS,以减少元空间的内存占用空间

另外,还简化了元空间的代码,以降低维护成本

JDK 支持 Alipine Docker 镜像(5MB 左右,Ubuntu 近 200 MB,musl 作为 C 标准库)