天天看点

类的加载过程和类加载器

一般分为三个比较大的阶段,分别是加载阶段,连接阶段和初始化阶段,五个主要的阶段。

  在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

如下图所示:

类的加载过程和类加载器

  (1)通过一个类的全限定名(包名+类名)来获取其定义的二进制字节流

  (2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构

  (3)在堆中生成一个代表这个类的java.lang.class对象,作为方法区中这些数据的访问入口。

验证的主要作用就是确保被加载的类的正确性。也是链接阶段的第一步。主要是完成四个阶段的验证:

  (1)文件格式的验证: 验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验

  (2)元数据验证: 主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求

  (3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。

  (4)符号引用验证:在解析阶段,会将虚拟机中的符号引用转化为直接引用,该阶段则负责对各种符号引用进行匹配性校验,保证外部依赖真实存在,并且符合外部依赖类、字段、方法的访问性

  准备阶段主要为静态变量分配内存并设置初始值(默认值)

  注意:

    1.实例变量不会进行内存的分配,实例变量主要随着对象的实例化一块分配到java堆中

    2.为静态变量设置的是默认值,如static int a=5; 这个时候a的值是0

    3.静态常量由于是用final static进行修饰的,final修饰的静态变量不会导致类的初始化,final static int b=5,在这个时候b的值就是5

  解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程

  符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

  直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。如果有了直接引用,那引用的目标必定已经在内存中存在。

  初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static块)中的语句合并产生的。

 也就是说会按照一定的顺序来继续,静态变量的赋值,静态代码块的调用,静态变量的赋值等等,jvm会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

  当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。

运行结果

类的加载过程和类加载器

所有静态的代码块和静态变量会先于非静态变量进行加载

1、父类的静态变量

2、父类的静态代码块

3、子类的静态变量

4、子类的静态代码块

5、父类的非静态变量

6、父类的非静态代码块

7、父类的构造方法

8、子类的非静态变量

9、子类的非静态代码块

10、子类的构造方法

接下来调整代码的顺序

类的加载过程和类加载器

 将静态变量和非静态变量移到后面然后执行

类的加载过程和类加载器

 java中的类加载器主要分为下图的四种

类的加载过程和类加载器

引导类加载器(bootstrap class loader)

(1)它用来加载 java 的核心库(java_home/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(c语言)来实现的,并不继承自 java.lang.classloader。

(2)加载扩展类和应用程序类加载器。并指定他们的父类加载器。

扩展类加载器(extensions class loader)

(1)用来加载 java 的扩展库(java_home/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 java类。

(2)由sun.misc.launcher$extclassloader实现。

应用程序类加载器(application class loader)

(1)它根据 java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 java 类。一般来说,java 应用的类都是由它来完成加载的。

(2)由sun.misc.launcher$appclassloader实现

自定义类加载器

(1)开发人员可以通过继承 java.lang.classloader类的方式实现自己的类加载器,以满足一些特殊的需求。

(2)遵守双亲委派模型:继承classloader,重写findclass()方法。

(3)破坏双亲委派模型:继承classloader,重写loadclass()方法。

双亲委派的执行流程:

  如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

双亲委派模型的好处

(1)主要是为了安全性,避免用户自己编写的类动态替换 java的一些核心类,比如 string。

(2)同时也避免了类的重复加载,因为 jvm中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 classloader加载就是不同的两个类。

沙箱安全机制:博客地址 javascript:void(0)

为什么要破坏双亲委派模型?

首先你要知道:双亲委派模型不是一种强制性约束,它是一种java设计者推荐使用类加载器的方式。

但是有的时候不得不违反这个约束,例如spi,他不是和api一样,它面向拓展的,spi全称service provider interface,是java提供的一套用来被第三方实现或者扩展的api,它可以用来启用框架扩展和替换组件。例如 数据库驱动加载接口实现类的加载、spring、日志接口实现类加载,

其中最常使用的就是jdbc

  位于java.sql包下的drivermanager类,它需要数据库的驱动器,这个驱动器一般是数据库厂商进行,按照双亲委派模型来说的。drivermanager类是会被引导类加载器进行加载的,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载driver实现,从而破坏了双亲委派。

在调用drivermanager的时候,会先初始化类,调用其中的静态块:

静态代码块中调用了loadinitialdrivers()方法,改方法中的其他的东西我们不关注,只关注标红的对象和方法serviceloader.load

serviceloader.load()方法

load方法调用获取了当前线程中的上下文类加载器,在launcher初始化的时候,会获取appclassloader,然后将其设置为上下文类加载器,所以上下文类加载器默认情况下就是系统加载器。

 可以使用 -verbose:class参数来查看类加载器所加载的类

类的加载过程和类加载器

  全盘负责是指当一个classloader加载一个类时,除非显示地使用另一个classloader,则该类所依赖与引用的类也由这个classloader加载。