一、类加载
(一)TraceClassLoading
TraceClassLoading参数可以显示JVM从进程开始到运行结束的时候,所有ClassLoad的相关信息。在JDK8上,用“-XX:+ TraceClassLoading”就可以显示,在JDK11上的话,要加上 “-Xlog: class+load=info”。
下方是JDK11上打出来的一些日志,可以看到时间,类,还有类从哪个模块里来,信息非常详细。
(二)类加载与虚拟机
关于类加载部分,首先用户有Java文件,然后Java文件用Java c去编译就可以得到.class文件,接着虚拟机会加载.class文件变成虚拟机的元数据。比如在c++里边会变成Klass *,Method *,ConstantPool * 等,这些都是Java虚拟机里元数据的描述。
比如一个Class会变成一个Klass*的结构体,这个Class里面所有方法会变成虚拟机里面Method*的结构体,然后常量池会被包装成一个ConstantPool*,这些在虚拟机里都有相关描述。
(三)ClassFile
上图为ClassFile的结构,它的反汇编是Java.lang.string。
如果用户想构造一个String,就必须要传给它一个字符串的自变量,自变量会被传到Value的数组里。可以看到,在JDK11当中Value是用Stable Annotation修饰的。
和上图对比,可以发现Private Final以及Byte的数组全都被很好地描述在Java p反汇编的Class文件中, Stable annotation被描述在ClassFile里。
我们来看一个例子。
rangeCheck是String里边的一个Static方法,这个方法有三个参数value、offset和count,它内部会调用一个static的方法,并且返回null。
对照上方的Java p反汇编的class文件,反汇编的文件分为三个部分,第一个部分是Code,第二个部分是LineNumberTable,以及LocalVariableTable。
Code当中iload_1,iload_2以及aload_10都是字节码,可以看到LineNumberTable里的第280行对应的0,这个0是上面Code的第0行,也就是iload_1。下面的line 281行的7对应的是aconst_null字节码。
LocalVariableTable的Start、Length对应的都是字节码的位置,后面还有名字等信息。
例如value这个变量是从第0号字节码,它的生命周期一直从0号到第9位字节码,第9位是左开右闭区间,因此不包括第9号字节码。可以看到,所有的信息都会被完整保存在ClassFile里。
可以看到,上图所示的Annotations类上面有无数的注解,例如IA、IB、IC,它们都是Annotations的定义, Annotations可以插在程序的各个地方,这张图只是为了一个直观的表示,然后来看一下Annotations是怎么样被Incode进ClassFile里面的,可以直观对比下图的变量。
(四)ClassLoader结构
Class这些元数据在JVM当中是如何被表示的?
假设有一个ClassLoader正在Loader一些类,然后把它们Load进虚拟机当中。JVM当中有一个结构体叫做SystemDictionary,它是一个Meta,会把Class的类名Meta到Class的Pointer当中,然后Pointer指向的就是Metaspace当中真正的Class结构描述。
Class当中有一些Mirror的字段,这些Mirror指向java.lang.Class。Mirror和上层的.class是一样的,是一个反射接口的作用。
可以看到,ClassLoader会索引到SystemDictionary,然后索引到Metaspace Chunk,接着索引到Heap,这几个可以互相引用。
图中Metaspace Chunk的Klass以及Heap里的java.lang.Class图形大小是不同的。因为用户自己写的Class有可能会继承自不同的父类以及不同的接口,它有可能实现了若干个父类和接口,实现接口和父类的数量有所不同, Class里的东西也是不尽相同,因此元数据的大小也是不一样的。
(五)双亲委派机制
Java的ClassLoader有双亲委派机制,先使用双亲类加载器进行加载,当 Parent加载失败的时候,再自己加载。
Bootstrap Class Loader、Extension Class Loader和System Class Loader(即APP Class Loader)这三个Class Loader是父子的关系。如果先从APP Class Loader加载用户的命令Class,会先去Extension Class Loader加载,然后去Bootstrap Class Loader加载,如果它们都没有加载到,最后才会轮到System Class Loader加载。
所有User Defined Class Loader的Parent基本都是System Class Loader,用户可以选择自己是否要写一个新的Class Loader。
LoadClass类是ClassLoader内部一个非常通用性的类,如果要重写一个ClassLoader的话,会选择重写里面的findLoadedClass这个方法,而不会选择LoadClass。
如上图所示,首先是一个synchronized,加上get ClassLoadingLock的同步锁。它下面会先调用一个findLoadedClass,这个函数会去SystemDictionary里去找到相应的类。如果它没有,那么就会到Parent中loadClass,如果Parent里也没有,就会到findClass的方法。
(六)破坏双亲委派机制
Ø Tomcat ClassLoader 为例,它会经过以下过程:
1)在本地 ResourceEntry 当中查找
2)调用 ClassLoader.findLoadedClass()
3)默认情况下调用 AppClassLoader.loadClass()
4)用自身加载
5)依旧没有加载出来的情况,最后才委派给Parent
Ø 意义:可以实现一个 VM 进程下加载不同版本的 jar 包
(七)ParallelCapable
从JDK1.7开始,ClassLoader引入了一个叫ParallelCapable的特性。
之前的JDK当一个ClassLoader在LoadClass的时候,它会锁ClassLoader,锁的粒度是整个ClassLoader。在1.7引入了ParallelCapable特性之后,锁的粒度变成了Class,大幅提高ClassLoader的性能。
先ClassLoader在loadClass 时同步整个 loader 对象,现在把锁变成了单个类名对应的Placeholder。如果要Load一个Class,检查类名就可以找到相应的Placeholder。
下面我们来看一下它到底是怎么实现的。
如上图所示,第一行的关键字synchronized锁住了getClassLoadingLock。这个方法会从非权限命名所对应的Object的Map里边搜索到它对应的Placeholder,也就是占位符,它只要锁住了占位符,后面的过程就全是进程安全了。
下面我们来看一下虚拟机里面的实现。
DoObject变量决定了当前的ClassLoader是否要锁住整个ClassLoader来加载一个类。如果是true,就会去锁住整个ClassLoader。如果它是false的话,它就会像刚才一样做synchronized操作,synchronized锁住的是它加载的类对应的名字所对应的Placeholder。这样的话它就把C++层锁住整个ClassLoader的代价,转移到了Java层,去锁住Class。
二、链接
Ø 链接的过程如下:
1)先递归地对所有父类和接口进行链接操作;
2)verify 当前类;
3)rewrite 当前类:
n 比如会把 java.lang.Object.<init> 构造函数的 _return 字节码重写为 _return_register_finalizer 字节码;
n 比如 _lookupswitch 这种不连续的 switch,跳转分支数在 BinarySwitchThreshold (default 5) 以下会被重写成_fast_linearswitch 字节码,否则会变成 _fast_binaryswitch 字节码;
n 比如 _aload_0 + _getfield (integer) 的组合最终会被 rewrite 成 _fast_iaccess_0 字节码
4)对类内部的所有方法进行链接操作,使其生效(设置方法执行的入口为解释器的入口)。
三、初始化
(一)初始化操作
在虚拟机规范当中,我们可以看到这样的描述:
1)在_new/_getstatic/_putstatic/_invokestatic字节码时/反射/lambda解析发现callee是一个static 函数时触发;
2)调用 class 的 <clinit> 方法;
3)实例化。
我们写Java程序的时候用的Static变量,在虚拟机内部会转化成一个叫<clinit>的方法,然后实例化。如果用反射去New一个Object,或者是走New字节码的时候,都会进行初始化的操作。
上图是一个<clinit>的方法,截取的是java.lang.Object的Static块,它只有一条的代码。
(二)编写自己的 ClassLoader
Ø 方法:
1)按照 ClassLoader.loadClass() 的模板来重写(不推荐);
2)仅重写 findClass() 方法,拿到并解析.class 文件为一个 byte[] 数组,并调用 defineClass()方法进入VM。
(三)Class Unloading
Ø JDK8与JDK11中都有-XX:+ClassUnloading (default true)
Ø Class Unloading发生在当一个类不被任何引用所引用时,就可以被unload掉
1)一个类被加载的时候,会产生 ClassLoader -> Class 的引用,因此 ClassLoader 自身需要先不被任何引用所引用
2)其他GC roots无对此类的引用,比如栈帧等
(四)向JDK11迁移
Ø JDK8和JDK11中JDK library中的ClassLoader有所不同
1)ExtClassLoader 更名为了PlatformClassLoader;
2)PlatformClassLoader和AppClassLoader不再继承自URLClassLoader;
3)如果指定了 -Djava.ext.dirs 这个变量,需要用-classpath 来加以替代;
4)如果指定了-Djava.endorsed.dirs来覆盖JDK内部的API,需要删掉参数。
(五)AppCDS (APPlication Class Data Sharing)
Ø AppCDS是OpenJDK做的一个特性,它有以下特点:
1)用程序加载的classes 产生 *.jsa 文件 (shared archive),给应用的启动进行加速;
2)JDK 1.5 时为 CDS,只能用dump BootstrapClassLoader 加载的类;
3)JDK10后变为AppCDS,可以用于AppClassLoader和custom ClassLoaders。
Ø AppCDS本质是动态分析流程,使用步骤如下:
1)第一次:java -Xshare:off -XX:DumpLoadedClassList=list.log <app>
2)第二次:java -Xshare:dump -XX:SharedClassListFile=list.log XX:SharedArchiveFile=dump.jsa <app>
3)第三次:java -Xshare:on -XX:SharedArchiveFile=dump.jsa <app>
JDK 在build 的时候,会使用Java加上AppCDS的参数自动产生一份.jsa 文件来加速启动,放在 ${JAVA_HOME}/lib/server 下,会什么参数都不加,裸跑一个.jsa 文件,产生的文件叫classes.jsa,用户搜自己JDK11的目录都可以搜到。