Java类加载机制

概述

  Java源代码文件(.Java)经过Java编译器(Java Complier)编译后,形成Java字节码文件(.class),class文件由类装载器加载后,在JVM的堆中将形成一个描述Class结构的元信息对象java.lang.Class实例,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,且该对象仅有一份。

  .class字节码文件装载成java.lang.Class实例只是整个类加载过程的第一步,接下来还有连接(验证、准备和解析)和初始化等流程。

  类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

  由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。

  类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

  

  其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。

加载Loading

  注意:这里说的加载是指整个加载过程中的第一阶段,JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。

  根据一个类的全限定名(如cn.edu.hdu.test.HelloWorld.class)来读取此类的二进制字节流到JVM内部。将字节流所代表的静态存储结构转换为方法区的运行时数据结构(java.lang.Class对象)(在java7以前,类的元信息文件存储在方法区中,Java 8中取消了方法区的概念,将类的元信息文件存在了本地内存中)。

类加载器种类

  • 启动类加载器,Bootstrap ClassLoader,加载JACA_HOME\lib,或者被-Xbootclasspath参数限定的类。C++编写,是JVM虚拟机的一部分
  • 扩展类加载器,Extension ClassLoader,加载\lib\ext,或者被java.ext.dirs系统变量指定的类。由Java语言实现,父类加载器为null
  • 应用程序类加载器(系统加载器),Application ClassLoader,加载ClassPath中的类库。由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类加载器,通过继承ClassLoader实现,一般是加载我们的自定义类。父类加载器为AppClassLoader。

类装载工作由ClassLoder和其子类负责。JVM在运行时会产生三个ClassLoader:根装载器ExtClassLoader(扩展类装载器)和AppClassLoader,其中根装载器不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar,charsets.jar等。ExtClassLoaderClassLoder的子类,负责装载JRE扩展目录ext下的jar类包;AppClassLoader负责装载classpath路径下的类包,这三个类装载器存在父子层级关系****,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下使用AppClassLoader装载应用程序的类。

<1>全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

<2>父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

<3>缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

双亲委派模式

  • 双亲委派模式工作原理

  双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下(他们之间的关系并非继承关系):

  双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

  • 双亲委派模式优势

  采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常:

java.lang.SecurityException: Prohibited package name: java.lang

 

连接Linking(验证、准备、解析)

验证

验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证;

准备

为类中的所有静态变量分配内存空间,并为其设置一个默认初始值(由于还没有产生对象,实例变量将不再此操作范围内);

解析

将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。

 

初始化Initialization

  在连接的准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员自己写的逻辑去初始化类变量和其他资源,举个例子如下:

public static int value1  = 5;
public static int value2  = 6;
static{
  value2 = 66;
}

  在准备阶段value1和value2都等于0;

  在初始化阶段value1和value2分别等于5和66;

    • 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用;
    • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;
    • 如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。
    • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。(所以可以利用静态内部类实现线程安全的单例模式)
    • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法;

  类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。什么时候会触发类初始化:

    1. 为一个类型创建一个新的对象实例时(比如new、反射、序列化)
    2. 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
    3. 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
    4. 调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
    5. 初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
    6. JVM启动包含main方法的启动类时。

一个经典例子:

class SingleTon
 {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon()
  { count1
++; count2++;   } public static SingleTon getInstance() { return singleTon; } } public class Test {   public static void main(String[] args) {     SingleTon singleTon = SingleTon.getInstance();     System.out.println("count1=" + singleTon.count1);     System.out.println("count2=" + singleTon.count2);   } }

1:SingleTon singleTon = SingleTon.getInstance();调用了类的SingleTon调用了类的静态方法,触发类的初始化
2:类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3:类初始化化,为类的静态变量赋值和执行静态代码快。singleton赋值为new SingleTon()调用类的构造方法
4:调用类的构造方法后count=1;count2=1
5:继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0

参考链接

https://blog.csdn.net/javazejian/article/details/73413292#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E6%9C%BA%E5%88%B6%E7%9A%84%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84

https://blog.csdn.net/zjkC050818/article/details/78376195

posted @ 2018-08-31 17:03  Allegro  阅读(226)  评论(0编辑  收藏  举报