1. AppCDS
1) 现状
CDS(Class Data Sharing)在Oracle JDK1.5被首次引入,在Oracle JDK8u40中引入了AppCDS,支持JDK以外的类,但是作为商业特性提供。随后Oracle将AppCDS贡献给了社区,在JDK10中CDS逐渐完善,也支持了用户自定义类加载器(又称AppCDS v2)。
目前CDS在阿里的落地情况:
• 热点应用A使用CDS减少了10秒启动时间。
• 云产品SAE和FC在使用Dragonwell11时开启CDS、AOT等特性加速启动。
经过十年的发展,CDS已经发展为一项成熟的技术。但是很容易令人不解的是CDS不管在阿里的业务还是业界(即便是AWS Lambda)都没能被大规模使用。关键原因有两个。
a) AppCDS在实践中效果不明显
jsa中存储的InstanceKlass是对class文件解析的产物。对于boot classloader(加载jre/lib/rt.jar下面的类的类加载器)和system(app)类加载器(加载-classpath下面的类的类加载器),CDS有内部机制可以跳过对class文件的读取,仅仅通过类名在jsa文件中匹配对应的数据结构。
Java语言还提供用户自定义类加载器(custom class loader)的机制,用户通过Override自己的Classloader.loadClass()查找类,AppCDS在为customer class loade时加载类是需要经过如下步骤:
• 调用用户定义的Classloader.loadClass(),拿到class byte stream
• 计算class byte stream的checksum,与jsa中的同类名结构的checksum比较
• 如果匹配成功则返回jsa中的InstanceKlass,否则继续使用slow path解析class文件
b) 工程实践不友好
使用AppCDS需要如下步骤:
• 针对当前版本在生产环境启动应用,收集profiling信息
• 基于profiling信息生成jsa(java sahred archive)dump
• 将jsa文件和应用本身打包在一起,发布到生产环境
由于这种trace-replay模式的复杂性,在SAE和FC云产品的落地都是通过发布流程的定制以及开发复杂的命令行工具来解决的。
2) 解决方案
针对上述的问题1,在热点应用A上CDS配合JarIndex或者使用编译器团队开发的EagerAppCDS特性(原理见5.1.3.1)都能让CDS发挥最佳效果。
经验证,在热点应用A已经使用JarIndex做优化的前提下进一步使用EagerAppCDS依然可以获得15秒左右的启动加速效果。
3) 原理
面向对象语言将对象(数据)和方法(对象上的操作)绑定到了一起,来提供更强的封装性和多态。这些特性都依赖对象头中的类型信息来实现,Java、Python语言都是如此。Java对象在内存中的layout如下:
+-------------+ | mark | +-------------+ | Klass* | +-------------+ | fields | | | +-------------+
mark表示了对象的状态,包括是否被加锁、GC年龄等等。而Klass*指向了描述对象类型的数据结构InstanceKlass:
/ InstanceKlass layout: // [C++ vtbl pointer ] Klass // [java mirror ] Klass // [super ] Klass // [access_flags ] Klass // [name ] Klass // [methods ] // [fields ] ...
基于这个结构,诸如o instanceof String这样的表达式就可以有足够的信息判断了。要注意的是InstanceKlass结构比较复杂,包含了类的所有方法、field等等,方法又包含了字节码等信息。这个数据结构是通过运行时解析class文件获得的,为了保证安全性,解析class时还需要校验字节码的合法性(非通过javac产生的方法字节码很容易引起jvm crash)。
CDS可以将这个解析、校验产生的数据结构存储(dump)到文件,在下一次运行时重复使用。这个dump产物叫做Shared Archive,以jsa后缀(java shared archive)。
为了减少CDS读取jsa dump的开销,避免将数据反序列化到InstanceKlass的开销,jsa文件中的存储layout和InstanceKlass对象完全一样,这样在使用jsa数据时,只需要将jsa文件映射到内存,并且让对象头中的类型指针指向这块内存地址即可,十分高效。
Object: +-------------+ | mark | +-------------------------+ +-------------+ |classes.jsa file | | Klass* +--------->java_mirror|super|methods| +-------------+ |java_mirror|super|methods| | fields | |java_mirror|super|methods| | | +-------------------------+ +-------------+
a) Alibaba Dragonwell对AppCDS的优化
上述AppCDS for custom classloader的加载流程更加复杂的原因是JVM通过(classloader,className)二元组来唯一确定一个类。
• 对于BootClassloader、AppClassloader在每次运行都是唯一的,因此可以在多次运行之间确定唯一的身份。
• 对于customClassloader除了类型,并没有明显的唯一标识。AppCDS因此无法在加载类阶段通过classloader对象和类型去shared archive定位到需要的InstanceKlass条目。
Dragonwell提供的解决方法是让用户为customClassloader标识唯一的identifier,加载相同类的classloader在多次运行间保持唯一的identifier。并且扩展了shared archive,记录用户定义的classloader identifier字段,这样AppCDS便可以在运行时通过(identifier,className)二元组来迅速定位到shared archive中的类条目。从而让custom classloader下的类加载能和buildin class一样快。
在常见的微服务workload下,我们可以看到Dragonwell优化后的AppCDS将基础的AppCDS的加速效果从10%提升到了40%。
2. 启动profiling工具
1) 现状
目前有很多Java性能剖析工具,但专门用于Java启动过程分析的还没有。不过有些现有的工具,可以间接用于启动过程分析,由于不是专门的工具,每个都存在这样那样的不足。
比如async-profiler,其强项是适合诊断CPU热点、墙钟热点、内存分配热点、JVM内锁争抢等场景,展现形式是火焰图。可以在应用刚刚启动后,马上开启aync-profiler,持续剖析直到应用启动完成。async-profiler的CPU热点和墙钟热点能力对于分析启动过程有很大帮助,可以找到占用CPU较多的方法,进而指导启动加速的优化。
async-profiler有2个主要缺点:
• 第1个是展现形式较单一,关联分析能力较弱,比如无法选择特定时间区间,也无法支持选中多线程场景下的火焰图聚合等。
• 第2个是采集的数据种类较少,看不到类加载、GC、文件IO、SocketIO、编译、VM Operation等方面的数据,没法做精细的分析。
再比如arthas,arthas的火焰图底层也是利用async-profiler,所以async-profiler存在的问题也无法回避。
最后我们自然会想到OpenJDK的JDK Flight Recorder,简称JFR。AJDK8.5.10+和AJDK11支持JFR。JFR是JVM内置的诊断工具,类似飞机上的黑匣子,可以低开销的记录很多关键数据,存储到特定格式的JFR文件中,用这些数据可以很方便的还原应用启动过程,从而指导启动优化。JFR的缺点是有一定的使用门槛,需要对虚拟机有一定的理解,高级配置也较复杂,同时还需要搭配桌面软件Java Mission Control才能解析和阅读JFR文件。
面对上述问题,JVM工具团队进行了深入的思考,并逐步迭代开发出了针对启动过程分析的技术产品。
2) 解决方案
我们选择JFR作为应用启动性能剖析的基础工具。JFR开销低,内建在JDK中无第三方依赖,且数据丰富。JFR会周期性记录Running状态的线程的栈,可以构建CPU热点火焰图。JFR也记录了类加载、GC、文件IO、SocketIO、编译、VM Operation、Lock等事件,可以回溯线程的关键活动。对于早期版本JFR可能存在性能问题的特性,我们也支持自动切换到aync-profiler以更低开销实现相同功能。
为了降低JFR的使用门槛,我们封装了一个javaagent,通过在启动命令中增加javaagent参数,即可快速使用JFR。我们在javaagent中内置了文件收集和上传功能,打通数据收集、上传、分析和交互等关键环节,实现开箱即用。
我们开发了一个Web版本的分析器(或者平台),它接收到javaagent收集上传的数据后,便可以直接查看和分析。我们开发了功能更丰富和易用的火焰图和线程活动图。在类加载和资源文件加载方面我们也做了专门的分析,类似URLClassLoader在大量Jar包场景下的Class Loading开销大、Tomcat的WebAppClassLoader在大量jar包场景下getResource开销大、并发控制不合理导致锁争抢线程等待等问题都变得显而易见,未来还将提供评估开启CDS(Class Data Sharing)以及JarIndex后可以节省时间的预估能力。
3) 原理
当Oracle在OpenJDK11上开源了JDK Flight Recorder之后,阿里巴巴也是作为主要的贡献者,与社区包括RedHat等,一起将JFR移植到了OpenJDK 8。
JFR是OpenJDK内置的低开销的监控和性能剖析工具,它深度集成在了虚拟机各个角落。
JFR由两个部分组成:
• 第1个部分分布在虚拟机的各个关键路径上,负责捕获信息。
• 第2个部分是虚拟机内的单独模块,负责接收和存储第1个部分产生的数据。
这些数据通常也叫做事件。JFR包含160种以上的事件。JFR的事件包含了很多有用的上下文信息以及时间戳。比如文件访问,特定GC阶段的发生,或者特定GC阶段的耗时,相关的关键信息都被记录到事件中。
尽管JFR事件在他们发生时被创建,但JFR并不会实时的把事件数据存到硬盘上,JFR会将事件数据保存在线程变量缓存中,这些缓存中的数据随后会被转移到一个global ring buffer。当global ring buffer写满时,才会被一个周期性的线程持久化到磁盘。
虽然JFR本身比较复杂,但它被设计为低CPU和内存占用,总体开销非常低,大约1%甚至更低。所以JFR适合用于生产环境,这一点和很多其它工具不同,他们的开销一般都比JFR大。
JFR不仅仅用于监控虚拟机自身,它也允许在应用层自定义事件,让应用程序开发者可以方便的使用JFR的基础能力。
有些类库没有预埋JFR事件,也不方便直接修改源代码,我们则用javaagent机制,在类加载过程中,直接用ASM修改字节码插入JFR事件记录的能力。比如Tomcat的WebAppClassLoader,为了记录getResource事件,我们就采用了这个方法。
整个系统的结构如下: