天天看点

类加载过程

一、类的生命周期

类被加载到jvm虚拟机内存开始,到卸载出内存为止,他的生命周期可以分为:加载->验证->准备->解析->初始化->使用->卸载。

其中验证、准备、解析统一称为链接阶段

1、加载

  将类的字节码载入方法区中,内部采用 c++ 的 instanceklass 描述 java 类,它的重要 field 有:

    _java_mirror 即 java 的类镜像,例如对 string 来说,就是 string.class,作用是把 klass 暴露给 java 使用

    _super 即父类

    _fields 即成员变量

    _methods 即方法

    _constants 即常量池

    _class_loader 即类加载器

    _vtable 虚方法表

    _itable 接口方法表

  如果这个类还有父类没有加载,先加载父类 加载和链接可能是交替运行的

2、链接

2.1验证

  验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

  1)文件格式验证:验证字节流是否符合class文件格式的规范;例如:是否以0xcafebabe开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

  2)元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合java语言规范的要求;例如:这个类是否有父类,除了java.lang.object之外。

  3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  4)符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用<code>-xverifynone</code>参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2.2准备

  当完成字节码文件的校验之后,jvm 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

内存分配的对象。java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。

在准备阶段,jvm 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

初始化的类型。在准备阶段,jvm 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

2.3解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用:简单的理解就是字符串,比如引用一个类,java.util.arraylist 这就是一个符号引用,字符串引用的对象不一定被加载。

直接引用:指针或者地址偏移量。引用对象一定在内存(已经加载)。

3、初始化

  初始化,这个阶段就是执行类构造器&lt; clinit &gt;()方法的过程,为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。

在java中对类变量进行初始值设定有两种方式:

  声明类变量是指定初始值

  使用静态代码块为类变量指定初始值

jvm初始化步骤

  1)假如这个类还没有被加载和连接,则程序先加载并连接该类

  2)假如该类的直接父类还没有被初始化,则先初始化其直接父类

  3)假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  1)创建类的实例,也就是new的方式

  2)访问某个类或接口的静态变量,或者对该静态变量赋值

  3)调用类的静态方法

  4)反射(如class.forname(“com.shengsiyuan.test”))

  5)初始化某个类的子类,则其父类也会被初始化

  6)java虚拟机启动时被标明为启动类的类(java test),直接使用java.exe命令来运行某个主类

不会导致类初始化的情况

  1)访问类的 static final 静态常量(基本类型和字符串)不会触发初始化

  2)类对象.class 不会触发初始化

  3)创建该类的数组不会触发初始化

4、使用

  当 jvm 完成初始化阶段之后,jvm 便开始从入口方法开始执行用户的程序代码。

5、卸载

  当用户程序代码执行完毕后,jvm 便开始销毁创建的 class 对象,最后负责运行的 jvm 也退出内存。

二、类加载器

1、类加载器分类

  1)启动类加载器:bootstrap classloader,负责加载存放在jdk\jre\lib(jdk代表jdk的安装目录,下同)下,或被-xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被bootstrap classloader加载)。启动类加载器是无法被java程序直接引用的。

  2)扩展类加载器:extension classloader,该加载器由sun.misc.launcher$extclassloader实现,它负责加载jdk\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

  3)应用程序类加载器:application classloader,该类加载器由sun.misc.launcher$appclassloader来实现,它负责加载用户类路径(classpath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。

2、双亲委派

类加载过程

 类加载器加载类的源码

 从图中我们发现除启动类加载器外,每个加载器都有父的类加载器。

双亲委派机制:如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;

只有父类加载器无法完成此加载任务时,才自己去加载。

类加载过程

从类的继承关系来看,extclassloader和appclassloader都是继承urlclassloader,都是classloader的子类。而bootstrapclassloader是有c写的,不再java的classloader子类中。

从图中可以看到类加载器间的父子关系不是以继承的方式实现的,而是以组合关系的方式来复用父加载器的代码。

如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给父加载器去完成,每一个层次的类加载器都是如此。

双亲委派模型的好处

  java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,java中的object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的object类。

3、打破双亲委派

  3.1 自定义加载器重写loadclass()方法,具体可以参考https://www.cnblogs.com/itpower/p/13211490.html

  3.2线程上下文类加载器(利用了java的spi机制)

这里以jdbc为例来讲解,我们在使用 jdbc 时,都需要加载 driver 驱动,不知道你注意到没有,不写class.forname("com.mysql.jdbc.driver"),也是可以让 com.mysql.jdbc.driver 正确加载,那是怎么做到的呢,我们看一下源码

我们手动输出一下drivermanager的类加载器

system.out.println(drivermanager.class.getclassloader());

打印 null,表示它的类加载器是 bootstrap classloader,会到 java_home/jre/lib 下搜索类,但 java_home/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,

这样问题来了,在 drivermanager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.driver 呢

继续看 loadinitialdrivers() 方法:

先看 2)发现它最后是使用 class.forname 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 service provider interface (spi)约定如下,在 jar 包的 meta-inf/services 包下,以接口全限定名名为文件,文件内容是实现类名称

类加载过程

这样就可以使用serviceloader来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  jdbc

  servlet 初始化器

  spring 容器

  dubbo(对 spi 进行了扩展)

接着看 serviceloader.load 方法:

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由 class.forname 调用了线程上下文类加载器完成类加载,具体代码在 serviceloader 的内部类 lazyiterator 中:

 4、自定义加载器

问问自己,什么时候需要自定义类加载器

  1)想加载非 classpath 随意路径中的类文件

  2)都是通过接口来使用实现,希望解耦时,常用在框架设计

  3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:1) 继承 classloader 父类

   2)要遵从双亲委派机制,重写 findclass 方法,注意不是重写 loadclass 方法,否则不会走双亲委派机制

   3)读取类文件的字节码

   4)调用父类的 defineclass 方法来加载类

   5)使用者调用该类加载器的 loadclass 方法