那些年,我用过jlink定制的优化镜像,真够优雅!

那些年,我用过jlink定制的优化镜像,真够优雅!

首页冒险解谜Mirror.mb更新时间:2024-07-31

一 使用jlink插件优化镜像

“使之工作,工作得正确,工作得快速”,Kent Beck(极限编程的创建者和《测试驱动开发:实战与模式解析 》的作者)如是说。

因此,在介绍了创建运行时和应用程序镜像(甚至跨操作系统)的细节之后,我们将转向优化。这可以极大地减小镜像尺寸,并略微提高运行时性能,特别是启动时间。

jlink中,优化由插件处理。因此,在使镜像更小和更快之前,有必要先讨论一下插件架构。

1 jlink的插件

jlink的核心是它的模块化设计。除了选择正确的模块并生成镜像的基本步骤之外,jlink将镜像的进一步处理留给了插件。你可以通过jlink --list-plugins查看可用的插件,或者查看表14-1(我们将在后文中查看每个插件)。

14-1 字母序的jlink插件列表,指明插件是减小镜像大小还是提高

运行时性能

注意 文档和jlink本身也列出了vm插件,让你能从几个HotSpot虚拟机(客户机、服务器或最小虚拟机)中选择一个,并包含在镜像中。

理论上这是可行的,因为64位JDK只与服务器VM一起发布。大多数情况下,你只有一个选择。

01. 为jlink开发插件

在本书出版时,只有支持的插件是可用的,但当添加更多的实验功能时,这一点在未来可能发生改变。优化镜像的工作还处在开发早期,很多工作仍在进行中。由于没有标准化,也没有在Java 9及以上版本中导出,因此插件的API将来可能会改变。

这使得为jlink开发插件变得非常复杂 ,也意味着在社区真正开始贡献插件之前,你必须等待一段时间。这样做的意义是什么?

首先,编写jlink插件有点像编写代理程序或构建工具插件,而不是在开发典型的应用程序。对类库、框架和工具的支持是一项专门的任务。

但是让我们回到社区提供的插件可以做什么的问题上。一个用例来自profilers,它使用代理将性能跟踪代码注入正在运行的应用程序中。

使用jlink插件,你可以在链接的时候完成注入,而不是在执行应用程序时将时间花费于此。如果你需要快速加载,那么这可能是一个明智的选择。

另一个用例是增强Java Persistence API(JPA)实体的字节码。例1如,Hibernate已经使用代理来跟踪哪些实体发生了变化[所谓的脏检查(dirty checking)],而不必检查每个字段。

这在链接时而非启动时是有意义的,这就是为什么Hibernate已经为构建工具和IDE提供了可以在它们构建过程中实现这些功能的插件。

最后一个例子是一个非常好的、有潜力的jlink插件,此插件在链接时索引注解并使该索引在运行时可用。这将大大减少应用程序的启动时间,这些应用程序将扫描模块路径以查找带注解的bean实体。

02. 使用jlink插件

定义:插件命令行选项--${name}

掌握了理论知识,现在让我们真正使用一些插件吧。插件的使用非常简单:jlink根据每个插件的名称自动创建一个命令行选项--${name}。进一步的参数传递取决于插件,并在jlink--list-plugins中进行了描述。

去除调试符号是减小镜像尺寸的好方法,为此,使用--stripdebug来创建镜像。

这样就可以了:lib/modules中的基本模块大小从23 MB压缩到了18MB(在Linux上)。

通过把重要文件放在前面来对lib/modules中的内容进行排序可以减少启动时间(尽管我怀疑效果是否明显)。

这样,首先是模块描述符,然后是java.lang包中的类。

既然你已经知道了如何使用插件,现在就该测试一些插件了。我们将分两个部分进行讲解,第一部分关注缩减尺寸,第二部分关注性能改进。

因为这是一个不断演变的特性,同时也是相当专业的特性,所以我不会详细介绍官方的jlink文档和jlink --list-plugins,而是尽量用尽可能少的文字进行讲解,但更精确地展示它们的用法。

2 减小镜像尺寸

让我们逐个检查缩小尺寸的插件并测量它们的效果。我本想在应用程序镜像上测试它们,但ServiceMonitor只有大约12个类,所以减小它的尺寸毫无意义。

我找不到一个可以免费使用且完全模块化的应用程序,包括它的依赖。(在镜像中没有自动模块,还记得吗?)相反,我将对这3个不同的运行时镜像上的工作量进行衡量(括号中为变更前的尺寸):

1)base——仅包含java.base(45 MB);

2)services——java.base加上所有的服务提供者(150 MB);

3)java——所有java.*javafx.*模块,但不包括服务提供者(221MB)。

有趣的是,java相对于services具有更大的尺寸并不是由于更多的字节码(lib/modulesjava中比在services中更小一些),而是由于本地库,尤其是为JavaFXWebView所捆绑的WebKit代码。这将在试图减小镜像尺寸时帮助你理解插件的行为。(顺便提一下,我正在为Linux做这件事情,但是其他操作系统的比例应该也差不多。)

01. 压缩镜像

定义:压缩插件

压缩插件意在减小lib/modules的尺寸。它通过--compress=${value}选项来控制,包含3个合法值:

1)0——不压缩(默认);

2)1——去重并且共享字符串内容(意为String s ="text";中的"text");

3)2——利用Ziplib/modules进行压缩。

可以通过--compress=${value}:filter=${pattern-list}来包含一个可选样式列表,用来仅压缩匹配这些样式的文件。

该命令创建了一个仅包含基础模块的压缩后的运行时镜像。

很明显,你不需要尝试0。对于1和2,我得到了以下结果:

1)base——45 MB → 39 MB (1) → 33 MB (2)

2)services——150 MB → 119 MB (1) → 91 MB (2)

3)java——221 MB → 189 MB (1) → 164 MB (2)

可以看到,压缩率对于每个镜像是不一样的。services镜像尺寸可以被减小将近40%,但更大的java镜像只减小了25%。

这是由于compress插件仅对lib/modules有效,正如我们所讨论的,它在两个镜像中几乎都是相同的尺寸。因此,减小的绝对尺寸是相近的:对于每个镜像都是大约60 MB,超过lib/modules初始尺寸的50%。

注意 通过--compress=2指定的Zip压缩会增加启动时间——总的来说,镜像越大,增加的时间越多。如果启动时间对你来说很重要,那么请确保关注它所带来的影响。

02. 排除文件和资源

定义:exclude-filesexclude-resources插件exclude-filesexclude-resources插件允许将文件从镜像中排除。

相应的选项--exclude-files=${pattern-list}--exclude-resources=${pattern-list}接受一个样式列表,用来匹配要排除的文件。

如同我在比较servicesbase镜像的初始尺寸时所指出的,主要是JavaFX WebView的二进制字节码导致了java的尺寸变大。

在我的机器上,它是一个73 MB的lib/libjfxwebkit.so文件。下面演示了如何通过--exclude-files将它排除。

这实现了将镜像减小73 MB的效果。下面是两个告诫:

1) 这与人工将它们从镜像中删除有着相同效果;

2) 这使得只包含WebViewjavafx.scene.web模块几近于无用,所以更好的选择是不要包含这个模块。

除了实验和学习,排除来自于平台模块的内容是糟糕的实践。一定要对任何这样的决定进行深入研究,因为这有可能影响JVM的稳定性。

对这些插件更好的用法是,将应用程序或依赖JAR所包含但在应用程序镜像中不需要的文件进行排除。可以是文档、不需要的源代码文件、不需关心的针对操作系统的二进制字节码、配置或者任何被别具匠心的开发者放入归档文件中的东西。对于压缩尺寸的比较也是没有意义的:被排除文件所占的空间会被节省出来。

03. 排除不需要的语言环境

语言环境确实是值得删除的来自于平台模块的内容。正如你在上文所发现的,基础模块仅能在英语语言环境中工作,而jdk.localedata模块包含了Java所支持的所有其他语言环境。

很不幸,这些语言环境加在一起大约有16 MB。如果你只需要一个或者几个非英语语言环境,那这个尺寸还是有点大。

定义:include-locales插件

include-locales插件的作用是这样的——通过--includelocales=${langs}选项生成的镜像将仅包含它所指定的语言环境,其中${langs}是一个逗号分隔的BCP 47语言标签(类似于en-US、zh-Hansfi-FI)列表。

该插件只在某个语言环境被jdk.localedata模块放入镜像时才有效果,所以它不会包括除基础模块所附带的语言环境之外的其他语言环境,这是因为它会排除jdk.localedata中的所有其他语言环境。

代码清单14-4创建了一个ServiceMonitor的应用程序镜像,其包含了所有的jdk.localedata,因为该应用程序在输出中使用了芬兰语格式。

这使得镜像尺寸额外增加了16 MB,而你清楚如何将它减小回来。代码清单14-7通过使用--include-locales=fi-FI来达到此目的。

相对于没有使用jdk.localedata的镜像,由此创建的镜像的尺寸只进行了最小限度的增加(准确地说,168 KB)。成功!代码清单14-7 创建带有芬兰语语言环境的ServiceMonitor应用程序镜像

通过排除语言环境能够减少多少镜像尺寸依赖于你需要多少种语言环境。如果是将一个国际化的应用程序交付给一个全球性的客户,那么将无法节省太多尺寸,但我认为这种情况并不常见。如果应用程序只支持少数或者甚至十几种语言,那么将其他语言排除会节省几乎16 MB。这个努力是否值得由你做主。

04. 剥离调试信息

当你用IDE调试Java代码时,通常会看到精致的被格式化、命名甚至注释过的源代码。这是由于IDE获取了相应的真实源文件,将它们绑定到当前正被执行的字节码,并且适宜地显示了出来。这是最佳场景。

在没有源文件时,如果除了字段和方法参数名(必定存在于字节码中)还能看到变量名(不是必须存在于字节码中),也许你仍然可以看到具有良好可读性的代码。

如果反编译代码包含调试符号,就会出现这种情况。这个信息使得调试更容易,但当然也会占用空间。而jlink允许你将这些符号剥离。

定义:strip-debug插件

如果通过--strip-debug选项激活jlinkstrip-debug插件,那么它将从镜像的字节码中删除所有的调试信息,进而减小lib/modules文件的尺寸。此选项没有其他参数。

我在上面中使用过--strip-debug选项,所以在此就不赘述了。来看一下它是如何减小镜像尺寸的:

1) base——45 MB → 40 MB

2) services——150 MB → 130 MB

3) java——221 MB → 200 MB

这相当于镜像总尺寸的10%,但是请记住,这只影响了lib/modules,其减小了大约20%。

要点 一点警告:在没有源文件和调试符号的情况下调试代码是一件非常可怕的事情。也许你偶尔会通过远程调试连接到一个正在运行的应用程序,并且分析出现的问题,如果放弃了那些调试符号,你则不会很开心,尤其是当节省的那几兆字节对你来说并不重要的时候。小心考虑--strip-debug

05. 将这些选项放在一起

虽然将文件和资源排除对于应用程序模块来说会更好,但其他选项在纯运行时镜像中运行良好。让我们把它们放在一起,并且尝试为挑选出来的这3个模块创建最小的镜像。下面仅是java.base的命令行。

这是执行的结果:

1)base——45 MB → 31 MB

2)services——150 MB → 75 MB(我同时删除了除fi-FI之外的所有语言环境)

3)java——221 MB → 155 MB(或者82 MB,如果去除JavaFXWebKit的话)

这个结果不坏,是吧?

3 提高运行时性能

如你所见,减小应用程序或运行时镜像尺寸的方法有很多。我的猜测是,大多数开发者在急切地盼望着性能的提高,尤其是在SpectreMeltdown抢走了一些CPU周期后。

要点 很不幸,在这个领域我没有太多好消息:基于jlink的性能优化仍然处于早期阶段,而已有的大多数或者已经预期的优化集中于提升启动时性能,而非长期运行时的性能。

一个现有的插件是system-modules。它默认被打开,会预先计算系统模块图并且将之存储,以便快速访问。这样,JVM就不需要在每次启动时都解析和处理模块声明以及验证可靠配置了。

另一个插件是class-for-name,它用some.Type.class来替换诸如Class.forName("some.Type")这样的字节码,进而相对昂贵且基于反射的按名称对类进行的搜索则可以避免。我们简要地看过orderresources,其并没有对性能有较大的改善。

目前,唯一支持的其他性能相关的插件是generate-jli-classes。合理配置后,它可以将初始化lambda表达式的代价从运行时移动到链接时,但需要对方法句柄有很好的理解后,才能有效对它进行学习,所以我不会在此过多涉及这个话题。

这就是性能提升相关的所有内容。如果你对于在此领域没有获得太多帮助而很失望,对此我表示理解。

但是请让我指出,JVM已经优化的非常彻底了。所有低垂的果实(以及一些相对较高的果实)都已经被摘掉了,而要摘取剩下的果实还需要精巧的设计、大把的时间以及专业的工程能力。

jlink工具仍然年轻,我相信JDK开发团队和社区会在适当的时候对它加以利用。

Java 10的应用程序类数据共享。

Java 10引入了一个与jlink间接相关的优化:应用程序类数据共享。2实验证实,它可以使得应用程序启动加快10%到50%。有趣的是,你可以在应用程序镜像中应用这项技术,创建一个更加优化的部署单元。

二 jlink选项

方便起见,表14-2列出了本书讨论的所有jlink命令行选项。更多信息可以参见官方文档,或者使用jlink --helpjlink --listplugins

14-2 经筛选的jlink字母序选项列表,包括插件。描述列基于官方文档,引用列指向本书中详细解释如何使用这些选项的章节

三 小结

1)命令行工具jlink基于指定的平台模块创建运行时镜像(使用jdeps来确定应用程序需要哪些平台模块)。要从这个工具中受益,应用程序需要运行于Java 9及以上版本,但模块化不是必需的。

2)一旦应用程序和依赖被完全模块化(而非利用自动模块),jlink就可以为它创建应用程序镜像,其中包含应用程序的模块。

3)所有对jlink的调用都需要指定以下参数。

1.--module-path,从哪里查找模块(包括平台模块)。

2.--add-modules,所解析的根模块。

3.--output,生成镜像的输出目录。

4) 注意jlink如何解析模块。

1.不会默认绑定服务。

2.requires static指定的可选依赖不会被解析。

3.不允许使用自动模块。

4.确保通过--add-modules单独地增加依赖的服务提供者或者可选依赖,或者通过--bind-services绑定所有服务提供者。

5) 当心那些无须实现就可以隐式依赖的平台服务。比如字符集(jdk.charsets)、语言环境(jdk.localedata)、Zip文件系统(jdk.zipfs)以及安全提供者(多个模块)。

6) 由jlink生成的运行时镜像。

1.绑定到通过--module-path选择的平台模块构建所针对的操作系统。

2.与JDKJRE有相同的目录结构。

3.将平台和应用程序模块(统称为系统模块)融合进lib/modules

4.仅包含所需模块的二进制文件(在bin目录中)。

7)使用bin/java --module ${initial-module}(无须模块路径,因为系统模块被自动解析)或者通过--launcher${name}=${module}/${main-class}创建的启动器来启动应用程序镜像。

8)利用应用程序镜像,模块路径可以用来增加额外的模块(尤其是那些提供服务的模块)。模块路径中与系统模块同名的模块会被忽略。

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved