天天看点

开发者学堂课程干货总结——Java 虚拟机原理(三)

各位同学,开发者学堂Java 图谱中Java 高级工程师篇的课程“Java 虚拟机原理”的课程给开始更新了,第三课时“类加载器原理”的干货总结来啦!一起学习新课程吧!

课程链接以及图谱地址小编已经为大家指路了,搭配学习效果更佳👇

课程名称:类加载器原理

课程地址:

https://developer.aliyun.com/learning/course/56/detail/1066

图谱名称:Alibaba Java 技术图谱

图谱地址:

https://developer.aliyun.com/graph/java

类加载器原理

一、类加载

(一)TraceClassLoading

TraceClassLoading参数可以显示JVM从进程开始到运行结束的时候,所有ClassLoad的相关信息。在JDK8上,用“-XX:+ TraceClassLoading”就可以显示,在JDK11上的话,要加上 “-Xlog: class+load=info”。

下方是JDK11上打出来的一些日志,可以看到时间,类,还有类从哪个模块里来,信息非常详细。

开发者学堂课程干货总结——Java 虚拟机原理(三)

(二)类加载与虚拟机

关于类加载部分,首先用户有Java文件,然后Java文件用Java c去编译就可以得到.class文件,接着虚拟机会加载.class文件变成虚拟机的元数据。比如在c++里边会变成Klass *, Method *,ConstantPool * 等,这些都是Java虚拟机里元数据的描述。

比如一个Class会变成一个Klass*的结构体,这个Class里面所有方法会变成虚拟机里面Method*的结构体,然后常量池会被包装成一个ConstantPool*,这些在虚拟机里都有相关描述。

(三)ClassFile

开发者学堂课程干货总结——Java 虚拟机原理(三)

上图为ClassFile的结构,它的反汇编是Java.lang.string。

如果用户想构造一个String,就必须要传给它一个字符串的自变量,自变量会被传到Value的数组里。可以看到,在JDK11当中Value是用Stable Annotation修饰的。

开发者学堂课程干货总结——Java 虚拟机原理(三)

和上图对比,可以发现Private Final以及Byte的数组全都被很好地描述在Java p反汇编的Class文件中, Stable annotation被描述在ClassFile里。

我们来看一个例子。

开发者学堂课程干货总结——Java 虚拟机原理(三)

rangeCheck是String里边的一个Static方法,这个方法有三个参数value、offset和count,它内部会调用一个static的方法,并且返回null。

开发者学堂课程干货总结——Java 虚拟机原理(三)

对照上方的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里。

开发者学堂课程干货总结——Java 虚拟机原理(三)

可以看到,上图所示的Annotations类上面有无数的注解,例如IA、IB、IC,它们都是Annotations的定义, Annotations可以插在程序的各个地方,这张图只是为了一个直观的表示,然后来看一下Annotations是怎么样被Incode进ClassFile里面的,可以直观对比下图的变量。

开发者学堂课程干货总结——Java 虚拟机原理(三)

(四)ClassLoader结构

开发者学堂课程干货总结——Java 虚拟机原理(三)

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 虚拟机原理(三)

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。

开发者学堂课程干货总结——Java 虚拟机原理(三)

如上图所示,首先是一个synchronized,加上get ClassLoadingLock的同步锁。它下面会先调用一个findLoadedClass,这个函数会去SystemDictionary里去找到相应的类。如果它没有,那么就会到Parent中loadClass,如果Parent里也没有,就会到findClass的方法。

(六)破坏双亲委派机制

开发者学堂课程干货总结——Java 虚拟机原理(三)

> 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。

下面我们来看一下它到底是怎么实现的。

开发者学堂课程干货总结——Java 虚拟机原理(三)

如上图所示,第一行的关键字synchronized锁住了getClassLoadingLock。这个方法会从非权限命名所对应的Object的Map里边搜索到它对应的Placeholder,也就是占位符,它只要锁住了占位符,后面的过程就全是进程安全了。

下面我们来看一下虚拟机里面的实现。

开发者学堂课程干货总结——Java 虚拟机原理(三)

DoObject变量决定了当前的ClassLoader是否要锁住整个ClassLoader来加载一个类。如果是true,就会去锁住整个ClassLoader。如果它是false的话,它就会像刚才一样做synchronized操作,synchronized锁住的是它加载的类对应的名字所对应的Placeholder。这样的话它就把C++层锁住整个ClassLoader的代价,转移到了Java层,去锁住Class。

二、链接

> 链接的过程如下:

1)先递归地对所有父类和接口进行链接操作;

2)verify 当前类;

3)rewrite 当前类:

* 比如会把 java.lang.Object. 构造函数的 _return 字节码重写为 _return_register_finalizer 字节码;

开发者学堂课程干货总结——Java 虚拟机原理(三)

* 比如 _lookupswitch 这种不连续的 switch,跳转分支数在 BinarySwitchThreshold (default 5) 以下会被重写成 _fast_linearswitch 字节码,否则会变成 _fast_binaryswitch 字节码;

开发者学堂课程干货总结——Java 虚拟机原理(三)
开发者学堂课程干货总结——Java 虚拟机原理(三)

* 比如 _aload_0 + _getfield (integer) 的组合最终会被 rewrite 成 _fast_iaccess_0 字节码

4)对类内部的所有方法进行链接操作,使其生效(设置方法执行的入口为解释器的入口)。

三、初始化

(一)初始化操作

在虚拟机规范当中,我们可以看到这样的描述:

1)在_new/_getstatic/_putstatic/_invokestatic字节码时/反射/lambda解析发现callee是一个static 函数时触发;

2)调用 class 的 方法;

3)实例化。

开发者学堂课程干货总结——Java 虚拟机原理(三)

我们写Java程序的时候用的Static变量,在虚拟机内部会转化成一个叫的方法,然后实例化。如果用反射去New一个Object,或者是走New字节码的时候,都会进行初始化的操作。

上图是一个的方法,截取的是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)> Ap

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

2) 第二次:java -Xshare:dump -XX:SharedClassListFile=list.log XX:SharedArchiveFile=dump.jsa

3) 第三次:java -Xshare:on -XX:SharedArchiveFile=dump.jsa

JDK 在 build 的时候,会使用Java加上AppCDS的参数自动产生一份.jsa 文件来加速启动,放在 ${JAVA_HOME}/lib/server 下,会什么参数都不加,裸跑一个.jsa 文件,产生的文件叫classes.jsa,用户搜自己JDK11的目录都可以搜到。