类加载机制总结

1,323 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

首先我们需要明确,JVM是动态加载class类的,而不是一次性加载程序中所有的class文件(延迟加载机制)。当我们需要这个类的时候,比如说程序在运行过程中classA需要调用另一个classB的方法,但classB没有被加载,这时候就需要通过类加载机制动态加载classB到内存中,只有当classB被加载到内存中,它的方法才能被classA调用。这种动态加载的方式虽然让Java进行提前编译的时候面临额外的困难,而且让类加载的时候产生额外的性能开销,但是为Java应用提供了极高的拓展性和灵活性。Java天生可以动态拓展的语言特性就是依赖于运行期间动态加载和动态连接这个特点。

类加载时机

Java虚拟机规范没有规定类加载时机,只规定了初始化的五种场景

  1. 实例化对象、读写类静态字段、调用静态方法的时候
  2. 使用反射调用的时候
  3. 初始化类时,需要先触发父类的初始化
  4. 虚拟机启动时,会先初始化包含 main() 方法的主类
  5. 使用JDK1.7 的动态语言支持的时候,如果 java.lang.invoke.MethodHandle 实例最后的解析结果REF_getStatic 、REF_putStatic、REF_invokeStatic的方法句柄,句柄对应的类会被初始化
  6. 当一个接口中定义了JDK8新加入的默认方法时,如果有这个接口的实现类发生初始化,那么该接口要在其之前被初始化

有且只有这五种情况会发生类加载,也就是主动引用,所有引用类的方式都不会触发初始化,叫做被动引用。

接口加载时机

接口也有初始化过程,但是接口中不能使用static 语句块,但是编译器仍然会为接口生成 类构造器初始化接口中定义的成员变量。 接口与类真正的区别是初始化场景中的第三种情况:当一类在初始化的时候,会要求其所有的父类都初始化完成,但是接口在初始化的时候,并不要求其父接口全部完成了初始化,只有在真正用到了父接口的时候(如引用接口中定义的常量)才会初始化。

类加载:通过加载,连接和初始化三个过程将class文件中的信息加载到JVM内存中。

注意点

第一,这里面的class文件应当指的是二进制字节流,包括但不限于磁盘文件、网络、数据库、内存或者动态产生等。

第二,关于类加载过程中这些过程的顺序:加载,校验,转换和初始化的先后顺序不会改变,解析可能会出现在初始化以后,这样做的原因是为了Java语言的运行时绑定特性(动态绑定特性)。

第三,加载和连接的部分内容是交叉进行的,加载尚未完成,连接可能已经开始了

底物,数组是直接生成的而不是由类加载实现的

loading

内容

总的来讲就是查找字节流,并生成类的对象。

具体来讲,主要完成三件事

  1. 通过全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。在堆中生成一个class对象

详细介绍

1、通过全限定名来获取定义此类的二进制字节流。

怎么找呢?在系统类和指定的类路径中寻找,如果是class文件的根目录,就直接查看是否有对应的子目录及文件,如果是jar文件,则现在内存中解压该文件,再查看有无对应的类。对于数组类来说,它并没有对应的字节流,而是由 Java 虚拟机直接生成的,但是数组类的元素类型是类加载器加载的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完成查找字节流的过程。

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。在堆中生成一个class对象。

这里讲一下对象访问的方式,讲完之后就能理解为什么堆中的实例能作为方法区访问的入口了。对象的访问有两种方式:句柄访问和直接指针访问

句柄访问:

![1](G:\A编程学习\01 java学习路线\基础知识\JVM\博客内容\1.jpg)

如果使用句柄访问,那么Java堆中首先会划出一块内存作为句柄池,这时候栈上的reference存储的就是对象句柄地址而不是指向堆中实例的地址。采用句柄访问方式获得完整的对象信息需要进行三次指针定位。

直接指针访问:

![2](G:\A编程学习\01 java学习路线\基础知识\JVM\博客内容\2.jpg)

如果直接指针访问,那么JVM堆中需要为对象分配额外的空间存储指向方法区对象类型数据的指针,此时对象引用直接存储堆中对象的地址,并且只需要两次指针定位,目前Hotspot采用这种访问方式。

句柄访问的好处是reference中存储的是稳定的句柄地址,每次对象位置发生变动的时候只需要改变句柄中指向实例数据的指针即可,而对象引用reference指针和指向对象类型数据的指针不需要改变。直接地址访问,reference指针需要改变。

直接指针的好处就是速度更快,因为他节省了一次指针定位的开销。由于对象的访问在Java中非常频繁,所以速度提升可观。

类加载器:

作用

类加载器在加载过程中扮演重要的角色,它主要有两个作用:

作用一:用来动态地加载class类,将class的字节码形式转换为内存形式的class对象。其中字节码可以来自与磁盘,也可以是jar包,也可以是远程服务器中的,字节码本质是字节数组[]byte。

为什么是动态的呢?这里就涉及到JVM的延迟加载机制,JVM并不是一次性加载程序中的所有类的,而是按需加载,也就是当一个程序中遇到不认识的新类的时候,调用ClassLoader来加载这个类,加载完成后就会将Class对象存在ClassLoader中,下次就不需要加载了。

比较形象的例子就是调用某个类的静态方法,那么需要加载这个类(因为静态方法在类被创建出来之后就有了),但并不需要加载这个类的实例,而实例是在被程序调用的时候才会被加载。

作用二:它是类的命名空间,起到了类隔离的作用。在同一个ClassLoader里面加载的类名是唯一不可重复的,不同类加载器中的同名类是两个不同的类。类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

一个写好的Java程序可能有很多类,这些类呢,有很多方法,在程序运行时候经常需要从一个class中调用另一个class中的方法,但是系统启动的时候不会一次性加载程序中的所有class文件,而是根据需要通过JVM的类加载机制来动态地加载某个class文件到内存中,只有当class文件被加载到内存中,才能被其他class引用,所以classLoader是用来动态加载class文件到内存中的。

拓展:很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

种类

微信截图_20210724112730

其中自顶向下分别为Bootsrtap、Extension、App和Custom自定义类加载器

启动类加载器:在Java 8 时,主要加载java的基础类,也就是JAVA_HOME/jre/lib/rt.jar下的所有class,我们日常使用的Java类库,比方说String、ArrayList都是在该包内。Java 9以后它负责加载启动时的基础模块类(如 Java.base; java.manager;java.xml)。它是通过C++实现的,并不存在于JVM体系中,所以输出是null。

在JVM启动时,通过Bootstrap ClassLoader加载rt.jar,并初始化sun.misc.Launcher从而创建Extension ClassLoader和Application ClassLoader的实例。

除了启动类加载器,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象,在虚拟机外部。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java虚拟机中,方能执行类加载。

Extension加载器,主要加载 jre lib 的ext,扩展 jar 包时使用,只有一个实例,由sun.misc.Launcher$ExtClassLoader实现;向上继承自URLClassLoader, URLClassLoader向上继承自SecueClassLoader,SecueClassLoade继承自ClassLoader,三层继承关系。ClassLoader是抽象类,它定义了一堆加载的模板,采用模板方法模式,NodeClass,DefineClass两个类是classloader自己的,提供给子类进行复写的是findClass。

Application应用类加载器只有一个实例,由sun.misc.Launcher$AppClassLoader实现。它的父加载器是拓展类加载器。

App加载器面向用户的加载器,在Java 8 中主要加载classpath所有的jar包和类,Java 9 中用于加载应用级别的模块,入jdk.compiler; jdk.jartool; jdk.jshell。

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。

Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base是由启动类加载器加载之外,其他的模块均由平台类加载器所加载