JEP 343: Java打包工具(孵化项目)
引入
该特性旨在创建一个用于打包独立Java应用程序的工具。Java应用的打包和分发一直是个老大难问题。用户希望Java应用的安装和运行方式和其他应用有相似的体验。比如,在Windows上只需要双击文件就可以运行。Java平台本身并没有提供实用的工具解决这个问题,通常都依靠第三方工具来完成]。这个JEP的目标是创建一个简单的Java打包工具jpackage
。相对于第三方工具,jpackage
只适用于比较简单的场景,不过对很多应用来说就已经足够好了。
详解
jpackage工具将Java应用程序打包到特定于平台的程序包中,该程序包包含所有必需的依赖项。该应用程序可以作为普通JAR文件的集合或作为模块的集合提供。受支持的特定于平台的软件包格式为:
描述
该jpackage
工具将Java应用程序打包到特定于平台的程序包中,该程序包包含所有必需的依赖项。该应用程序可以作为普通JAR文件的集合或作为模块的集合提供。受支持的特定于平台的软件包格式为:
-
Linux:
deb
和rpm
-
macOS:
pkg
和dmg
-
Windows:
msi
和exe
默认情况下,jpackage
以最适合其运行系统的格式生成软件包。
基本用法:非模块化应用
假设您有一个包含JAR文件的应用程序,所有应用程序都位于一个名为的目录中lib
,并且lib/main.jar
包含主类。然后命令
$ jpackage --name myapp --input lib --main-jar main.jar
初次运行会报第一个错:
WARNING: Using incubator modules: jdk.incubator.jpackage 找不到 WiX 工具 (light.exe, candle.exe) 从 https://wixtoolset.org 下载 WiX 3.0 或更高版本,然后将其添加到 PATH。 错误:类型 [null] 无效或不受支持 解决:我们可以根据提示进入https://wixtoolset.org下载安装就好了,然后类似添加jdk的bin一样,将它的bin目录添加到path
将以本地系统的默认格式打包应用程序,将生成的打包文件保留在当前目录中。如果MANIFEST.MF
文件中main.jar
没有Main-Class
属性,则必须显式指定主类:
$ jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main
软件包的名称将为myapp
,尽管软件包文件本身的名称将更长,并以软件包类型(例如myapp.exe
)结尾。该软件包将包括该应用程序的启动器,也称为myapp
。要启动该应用程序,启动程序会将从输入目录复制的每个JAR文件放在JVM的类路径上。
如果您希望以默认格式以外的其他格式制作软件包,请使用该--type
选项。例如,要在macOS上生成pkg
文件而不是dmg
文件:
$ jpackage --name myapp --input lib --main-jar main.jar --type pkg
基本用法:模块化应用
如果您有一个模块化应用程序,该应用程序由目录中的模块化JAR文件和/或JMOD文件组成,并且模块中lib
包含主类myapp
,则命令
$ jpackage --name myapp --module-path lib -m myapp
将其打包。如果myapp
模块未标识其主类,则必须再次明确指定:
$ jpackage --name myapp --module-path lib -m myapp/myapp.Main
(打包模块化JAR或JMOD文件时,可以使用和工具--main-class
选项指定主类。)jar``jmod
JEP 352: 非易失性映射字节缓冲区
JDK增加了一种文件映射模式,用于访问非易失性内存。非易失性内存能够持久保持数据,因此可以利用该特性来改进性能。
动机
NVM为应用程序程序员提供了在程序运行过程中创建和更新程序状态的机会,而减少了输出到持久性介质或从持久性介质输入时的成本。这对于事务程序特别重要,在事务程序中,需要定期保持不确定状态以启用崩溃恢复。
现有的C库(例如Intel的libpmem)为C程序提供了对基层NVM的高效访问。他们还以此为基础来支持对各种持久性数据类型的简单管理。当前,由于频繁需要进行系统调用或JNI调用来调用原始操作,从而确保内存更改是持久的,因此即使仅使用Java中的基础库也很昂贵。同样的问题限制了高级库的使用,并且由于C中提供的持久数据类型分配在无法从Java直接访问的内存中这一事实而加剧了这一问题。与C或可以低成本链接到C库的语言相比,这使Java应用程序和中间件(例如Java事务管理器)处于严重的劣势。
该特性试图通过允许映射到ByteBuffer的NVM的有效写回来解决第一个问题。由于Java可以直接访问ByteBuffer映射的内存,因此这可以通过实现与C语言中提供的客户端库等效的客户端库来解决第二个问题,以管理不同持久数据类型的存储。
描述
1. 初步变更
该JEP使用了Java SE API的两个增强功能:
-
支持implementation-defined的映射模式
-
MppedByteBuffer::force方法以指定范围
2. 特定于JDK的API更改
-
通过新模块中的公共API公开新的MapMode枚举值
一个公共扩展枚举ExtendedMapMode将添加到jdk.nio.mapmode程序包:
package jdk.nio.mapmode; . . . public class ExtendedMapMode { private ExtendedMapMode() { } public static final MapMode READ_ONLY_SYNC = . . . public static final MapMode READ_WRITE_SYNC = . . . }
在调用FileChannel::map
方法创建映射到NVM设备文件上的只读或读写MappedByteBuffer时,可以使用上述的枚举值。如果这些标志在不支持NVM设备文件的平台上传递,程序会抛出UnsupportedOperationException异常。在受支持的平台上,仅当目标FileChannel实例是从通过NVM设备打开的派生文件时,才能传递这些参数。在任何其他情况下,都会抛出IOException异常。
JEP 345: G1的NUMA内存分配优化
通过实现可识别非统一的内存访问(non-uniform memory access,NUMA)的内存分配,提高G1 在大型机器上的性能。该JEP优化了G1在使用NUMA(non-uniform memory access)时的内存分配。与硬件相关,不深入讨论了。
动机
现代的多插槽计算机越来越多地具有非统一的内存访问(non-uniform memory access,NUMA),即内存与每个插槽或内核之间的距离并不相等。插槽之间的内存访问具有不同的性能特征,对更远的插槽的访问通常具有更大的延迟。
并行收集器中通过启动-XX:+UseParallelGC
能够感知NUMA,这个功能已经实现了多年了,这有助于提高跨多插槽运行单个JVM的配置的性能。其他HotSpot收集器没有此功能,这意味着他们无法利用这种垂直多路NUMA缩放功能。大型企业应用程序尤其倾向于在多个多插槽上以大堆配置运行,但是它们希望在单个JVM中运行具有可管理性优势。 使用G1收集器的用户越来越多地遇到这种扩展瓶颈。
描述
G1的堆组织为固定大小区域的集合。一个区域通常是一组物理页面,尽管使用大页面(通过 -XX:+UseLargePages
)时,多个区域可能组成一个物理页面。
如果指定了+XX:+UseNUMA
选项,则在初始化JVM时,区域将平均分布在可用NUMA节点的总数上。
在开始时固定每个区域的NUMA节点有些不灵活,但是可以通过以下增强来缓解。为了为mutator线程分配新的对象,G1可能需要分配一个新的区域。它将通过从NUMA节点中优先选择一个与当前线程绑定的空闲区域来执行此操作,以便将对象保留在新生代的同一NUMA节点上。如果在为变量分配区域的过程中,同一NUMA节点上没有空闲区域,则G1将触发垃圾回收。要评估的另一种想法是,从距离最近的NUMA节点开始,按距离顺序在其他NUMA节点中搜索自由区域。
该特性不会尝试将对象保留在老年代的同一NUMA节点上。
JEP 349: JFR事件流
JEP 349:JFR事件流(JFR Event Streaming) — 公开JDK Flight Recorder (JFR)数据以便持续监控。这有助于简化各种工具和应用对JFR数据的访问,并激励进一步创新
动机
HotSpot VM通过JFR产生的数据点超过500个,但是使用者只能通过解析日志文件的方法使用它们。
用户要想消费这些数据,必须开始一个记录并停止,将内容转储到磁盘上,然后解析记录文件。这对于应用程序分析非常有效,但是监控数据却十分不方便(例如显示动态更新数据的仪表盘)。
与创建记录相关的开销包括:
-
发出在创建新记录时必须发生的事件
-
写入事件元数据(例如字段布局)
-
写入检查点数据(例如堆栈跟踪)
-
将数据从磁盘存储复制到单独的记录文件
如果有一种方法,可以在不创建新记录的情况下,从磁盘存储库中读取正在记录的数据,就可以避免上述开销。
描述
jdk.jfr模块里的jdk.jfr.consumer包,提供了异步订阅事件的功能。用户可以直接从磁盘存储库读取记录数据,也可以直接从磁盘存储流中读取数据,而无需转储记录文件。可以通过注册处理器(例如lambda函数)与流交互,从而对事件的到达进行响应。
下面的代码展示了事件流的用法。通过RecordingStream
来创建事件流,使用onEvent()
方法注册对特定事件的处理器,最后启动流即可。FlightRecorder.getFlightRecorder().getEventTypes()
方法可以得到全部的事件。下面的代码对所有的事件都注册了处理器,直接把事件输出到控制台。
import jdk.jfr.FlightRecorder; import jdk.jfr.consumer.RecordingStream; public class JFR { public static void main(String[] args) { try (var rs = new RecordingStream()) { FlightRecorder.getFlightRecorder().getEventTypes() .forEach(eventType -> rs.onEvent(eventType.getName(), System.out::println)); rs.start(); } } }
下面的命令用来运行上面的代码,其中选项-XX:StartFlightRecording
用来启动JFR。
$ java -XX:StartFlightRecording JFR.java
JVM每秒一次将线程本地缓冲区中存储的事件定期刷新到磁盘存储库。 一个单独的线程解析最近的文件,直到写入数据为止,然后将事件推送给订阅者。
JEP 370: 外部存储器API(孵化)
引入一个API,以允许Java程序安全有效地访问Java堆之外的外部内存。
动机
许多Java的库都能访问外部存储,例如Ignite、mapDB、memcached及Netty的ByteBuf API。这样可以:
-
避免垃圾回收相关成本和不可预测性
-
跨多个进程共享内存
-
通过将文件映射到内存中来序列化、反序列化内存内容。
但是Java API却没有提供一个令人满意的访问外部内存的解决方案。
当Java程序需要访问堆内存之外的外部内存时,通常有两种方式:
-
java.nio.ByteBuffer
:ByteBuffer
允许使用allocateDirect()
方法在堆内存之外分配内存空间。 -
sun.misc.Unsafe
:Unsafe
中的方法可以直接对内存地址进行操作。
ByteBuffer
有自己的限制。首先是ByteBuffer
的的大小不能超过2G,其次是内存的释放依靠垃圾回收器。Unsafe
的API在使用时是不安全的,风险很高,可能会造成JVM崩溃,另外Unsafe
本身是不被支持的API,并不推荐使用。
该JEP引入了一种安全高效的API来访问外部内存地址。目前该API处于孵化状态。相关的API在jdk.incubator.foreign
模块的jdk.incubator.foreign
包中。该API中有3个重要接口:MemorySegment
、MemoryAddress
和MemoryLayout
。
虽然也可以使用JNI访问内存,但是与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为JNI要求开发人员编写和维护C代码段。 JNI本质上也很慢,因为每次访问都需要Java到native的转换。
在访问外部内存时,开发人员面临一个难题:应该使用安全但受限(可能效率较低)的方法(例如ByteBuffer),还是应该放弃安全保证并接受不受支持和危险的Unsafe API?
该JEP引入了受支持的,安全且有效的外部内存访问API。
描述
外部存储器访问API引入了三个主要的接口:MemorySegment,MemoryAddress和MemoryLayout。
可以从多种来源创建内存段,例如本机内存缓冲区,Java数组和字节缓冲区(直接或基于堆)。例如,可以如下创建本机内存段:
MemorySegment
接口表示一个连续的内存区域。MemorySegment
接口表示的内存分段有空间上和时间上的约束。空间上的约束指的是不能访问所分配内存空间之外的地址;时间上的约束指的是当MemorySegment
被关闭之后,不能继续对它进行操作。关闭一个MemorySegment
会释放内存。
MemoryAddress
接口描述在MemorySegment
中的相对位置。通常是用法是通过MemorySegment.baseAddress()
方法得到起始地址,再使用offset(long l)
方法移到到新的地址。
MemoryLayout
接口描述MemorySegment
中的内存布局。MemoryLayout
使用组合的方式来描述内存布局。
MemorySegment用于对具有给定空间和时间范围的连续内存区域进行建模。可以将MemoryAddress视为段内的偏移量,MemoryLayout是内存段内容的程序描述。
可以从多种来源创建内存段,例如本机内存缓冲区,Java数组和字节缓冲区(直接或基于堆)。例如,可以如下创建本机内存段:
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
...
}
上述代码将创建大小为100字节的,与本机内存缓冲区关联的内存段。
通过获取内存访问var句柄可以取消引用与段关联的内存。这些特殊的var句柄具有至少一个强制访问坐标,类型为MemoryAddress,即发生取消引用的地址。它们是使用MemoryHandles类中的工厂方法获得的。要设置本机段的元素,我们可以使用如下所示的内存访问var句柄:
VarHandle intHandle = MemoryHandles.varHandle(int.class); try (MemorySegment segment = MemorySegment.allocateNative(100)) { MemoryAddress base = segment.baseAddress(); for (int i = 0 ; i < 25 ; i++) { // 内存片段的起始地址、内存修改位置序号、以及要设置的int类型的值 intHandle.set(base.offset(i * 4), i); } }
JDK 14的其他新特性
JEP 362: 弃用Solaris和SPARC端口
不建议使用Solaris/SPARC,Solaris/x64和Linux/SPARC端口,以在将来的发行版中删除它们。
动机
放弃对这些端口的支持将使OpenJDK社区中的贡献者能够加速新功能的开发,这些新功能将推动平台向前发展。
JEP 363: 移除CMS垃圾收集器
移除CMS(Concurrent Mark Sweep)垃圾收集器。
动机
在两年多以前的JEP 291中,就已经弃用了CMS收集器,并说明会在以后的发行版中删除,以加快其他垃圾收集器的发展。在这段时间里,我们看到了2个新的垃圾收集器ZGC和Shenandoah的诞生,同时对G1的进一步改进。G1自JDK 6开始便成为CMS的继任者。我们希望以后现有的收集器进一步减少对CMS的需求。
描述
此更改将禁用CMS的编译,删除源代码中gc/cms
目录的内容,并删除仅与CMS有关的选项。尝试使用命令-XX:+UseConcMarkSweepGC
开启CMS会收到以下警告:
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; support was removed in <version>
VM将使用默认收集器继续执行。
JEP 364: macOS系统上的ZGC(实验)
将ZGC垃圾收集器移植到macOS。
动机
尽管我们希望需要ZGC可伸缩性的用户使用基于Linux的环境,但是在部署应用程序之前,开发人员通常会使用Mac进行本地开发和测试。 还有一些用户希望运行桌面应用程序,例如带有ZGC的IDE。
描述
ZGC的macOS实现由两部分组成:
-
支持macOS上的多映射内存。 ZGC设计大量使用彩色指针,因此在macOS上我们需要一种将多个虚拟地址(在算法中包含不同颜色)映射到同一物理内存的方法。我们将为此使用mach microkernel mach_vm_remap API。堆的物理内存在单独的地址视图中维护,在概念上类似于文件描述符,但位于(主要是)连续的虚拟地址中。该内存被重新映射到内存的各种ZGC视图中,代表了算法的不同指针颜色。
-
ZGC支持不连续的内存保留。在Linux上,我们在初始化期间保留16TB的虚拟地址空间。我们假设没有共享库将映射到所需的地址空间。在默认的Linux配置上,这是一个安全的假设。但是在macOS上,ASLR机制会侵入我们的地址空间,因此ZGC必须允许堆保留不连续。假设VM实现使用单个连续的内存预留,则共享的VM代码也必须停止。如此一来,is_in_reserved(),reserved_region()和base()之类的GC API将从CollectedHeap中删除。
JEP 365: Windows系统上的ZGC(实验)
将ZGC垃圾收集器移植到Windows系统上。
描述
ZGC的大多数代码库都是平台无关的,不需要Windows特定的更改。现有的x64负载屏障支持与操作系统无关,也可以在Windows上使用。需要移植的特定于平台的代码与如何保留地址空间以及如何将物理内存映射到保留的地址空间有关。用于内存管理的Windows API与POSIX API不同,并且在某些方面不太灵活。
Windows实现的ZGC需要进行以下工作:
-
支持多映射内存。 ZGC使用彩色指针需要支持堆多重映射,以便可以从进程地址空间中的多个不同位置访问同一物理内存。在Windows上,分页文件支持的内存为物理内存提供了一个标识(句柄),该标识与映射它的虚拟地址无关。使用此标识,ZGC可以将同一物理内存映射到多个位置。
-
支持将分页文件支持的内存映射到保留的地址空间。 Windows内存管理API不如POSIX的mmap/munmap灵活,尤其是在将文件支持的内存映射到以前保留的地址空间区域中时。为此,ZGC将使用Windows概念的地址空间占位符。 Windows 10和Windows Server版本1803中引入了占位符概念。不会实现对Windows较早版本的ZGC支持。
-
支持映射和取消映射堆的任意部分。 ZGC的堆布局与其动态调整堆页面大小(以及重新调整大小)相结合,需要支持映射和取消映射任意堆粒子。此要求与Windows地址空间占位符结合使用时,需要特别注意,因为占位符必须由程序显式拆分/合并,而不是由操作系统自动拆分/合并(如在Linux上)。
-
支持提交和取消提交堆的任意部分。 ZGC可以在Java程序运行时动态地提交和取消提交物理内存。为了支持这些操作,物理内存将被划分为多个分页文件段并由其支持。每个分页文件段都对应一个ZGC堆粒度,并且可以独立于其他段进行提交和取消提交。
JEP 366: 弃用Parallel Scavenge
弃用Parallel Scavenge和Serial Old垃圾收集算法的组合。
动机
有一组GC算法的组合很少使用,但是维护起来却需要巨大的工作量:并行年轻代GC(ParallelScavenge)和串行老年代GC(SerialOld)的组合。用户必须使用-XX:+UseParallelGC -XX:-UseParallelOldGC
来启用此组合。
这种组合是畸形的,因为它将并行的年轻代GC算法和串行的老年代GC算法组合在一起使用。我们认为这种组合仅在年轻代很多、老年代很少时才有效果。在这种情况下,由于老年代的体积较小,因此完整的收集暂停时间是可以接受的。但是在生产环境中,这种方式是非常冒险的:年轻代的对象容易导致OutOfMemoryException。此组合的唯一优势是总内存使用量略低。我们认为,这种较小的内存占用优势(最多是Java堆大小的约3%)不足以超过维护此GC组合的成本。
描述
除了弃用选项组合-XX:+UseParallelGC -XX:-UseParallelOldGC
外,我们还将弃用选项-XX:UseParallelOldGC
,因为它唯一的用途是取消选择并行的旧版GC,从而启用串行旧版GC。
因此,任何对UseParallelOldGC选项的明确使用都会显示弃用警告。
JEP 367: 移除Pack200工具和API
删除java.util.jar软件包中的pack200和unpack200工具以及Pack200 API。这些工具和API在Java SE 11中已经被注明为不推荐,并明确打算在将来的版本中删除它们。
动机
Pack200是JSR 200在Java SE 5.0中引入的一种JAR文件压缩方案。其目标是“减少Java应用程序打包,传输和交付的磁盘和带宽需求”。开发人员使用一对工具pack200和unpack200压缩和解压缩其JAR文件。在java.util.jar包中提供了一个API。
删除Pack200的三个原因:
-
从历史上看,通过56k调制解调器缓慢下载JDK阻碍了Java的采用。 JDK功能的不断增长导致下载量膨胀,进一步阻碍了采用。使用Pack200压缩JDK是缓解此问题的一种方法。但是,时间已经过去了:下载速度得到了提高,并且JDK 9为Java运行时(JEP 220)和用于构建运行时的模块(JMOD)引入了新的压缩方案。因此,JDK 9和更高版本不依赖Pack200。 JDK 8是在构建时用pack200压缩的最新版本,在安装时用unpack200压缩的最新版本。总之,Pack200的主要使用者(JDK本身)不再需要它。
-
除了JDK,Pack200还可以压缩客户端应用程序,尤其是applet。某些部署技术(例如Oracle的浏览器插件)会自动解压缩applet JAR。但是,客户端应用程序的格局已经改变,并且大多数浏览器都放弃了对插件的支持。因此,Pack200的主要消费者类别(在浏览器中运行的小程序)不再是将Pack200包含在JDK中的驱动程序。
-
Pack200是一项复杂而精致的技术。它的文件格式与类文件格式和JAR文件格式紧密相关,二者均以JSR 200所无法预料的方式发展。(例如,JEP 309向类文件格式添加了一种新的常量池条目,并且JEP 238在JAR文件格式中添加了版本控制元数据。)JDK中的实现是在Java和本机代码之间划分的,这使得维护变得很困难。 java.util.jar.Pack200中的API不利于Java SE平台的模块化,从而导致在Java SE 9中删除了其四种方法。总的来说,维护Pack200的成本是巨大的,并且超过了其收益。包括在Java SE和JDK中。
描述
JDK最终针对的JDK功能发行版中将删除以前用@Deprecated(forRemoval = true)
注解的java.base模块中的三种类型:
-
java.util.jar.Pack200
-
java.util.jar.Pack200.Packer
-
java.util.jar.Pack200.Unpacker
包含pack200和unpack200工具的jdk.pack模块先前已使用@Deprecated(forRemoval = true)
进行了注解,并且还将在此JEP最终针对的JDK功能版本中将其删除。