热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

毕昇JDK8中AppCDS实现介绍

编者按:笔者通过在Hive的场景发现AppCDS技术存在的价值,然后分析了A

编者按:笔者通过在 Hive 的场景发现 AppCDS 技术存在的价值,然后分析了 AppCDS 的工作原理,并将 JDK 11 中的特性移植到毕昇 JDK 8,在移植过程中由于 JDK 8 和 JDK 11 在类加载实现有所不同,JDK 11 在加载过程中增强了安全性检查,为了达到相同的效果没有对 JDK 8 中的类加载进行修改,而是通过额外的安全检查保证共享类的安全性。最后笔者介绍了如何使用 AppCDS 以及使用的注意事项,希望对读者有所帮助。

基于某产品集群业务中有使用 Hive 场景,我们发现该集群在执行任务时会启动大量的 Java 进程,且进程很快就执行结束。对于这种情况一个有效的解决方法是让 Java 进程之间重用部分公共数据,从而减少 Java 进程产生公共数据的耗时。在 JDK 11 中支持 CDS 和 AppCDS,CDS 的全称是 Class-Data Sharing,CDS 的作用是可以让类被预处理放到一个归档文件中,后续 Java 程序启动的时候可以直接带上这个归档文件,这样 JVM 可以直接将这个归档文件映射到内存中,以节约应用启动的时间。AppCDS 是 CDS 的扩展,不止能够作用于 Boot Class Loader,App Class Loader 和自定义的 Class Loader 也都能够起作用,加大了 CDS 的适用范围 [1]。技术原理是让多个 Java 进程共享类的元数据,其基本原理如下所示:

左右两边分别都是一个 Java 进程,双方通过共享内存达到节约内存、减少类解析的目的。下面通过案例介绍一下我们发现的现象,然后再介绍类共享机制的内部原理。

1. 技术背景

1.1 环境介绍

Hive on tez 集群,4 个节点,一个管理节点,其他三个节点作为工作节点。软件版本:Hadoop 3.1.0,hive 3.1.2,tez-0.9.1,ambari2.6,TPC-DS 标准测试套。硬件环境:Kunpeng 920,128 核,512G 内存。

1.2 系统负载情况

运行 TPC-DS 自带的 sql8,使用 top 观察其中的一个节点的工作负载,发现启动了大量的 JVM 进程。

1.3 热点分布

同时使用 perf 采集整机的热点数据,发现 JVM 的 C1, C2 编译线程占比很高,如下图所示。因此判断 hive sql 的 startup 和 warmup 阶段占比比较高,可以考虑从这两个方向优化。

考虑到目前 JVM 的 C2 代码比较难维护,也很难引进新的 feature。因此考虑看看在启动阶段有没有优化的可能。jdk1.8 带有 cds 功能,能够 share jdk 自带的 java class,能够减少 JVM 的类加载时间,因此首先尝试使用默认的 cds 功能,测试发现效果不明显。原因是 hive sql 执行过程中加载的类大部分是应用程序 classpath 下的类,jdk 自身的类占比很小,因此收益不明显。考虑到 jdk10 以后有了 AppCDS 功能,能够 share classpath 下的 class,因此考虑在 jdk1.8 上面实现 AppCDS 功能。

2. AppCDS 实现

毕昇 JDK 支持 AppCDS,即在原生 CDS 的基础上也支持用户自己程序 classpath 中的类共享。原生 CDS 只支持由 Bootstrap classloader 和 Ext Classloader 加载的 class,即 jdk 自带的 class 在多进程之间共享。但是在实际场景中,尤其是在使用了大量开源软件的情况下,用户指定 classpath 下的 jar 远远大于 jdk 自带的 class,这样 CDS 的实际效果比较有限。

AppCDS 能够扩大 class 共享的范围,从而在节省内存使用以及 JVM 的启动时间两个方面进一步提升效果。

2.1 create class list 文件

java class 文件被 JVM 执行之前首先要被加载进 JVM 内存里面,作为 meta data 存储在 JVM 的 metaspace 内存区域。其中一个关键步骤就是解析 class 文件。JVM 内部有个 ClassFileParser 类,提供了 parseClassFile 方法,解析成功之后会生成一个对应 Klass。AppCDS 的第一步逻辑就是在类被解析完成之后,如果 class 不是匿名类,且 class 文件的 major version 大于 JAVA_1_5 Version,则会把 class 对应的 qualified name 写去到 list 文件中去。

2.2 create jsa 归档文件

在这一步 JVM 会按行读取 appcds.lst 文件中内容,然后使用 App ClassLoader 去加载对应的 class。由于 Java classloader 自身的 delegate 机制,能够确保 jdk 自身的 class 也能够得到加载。Class 被加载之后存在 JVM 的 metaspace 区域,这个 metaspace 是 JVM Native Memory 的一部分,里面包含 klass, ConstantPool, Annotation, ConstMethod, Method, MethodData 等 meta 信息。

当 lst 文件中对应的 class 都被 JVM 加载完成之后,JVM 会把其对应的 metadata 写入到从虚拟地址 0x800000000 开始的内存区域,其格式是 JVM 内部私有的一种表示,然后再把这部分数据 dump 到归档文件中。

2.3 与 JDK 11 实现的差别

与 JDK 11 中的 AppCDS 实现差别主要体现在加载用户指定的 class 逻辑上。当加载用户 class 时,是需要计算获取一个 ProtectionDomain 来做安全验证的。JDK 11 是直接在 hotspot 中使用 JavaCalls 模块构造这个 ProtectionDomain,而毕昇 JDK 8 是在 Java 的 ClassLoader 中提供了一个 getProtectionDomainInternal 函数来获取,并在加载 class 的时候使用 JavaCalls 调用这个 java 方法。

3. AppCDS 使用说明

3.1 创建 lst 文件

启动 JVM 的时候添加 - Xshare:off 和 - XX:+DumpLoadedClassList=/path/to/class.lst 这两个选项。这里比较重要的是第二个选项,这个选项同时支持 %P,可以按进程生成 class.lst 文件,生成的文件名会带上 JVM 进程的 pid。class.lst 本质上是个纯文本文件,里面记录 JVM 运行过程中加载的 class 列表。在 java 程序被 JVM 执行的过程中,会不断地从 classpath 中加载 class,并在 JVM 内部创建对应的 klass 对象。Klass 对象创建成功之后 JVM 会把 class 在 JVM 内部的 qualified name 写入到 class.lst 文件中,比如 java.lang.Objectd 对应的是 java/lang/Object。

使用例子

java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=appcds.lst com.example.App

3.2 创建 archive 文件

第二步:创建 cds 归档文件。使用方式:

java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=appcds.lst -XX:SharedArchiveFile=appcds.jsa com.example.App

3.3 使用 archive 文件

使用示例

java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=appcds.jsa com.example.App

第三步:JVM 在启动的过程中会把 jsa mmap 到内存中,然后在触发类加载时候 JVM 会首先尝试从这块 mmap 进来的内存中去获取 klass 对象,如果获取到则会更新 JVM 内部的 SystemDictionary,以后再来获取对应的 klass 对象则不需要从 jsa 中获取。如果获取失败,JVM 则会走正常的类加载流程,尝试从 classpath 中加载对应的 class 文件,解析,链接,初始化,然后再放入 SystemDictionary 中。

4. AppCDS 效果

TPC-DS 测试套的 10 个 sql 在 5T 数据的场景下测试了一下 AppCDS 的效果,结果收益好,最低是 8.42% 的性能提升,最高是 26.9% 的性能提升。

5. 注意事项

  1. 使用 AppCDS 三部曲过程中要保持 JVM 版本、JAVA_HOME 路径一致,不然会校验失败,无法启动 JVM。
  2. 在第二步加载 class 并创建 jsa 归档文件的时候,JVM 会记录 class 的 fingerprint 信息 (相当于 hash 值),在第三步使用 jsa 中的 class 信息时会校验这个 fingerprint 信息,因此如果业务侧代码发生变更,要重新部署的话,jsa 归档文件要重新制作。
  3. 使用 AppCDS 或者 CDS 的时候不能 debug java 程序。因为 debug 的时候 jvmti 会修改 class 对应的内容,而根据第二点,必然会导致 fingerprint 不一致。
  4. 目前 AppCDS 并不支持 Custom ClassLoader,因此在 tomcat、jetty、SpringBoot 这些使用较多 Custom ClassLoader 场景的,整体收益跟原生 CDS 应该差不多,class 可以 share 的范围仅仅多了一些 Application 自身的 boot class。

后记

如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击阅读原文进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。

参考

[1] https://juejin.cn/post/6844903581246914574



推荐阅读
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 本文介绍了win7系统休眠功能无法启动和关闭的解决方法,包括在控制面板中启用休眠功能、设置系统休眠的时间、通过命令行定时休眠、手动进入休眠状态等方法。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
  • Mono为何能跨平台
    概念JIT编译(JITcompilation),运行时需要代码时,将Microsoft中间语言(MSIL)转换为机器码的编译。CLR(CommonLa ... [详细]
  • C#设计模式之八装饰模式(Decorator Pattern)【结构型】
    一、引言今天我们要讲【结构型】设计模式的第三个模式,该模式是【装饰模式】,英文名称:DecoratorPattern。我第一次看到这个名称想到的是另外一个词语“装修”,我就说说我对“装修”的理 ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 本文比较了eBPF和WebAssembly作为云原生VM的特点和应用领域。eBPF作为运行在Linux内核中的轻量级代码执行沙箱,适用于网络或安全相关的任务;而WebAssembly作为图灵完备的语言,在商业应用中具有优势。同时,介绍了WebAssembly在Linux内核中运行的尝试以及基于LLVM的云原生WebAssembly编译器WasmEdge Runtime的案例,展示了WebAssembly作为原生应用程序的潜力。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • Harmony 与 Game Space 达成合作,在 Shard1 上扩展 Web3 游戏
    旧金山20 ... [详细]
  • 【技术分享】一个 ELF 蠕虫分析
    【技术分享】一个 ELF 蠕虫分析 ... [详细]
  • 如何搭建服务器环境php(2023年最新解答)
    导读:本篇文章编程笔记来给大家介绍有关如何搭建服务器环境php的相关内容,希望对大家有所帮助,一起来看看吧。本文目录一览:1、怎么搭建p ... [详细]
  • 问题描述:域名已经备案,我全部都有,也在后台配置了,但是手机预览,还是请求失败,PC端是可以请求 ... [详细]
author-avatar
吴燕凤怡雨家琴
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有