在Spring Boot和AppCDS上构建铸铁助行器


应用程序类数据共享(AppCDS)-JVM功能可加快启动速度并节省内存。 早在JDK 1.5(2004)中就在HotSpot中出现时 ,它长期以来仍然非常有限,甚至部分地商业化。 仅在OpenJDK 10(2018)中,它才可供凡人使用,同时扩大了范围。 最近发布的 Java 13试图简化此应用程序。


AppCDS的想法是在同一主机上的同一JVM实例之间“共享”已加载的类。 看来这对微服务特别有用,特别是Spring Boot上具有成千上万个库类的“ broilers”,因为现在这些类将不需要在每个JVM实例的每次启动时都进行加载(解析和验证),并且不会在内存中重复。 这意味着启动应该变得更快,并且内存消耗应该更低。 太好了,不是吗?


一切都是这样,一切都是如此。 但是,如果您(odnokhabryanin)过去不相信林荫大道的标志,而是相信特定的数字和示例,那么欢迎来到Kat –让我们尝试弄清楚它的真正含义是...


代替免责声明


在您之前,它不是使用AppCDS的指南,而是小研究结果的摘要。 我很想了解这个JVM函数如何在我的工作项目中应用,我试图从企业开发人员的角度对其进行评估,并在本文中阐述了结果。 其中不包括在模块路径上使用AppCDS,在其他虚拟机(不是HotSpot)上实现AppCDS以及使用容器的复杂性等主题。 但是,有一个理论部分用于探讨该主题,还有一个实验性部分,您可以自己重复体验。 尚未将任何结果应用到生产中,但谁知道明天会是...


理论


AppCDS简介


您可能在多个来源中都熟悉过此主题,例如:


  • 在Nikolai Parlog的文章中 (包括Java 13包子,但没有Spring Boot)
  • 在Volker Simonis的报告文章中 (没有Java 13,但有详细信息)
  • 这些行作者在一份报告中 (没有Java 13,但重点是Spring Boot)

为了不进行转述,我仅强调对本文很重要的几点。


首先,AppCDS是CDS功能的扩展,该功能在HotSpot中早已出现,其实质如下:



为了使这两个想法都变为现实,您需要执行以下操作(一般而言):


  1. 获取要在应用程序实例之间共享的类的列表
  2. 将这些类合并到适合内存映射的存档中
  3. 启动时将存档连接到应用程序的每个实例

看来算法只是3个步骤-采取并执行。 但是,新闻开始了,各种各样的事情。


不好的是,在最坏的情况下,这些项目中的每一个都会变成至少一个具有自己特定选项的JVM启动,这意味着整个算法是对相同类型的选项和文件的微妙处理。 听起来不是很有希望,不是吗?


但是有个好消息:改进此算法的工作正在进行中 ,并且随着Java的每个发行版,其应用变得更加容易。 因此,例如:


  • 在OpenJDK 10和11中,如果您只想共享主要的JDK类,则可以跳过第1步,因为它们已经为我们编译过,并放入$JAVA_HOME\lib\classlist (≈1200个)中。
  • 在OpenJDK 12中,您可以跳过第2步 ,因为与类列表一起,发行档案还包括一个现成的档案,它们可以直接使用,并且不需要显式连接。
  • 如果您想共享其他所有内容(通常只是想共享)
    OpenJDK 13 提供了动态CDS存档-在应用程序运行期间收集的存档,并在配备人员后保存。 这使您可以将点1和2折叠为一个不太混乱的点(尽管并不是所有事情都那么简单,但稍后会介绍更多)。

因此,无论准备AppCDS的过程是什么,上面列出的3个步骤始终是紧随其后的,只是在某些情况下它们是被遮盖的。


您可能已经注意到,随着AppCDS的出现,许多应用程序类开始出现双重生活:它们同时生活在以前的位置(通常是JAR文件)和新的共享存档中。 同时,开发人员继续在同一位置更改/删除/补充它们,并且JVM在工作时从新的位置获取它们。 不必担心这种情况的危险:如果不采取任何措施,迟早该课程的副本将受到腐蚀,我们将获得典型的“ JAR地狱”的许多魅力。 显然,JVM无法阻止类更改,但是它应该能够及时检测到差异。 但是,通过成对比较类(甚至通过校验和)来做到这一点是一个想法。 它可以抵消其余的生产率提高。 这可能就是为什么JVM工程师没有选择单个类作为比较对象,而是选择整个类路径的原因,并在AppCDS文档中进行了说明:“创建共享存档时,类路径应与应用程序的后续启动相同(或至少是前缀)。”


请注意,在归档创建时使用的类路径必须与运行时使用的类路径相同(或前缀)。

但这并不是明确的声明,因为您记得,可以通过不同的方式来形成类路径,例如:


  • 从已编译的软件包目录中读取裸露的.class文件,
    例如java com.example.Main
  • 使用通配符时,使用JAR文件扫描目录,
    例如java -cp mydir/* com.example.Main
  • 明确列出JAR和/或ZIP文件,
    例如java -cp lib1.jar;lib2.jar com.example.Main

(这不包括还可以通过例如JVM选项-cp/-classpath/--class-pathCLASSPATH环境变量或要启动的Class-Path JAR文件的属性来不同地设置类路径的事实)


在这些方法中,AppCDS仅支持一种方法-JAR文件的显式枚举。 显然,HotSpot JVM工程师认为,只有在尽可能明确地指定它们(使用通常的详尽列表)的情况下,比较AppCDS存档和已启动的应用程序中的类路径才足够快速和可靠。


CDS / AppCDS仅支持从JAR文件归档类。

在此必须注意,该语句不是递归的,即 不适用于JAR文件中的JAR文件(除非它与动态CDS有关,请参见下文)。 这意味着Spring Boot发行的普通JAR娃娃无法与常规AppCDS一起使用,您必须坐下来。


CDS工作中的另一个问题是共享归档文件被投影到具有固定地址的内存(通常从0x800000000开始)。 这本身还不错,但是由于大多数操作系统默认情况下启用了地址空间布局随机化(ASLR),因此可能会部分占用所需的内存范围。 在这种情况下,HotSpot JVM的作用是特殊选项 -Xshare ,它支持三个值:


  • -Xshare:on强制CDS / AppCDS; 如果范围繁忙,则JVM退出并显示错误。 不建议在生产环境中使用此模式,因为这会在启动应用程序时导致偶发的崩溃。
  • -Xshare:off -(您)切换CDS / AppCDS; 完全禁用共享数据的使用(包括嵌入式存档)
  • -Xshare:auto -JVM的默认行为,如果无法分配所需的内存范围,则-Xshare:auto放弃并照常加载类

在撰写本文时,Oracle正在努力解决此类问题,但尚未分配发行号。


这些选项稍后对我们有用,但现在让我们看一下...


AppCDS应用


您可以通过多种方式使用AppCDS。 毁了你的生活 优化微服务的工作。 它们的复杂性和潜在利润差异很大,因此立即决定稍后再讨论哪个至关重要。


最简单的方法是不使用AppCDS,而仅使用CDS-这是只有平台类进入共享档案库的时候(请参阅“ AppCDS简介”)。 我们将立即删除此选项,因为在Spring Boot上应用于微服务时,它带来的利润太少。 这可以通过使用一个真正的微服务的示例(参见绿色部分)通过共享类的数量在其一般分布中的比例来看出:



更复杂,但很有前途的是使用成熟的AppCDS,即在同一个归档文件中同时包含库和应用程序类。 这是一整套选项,它们是从参与的应用程序数量和实例数量的组合中得出的。 以下是作者对AppCDS各种应用程序的好处和困难的主观评估。


不行应用领域执行个体CPU利润RAM利润难点
1个一个一个+±低位
2一个一些++++低位
3一些一次一个++++
4一些一些++++++

注意事项:


  • 在一个实例中的一个应用程序中(第1个),内存利润可能变为零甚至为负(尤其是在Windows下测量时)
  • 创建正确的共享存档需要采取措施,其复杂程度并不取决于应用程序随后将启动多少个副本(比较选项对1-2和3-4)。
  • 同时,从一个实例到多个实例的过渡显然会增加两个指标的利润,但不会影响准备工作的复杂性。

在本文中,我们将仅提供选项2 (通过选项 1),因为它足够简单,可以与AppCDS熟识,并且只有简单的操作,我们才能使用最近发布的JEP-350动态CDS存档,我想在实际中使用它。


动态CDS档案


JEP-350动态CDS存档是Java 13的主要创新之一,旨在使AppCDS易于使用。 要感到简化,您必须首先了解复杂性。 让我提醒您,AppCDS的经典“干净”应用程序算法包括3个步骤:(1)获取共享类的列表,(2)从它们中创建一个存档,以及(3)在连接了存档的情况下运行该应用程序。 在这些步骤中,只有第3个实际上有用,其余的只是为此做准备。 尽管获得类列表(步骤1)似乎非常简单(在某些情况下甚至没有必要),但实际上,在处理非平凡的应用程序时,事实证明这是最困难的,尤其是对于Spring Boot。 因此,仅JEP-350就是为了消除这一步骤,或者使其自动化。 想法是,JVM本身会绘制应用程序所需的类的列表,然后由它们自己形成所谓的“动态”归档。 同意,听起来不错。 但是要注意的是,现在尚不清楚何时停止累积类,然后继续将其放入存档中。 以前,在经典的AppCDS中,我们自己选择了此刻,甚至可以在这些操作之间切换以更改类列表中的某些内容,然后再将其转换为存档。 现在,这是自动发生的,并且仅在瞬间,JVM工程师为此选择了唯一的折衷方案-定期关闭JVM。 这意味着在应用程序停止之前将不会创建存档。 该解决方案有两个重要的后果:


  • 在JVM崩溃的情况下,无论届时累积的类列表有多美妙,都不会创建归档文件(您以后无法使用常规方法将其提取出来)。
  • 仅从那些在应用程序会话期间成功加载的类创建存档。 对于Web应用程序,这意味着从头开始和停止创建归档文件是不正确的,因为那样的话,许多重要的类都不会进入归档文件。 必须至少对应用程序执行一个HTTP请求(最好在所有情况下正确运行它),以便加载其实际使用的所有类。

动态和静态归档之间的一个重要区别是,它们总是构成基本静态归档的“附加组件”,基本归档可以是内置在Java分发工具包中的归档,也可以以经典的三步方式单独创建。


从语法上讲,使用动态CDS存档归结为两个带有两个选项的JVM启动:


  1. 使用-XX:ArchiveClassesAtExit=archive.jsa选项进行试运行-XX:ArchiveClassesAtExit=archive.jsa ,最后将创建一个动态档案(您可以指定任何路径和名称)
  2. 使用-XX:SharedArchiveFile=archive.jsa选项的有用启动-XX:SharedArchiveFile=archive.jsa ,它将使用先前创建的存档

第二个选项与连接常规静态存档没有什么不同。 但是,如果基本静态存档突然不在默认位置(在JDK内),则此选项可能还包括指向其的路径的指示,例如:


 -XX:SharedArchiveFile=base.jsa:dynamic.jsa 

(在Windows中,路径分隔符必须为“;”字符)


现在您已经对AppCDS了如指掌,因此可以实际使用它。


练习


实验兔


因此,我们在实践中对AppCDS的应用不仅限于典型的HelloWorld,我们将以Spring Boot上的实际应用为基础。 我和我的同事们经常不得不在远程测试服务器上观看应用程序日志,并观看“实时”日志,就像编写它们一样。 为此,使用成熟的日志聚合器(例如ELK)通常是不合适的。 无休止地下载日志文件-很长时间了,看着tail的灰色控制台输出令人沮丧。 因此,我制作了一个Web应用程序,该应用程序可以将任何日志实时直接直接输出到浏览器,按重要性级别为行着色(同时格式化XML),将多个日志聚合为一个,以及其他技巧。 它称为ANALOG (例如“ log分析器”,尽管并非如此),位于GitHub上 。 单击屏幕截图放大:



从技术上讲,这是Spring Boot + Spring Integration上的一个应用程序,在tailkubectlkubectl支持下kubectl (分别支持来自文件,Docker容器和Kubernetes资源的日志)。 它以经典的“厚” Spring Boot JAR文件的形式出现。 在运行时, ≈10K类挂在应用程序内存中,其中绝大多数是Spring和JDK类。 显然,这些类很少更改,这意味着可以将它们放入共享存档中,并在应用程序的所有实例中重用,从而节省内存和CPU。


单项实验


现在,让我们将Dynamic AppCDS的现有知识应用于实验兔子。 由于所有内容都是比较已知的,因此我们需要一些参考点-程序的状态,我们将与该状态比较实验期间获得的结果。


引言


  • 所有其他命令均适用于Linux。 Windows和macOS的差异不是根本。
  • JIT编译会显着影响结果,并且从理论上讲,为了保证实验的纯洁性,可以将其关闭(使用-Xint选项,如上述文章所述 ),但是出于最大可信度的考虑,决定不这样做。
  • 在快速测试服务器上获得了以下有关开始时间的数字。 通常,在工作机器上,相似的数字较为适中,但由于我们对绝对值不感兴趣,而对百分比增量感兴趣,因此我们认为这种差异不明显。
  • 为了避免过早进入测量共享内存的复杂性,现在我们将省略以字节为单位的准确读数。 取而代之的是,我们引入“ CDS潜力 ”的概念表示为共享类数相对于已加载类总数的百分比。 当然,这是一个抽象的数量,但另一方面,它直接影响实际的内存消耗。 此外,其定义完全不依赖于操作系统,并且对于其计算,仅日志就足够了。

参考点


让这一点成为新下载的应用程序的状态,即 无需明确使用任何AppCDS'ov等。 要对其进行评估,我们需要:


  1. 安装OpenJDK 13(例如,国内Liberica发行版,但不是lite版本)。
    例如,还需要将其添加到PATH环境变量或JAVA_HOME ,如下所示:


     export JAVA_HOME=~/tools/jdk-13 

  2. 下载 ANALOG(在撰写本文时,最新版本为v0.12.1)。


    如有必要,您可以在server.address参数的config/application.yaml文件中指定用于访问该应用程序的外部主机名(默认情况下,在其中指定localhost )。


  3. 启用JVM类负载日志记录。
    为此,您可以使用以下值来启动JAVA_OPTS环境变量:


     export JAVA_OPTS=-Xlog:class+load=info:file=log/class-load.log 

    该选项将传递给JVM,并告诉它保证每个类源。


  4. 运行测试运行:


    1. 使用bin/analog脚本运行应用程序
    2. 在浏览器中打开http://本地主机:8083 ,戳按钮和daws
    3. 通过在bin/analog脚本控制台中按Ctrl+C来停止应用程序

  5. 获取结果(来自log/目录中的文件)


    • 加载的类总数(通过class-load.log ):


       cat class-load.log | wc -l 10463 

    • 从共享归档文件中下载了多少个(根据它):


       grep -o 'source: shared' - class-load.log 1146 

    • 平均启动时间(一系列启动后;通过analog.log ):


       grep -oE '\(JVM running for .+\)' analog.log | grep -oE '[0-9]\.[0-9]+' | awk '{ total += $1; count++ } END { print total/count }' 4.5225 



因此,在此步骤中,CDS的潜力为1146/10463=0,1095 0.1095≈11% 。 如果您对共享类的来源感到惊讶(毕竟,我们还没有包含任何AppCDS),那么我想提醒您,从第12版开始,JDK 包括完成的CDS归档文件$JAVA_HOME/lib/server/classes.jsa ,已构建不少于准备好的班级清单:


 cat $JAVA_HOME/lib/classlist | wc -l 1170 

现在,在评估了应用程序的初始状态之后,我们可以将AppCDS应用于该应用程序,并通过比较来了解它的作用。


核心经验


正如文档所遗留的那样,要创建一个动态AppCDS存档,您只需使用-XX:ArchiveClassesAtExit选项对应用程序进行一次试运行-XX:ArchiveClassesAtExit 。 从下一次启动以来,就可以使用存档并从中获利。 要在同一只实验兔子(AnaLog)上进行验证,您需要:


  1. 将指定的选项添加到运行命令:


     export JAVA_OPTS="$JAVA_OPTS -XX:ArchiveClassesAtExit=work/classes.jsa" 

  2. 扩展日志记录:


     export JAVA_OPTS="$JAVA_OPTS -Xlog:cds=debug:file=log/cds.log" 

    此选项将强制在应用程序停止时记录构建CDS存档的过程。


  3. 执行与参考点相同的测试运行:


    1. 使用bin/analog脚本运行应用程序
    2. 在浏览器中打开http://本地主机:8083 ,戳按钮和daws
    3. 通过在bin/analog脚本控制台中按Ctrl+C来停止应用程序
      在那之后,带有各种警告的巨大脚步应该落入控制台,并且log/cds.log应该填充详细信息; 他们对我们还不感兴趣。

  4. 将启动模式从试用模式切换为有用模式:


     export JAVA_OPTS="-XX:SharedArchiveFile=work/classes.jsa -Xlog:class+load=info:file=log/class-load.log -Xlog:class+path=debug:file=log/class-path.log" 

    在这里,我们不补充JAVA_OPTS变量,而是用新值重新擦除它,这些新值包括(1)使用共享档案库,(2)记录类源和(3)记录类路径检查。


  5. 根据第3段中的方案对应用程序进行有用的启动。


  6. 获取结果(来自log/目录中的文件)


    • 验证AppCDS是否确实适用(通过class-path.log ):


       [0.011s][info][class,path] type=BOOT [0.011s][info][class,path] Expecting BOOT path=/home/upc/tools/jdk-13/lib/modules [0.011s][info][class,path] ok [0.011s][info][class,path] type=APP [0.011s][info][class,path] Expecting -Djava.class.path=/home/upc/tmp/analog/lib/analog.jar [0.011s][info][class,path] ok 

      type=BOOTtype=APP行之后的ok标记分别表示成功打开,验证和加载了内置CDS存档和应用CDS存档。


    • 加载的类总数(通过class-load.log ):


       cat class-load.log | wc -l 10403 

    • 从共享归档文件中下载了多少个(根据它):


       grep -o 'source: shared' -c class-load.log 6910 

    • 平均启动时间(一系列启动后;通过analog.log文件):


       grep -oE '\(JVM running for .+\)' analog.log | grep -oE '[0-9]\.[0-9]+' | awk '{ total += $1; count++ } END { print total/count }' 4.04167 



但是在此步骤中,CDS的潜力已经为6910/10403≈0,66 66% ,即与参考点相比增加了55% 。 同时,平均发射时间减少了(4,5225-4,04167)=0,48秒,即 启动速度快了初始值的10.6%


结果分析


该项目的工作标题是:“为什么这么少?”


就像我们一样,我们按照说明进行了所有操作,但是并不是所有的类都在存档中。 它们的数量对发射时间的影响不小于实验者机器的计算能力,因此我们将集中在这个数字上。


如果您还记得的话,我们会在试运行后忽略实验应用程序停止期间创建的log/cds.log文件。 在此HotSpot文件中,JVM友好地指出了未出现在CDS归档文件中的每个警告类。 这是此类标记的总数:


 grep -o '[warning]' cds.log -c 3591 

考虑到class-load.log日志中仅提及10K +类,并且其中66%的类是从存档中加载的,因此不难理解cds.log中列出的3600个类是CDS潜力的“缺失” 44%。 现在,您需要找出为什么跳过它们。


如果查看cds.log日志,结果发现跳过类只有四个独特的原因。 以下是每个示例的示例:


 Skipping org/springframework/web/client/HttpClientErrorException: Not linked Pre JDK 6 class not supported by CDS: 49.0 org/jrobin/core/RrdUpdater Skipping java/util/stream/Collectors$$Lambda$554: Unsafe anonymous class Skipping ch/qos/logback/classic/LoggerContext: interface org/slf4j/ILoggerFactory is excluded 

在所有3591个错过的课程中,这些原因的出现频率如下:



仔细看看它们:


  • Unsafe anonymous class
    JVM “” , -, .


  • Not linked
    , “” , , . , StackOverflow . , , “” () JAR- , AppCDS. , ( ).


  • Pre JDK 6 class
    , CDS Java 5. class- , CDS . , , 6, Java, . - , runtime- (, slf4j).


  • Skipping ... : super class/interface ... is excluded
    , “” . CDS', . 例如:


     [warning][cds] Pre JDK 6 class not supported by CDS: 49.0 org/slf4j/spi/MDCAdapter [warning][cds] Skipping ch/qos/logback/classic/util/LogbackMDCAdapter: interface org/slf4j/spi/MDCAdapter is excluded 


结论


CDS 100%.

, , , , , . .



JEP-310 , AppCDS JDK. . , . CDS (, , ) .


( ), - ; “ ”. Spring Boot, - ; JVM. ANALOG_OPTS , Gradle'.


 export ANALOG_OPTS="-Djavamelody.enabled=false -Dlogging.config=classpath:logging/logback-console.xml" export ANALOG_OPTS="$ANALOG_OPTS -Dnodes.this.agentPort=7801 -Dserver.port=8091" 

JavaMelody, , , . TCP- ; .


, , JVM AppCDS . JAVA_OPTS JVM Unified Logging Framework :


 export JAVA_OPTS="-Xlog:class+load=info:file=log/class-load-%p.log -Xlog:class+path=debug:file=log/class-path-%p.log" export JAVA_OPTS="$JAVA_OPTS -XX:SharedArchiveFile=work/classes.jsa" 

%p , JVM (PID). AppCDS , ( ).



, . . :


  1. server.port nodes.this.agentPort , :


     export ANALOG_OPTS="$ANALOG_OPTS -Dnodes.this.agentPort=7801 -Dserver.port=8091" 

    , ( ).


  2. bin/analog


    () http://localhost:8091 ,


  3. PID ( ), :


     pgrep -f analog 13792 

  4. pmap ( ):


     pmap -XX 13792 | sed -n -e '2p;$p' Address Perm Offset Device Inode Size KernelPageSize MMUPageSize Rss Pss Shared_Clean Shared_Dirty Private_Clean Private_Dirty Referenced Anonymous LazyFree AnonHugePages ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked ProtectionKey VmFlagsMapping 3186952 1548 1548 328132 325183 3256 0 10848 314028 212620 314024 0 0 0 0 0 0 0 325183 0 KB 

    ; .


  5. 1-4 (, ).




pmap . CDS' . , , PSS:


The "proportional set size" (PSS) of a process is the count of pages it has in memory, where each page is divided by the number of processes sharing it. So if a process has 1000 pages all to itself, and 1000 shared with one other process, its PSS will be 1500.

, , “ ” . , .


PSS , :


Iteration:1个2345
PSS of inst#1:339 088313 778305 517301 153298 604
PSS of inst#2:314 904306 567302 555299 919
PSS of inst#3:314 914311 008308 691
PSS of inst#4:306 563304 495
PSS of inst#5:294 686
Average:339 088314 341308 999305 320301 279

, - :


  • “”
  • , PSS
  • “” , PSS

, . AppCDS. , -XX:SharedArchiveFile=work/classes.jsa -Xshare:off , CDS . , .



:


  • PSS AppCDS CDS.
    . , , HelloWorld- JVM CDS 2 , CDS. PSS CDS, . :


  • PSS AppCDS 2- ; 3- .
    , , , . , AppCDS, , , 3- .
    : , CDS? :


  • CDS/AppCDS JVM , PSS . , , pmap , “” sed '. :


     pmap -X `pgrep -f analog` 14981: # ... Address Perm Offset Device Inode Size Rss Pss ... Mapping # ... ... 7faf5e31a000 r-xp 00000000 08:03 269427 17944 14200 14200 ... libjvm.so # ... ... 7faf5f7f9000 r-xp 00000000 08:03 1447189 1948 1756 25 ... libc-2.27.so 

    ( Mapping ) , “” . JVM ( libjvm.so ), ( libc-2.27.so ). :


    For the Java VM, the read-only parts of the loaded shared libraries (ie libjvm.so ) can be shared between all the VM instances running at the same time. This explains why, taking together, the two VM's consume less memory (ie have a smaller memory footprint) than the simple sum of their single resident set sizes when running alone.


. , , . , , JVM , Java- . GeekOut:



, , , AppCDS , .. Java-. , JVM, , - .


VisualVM Metaspace AppCDS , :


AppCDS



AppCDS



, 128 Metaspace AppCDS 64.2 MiB / 8.96 MiB ≈7,2 , CDS . (. ) 66.4 MiB / 13.9 MiB ≈4,8 . , AppCDS , Metaspace. Metaspace, , CDS .


而不是结论


Spring Boot AppCDS – JVM, .


  • JEP-350 Dynamic CDS Archives – JDK 13.
  • Spring Boot ó CDS ( ). , 100% - 66% . , ≈11% ( 15%, ).
  • , 5- PSS ( ). , AppCDS , , 8% (PSS). , CDS, , . AppCDS .
  • Metaspace, , AppCDS 5 , CDS.

, , AppCDS, , “killer feature”. Spring Boot. , , AppCDS . , , AppCDS Spring Boot. , …


by Nick Fewings on Unsplash

Source: https://habr.com/ru/post/zh-CN472638/


All Articles