Java类加载机制

1:类的加载时机

类从被加载到虚拟机内存到移除虚拟机内存后,其生命周期经过过一下7个过程,加载loading,验证verfication,准备preparetion,解析resolution,初始化initaltion,使用using,卸载outloading。其中必须要经过阶段有5个,是加载loading,验证verfication,准备preparetion,初始化initaltion,卸载outloading且这5中操作的顺序是固定的。如下图所示:

 

 

 JVM对类的加载的时机没有硬性要求,而对初始化做出了极为苛刻的要求,当且仅当一下5中情况下会初始化类,如下表:

类初始化的操作 具体情况说明 备注
new,getstatic,putstatic,ivokerstatic new实例化对象,读取或者设置一个类的静态字段以及体条用类的静态方法 对应的操作都会进行类的初始化操作                                                 
Java.lang.reflect 使用反射方法读类进行反射操作时,若没有进行初始化则会进行初始化。  
当初始化一个类时,若父类还没初始化 初始化该类之前,会先初始化没有初始化的父类  
JVM启动后,初始化主类 包括main方法的类被被称为主类  
java.langinvoke.MethodHandle 解析结果为:REF_getstatic,REF_putstatic  

JVM中硬性规定,当且仅当这5种方法会进行类初始化.

1.1:测试1

测试1通过引用子类的静态变量(有父类继承来的),则只会初始化父类,而子类不会初始化。测试程序如下。

public class FanXing{
    public static void main(String[] args) throws InterruptedException {
        System.out.println(SubClass.value);
    }
}

class SuperClass{
    static{
        System.out.println("SuperClass initial!");
        }
    public static int value =88;
}

class SubClass extends SuperClass{
    static{
        System.out.println("SubClass initial!");
    }
}

运行结果:SuperClass initial 88我们可以知道,通过方位静态变量的操作只会初始化定义该静态方法的类,即只有父类被初始化,至于子类有没有加载,验证,准别则根据JVM规范种并没有明确,与虚拟机有关。

1.2:测试2

通过数组来定义引用类,不会触发初始化操作。

public class FanXing{
    public static void main(String[] args) throws InterruptedException {
        //System.out.println(SubClass.value);
        SubClass[] s = new SubClass[10];
    }
}

接着使用上面的SuperClass和SubClass类,运行发先,不会出发初始化操作。可知创建引用类的数据引用(不放进去实例)是不会触发类的初始化动作的!

1.3:测试3

class Noinitialion{
 static{
  System.out.println("初始化阶段");
;
public static final int value = 88; 
}
public class FanXing{ public static void main(String[] args) throws InterruptedException { //System.out.println(SubClass.value); System.out.println(Noinitialion.value); } }

我们发现通过调用Noinitaltion类中的静态方法并未触发该类的初始化,这是因为final修饰的static的变量进入FanXing的方法区的常量池,所以在两个类的class文件之间不会有任何接口的存在,指向!

上面介绍的是类的加载过程,相比于类,接口的加载过程有一些区别,那就是上面触发初始化的要使用子类,必须保证父类也被初始化了!然而,在接口中,不需要先初始化父接口,当使用到该父接口时,才初始化父接口!

2:类的加载过程

这里主要讲解Java虚拟机中类加载的过程包括:加载,验证,准备,解析和初始化的具体动作。

2.1:加载

该阶段组要的事情有以下三个;

  1. 通过该类的全限定名来获得该类的字节流。
  2. 将这个静态的字节流转化为方法区的数据结构。
  3. 在内存中生产一个Java.lang.Class类的实例(虽然是对象但是在hotspot中是在方法区中),作为方法区这个类的数据结构的入口。

其中针对第一点JVM并没有规定类的字节流来源,所在有大量的研发人员这里进行了创造如以下:

ZIP中加载类字节流;网络中获取,运行算计算动态代理计数;数据库和其他文件读入。

针对第三点:是在内存中创建一个对应的Class类的实例对象,但是并没有明确是放在Java堆当中,在HotSpot虚拟机中Class对象比较特殊,虽然是对象,但是放在方法区中。

2.2:验证

验证是连接的第一阶段,目的是保证加载的字节流符合JVM的要求,不会损害JVM的工作。因为Class文件是字节码文件,甚至你用文本编译器写的字节码文件都可以被加载到JVM中,所以这个如果不进行验证,很有可能损害JVM的系统,造成崩溃,验证是一种JVM自我保护的手段。

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

2.3:准备

该阶段正式为类变量在方法去分配内存同时配置初始值,只为static变量的类变量来分配内存和初始化,实例变量随着对象的实例化在堆里分配内存,public staic int value = 123;在准备阶段,该值是0,这里初始化值指的是默认值,把value赋值123的putstatic指令,被程序编译后放在类的构造器中clint方法中,所以value 赋值123操作会在类初始化阶段中完成赋值。数据的默认值如下 表所示;这里是指一般情况,当遇到final修饰的sitatic变量时,这一步也会直接初始化为final赋值的值!

2.4:解析

解析过程是将虚拟机的常量池的符号引用替换为直接引用的过程,在Class文件中CONSTANT_Class_info,CONSTANT_Fieldref_info,

CONSTANT_Methodref_info,等类型的常量出现,同时还包含静态连接,将该类中要用到的一些类加载进来。

2.5:初始化

初始化阶段是类加载过程的最后一步,到这一步才开始真正的执行类中的代码。在前面的准备节点,所有的类变量都已经被值了默认值,而在初始化阶段,则按照用户的意图进行初始化一些变量和资源,换句话说就是执行类的<clinit>方法的过程。

下面介绍clinit方法特点:

  1. 该方法会自动的收集,类变量的初始化语句,和静态代码库的赋值语句,并按照在Class文件的前后顺序进行整合。
  2. 该方法与类的构造器之间的区别是,不需要显示调用父类的clinit方法,JVM保证子类的clinit方法运行之前,父类的clinit方法一定运行了,
  3. 由于父类的clinit方法先执行,所有父类的静态代码快,类变量一定是比子类更早的执行的。
  4. clinit方法对类和接口来说不是必须的,当代码中么有静态代码块和类变量时,编译器可以为其不生产clinit方法。
  5. 虚拟机会保证在多线程下clinit方法是同步的,会自动的上锁。

2.6:卸载

  什么时候才能卸载一个类尼?当且仅当该类没有任何用处的是时候,才会被回收。一般至少要满足下面三个条件的。

  1. 该类的所以实例对象都已经被回收了,也就是Java堆中不存在该类任何实例。
  2. 加载该类的ClassLoader也已经被回收了。
  3. 该类对象的java.lang.Class对象在任何地方都没有引用了,也就是说无法在任何地方通过反射访问该类的方法。

满足,上面三个条件就可以对类进行回收,这里说的仅仅是可以,而不是一定!

3:类的加载器

  类加载器是加载类的工具。同时类加载器和该类的class的全限定名共同确定类的唯一。本质上,加载器就是找到类的位置

3.1:启动类加载器Bootstrap ClassLoader

  负责加载jre核心类库,如jre目标下的rt.jar和charsets.jar等等。启动类加载器使用C语言编写地本地方法。

3.2:其他类加载器

3.2.1:拓展类加载器Extention ClassLoader

  负责jre拓展部ext中的jar类包的中类的加载。

3.2.2:系统类加载器Apllication ClassLoader

  负责ClassPath路径下的类包的加载器。

3.2.3:用户自定义类加载器User ClassLoader

  负责加载用户自定义路径下的类包加载。

3.3:双亲委托机制

 

 

 流程如上图所示:

  1. 类加载器收到类加载地请求。
  2. 把这个加载请求向上委托,一直到启动类加载器,直到启动类加载器加载该类。
  3. 如果启动类加载器使用findClass发现无法加载,则抛出异常,通知子加载器加载,如果能加载则直接加载。
  双亲委派模型最大的好处就是让Java类同其类加载器一起具备了一种带优先级的层次关系。这句话可能不好理解,我们举个例子。比如我们要加载java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类(确定类的唯一性是通过类加载和类的全限定名来实现了)。如果没有双亲委派模型,那就乱了套了,完全可能搞出多个不同的Object类。

 

posted @ 2020-02-29 16:36  大朱123  阅读(206)  评论(0编辑  收藏  举报