美团架构师探秘Java生态系统,介绍JDK、JVM、JEP

OpenJDK

OpenJDK原是Sun MicroSystems公司(下面简称Sun公司)为Java平台构建的Java开发环境,于2009年4月15日由Sun公司正式发布。后来Oracle公司在2010年收购Sun公司,接管了这项工作。

随着OpenJDK的发布,越来越多的公司和组织都基于OpenJDK深度定制了一些独具特色的JDK分支,为用户提供更多选择。例如,国内厂商阿里巴巴的Dragonwell支持JWarmup,可以让代码在灰度环境预热编译后供生产环境直接使用;腾讯的Kona 8将高版本的JFR和CDS移植到JDK 8上;龙芯JDK支持包含JIT的MIPS架构,而非Zero的解释器版本;

国外厂商Amazon、Azul、Google、Microsoft、Red Hat、Twitter等都有维护自用或者开源的JDK分支。

回到OpenJDK本身。OpenJDK包含很多子项目,它们大都是为了实现某一较大的特性而立项,关注它们可以了解Java社区的最新动向和研究方向。一些重要和有趣的子项目如下所示。

1)Amber:探索与孵化一些小的、面向生产力提升的Java语言特性。Amber项目的贡献包括模式匹配、Switch表达式、文本块、局部变量类型推导等语言特性。

2)Coin:决定哪些小的语言改变会添加进JDK7。常用的钻石形式的泛型类型推导语法以及try-with-resource语句都来自Coin项目。

3)Graal:Graal最初是基于JVMCI的编译器,后面进一步发展出Graal VM,旨在开发一个通用虚拟机平台,允许JavaScript、Python、Ruby、R、JVM等语言运行在同一个虚拟机上而不需要修改应用自身的代码。

4)Jigsaw:Jigsaw孵化了Java 9的模块系统。

5)Kulla:实现一个交互式REPL工具,即JEP 222的JShell工具。

6)Loom:探索与孵化JVM特性及API,并基于此构建易用、高吞吐量的轻量级并发与编程模型。目前Loom的研究方向包括协程、Continuation、尾递归消除。

7)Panama:沟通JVM和机器代码,研究方向有Vector API和新一代JNI。

8)Shenandoah:拥有极低暂停时间的垃圾回收器。相较并发标记的CMS和G1,Shenandoah增加了并发压缩功能。

9)Sumatra:让Java程序享受GPU、APU等异构芯片带来的好处。

目前关注于让GPU在HotSpot VM代码生成、运行时支持和垃圾回收上发挥作用。

10)Tsan:为Java提供Thread Sanitizer检查工具,可以检查Java和JNI代码中潜在的数据竞争。

11)Valhalla:探索与孵化JVM及Java的语言特性,主要贡献有备受瞩目的值类型(Value Type)、嵌套权限访问控制(Nest-based AccessControl),以及对基本类型作为模板参数的泛型支持。

12)ZGC:低延时、高伸缩的垃圾回收器。它的目标是使暂停时间不超过10ms,且不会随着堆变大或者存活对象变多而变长,同时可以接收(包括但不限于)小至几百兆,大至数十T的堆。ZGC的关键字包括并发、Region、压缩、支持NUMA、使用着色指针、使用读屏障。

JEP

JEP(Java Enhancement Proposal)即Java改进提案。所谓提案是指社区在某方面的努力,比如在需要一次较大的代码变更,或者某项工作的目标、进展、结果值得广泛讨论时,就可以起草书面的、正式的JEP到OpenJDK社区。每个JEP都有一个编号,为了方便讨论,通常使用JEPID代替某个改进提案。

JEP之于Java就像PEP之于Python、RFC之于Rust,它代表了Java社区最新的工作动向和未来的研究方向。在较大的Java/JVM特性实现前通常都有JEP,它是Java社区成员和领导者多轮讨论的结果。JEP描述了该工作的动机、目标、详细细节、风险和影响等,通过阅读JEP(如果有的话),可以更好地了解Java/JVM某个特性。下面摘录了一些较新的JEP,注意,处于“草案”和“候选”状态的JEP不能保证最终会被加入JDK发行版。

1)JEP 386(候选):将OpenJDK移植到Alpine Linux/x64。Alpine是一个极简的Linux发行版,作为Docker基础镜像,它的大小不到6MB,被广泛用于程序的云部署,但是Alpine使用musl作为C语言运行时库,与广泛使用的glibc有些出入,而JEP 386可以很好地解决这个问题。

2)JEP 378:Java文本块。文本块即多行的字符串字面值,其功能类似于其他编程语言的raw字符串功能,不需要为大多数特殊字符转义。将于JDK 15发布。

3)JEP 337(候选):让高性能计算和云端程序充分利用网络硬件并不容易,当前JDK的网络API使用操作系统内核的socket协议,在数据传输时涉及内核态和用户态的多次切换,会影响内存带宽和CPU周期。

为了改善这种情况,Java准备拟定实现rsocket协议,允许网络API访问远端内存(RDMA),提高吞吐量并降低网络延时。

4)JEP 369:使用GitHub作为OpenJDK的Git仓库。

5)JEP 384:提供Java记录支持。很多人都说Java不灵活,比如equals/hashCode等写起来太长了。在Spring或者一些RPC框架中,有时候仅仅想写一个单纯用作数据传输的类,也少不了要写一大堆重复方法和近乎刻板的getter/setter、toString、hashCode等方法,而且容易出错。

尽管IDE或者框架可以自动生成这些类,但是它们没有明确指出该类是POJO类,仅供数据传输使用。实际上,很多语言都可以声明只携带数据的类,如Scala的case类,Kotlin的data类以及C#的record类,它们被证明是很有用的。为此,Java 15将使用记录record来建模POJO类,从而方便简洁地声明某个类只携带数据,而不会改变类的状态,同时自动实现一些用于数据生产/消费的方法。

另一个常常与JEP一起出现的是JSR(Java Specification Request,Java规范提案)。有时人们想开发一些实验性特性,例如探索新奇的点子、实现特性的原型,或者增强当前特性,在这些情况下都可以提出JEP,让社区也参与实现或者讨论。这些JEP中的少数可能会随着技术的发展愈发成熟,此时,JSR可以将这些成熟的技术提案进一步规范化,产生新的语言规范,或者修改当前语言规范,将它们加入Java语言标准中。

Java虚拟机

简单定义下的JDK包括Java虚拟机和Java语言库,除了JDK级别的深度定制,历史上也存在许多Java虚拟机实现。

1)Graal VM:有一统天下野心的通用语言虚拟机平台,具体将在1.5节详细讨论。

2)Substrate VM:在静态编译时分析并发现代码依赖的所有JDK类和用户类,然后完全使用静态编译将它们打包成一个独立的二进制程序,具体将在1.5节详细讨论。

3)JRockit JVM:曾是最快的Java虚拟机,主要面向服务端应用场景,不提供解释器,所有Java代码均使用JIT编译。JRockit的JFR(JavaFlight Record,Java飞行记录器)功能现已被吸收进HotSpot VM。

4)Apache Harmony:Apache基金会主导的开源Java虚拟机项目,由于Sun公司的态度导致Harmony项目只有一个受限的TCK,在Oracle公司收购Sun公司后冲突进一步延续。出于这些原因,Apache基金会宣布退出JCP,同时Harmony的主导者IBM加入OpenJDK项目,Harmony日渐衰落。

5)Dalvik:为Android系统量身定做的基于寄存器的虚拟机实现。

将.class转换为专属的.dex然后运行。.dex是转为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。

6)ART:为Android系统量身定做的虚拟机,用于替换之前的Dalvik。Dalvik虚拟机每次运行应用程序时都需要经过JIT编译器,而使用ART(Android Runtime)后,在安装应用程序时字节码就会被AOT编译为机器代码,加快了应用程序的启动时间,同时减少了运行时内存占用。

7)Jikes RVM:使用Java语言实现的Java虚拟机,这种使用X语言实现的X语言(或者X语言的运行时环境)虚拟机也被称为元语言循环虚拟机。

8)Azul Zing VM:拥有领先业界数十年的C4垃圾回收器和基于LLVM的JIT编译器Falcon。

9)IBM J9VM:高度模块化的虚拟机,它将一些组件如垃圾回收器、JIT编译器、检测工具等单独抽离出来构成了IBM OMR项目。

10)Microsoft JVM:微软为了让IE运行Java Applets开发的仅用于Windows平台的Java虚拟机,是当时Windows平台上性能最好的虚拟机,但是微软在1997年被Sun公司以侵犯商标等罪名控告并输掉了官司后,也终止了Microsoft JVM的开发。

除了以上提到的Java虚拟机外,还有“冠绝天下”的虚拟机HotSpotVM,它也是本文的主角。

HotSpot VM

横看成岭侧成峰,远近高低各不同。不同的人从不同的角度看到的HotSpot VM也不尽相同。

从Java应用开发者的角度出发,虚拟机如图1-1所示。

Java应用开发者关注Java语言,关注应用的实现和库的实现,用合法的Java代码表达思想,通过编译器工具编译产出字节码交给虚拟机运行。在他们眼中虚拟机是一个黑盒,所以更期望虚拟机的行为能遵循Java相关规范,这样才能放心地用语言集实现应用程序或库,进而供用户使用。

虚拟机开发者关注虚拟机内部,在他们眼中,虚拟机不再是黑盒,而是各个组件根据规则交互的一套“Java操作系统”。当上层应用出现问题时,他们可以从虚拟机层找出问题致因,当上层语言需要新特性、新功能,或者下层操作系统提供新特性时,他们可以在虚拟机层实现,然后以某种方式暴露给上层。

从虚拟机开发者的角度出发,虚拟机如图1-2所示。

本文将从虚拟机开发者的角度深入虚拟机内部,了解各个组件的具体实现和交互方式,探索虚拟机层是如何实现上层特性的。

源码模块

本文主要描述位于openjdk/src/hotspot目录的Java虚拟机HotSpot VM的实现。HotSpot VM根据目录可以分为很多模块,每个模块的功能大致如下。

├── cpu # 与CPU架构相关的代码├── os # 与操作系统相关的代码

├── os_cpu # 与CPU和操作系统相关的代码

└── share

├── adlc # 平台描述语言编译器(编译cpu目录中的*.ad文件)

├── aot # AOT支持,加载验证AOT库等

├── asm # 宏汇编器,为宏形式的JIT代码生成机器代码

├── c1 # Client即时编译器(C1 JIT)

├── ci # 编译器接口,定义JIT编译器通用的一些结构

├── classfile # 字节码文件解析和处理

├── code # 描述JIT编译后的代码结构等

├── compiler # JIT编译器代理,虚拟机通过它选择特定的JIT编译器

├── gc # 垃圾回收。gc/shared表示共享代码,gc/g1,gc/cms表示特定代码

├── include # 一些JVM函数和常量的导出

├── interpreter # 模板解释器和CPP解释器实现

├── jfr # 诊断工具Java Flight Record

├── jvmci # JVMCI编译器接口,可以开启Graal编译器代替C2

├── libadt # 内部使用的数据结构

├── logging # 日志记录模块

├── memory # 内存相关,包括内存划分,metaspace划分等

├── metaprogramming # 元编程的一些type_traits

├── oops # Java类,对象在JVM中的表示

├── opto # Server即时编译器(C2 JIT)

├── precompiled # 预编译文件

├── prims # JNI、JVMTI、Unsafe类具体实现

├── runtime # 包罗万象的JVM运行时模块

├── services # HeapDump、MXBean、jcmd、jinfo等辅助工具支持

└── utilities # 工具组件,如hashtable、JSON解析器、elf格式、快排算法等。

构建和调试

本文涉及的源码是jdk-12+31,操作系统为macOS 10.15.2,CPU型号为Intel Core i7,JDK构建使用slowdebug类型(以下构建演示使用fastdebug类型)。如无特殊说明,文中均基于该配置分析和描述源码。

为了方便读者自行尝试,这里给出在三大主流操作系统上构建OpenJDK和断点调试HotSpot VM的方式。

1. 在Windows上构建,用Visual Studio调试

下载并编译好freetype,然后安装cygwin及必要工具,如autoconf、make、zip、unzip,打开cygwin,进入源码目录输入命令进行编译,如代码清单1-1所示:

代码清单1-1 Windows编译

$ ./configure

--with-freetype-include=/your_path/freetype-2.9.1/src/include

--with-freetype-lib=/your_path/freetype-2.9.1/lib

--with-boot-jdk=/your_path/openjdk-12-x64_bin

--disable-warnings-as-errors

--with-toolchain-version=2017

--with-target-bits=64 --enable-debug

$ make all # 构建OpenJDK$ make hotspot-ide-project # 生成vs项目文件

生成的vs工程文件位于build目录下的

ide/hotspot-visualstudio/jvm.vcproj中,使用Visual Studio双击载入即可,在菜单栏选择server-fastdebug即可开始调试。在调试时若遇到如图1-3所示的异常提示(safefetch32抛出异常),属于正常情况,继续调试即可。该异常会被外部SEH捕获。

2. 在macOS上构建,用Xcode调试

可以在macOS平台下载brew,然后使用brew安装hg、freetype、ccache,如代码清单1-2所示:

代码清单1-2 macOS编译

$ brew install ccache

$ brew install freetype

$ cd openjdk12

$ chmod +x configure

$ ./configure --enable-ccache --witt-debug-level=fastdebug

$ make all # or make hotspot

一切完成后,

openjdk12/build/macos-x86_64-server-fastdebug/jdk就是编译产出。打开Xcode创建一个项目,选择macOS创建一个命令行项目,然后选中新项目自动创建的文件右键删除,接着配置启动项。对着停止方块按钮旁边的按钮右键Edit Scheme,在“运行”中选择basicconfiguration,并选择other。这之后需要选择之前编译出的jvm,比如/build/macosx-x86_64-server-fastdebug/jdk/bin/java。继续选择Argument,为虚拟机增加一个启动参数,用javac编译得到字节码文件,用-cp指定字节码所在目录,后面加上类名。然后选中工程add files toproject,将HotSpot源代码导入项目。

到这里已经可以运行了,但是会出现sigsegv信号,这是正常情况,可以在lldb中使用process handle SIGSEGV -s false命令忽略sigsegv。不过这种方法在每次运行时都需要输入该指令,比较麻烦。也可以设置符号断点忽略sigsegv信号,具体操作是选择左边创建箭头,然后在最下面单击加号选择symbolic breakpoint,任意加一个断点,比如忽略Threads::create_vm模块的sigsegv。最终效果1-4所示。

3. 在Linux上构建,用Visual Code调试

Linux和macOS的编译方式基本类似,安装了必要工具和组件后,配置并运行即可,如代码清单1-3所示:

代码清单1-3 CentOS编译

$ yum install java-11-openjdk* # 安装Bootstrap JDK

$ yum install autoconfunzip zip alsa-lib-devel

$ yum install libXtst-devel libXt-devel libXrender-devel

$ yum install cups-devel freetype-devel fontconfig-devel

$ cd openjdk12

$ chmod +x configure$./configure --with-debug-level=fastdebug

$ make all

在Linux开发机上可以使用Visual Code进行调试。Visual Code也是笔者推荐使用的智能编辑器,它同时支持Linux/Windows/macOS三大平台,只需简单的launch.json配置即可进行断点调试。

具体操作是在Visual Code菜单中选择File→Open,打开OpenJDK 12源码目录,然后选择Debug→Start Debugging添加launch.json文件,如代码清单1-4所示:

代码清单1-4 Visual Codelaunch.json

{"version":"0.2.0","configurations": [{"cwd":"${workspaceFolder}","name":"HotSpot Linux Debug","type":"cppdbg","request":"launch","program":"<构建生成的JDK目录>","args": [""],"setupCommands": [{"description":"ignore sigsegv","ignoreFailures":false,"text":"handle SIGSEGV nostop"}]}]}

如图1-5所示,打上断点后,点击调试按钮即可开始调试。-XX:+PauseAtStartup和-XX:+PauseAtExit参数分别代表让虚拟机在启动和退出的地方停顿。

随着社区的不断发展,JDK的构建愈发成熟和简单,读者如果在构建过程中遇到问题,可以尝试根据报错自行解决,可以参见官方提供的构建文档(openjdk/doc/building.html),也可以在互联网中寻求解决方案。构建一个可调试的虚拟机是探索虚拟机实现的第一步,也是必要的一步。

回归测试

当为虚拟机添加或者修改某些功能时,新增对应的测试是有必要的。常用的测试虚拟机和JDK的工具是jtreg。jtreg是JDK测试框架的一部分,它主要用于回归测试[1],当然也可以用于单元测试、功能测试等。

下面简单展示jtreg的使用方法。假设我们想为HotSpot VM新增一个虚拟机参数-XX:+DummyPrint,在开启时输出“Hello World”。为了实现该功能,可以在

hotspot/share/runtime/globals.hpp文件中新增如代码清单1-5所示的代码:

代码清单1-5 添加DummyPrint参数

develop(bool, DummyPrint,false, \"Print hello world on the screen") \

然后在

hotspot/share/runtime/thread.cpp的Threads::create_vm()函数的尾部增加一段代码,如代码清单1-6所示:

代码清单1-6 DummyPrint功能实现

jint Threads::create_vm(JavaVMInitArgs* args,bool* canTryAgain) {...if(DummyPrint){tty->print_cr("Hello World");}returnJNI_OK;}

修改完后,使用make hotspot增量式构建项目,然后附加虚拟机参数-XX:+DummyPrint进行测试,结果应该符合功能预期。但是要想确保新增的代码在较长的软件生命周期内正常运行,手动测试仍然显得太过麻烦。为了解决这个问题,可以使用自动回归测试。在

openjdk/test/hotspot/jtreg/下新增测试文件TestDummy.java,如代码清单1-7所示:

代码清单1-7 TestDummy.java

/*

* @test TestDummy

* @summary Test whether flag -XX:+DummyPrint works correctly

* @library /test/lib

* @run main/othervm TestDummy

* @author kelthuzadx

*/importjdk.test.lib.process.OutputAnalyzer;importjdk.test.lib.process.ProcessTools;publicclassTestDummy{staticclassWrap{publicstaticvoidmain(String... args){} }staticvoidrunWithFlag(booleanenableFlag)throwsThrowable{ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(enableFlag ?"-XX:+DummyPrint":"-XX:-DummyPrint",Wrap.class.getName());OutputAnalyzer out =newOutputAnalyzer(pb.start());if(enableFlag){out.shouldContain("Hello World");}else{out.shouldNotContain("Hello World");}}publicstaticvoidmain(String[] args)throwsThrowable{runWithFlag(true); runWithFlag(false);}}

自行构建jtreg或者下载预构建的jtreg,使用如代码清单1-8所示的命令进行测试:

代码清单1-8 jtreg命令

$./jtreg -jdk:<待测试的JDK路径> openjdk/test/hotspot/jtreg/TestDummy.javaTest results: passed: 1

如果测试成功,则会看到passed字样,失败则会出现failed字样。可以在jtreg工作目录下的JTWork/TestDummy.jtr日志文件中找到详细失败原因。

jtreg的核心是文件头注释中的各种符号,其中:@summary用于总结该测试的用途和测试内容;@library用于指定一个或多个路径名或者jar文件,如果是多个可使用空格隔开;@run用于指定以何种方式运行此测试。更多关于jtreg符号的详细用法可参见其相关文档。

Graal VM

如果说HotSpot VM代表了传统的Java保守阵营,那么Graal VM无疑是Java改革阵营的代表。

大部分脚本语言或者有动态特性的语言(比如CPython、Lua、Erlang、Java、Ruby、R、JS、PHP、Perl、APL等)都需要用到一个语言虚拟机,但是这些语言的虚拟机实现差别很大,比如CPython/PHP的虚拟机性能相对较差,Java的HotSpot VM、C#的CLR和JS的v8却是业界顶尖级别。那么,能不能付出较小努力,用一个业界顶尖级别的虚拟机来运行这些语言,享受该虚拟机的一些工匠特性,如GC、锁优化、JIT编译器呢?

答案是肯定的。首先,对于Java、Scala、Groovy这些本来就是基于JVM的语言,通过编译器前端工具得到Java字节码后直接在JVM上运行即可。对于CPython、R、Ruby、PHP、Perl乃至自己写的一门新的语言,其开发流程一般分为如下4个阶段:

1)首先解析源代码到AST(Abstract Syntax Tree,抽象语法树),写一个AST解释器。

2)当有人使用这门语言时,语言设计者可以继续迭代,实现一个完整的语言虚拟机,包括GC、运行时等,代码的执行仍然使用AST解释器。

3)用的人多了,语言继续迭代,将AST转换为字节码,代码执行使用字节码解释器。4)用的人特别多,性能也很关键,如果这个语言社区有足够的资金和人力,那么可以写JIT编译器,提升GC性能等,不过大部分语言都到不了这一步。

一门语言至少要达到阶段3才算基本满足工业生产的要求,但是人们希望一门语言在阶段1时性能就足够好,而不用花那么多精力和财力达到阶段3甚至阶段4,这就是Truffle语言实现框架出现的原因。

Truffle是一个Java框架,自然运行在JVM上。在这个框架下,用户只需要实现具体语言的AST解释器,付出的努力比较小,性能也足够好。因为Truffle框架可以使AST在解释过程中根据节点的类型反馈信息对节点进行变形,也可以在AST解释过程中进行部分求值(PartialEvaluation)[1],将这个AST的一部分节点编译为机器代码,不用解释执行AST节点,即可直接执行。

Truffle将AST节点编译为机器代码使用的编译器是Graal,这是一个用Java编写的即时编译器。前面提到,Truffle是一个Java框架,那么一个用Java语言编写的即时编译器要如何编译Java代码呢?答案是通过JEP243的JVMCI。JVM是用C++语言编写的,在JVM中内置了两个用C++编写的即时编译器,C1和C2。一般频繁的代码先用C1编译,这些代码即热点,如果热点继续,则使用C2编译。JVMCI相当于把本该交给C2编译的代码交给Graal编译,然后使用编译后的代码。用Java写即时编译器看起来很神奇,其实很正常,因为即时编译说到底就是将一段byte[]代码在运行时转换为另一段byte[]代码,可以用任何语言实现,只是实现过程中的难易程度不同。

到目前为止,Java、Scala、Groovy已经可以在JVM上运行了,CPython、R、Ruby、JS通过Truffle框架实现一个AST解释器后也可以在JVM上运行。那么如何处理如C/C++、Go、Fortran这类静态语言呢?对于这个问题,Graal VM给出的解决方案是Sulong框架。用户用一些工具(如clang)将C/C++这类语言转换为LLVM IR,然后使用基于Truffle的AST解释器解释LLVM IR。这里基于Truffle的AST解释器就是Sulong,如图1-6所示。

现在绝大部分语言都可以在JVM上运行了,将上面提到的所有技术放到一起,这个整体就叫作Graal VM。Graal VM就像皇帝的新衣,人人都在讨论,但是如果要回答它到底是什么却言之无物。实际上Graal VM这个语言虚拟机并不是真正存在的,Graal VM是指以Java虚拟机为基础,以Graal编译器为核心,以能运行多种语言为目标,包含一系列框架和技术的大杂烩,如图1-7所示。

但这并不是Graal VM的全部。图1-7中的所有语言最终都运行在JVM上,需要运行机器提前安装JDK环境。JVM由于自身原因,启动速度比较慢,内存负载较高。那么,能不能把程序直接打包成平台相关的可执行文件,后面直接执行这个可执行文件,而不依赖JVM呢?

交出这份答卷的是Substrate VM。Substrate VM借助Graal编译器,可以将Java程序AOT编译为可执行程序。它首先通过静态分析找到Java程序用到的所有类、方法和字段以及一个非常小的SVM运行时,然后对这些代码进行AOT编译,生成一个可执行文件。

Substrate VM的想法很美好,但是在实践中会遇到诸多问题,因为Java有反射等动态特性,这些特性可能导致新类加载无法通过静态分析解决。目前Substrate VM的GC是一个比较简单的分代GC,缺少很多调试工具和性能分析支持,编译速度较慢,不过这些都在慢慢完善,生产环境上也有阿里巴巴和Twitter等公司在不断尝试Substrate VM的实际落地,并取得了显著的效果。

本章小结

1.1节介绍了各具特色的JDK分支和OpenJDK的子项目。1.2节介绍了Java改进提案,它们代表类Java社区最新的工作动向。1.3节简单描述了历史长河中存在或者曾经存在的Java虚拟机。1.4节讨论了HotSpotVM的组件、源码结构、构建、调试以及修改代码后如何回归测试。最后1.5节展望未来,讨论了Java的前沿技术Graal VM。

本文给大家讲解的内容是Java生态系统,介绍JDK、JVM、JEP,带领大家走进虚拟机

原文链接:

https://www.toutiao.com/a6932714185818259982/?log_from=dc90ac6bbcf4f_1637993481184

你可能感兴趣的:(美团架构师探秘Java生态系统,介绍JDK、JVM、JEP)