一 AppCDS
CDS的全称是Class Data Sharing,直译为类数据共享。其作用是在内存或者磁盘上缓存预先处理过的类元数据,在启动时提高应用加载速度,并可以减少内存占用。
这个特性是从Java5开始就有了,最初仅对客户端VM有效,只能应用于系统类和串行GC上。
Java9发布时功能得到了增强,可以用在服务端VM上,也能支持各种GC,包括G1,并行GC等。在OracleJD9K中,还可以通过设置启用商业特性,支持应用类。也就是说,在OpenJdk中以及OracleJdk未做设置时,还只对系统类有效,这样极大的限制了这个特性的应用。
JDK10发布时,这个功能被彻底解锁。通过JEP310实现这个功能AppCDS(Application Class-Data Sharing),这样应用层的类也完全可以用这个特性了。这个特性被称为Oracle第一个开源的商业特性。
1. 运行范例
我们创建一个最简单Java程序Hello.java,并编译
class Hello {
public static void main (String[] args) {
System.out.println("Hello Gongzhonghao");
}
}
使用如下命令,dump出启动时加载的类清单
$ x1/java/javalib/jdk-11/bin/java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst Hello
OpenJDK 64-Bit Server VM warning: Ignoring obsolete option UseAppCDS; AppCDS is automatically enabled
Hello Gongzhonghao
可以看到AppCDS这个特性已经在Java11中自动可用了,后续我们不再专门设置这个参数
生成的hello.lst文本文件有470行,加载了最常用的Java类清单
$ wc hello.lst
470 470 14599 hello.lst
$ head hello.lst
java/lang/Object
java/lang/String
java/io/Serializable
java/lang/Comparable
....
我们进一步dump出AppCDS的Archive,可以看作是加载后类在内存中的镜像文件
$ x1/java/javalib/jdk-11/bin/java -Xshare:dump -XX:SharedClassListFile=hello.lst -XX:SharedArchiveFile=hello.jsa Hello
narrow_klass_base = 0x0000000800000000, narrow_klass_shift = 3
Allocated temporary class space: 1073741824 bytes at 0x00000008c0000000
Allocated shared space: 3221225472 bytes at 0x0000000800000000
Loading classes to share ...
Loading classes to share: done.
Rewriting and linking classes ...
Rewriting and linking classes: done
Number of classes 535
instance classes = 470
obj array classes = 57
type array classes = 8
...
Dumping symbol table ...
Dumping objects to closed archive heap region ...
Dumping objects to open archive heap region ...
...
如果我们查看Jdk的Hotspot源码,比如在metaspaceShared.cpp中
if (SharedClassListFile == NULL) {
...
class_list_path = class_list_path_str;
} else {
class_list_path = SharedClassListFile; 使用已经存储的CDS list
}
tty->print_cr("Loading classes to share ...");
_has_error_classes = false;
int class_count = preload_classes(class_list_path, THREAD);
if (ExtraSharedClassListFile) {
class_count += preload_classes(ExtraSharedClassListFile, THREAD);
}
tty->print_cr("Loading classes to share: done.");
log_info(cds)("Shared spaces: preloaded %d classes", class_count);
tty->print_cr("Rewriting and linking classes ..."); Rewrite and link classes
link_and_cleanup_shared_classes(CATCH);
tty->print_cr("Rewriting and linking classes: done");
SystemDictionary::clear_invoke_method_table();
VM_PopulateDumpSharedSpace op;
VMThread::execute(&op);
可以看到这个dump过程的实现片段,hello.jsa文件就是存储内存的信息,包括符号表,堆信息等。
后续再次启动这个类时,我们就可以直接使用hello.jsa文件来快速构建加载类和内存信息了
$ x1/java/javalib/jdk-11/bin/java -Xshare:on -XX:SharedArchiveFile=hello.jsa Hello
Hello Gongzhonghao
可以进行运行时长的对比
$ time x1/java/javalib/jdk-11/bin/java Hello
Hello Gongzhonghao
real 0m0.303s
user 0m0.142s
sys 0m0.040s
$ time x1/java/javalib/jdk-11/bin/java -Xshare:on -XX:SharedArchiveFile=hello.jsa Hello
Hello Gongzhonghao
real 0m0.103s
user 0m0.116s
sys 0m0.027s
对于大型应用软件,这个性能优化就很可观了。
2. JDK11目前的能力
在OpenJdk的lib目录下,有一个文件classlist,这个文件就是默认情况下,JDK可以预处理的类。如果我们执行
$ x1/java/javalib/jdk-11/bin/java -Xshare:dump Hello
而不带加载类的清单,则JDK就使用classlist,并把dump出来的classes.jsa文件放在lib/server目录下。因为Java11以及缺省使用AppCDS特性,则后续只要使用
$ x1/java/javalib/jdk-11/bin/java -Xshare:on Hello
Hello Gongzhonghao
就可以,这样对于采用AOT生成的可执行运行环境来说非常方便。
如果要加载的类不在classlist清单上,比如应用自身的类肯定都不在这个列表,就需要采用-XX:DumpLoadedClassList来先生成类加载列表。
不过目前有一个不足就是-XX:DumpLoadedClassList无法列出自己定义的ClassLoader加载的类,而目前主流的框架几乎都使用自行定义的扩展ClassLoader进行加载。
怎么处理这个问题呢?就要通过JDK的另一个新优特性Unified JVM Logging来帮助处理。
$ x1/java/javalib/jdk-11/bin/java -Xshare:on -Xlog:class+load Hello | head
[0.003s][info][class,load] opened: /x1/java/javalib/jdk-11/lib/modules
[0.018s][info][class,load] java.lang.Object source: shared objects file
[0.018s][info][class,load] java.io.Serializable source: shared objects file
[0.018s][info][class,load] java.lang.Comparable source: shared objects file
...
使用-Xlog:class+load,告诉JVM虚拟机打印出类加载的信息,我们会发现绝大多数类已经在shared object file中了。
$ /x1/java/javalib/jdk-11/bin/java -Xshare:on -Xlog:class+load Hello | grep -v "shared object"
[0.004s][info][class,load] opened: /x1/java/javalib/jdk-11/lib/modules
[0.072s][info][class,load] jdk.internal.loader.URLClassPath$FileLoader source: jrt:/java.base
[0.072s][info][class,load] jdk.internal.loader.URLClassPath$FileLoader$1 source: jrt:/java.base
[0.077s][info][class,load] Hello source: file:/tmp/
Hello Gongzhonghao
只有少数类,是没有在classlist定义,以及我们自己的应用类。
这样给我们的启示,就是我们可以通过-Xlog:class+load,来打印出所有加载类信息
然后使用一个开源工具cl4cds,通过类加载信息,来重新推导出cls文件,这样就得到全面的加载的类清单文件,再进行内存的dump操作。
$ /x1/java/javalib/jdk-11/bin/java -Xshare:on -Xlog:class+load:file=hello.clstrace Hello
$ /x1/java/javalib/jdk-11/bin/java io.simonis.cl4cds hello.clstrace hello_cl4cds.cls
在大型应用,如应用服务器启动时,使用AppCDS特性,可以使启动速度有20%左右的提升。
二 Unified JVM Logging
上面我们提到了Unified JVM Logging(统一JVM日志),这个功能是在Java9通过JEP158加入的。用来统一为JVM中所有的组件打印日志。
比如之前打印GC信息-XX:+PrintGC,如今可以用-Xlog:gc来代替。
借用https://blog.codefx.org/java/unified-logging-with-the-xlog-option/ 一副图来说明
-Xlog:<selectors>:<output>:<decorators>:<output-options>
从几个方面来打印日志
Tag标签来表示组件,比如gc,class+load,compiler,thread等等,也可以进行组合
Level层级表示日志等级。分为error, warning, info, debug, trace and develop
Output表示输入方式,分别是stdout控制台输出,stderr错误输出,file文本文件
Decoration定义输出的内容,包括time当前时间,uptime启动时长(可以用毫秒和纳秒表示),pid进程标示,tid线程标示等等
通过以上几个维度的组合定义,我们就可以得到非常详尽的JVM日志信息。对于我们调试程序,排查错误非常有帮助。