首页 >> 大全

一些杂想:Java老矣,尚能饭否?

2023-08-29 大全 18 作者:考证青年

最近抽空看了Go、Rust等一些语言的新版本特性,还有云原生的一些基础设施(,,,Dapr,),有点感慨Go真的是云原生的“一等公民”,像是启动速度快、依赖少、内存占用少、 并发等无一不是击中Java的软肋。然后突发奇想在上搜了下“Java老矣”,能搜出520,000条结果。不禁想问:Java真的老了吗?

“落寞”的Java

自1995年出生以来,Java已经有27年历史了,曾经的风流雨打风吹去,一些优秀的设计在今天看来似乎并不那么重要甚至过时了。比方说:

另一个广为诟病的是Java的资源占用问题,这主要包含两方面:静态的程序大小和动态的内存占用。

Java的启动时间也是一大心病,主要原因在于启动时虚拟机初始化和大量类加载的时间开销(当然还有一个罪魁祸首是的bean初始化,我之前写了个异步初始化 Bean的 rhino-boot-turbo,把串行改并行启动速度会快很多)。本身镜像体积大,拉取时间就长,再加上分钟级的启动时间,部署应用就更显得慢了。传统的企业应用更看重长时间运行的稳定性,重启和发布频率相对较低,对启动时间相对没那么敏感,然而对于需要快速迭代、水平扩展的微服务应用而言,更快的的启动速度就意味着更高的交付效率和更加快速的回滚。尤其是对于应用或函数,冷启动速度至关重要,之前看AWS 函数允许最多运行5分钟,很难想象还要花一分钟时间先启动。

云原生的潮流滚滚而来,Java的这些缺陷在要求快速交付的大环境下显得格格不入,难怪Java与Go、Rust等原生语言相比,会显得“落寞”了。

作为一个Java程序员,肯定想问,Java还有机会吗?想起有位长者说过:一个人的命运啊,当然要靠自我的奋斗,另一方面,也要考虑历史的进程。我想把它改成:Java的命运啊,当然要靠自身的努力,另一方面,也要考虑队友们给不给力。

JDK的演进

我们的大部分系统都还跑在Java 8之上,因此作为开发同学对Java 8也是最熟悉的。从Java 9开始,JDK的版本号堪比版本狂魔涨得飞快,除去开发者能够肉眼感知的语法和API的变动()之外,Java也在性能()上一直努力。

我捋了一下官网[1]从Java 9开始的JEP列表,按照个人理解列出了关键的一些特性。

Java 9:难产的模块化

在数次delay之后,Java 9终于正式引入了Java平台模块系统(JPMS),项目代号。在这之前,Java以对代码进行组织,再将和资源打成Jar包,模块则在的概念上将多个逻辑上、功能上相关的包以及相关的资源文件封装成模块。关于模块的详细介绍,可以参考下官方的介绍文档: Java 9 [2]。

此前,Java 的庞大臃肿一直为人诟病(一个rt.jar就有60多M,整个JRE环境可以达到上百M),瘦身正是 的目标[3]之一。此外,还有Jar Hell、安全性等等问题。

不过模块化看着很好,也隐藏着陷阱:

对于新的项目,使用模块构建似乎是值得的,但现状是,大多数开发者会忽略模块系统,尤其是对于已经运行了多年的大型项目,改造的成本令人望而却步。我猜测肯定会有人吐槽类似的问题:

搜了一下,似乎国外网友也有一样的疑惑[7]。不过,我认为让程序员可以定义应用程序的模块是什么,它们将如何被其他模块使用,以及它们依赖于哪些其他模块,这些事情还是有必要做的。

当然Java9除了模块化之外,还有一些其他特性也值得关注:

关于 JVMCI 多介绍一些。相比用 C 或 C++ 编写的现有编译器(说的就是你,C2),用Java写编译器更容易维护和改进。JVMCI的API 提供了访问 JVM 结构、安装编译代码和插入 JVM 编译系统的机制,后面讲到的Graal正是基于JVMCI。

编译器与JVM的交互可以分为如下三个方面。

响应编译请求;

获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的;

将生成的二进制码部署至代码缓存(code cache)里。

即时编译器通过这三个功能组成了一个响应编译请求、获取编译所需的数据,完成编译并部署的完整编译周期。

传统情况下,即时编译器是与Java虚拟机紧耦合的。也就是说,对即时编译器的更改需要重新编译整个Java虚拟机。这对于开发相对活跃的Graal来说显然是不可接受的。

为了让Java虚拟机与Graal解耦合,引入 JVMCI 将上述三个功能抽象成一个Java层面的接口。这样一来,在Graal所依赖的JVMCI版本不变的情况下,我们仅需要替换Graal编译器相关的jar包(Java 9以后的jmod文件),便可完成对Graal的升级。

其实JVMCI接口就长这样:

{

/**

* a . This the to code and

* it in the code cache if the is .

*/

sult ( );

Java 10:小升级

的性能提升点并不多(6个月一次的版本节奏难免要挤挤牙膏):

Java 11:ZGC闪亮登场

Java 11是LTS版本,也可能是企业选择从万年Java 8升级到的第一个版本。最大的改动是引入了新一代的垃圾回收器-ZGC[14]。ZGC的首要目标是实现低停顿(暂停时间不超过10ms)、高并发的垃圾回收,ZGC回收器与G1一样基于内存布局,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理。

但ZGC并不是完美的,逃不过内存占用()、吞吐量()和延迟()的三元悖论。与G1相比,它的强项是低延迟,缺点是内存占用更高,吞吐量比G1稍低(不过这强依赖于测试用例,我也看到一些显示ZGC的吞吐量高于G1),另外还有一些其他问题[15]也值得注意。总的来说,如果考虑使用ZGC替代CMS,建议是使用Java 15之后的版本。

数据来源: the JDK’s New [16]

另一个容易被人忽略的特性是Java 11中引入了一个号称无操作的垃圾回收器[17],即不会做GC的垃圾回收器。这个很有意思,但确实对于一些不需要长时间运行、小规模的程序来说,会更关注启动时间、内存占用等指标,很典型的就比如函数。只要JVM能正确分配内存,然后在堆耗尽之前退出,那显然运行负载极小、没有任何回收行为的便是很恰当的选择。

Java 12:和内存返还

Java 12中引入了一个新的实验性的垃圾回收器-[18],与ZGC一样是以低停顿为目标(注意这里说的是,因为非亲生的缘故,中并没有包含)。

另一个是G1上的改动,能够自动将未使用的堆内存返还给操作系统[19]。我们经常看到,Java程序占用的内存比实际应用本身运行产生的对象占用要多,即使在应用本身没有流量时也是如此,原因是多方面的(这里不谈JVM、类的元数据、编译后的本地代码等等对内存的额外占用):

标记-复制的算法需要有两块内存区域,一个典型的例子是新生代的区;标记-清除的算法很多时候同样需要更大的内存区域,因为在GC结束时会有大量的空间碎片,在分配大对象时会很麻烦。

像CMS/G1这样的并发回收器,因为在垃圾收集阶段用户线程还需要持续运行,那就需要预留足够内存空间提供给用户线程使用。

CMS的做法是在老年代达到指定的占用率后(Java 6后默认为92%)开始GC,可以通过-XX:参数调高这个值,但调得太高又容易碰到 Mode ;

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

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

在这之前,G1只有在Full-GC或并发周期期间才能返还内存,而G1的目标之一是避免Full-GC,并且仅根据 Java 堆占用和分配活动触发并发循环,因此多数场景下,除非强制触发,并不会有内存返回行为。在Java 12后,G1会在应用不活动的空闲期间定期尝试继续或触发并发循环以确定整体 Java 堆使用情况,并自动将 Java 堆中未使用的部分返回给操作系统。

杂想_杂佛鳄龟1年能长多大_

JEP中举了一个服务器的示例,服务器在白天提供HTTP请求,而在夜间大部分时间处于空闲状态,新的内存返还特性可以使得JVM提交的内存减少85%。

Java 13:小升级+1

同Java 10一样,Java 13也是一个小升级版本:

Java 14:小升级+2

Java 15:ZGC和转正

从Java 11和Java 12分别引入ZGC和以来,一直是的两大垃圾回收器终于了。

Java 16: Linux的支持

Java 16中跟性能提升相关的特性主要包括:

另外值得一提的是Java 16将JDK移植到了 Linux[26]。 Linux[27]是一个非常轻量的Linux发行版,其镜像只有5MB左右(对比系列镜像接近200 MB)。更小的镜像意味着容器环境中更小的磁盘占用和更快的镜像拉取速度,正因如此, 官方已开始推荐使用 替代之前的 作为基础镜像。为了瘦身, Linux默认是用musl[28]而非传统的glibc作为C标准库,因此之前的JDK并不直接支持,而是需要在基础上安装glibc。

基于 Linux基础镜像,再结合Java 9引入的模块化能力,如果程序只依赖 java.base模块,镜像的大小可以小至38 MB。

Java 17:最新的LTS版本

激进的团队可能会跳过Java 11,直接从Java 8升级到Java 17,因为这是最新的LTS版本。Java 17(包括最新的Java 18)本身并没有包含太多的性能提升特性,更多的是语法和API的变动,也没啥好列的了。

X

标题的 X只是代称,代表了Java官方或社区所推进的一系列项目。这些项目出于不同的动机,但最终的目的都是为了让Java更适应新的时代。完整的项目列表可以看这里[29],其中比较有代表性的有:

图片来源:周志明(就是写《深入理解Java虚拟机》的大牛)的文章:云原生时代,Java 的危与机[43]

截至今天,最新的Java 18中仅包含了 Amber和 的一些特性,像 Loom、 等并没有包含,更别提难度最大的 了,确实是有点落后了。不管如何,了解下这些项目做的事情可以让我们更好地理解Java未来的发展方向。

提前编译-AOT

我们一直说Java速度慢,我觉得这是一个不严谨的误会,因为实际上经过JIT编译后Java运行并不慢。为什么Java给人“更慢”的印象?可能这两方面因素是罪魁祸首:

Java是一门跨平台语言,但JVM并不是跨平台的,Java将源码编译成字节码,交给JVM执行,这中间装载的开销很高。

一段程序想要被加载需要经过的流程:

上面这张图能够清晰地看出Java从启动到达到最佳性能的不同阶段。

如果跳过字节码,直接将Java代码编译成本地代码,那么所有代码都是在编译期编译和优化好的,是不是就不存在JVM初始化和类加载的开销问题,也不用等预热到JIT编译(编译时还要耗费额外的运行期CPU资源),马上就能达到最大性能?这就是AOT(Ahead-Of-Time )提前编译的思想。

当然AOT编译也有劣势:

目前来看使用AOT难免需要有一些折中,例如后面要讲到的 VM就要求以配置的方式明确告知编译器程序代码中有哪些方法是只通过反射来访问的,哪些类会被动态加载等等。然而另一些功能可能只能妥协或者放弃了,就像动态生成字节码这类十分常用的功能,我们熟知的默认就会使用CGLib生成动态代理。从 5.2 开始增加了@注解来排除对 CGLib 的依赖,仅使用标准的动态代理去增强类,但这也就限制了动态代理的能力。

要获得有实用价值的提前编译能力,只有依靠提前编译器、组件类库和开发者三方一起协同才有可能办到。这就要靠后面说的队友的助攻了。

协程(虚拟线程)

协程[44](,有的地方也称为纤程/Fiber)并不算一个新鲜的概念,但与线程相比一直让开发者感觉陌生,我觉得最主要的原因是大多数编程语言对于协程的支持并不像线程一样“原生”。直到Go和这些热门的语言直接内置了协程,协程才成为“一等公民”被开发者重新审视。

对于协程的定义,不仅在不同语言中有差异,随着时代的变化定义也在变化,我试着将主流印象中的协程和线程做一个不严谨的对比:

回到Java,基本上线程模型分成1:1、N:1,N:M三种,虽然说JVM并没有限定 Java 线程需要使用哪种线程模型来实现,但一般来说Java目前主流的线程模型是直接映射到操作系统内核上的1:1 模型[45],即一个用户线程就唯一地对应一个内核线程(这里不谈在遥远的JDK1.2之前,那会也使用过称为“绿色线程”的N:1模型)。

1:1的模型对于计算密集型任务这很合适,既不用自己去做调度,也利于一条线程跑满整个处理器核心;但对于 I/O 密集型任务,譬如访问磁盘、访问数据库占主要时间的任务,这种模型就显得成本高昂,主要在于内存消耗和上下文切换上:64 位 Linux 上 的线程栈容量默认是 1MB,线程的内核元数据( )还要额外消耗 2-16KB 内存,所以单个虚拟机的最大线程数量一般只会设置到 200 至 400 条,当程序员把数以百万计的请求往线程池里面灌时,系统即便能处理得过来,其中的切换损耗也是相当可观的。

Loom 项目的目标是让 Java 支持额外的N:M 线程模型[46],实际上是将 JVM 线程与 OS 线程解耦。Loom项目新增加一种用户态的“虚拟线程”( )[47],本质上它是一种有栈协程( )[48],多条虚拟线程可以映射到同一条物理线程之中。

在此之前,Java中已经有一些三方的实现支持协程,比如[49]和[50],貌似都是需要挂载agent利用字节码注入的方式实现,我没有细看,有兴趣的可以了解下。

虚拟线程并不是万能的,虽然可以显著提高应用程序吞吐量,但也有前提:

并发任务的数量很高(超过几千个)

工作负载不受 CPU 限制,换句话说是I/O密集型的任务。如果是计算密集型任务,拥有比处理器内核多得多的线程并不能提高吞吐量

举个例子,假设有这样一个场景,需要同时启动10000个任务做一些事情:

// 创建一个虚拟线程的Executor,该Executor每执行一个任务就会创建一个新的虚拟线程try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    IntStream.range(0, 10_000).forEach(i -> {        executor.submit(() -> {            doSomething();            return i;        });    });}  // executor.close() is called implicitly, and waits

如果()里执行的是某类I/O操作,那么使用虚拟线程是非常合适的,因为虚拟线程创建和切换的代价很低,底层对应的可能只需要几个OS线程。如果没有虚拟线程,使用线程的话可能要这样写了:

如果()里执行的是某类计算任务,例如给一个大数组排序,那么虚拟线程还是平台线程都无济于事。JEP中提到了很关键的一点就是:虚拟线程不是更快的线程—它们运行代码的速度并不比平台线程快。它们的存在是为了提供scale(更高的吞吐量),而不是speed(更低的延迟)。

虚拟线程的提案[51]目前还是状态,因此我们还无从知晓其最终形态,也许可以确定的几点:

// 直接创建一个虚拟线程Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
// 通过builder创建一个虚拟线程Thread virtualThread = Thread.builder().virtual().task(() -> {            System.out.println("Fiber Thread: " + Thread.currentThread().getName());        }).start();
// 创建一个基于虚拟线程的ExecutorServiceExecutorService executor = Executors.newVirtualThreadExecutor()

值类型

在Java架构师Brian Goetz的演讲[53]中讲到, 的目标是" the of data in "。他提到Java的一些设计在刚开始是完全OK的,但过去25年中硬件发生了很大变化:

Java是一门重指针("")的语言,除了基本类型,可以说“一切皆为对象”,每个对象都有其对象标识符[55]( )。面向对象的内存布局中,对象标识符存在的目的是为了允许在不暴露对象结构的前提下,依然可以引用其属性与行为,是Java实现多态性、可变性、锁等一系列功能的基础。尴尬的是,不管你需不需要什么多态、可变性、锁,对象标识符就在那里,也就是演讲中说的:Not all need that! But all pay for it。

Java通过对象标识符进行链式访问,与之相对的是集中访问模式,例如C/C++中的会将对象在内存中拍平。两者的关键区别在于,链式访问需要读多次内存才能命中,而集中访问一次就可以将相关数据全部取出。打个比方,类A中包含类B,类B中包含类C,从A->B->C,链式访问在最坏情况下要读3次内存;而集中访问只需要读一次。

以一个常见的Point类为例:

final class Point {  final int x;  final int y;}

一个Point对象数组在内存中的布局是长这样的:

杂佛鳄龟1年能长多大__杂想

为了提升性能,有的小伙伴可能会用“曲线救国”的方法,把Point[] pts变成两个int数组int[] xs和int[] ys,这就成"Good Code"和" Code"的两难选择了。

引入的值类型有点向C#中的偷师的味道。值类型的想法是,像Point一类的对象,本质上是纯数据的聚合,只有数据,没有标识。没有标识意味着不再有多态性、可变性,不能在对象上加锁,不能为Null,只能基于状态做对象比较,但优势是:

可以使用关键词定义一个值类型:

inline public class Point {    public int x;    public int y;
    public Point(int x, int y) {        this.x = x;        this.y = y;    }}

值类型的内存布局长这样:

看上去值类型跟基础类型很像(某些小伙伴要说了,这跟我之前干的用两个int[]来代替Point[]的方式有什么区别?),不同之处在于可以将其看做一种可以快速访问的带限制的特殊对象,因此有对象的特征(Codes like a class, works like an int),比如:

有了值类型的支持后,的另一个JEP: over Types [56]就很自然了,Java 泛型中令人诟病的不支持原数据类型( Type)、频繁装箱等问题也能迎刃而解了。想象一下你只是需要一个数字列表,然后只能被定义成一个。对于API设计者,也不用再搞什么和了。

最后说一点,一个值类型看似简单,实际上创建一种新的数据类型需要对编译器、类文件结构和 JVM 都进行更改,还要支持现有的库,譬如、等。从14年到现在,Java 团队已经对六种同的解决方案进行了原型设计,值类型(value types)这一术语也被重命名为内联类( ),然后又变成原始类( )。总之,耐心等待吧…

队友的助攻

Java最牛逼的是什么,是它的生态圈和圈里的队友们啊。我列了几个我觉得比较有代表性的。

在18年官宣了[57]的1.0版本。虽然名字里带着VM,但实际上它既是 的新型 JIT 编译器[58],又可以用作AOT编译器,也是一个新的多语言虚拟机。有3个关键的组件:

算是近年来的明星Java项目,发展很快。这里我只做个简单的介绍,感兴趣的同学建议直接上官网[60]看官方文档。

Graal

我们熟知的有两个JIT编译器,C1和C2。Java 程序首先在解释模式下启动,执行一段时间后,经常被调用的方法会被识别出来,并使用 JIT 编译器进行编译——先是使用 C1,如果 检测到这些方法有更多的调用,就使用 C2 重新编译这些方法。这种策略被称为“分层编译”,是 默认采用的方式。经过这么多年优化下来,C2编译后的代码效率非常出色,可以与 C++ 相媲美(甚至更快)。不过,近年来 C2 并没有带来多少重大的改进。不仅如此,C2 中的代码变得越来越难以维护和扩展,新加入的工程师很难修改使用 C++ 特定方言编写的代码。

Graal编译器的目标之一就是替代C2,因此这两者难免会拿来做比较。可以说最明显的区别就是Graal是用Java写的,C2则是C++。一种普遍的看法(来自 等公司和 Cliff Click 等专家)认为,C2在当前设计中不可能再进行重大改进,而Graal使用Java开发的一大优势在于可以很方便地将C2的新优化移植到Graal中,反之则不然,比如,在Graal中被证实有效的部分逃逸分析( )至今未被移植到C2中。

从我目前搜到的一些测试结果来看,总的来说Graal编译结果的性能与C2相比略优但相差不大。Graal在基于假设的优化手段上相对更激进,因此在某些场景下优势会更明显(比如这篇文章[61],再比如的报告[62]讲的Scala代码性能上Graal有10%的优势)。最关键的是,Graal还在不断演进中,未来可期。

VM

VM简单来说就是 image + ,分别对应原生镜像( Image)[63]的build time和run time。

官网放了一张图来展示Graal Image的两大优势:快速启动和低内存占用。不过我看到的其他一些资料上说在低时延和高吞吐(/)场景下并不占优。

VM的限制其实就是前面说的AOT编译的限制,要求目标程序满足"-world"假设,即所有代码在编译器已知。如果不满足,那只能同时构建一个 image了(使用传统JVM执行,需要JDK依赖)。一些限制条件可以通过在镜像构建时进行配置[64]来绕过,其中最关键的就是类的元数据()相关的一些限制:

还有一些限制条件,像是字节码和 ,是直接无法兼容的。还有一些功能跟有区别,具体可以参考这篇文档[66]。

是一个用Java写的语言实现框架,也可以说是一套通用语言设计的框架和API。除了像 Java、Scala、、 等基于JVM的语言外,官方在此之上还支持了[67]、Ruby[68]、R[69]、[70]、[71](LLVM-based C/C++等),也就是说这些语言都可以“跑在”上,号称"Run "。

完整的列表参考这里[72]。

这是我找到的一份17年的性能数据,可以看到除了C/C++和JS之外,的性能优势还是挺大的,尤其是对于Ruby、R这类解释型语言。

提供了一套API,基于的语言实现仅需用Java实现词法分析、语法分析以及针对语法分析所生成的抽象语法树(AST)的解释器,理论上实现一个解释器要比开发一个优化的编译器要容易得多。将这些语言的源代码或源代码编译后的中间格式(例如,LLVM 字节码、Class 字节码)通过解释器转换为能被 接受的中间表示( ,IR),然后就可以使用Graal编译器对这些解释器进行优化,因此性能上有时候比传统编译器反而还有优势。

此外,的精华之处在于,运行时所有的解释器都通过同样的协议来互相操作不同编程语言中的对象,这就为所有生态系统下的库和模块都敞开了大门,你只需要选择最合适的语言去解决你要解决的问题就可以了,而不用为了项目所用的某个语言去专门实现一些缺少的模块。

这是一个官方的示例,展示了多语言如何直接进行交互:

const express = require('express')const app = express()
const BigInteger = Java.type('java.math.BigInteger')
app.get('/', function (req, res) {  var text = 'Hello World from Graal.js!
'
// Using Java standard library classes text += BigInteger.valueOf(10).pow(100) .add(BigInteger.valueOf(43)).toString() + '
'
// Using R methods to return arrays text += Polyglot.eval('R', 'ifelse(1 > 2, "no", paste(1:42, c="|"))') + '
'
// Using R interoperability to create graphs text += Polyglot.eval('R', `svg(); require(lattice); x <- 1:100 y <- sin(x/10) z <- cos(x^1.3/(runif(1)*5+10)) print(cloud(x~y*z, main="cloud plot")) grDevices:::svg.off() `); res.send(text)})app.listen(3001, function () { console.log('Example app listening on port 3001!')})

是Java生态圈的绝对大佬,曾几何时,也称得上一个轻量级框架(相比EJB?),然而现在看看,的模块量级、启动速度、内存占用恐怕都谈不上多轻量了。是一个动态性很强的框架,其核心的IoC和AOP功能大量使用了反射、动态字节码生成等技术,这与前面说的AOT编译的封闭世界假设是冲突的。所以尴尬的事情出现了,我想要使用AOT或者说,但是第一个难题居然是代码中的框架不支持…

基于此,社区中出现了-[73]和-fu[74]这样的项目(目前都还是实验阶段),其中-基本确定会在 6和 Boot 3中直接集成。

关于-,ATA上已经有大佬们做过比较深入的分析了,比如:让启动提速95.5倍,项目解读之--,也可以参考下官方的---beta[75]。

我理解 做的事情关键就是用 AOT 插件(Maven/)生成 的配置(反射、资源、动态代理、-Image选项):

从测试结果看, 的启动速度、镜像大小、内存占用与传统 Boot相比有非常明显的提升,但峰值性能、构建时长等方面还处于劣势(同样的话好像说了好几次了?)

其他://等等

近几年来,开源社区涌现了[76]、[77]、[78]等一批以提升 Java 在云原生环境下的适应性为卖点的微服务框架,从他们的中可以提取到一些高频关键词:

相比更常见的 Boot,这些新的框架天生对 有更好的适配,更轻量、启动更快、内存占用更低,非常适合容器化交付。虽然目前看起来尚显稚嫩,生态系统相比还不算成熟,但就我个人而言,非常愿意在小的项目里使用这些框架。

其他的,像、JBoss还有等等社区,其实都很活跃,仍然充满活力。

未来?

捋完这么多,我发现对于Java的未来我还是充满迷茫。一方面,在新生语言的挑战下,Java似乎不可避免地慢慢变成一种“传统”,“老旧”,“经典”的语言;另一方面,Java和它的队友们一直在努力开创或者吸纳各种新特性、新功能,包括但不限于:

虽然很多特性短期内还不能落地,但道阻且长,行则将至。至少就目前看来,Java在传统的企业级和服务端应用领域构筑的堡垒还是牢不可破,再加上由强大生态所构建的护城河,留给Java的时间还有很多。

最后,作为一个Java开发者,很诚实地希望,在可见的未来,Java能一直流行下去。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了