以是否应该升级到JDK 17为例看to B行业的技术选型和升级

前言

  虽然这篇文章的标题写的是正确看待Java以及何时应该升级到JDK 17,但是实际上可以认为是我对技术选型和系统性软件工程的一些总结,其中包含了一些可以用于其它技术的参考性讨论。做了很多年的Java之后,这几年笔者在做lightdb数据库内核开发中以c/c++为主,所以可维护性和是否有显而易见的收益会成为我评估的很重要的因素。我会尽可能客观的描述从运行时和可维护性角度(我并不是说其它维度不重要,只是相比来说,to B系统这两方面会更重要),如何去评测或者客观的评价一个选型或者使用某个特性是否划算,而且我希望这个结论是用户、ISV、开发人员三方都赢。虽然以JDK什么时候应该升级为例,我希望它能在一些评估选型或升级时参考。

  对于Java作为企业级开发语言,我认为应该首先区分抖动高度敏感系统(包括交易系统、数据库)和其它系统看待Java的适用性,因为现在的系统都很庞大,所以应该成为那个应用场景而非系统更合适,这里就不区分,大家能理解即可。从软件工程维度来说,这分为运行时成效和长期开发成效两方面。

  l  运行时成效:分吞吐量和抖动容忍性。就单请求数据量不是很大的事务比如MB级别来说,吞吐量上java不存在明显的问题,即使单实例不满足要求,完全可以通过分布式达到线性扩展能力。从时延[张君华1] 方面来看,因为不管是minor gc(它也会stop the world,但因为大部分应用的大部分对象在新生代的时候几乎都会被回收,它会快到微秒级,以至于绝大部分应用基本无感知)还是majar gc、full gc,或多或少的都会stop the world,由于任何时候,一个服务中都可能引用对象、新执行某个需要创建很多对象的操作,都可能导致gc来带不确定的停顿,所以在抖动容忍性上,java是不太能打包票预估会不会发生的,我们能做的是尽可能的让它足够低(比如每次都控制在1毫秒内,通过24小时的持续压测等)。这个问题和采用异步复制的高可用架构是一样的,好像概率很低,但是出现灾备切换时,丢数据或不正确的概率很高。在Java中,该问题一样可以验证,缓存在JVM堆中的数据[张君华2] 越多,gc时间[张君华3] 会越长,即使它是不必要的(当然,我们可以通过堆外解决,这也是条路,它仅仅是为了减少不必要的GC,仍然比c/c++容易得多,比redis靠谱得多)。

  l  长期开发成效:为什么有个修饰语长期呢?不同于to C系统,对于to B系统来说,没有上生产还好说,一旦上了生产,下线那是极为复杂的事情,不是一句简单的跟客户说系统我们打算不要了,限定你在某年某月末日之前把数据迁移走,这会导致用户很无奈、而你没有生意可做。具体展开包括几个方面:一、从应用开发角度来说,java生态对于企业应用开发提供了现成的基建和可复用的各种库,涵盖从数据库、中间件、文件操作到图片、PDF、大数据、批处理等,大大降低了入门开发门槛和开发速度。但是反过来,这使得库出现什么问题的话,排查和解决可能会比较耗时,spring全家桶自己的大基建以及在各个库的基础上又增加了二次封装使得排查更为复杂。其二,开发人员很关心时髦的新技术、新框架、新的工具库(所谓的面向面试编程,一个系统完成同一类功能的库找出几个一点不奇怪,比如json、xml、连接池、字节码),很多库在系统开发完成后通常已经升级了多个版本或者已经不在主流,旧客户端不兼容新服务端的情况,三方库版本之间的依赖兼容问题也会引起一连串版本的更新,也存在其他开发通常不够熟悉或者压根没有使用过的情况,这都会导致不精细化管理三方库和版本(注意:这不是说建一个系统维护依赖关系、定期检查一遍就高枕无忧,这是一个内容治理的问题,参考运营有些文章PV可以几十万,有些一直在几百),长期开发成效不达预期甚至看起来做无用功的情况。第三,关于安全,Java三方的安全漏洞是出了名的多,现代用户很关心安全性,虽然很多部署在内网的系统CVE漏洞其实压根不会发生,因为多层防火墙、应用安全已经保障的完全足够,但我仍然把它归类为中性看待,在技术栈升级成本与收益中我会讲述我的理由。

  这两方面比较难以评估在于,通常在系统开发完成上线后至少一两年后或原始开发不在之后才会显现出来,然后你发现当初真应该规范下。其次,我们通常在没有异常之前不愿意承认别人指出自己的方案存在瑕疵(注:这两年我被别人指出不合理后,主动去调整特别多,包括功能完善、架构上、规范上,不乏一些我们需要两个RP甚至三个RP才能切换到位甚至较大返工的)。

spring对JDK版本支持的滚动周期

  讲完了上下文,现在切入正文,讨论升级到JDK 17是否是一件值得考虑的事。每出一个大版本,网上关于为什么应该升级到新版本的JDK都会有较多的文章,一般都是总结性或罗列各种新特性,至于那些特性对用户来说是否长期适用,分析的会比较少。从事后来看,你会发现很多JDK 17甚至更早时候厂商、咨询公司的一些预测其实噱头成分更多一些,只是如果不跟随好像显得不够入流,举个典型的例子如AOT(AOT 编译:GraalVM 可以将 Java 程序静态编译成本地机器码,这被称为 Ahead-of-Time(AOT)编译。AOT 编译可以提供更快的启动时间和更低的内存消耗,适用于一些对性能要求较高的场景),它在理论上不太行得通,因为java使用c/c++写的,而C++一致都未能很好的支持反射机制,所以注定了AOT和native image的灵活性很受限,用于需要广泛灵活可变的框架和企业级开发更是根基不够成熟。所以,分析哪些特性已经真正能带来本质性改进,它是否带来的收益高于成本,以及如何度量反而是升级前更重要的事。

  java之所以如此流行,核心生态spring功不可没,Spring对JAVA有风向标作用,所以spring对jdk版本的态度会很大的影响整个Java界对JDK版本的态度,因为新启动和开发的项目、新入行的开发人员可能不会关心使用jdk 8还是jdk 11或更新版本,但它一定会使用最新主版本的spring框架,因为官方的脚手架和入门示例默认就是最新主推的版本。自JDK 17在2021年发布后,2022年发布的SpringBoot3(Spring6)直接限制最低为JDK17,可预见:为了使用Spring最新框架,很多团队和开发者将被迫从Java 11(甚至Java 8)直接升级到Java 17版本。在此之前,Java社区一直是"新版任你发,我用Java 8",不管新版本怎么出,很少有人愿意升级,包括2018年发布的JDK 11 LTS[张君华4] 。

  那为什么spring 6开始从JDK 17开始直接支持呢?一方面,大多数框架,不管是开源还是商业中间件喜欢做宏大而优雅的事情,所以Spring的开发者会觉得,我都用Java 17的新API写代码,简化逻辑,使用新特性,很棒;但是为了支持Java8,需要写很多workaround,那就很不爽,很不优雅。

  但是站在工程的角度,作为Spring框架的使用者,自然是希望Spring框架能够支持更多的Java版本,更多的用户。最好不用改代码,最好不用升级Java版本,最好啥都不用改就能用。

  本质上来说,这个反映了框架维护者和框架使用者的矛盾。所以它不得不定期滚动往前,历史上spring也是这么做的。如下所示:

Spring[张君华5]  Version

Java Version

6.x

JDK 17+

5.x

JDK 8, 9, 10, 11, 12, 13, 17,18,19

Spring Framework 5.3.x:JDK 8-19
Spring Framework 5.2.x:JDK 8-15
Spring Framework 5.1.x:JDK 8-12
Spring Framework 5.0.x:JDK 8-10

4.3.x

JDK 6, 7, 8

4.2.x

JDK 6, 7, 8

4.1.x

JDK 6, 7, 8

4.0.x

JDK 6, 7, 8

3.2.x

JDK 6, 7

3.1.x

JDK 5, 6, 7

3.0.x

JDK 5, 6

  spring 5对JDK的最低版本要求是JDK 8,看起来spring 6要求最低JDK 17,并不是非常规操作。而JDK 11更像JDK 7是个陪跑的版本。

  注:说句题外话,实际上linux对glibc也是差不多的,只不过大部分用户感知不明显而已(我们很关心到底用gcc 4.8、gcc 7.3还是clang,以及centos和kylin的差异,所以还是挺明显)。

  如此看起来,未来两三年升级到JDK 17或21是不得不需要纳入日程考虑的事,只不过兼容性和过渡会花费不少的成本,如果系统还没有完全稳定,则更需要谨慎评估,这涉及照顾各个方面,这里就不展开。

那为什么Java8占比这么多

  Java8提供了很多特性,比如Lambda 表达式、Optional 类,相当不错的性能和稳定性,加上Java8超长的支持时间(令很多人意外的是,Java11支持的时间到2026年9月,Java8的支持时间到2030年12月,从这个层面上来说Java8比Java11甚至JDK 17[张君华6] 都要“长寿”),都导致Java8完全足够使用。

  这导致Java 8之后的版本,现在看来吸引力不是很大,模块化有break changes,异步接口有netty,AOT/GraalVM在后端也不必要、甚至被废弃。唯一比较期待的Project loom还遥遥无期,只能拿wisp2凑合用。

  Java 8之后的分发策略,支持方式的变更,也会让开发者在升级前要仔细考虑。比如Java11你用哪个发行版?Oracle会有潜在法律问题(虽然Oracle在JDK 17发布的时候声明后续JDK全部免费使用,最近又在查许可,所以很难让用户放心);AdoptOpenJDK的支持能不能跟上等等。

JDK9以来累计的主要新增特征

  现在来看一下近10年JDK引入的主要特性是否值得让我们为此升级。同样,主要从长期开发成效和运行时效果两方面来看。

1.局部变量类型推断

  这是自 Java 8 以来添加到 Java 中的最受开发者欢迎的功能之一,和C++ 11的auto自动类型推导类似。它允许你在不指定类型的情况下声明局部变量。类型是从表达式的右侧推断出来的。此功能也称为var类型。

  在上面的示例中,两个程序将生成相同的输出,但在 Java 10 的情况下,我们使用而var不是指定类型。说实话,这个特性很容易被误用,尤其是Java的类型名通常非常长,导致我们都不愿意写,很可能就会大面积使用,但调用层次深了之后,可能排查效率会降低。

5.模式匹配instanceof

  模式匹配instanceof是 Java 16 中添加的一项新功能。它允许你将instanceof运算符用作返回已转换对象的表达式。当你使用嵌套的 if-else 语句时,这非常有用。在下面的示例中,你可以看到我们如何使用instanceof运算符来捕获Employee对象,而不是进行显式转换。

  相比类型推导,instanceof通常之后对象都会强转,而且使用的点作用范围比较受控,所以算是个还不错的语法糖。

2.switch表达式

  在 Java 14 中使用 switch 表达式时,你不必使用关键字break来跳出 switch 语句或return在每个 switch case 上使用关键字来返回值;相反,你可以返回整个 switch 表达式。这种增强的 switch 表达式使整体代码看起来更清晰,更易于阅读。更重要的是,能避免不经意的遗漏导致逻辑不正确,以往只能依赖代码检测工具,还是能够改进质量的。

3.文本块

  文本块是 Java 15 中添加的一项新功能。它允许你在不使用转义序列的情况下创建多行字符串。这在你创建 SQL 查询或 JSON 字符串时非常有用。在下面的示例中,你可以看到使用文本块时代码看起来更加简洁。

  还是能提升更多的易读性,原来建议放在配置文件中的一些文本片段,如JSON、XML、SQL,可以直接硬编码在java源文件中,还是有帮助的。

4.Records

  记录Records是添加到 Java 14 的一项新功能。它允许你创建用于存储数据的类。它类似于 POJO 类,但代码少得多;大多数开发人员使用 Lombok 生成 POJO 类,但是有了记录,你就不需要使用任何第三方库。在下面的示例中,你可以看到创建记录类所需的代码非常少。

  虽然看起来不明显,但是这个语法糖是相当实用,因为lombok[张君华7] 并不是完全和pojo以及mybatis的惯例一致,还有漏洞[张君华8] 。有了record之后,就可以不用引入三方的lombok了。

6. 密封类

  密封类是添加到 Java 17 中的一项新功能。它允许你将类或接口的继承限制为一组有限的子类。当你想将类或接口的继承限制为一组有限的子类时,这非常有用。在下面的示例中,你可以看到我们如何使用sealed关键字将类的继承限制为一组有限的子类。

  密封类的子类可以声明为final或non-sealed。final 子类不能进一步扩展,而非密封子类可以进一步扩展。

  对于框架开发和公共模块的演进来说,该特性特别有用。尤其是某些子类我们从A系统复用到B系统,或者X和Y系统整合为Z系统的时候,或者一些开发规范做了调整,从而创建了新的子类,但是又不希望老的子类使用从而造成无法收编。

7. 有用的 NullPointerException

  NullPointerExceptions 是 Java 14 中添加的一项新功能。它允许你获取有关NullPointerExceptions. 这在调试时非常有用NullPointerExceptions。在下面的示例中,你可以看到相同的代码如何NullPointerExceptions在 Java 8 和 Java 14 中生成不同的结果,但在 Java 14 中,你可以获得有关异常的更多信息

  从问题排查的角度来看,对于异常的任何改进都是有百利而无一弊。

8. 性能提升[张君华9] 

  虽然说性能和GC停顿被我放在最后,但他俩其实是之前我以java为主期间最关心的特性,而且我觉得它俩就是最重要的。先说三方数据,从规划调度引擎 OptaPlanner 项目对 JDK8、JDK 17和 JDK 11 的性能基准测试进行了对比来看:

  1. 对于 G1GC(默认),Java 17 比 Java 11 快 8.66%;
  2. 对于 ParallelGC,Java 17 比 Java 11 快 6.54%;
  3. Parallel GC 整体比 G1 GC 快 16.39%;

  简而言之,JDK17 更快,高吞吐量垃圾回收器比低延迟垃圾回收器更快,G1表现最差(JDK 17默认GC)。从我实际跑renaissance-benchmarks基准测试(覆盖web、计算密集型、并发密集型)来看,总体来说JDK 17明显好于JDK 8,普遍在0%-20%之间,有几个特例是scala-doku和page-rank,相差高达250%。

9. 低时延停顿的ZGC[张君华10] 

  ZGC 是 Java 11 中引入的最为瞩目的垃圾回收特性,是一种可伸缩、低延迟的垃圾收集器,不过在 Java 11 中是实验性的引入,主要用来改善 GC 停顿时间,并支持几百 MB 至几个 TB 级别大小的堆,并且应用吞吐能力下降不会超过 15%,目前只支持 Linux/x64 位平台的这样一种新型垃圾收集器。

  Java 13 中对 ZGC 的改进,把之前的限制基本都解决了,包括:

  • 释放未使用内存给操作系统。默认情况下是开启的,不过可以使用参数:-XX:-ZUncommit 显式关闭,同时如果将最小堆大小 (-Xms) 配置为等于最大堆大小 (-Xmx),则将隐式禁用此功能。
  • 支持最大堆大小为 16TB
  • 添加参数:-XX:SoftMaxHeapSize 来软限制堆大小

  具体gc停顿方面,根据openJDK官方的性能测试数据显示(JEP333,基于SPECjbb 2015),ZGC的表现相当不错:

  • 在仅关注吞吐量指标下,ZGC超过了G1;
  • 在最大延迟不超过某个设定值(10到100ms)下关注吞吐量,ZGC较G1性能更加突出。
  • 在仅关注GC低延迟指标下,ZGC的性能高出G1很多。99.9th仅为G1的百分之一。zgc中没有分代的概念,所以,jstat中CGC/YGC并不体现ZGC的垃圾回收,需要通过-Xlog:gc进行分析。

  由于SPECjbb 2015是一个收费工具,没有提供开源版本,故我们使用renaissance-benchmark进行测试(其定位和SPECjbb 2015类似,也用于评估JVM的JIT、GC、解释器等),在设置-XX:MaxGCPauseMillis的情况下,ZGC具有比较明显的优势。因为使用Java的应用大部分都非时延高度敏感的系统,所以对ZGC的量化价值和竞争力其实并不那么直接,但金融行业交易类业务系统和做基础框架、中间件的社区通常会足够关注和重视。

整体来说,ZGC对低延迟大内存服务确实有着明显的好处。

 

  从主要的新特性来看,确实其实并不那么的明显,所以大家不积极并不奇怪,也说明JDK 8绝大部分场景都够用。从理性来看,需要整体完整的验证下是否一些密集型、高吞吐系统会带来明显的用户所需的提升,比如抖动下降、时延下降、吞吐量提升。

技术栈升级成本与收益

  这事从不同干系人的角度来看,都是有利有弊,而且夹杂着不得不做的刚需消费。总体来说利大于弊,因为安全漏洞、版权重视等其实是利好,虽然看起来会增加直接开发、维护成本,但是行业会更加健康、良性发展。其次,技术栈大版本升级通常都会从底层提升性能和易用性,利用新的硬件和内核特性、放弃过老的硬件支持的冗余代码,比纠结方法调用、内联函数、协议细微差别等带来更加明显的系统性全局性能提升,对于关心性能和使用了新硬件的用户,它有直接性收益体现。

  关于技术栈升级,有一个规律:越贴近底层,越会有标准化的抽象接口,独立升级会越容易。比如linux kernel升级,只需要升级下,验证下没有问题就可以了(最多改一些应用配置)。但是 Java 版本升级,就需要仔细检查各个depdency的依赖,然后还要做大量的业务测试才能升级。如果是Spring升级,就会更加麻烦,需要改代码,需要测试验证;甚至会发现旧的使用方式不能用,需要修改核心逻辑等。在这种情况下,Spring要新推出一个大版本,必然需要一些杀手特性才可以让使用者升级。要不然就只能万年Spring 5.x了,Spring社区自然也不希望这种事情发生。

总结和展望

  Spring 6出来后,会有新公司、新项目使用,但是对于旧的项目,会依然在旧的版本上继续运行。

  目前来说国内很多程序猿可能觉得升级会造成额外工作,出了问题费力不讨好,要是出了安全问题,更是头疼。也有说没有实质性的好处,而且还有风险,还有从企业角度说,未来也不升级,因为去Oracle化。但考虑到未来oracle不再维护JDK8,Spring也不再维护过去版本的时候,为了跟上时代,使用最新技术,必然会助推JDK的升级。

  当越来越多的公司加入到JDK17以上的大军中,未来更多的框架新版本都会最低支持JDK17,因为兼容旧JDK实在不值得,当大部分框架和社区、论坛都是讨论JDK17的技术和各种解决问题的方法时,必然会反推企业进行升级。

 [张君华9]JDK不同版本性能对比,基本和实际测试有差距,但通常JDK 17更快

JDK 17升级的第一个坑

Java反射机制的调整

相信刚入坑JDK17的朋友,大多数都会在服务运行的时候看到这么一条提示:

java.base does not “opens java.util“ to unnamed module

通过反射访问JDK模块内类的私有方法或属性,且当前模块并未开放指定类用于反射访问,就会出现以上告警。这是jdk 17强封装的后果。解决方式也必须使用模块化相关知识,可以使用遵循模块化之间的访问规则,也可以通过设置 –add-opens java.base/java.lang = ALL-UNNNAMED 破坏模块的封装性方式临时解决。参见https://docs.oracle.com/en/java/javase/17/migrate/migrating-jdk-8-later-jdk-releases.html#GUID-2F61F3A9-0979-46A4-8B49-325BA0EE8B66。

posted @ 2023-08-12 20:36  zhjh256  阅读(338)  评论(0编辑  收藏  举报