【jvm】类的加载过程

类加载过程

类加载步骤?

​ 按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:

  • 加载(loading)

  • 链接(linking):又包含有,验证、准备、解析

  • 初始化(initialization)

  • 使用(Using)

  • 卸载(Unloading)
    请添加图片描述

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

​ 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的链接、类的初始化这三个步骤来对类进行初始化。如果不出现意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者初始化。

过程一:类的加载(Loading)

​ 类的加载指的是将类的.class文件中的二进制数据读取到内存中,存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象【类模板对象,就是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息】。用来封装方法区内的数据结构在加载类时,Java虚拟机必须完成以下3件事情:

  1. 通过类的全名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  3. 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口。
    请添加图片描述

过程二:链接(Linking)

验证(Verify):

  • 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
  • 目的是确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
  • 格式检查:是否以魔术oxCAFEBABE开头,主版本和副版本是否在当前Java虚拟机的支持范围内,数据中每一项是否都拥有正确的长度等。

准备(Prepare):

  • 为类变量分配内存并且设置该类变量的默认初始化值。注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false。
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式赋值。
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

解析(Resolve):

  • 将常量池中的符号引用转换为直接引用的过程(将类、接口、字段和方法的符号引用转为直接引用)。
  • 虚拟机在加载Class文件时会进行动态链接,Class文件中不会保存各个方法和字段的最终内存布局信息。因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行起来时,需要从常量池中获得对应的符号引用,再在类加载过程中(初始化阶段)将其替换直接引用,并翻译到具体的内存地址中。

以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

过程三:初始化(Initialization)

  • 为类变量赋予正确的初始化值

  • 初始化阶段就是执行类构造器方法< clinit >()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码快中的语句合并而来。

  • 若该类具有父类,jvm会保证子类的< clinit >() 执行前,父类的< clinit >() 已经执行完成。clinit 不同于类的构造方法(init) (由父及子,静态先行)。

  • Java编译器并不会为所有的类都产生< clinit >()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含< clinit >()方法?

    • 一个类中并没有声明任何的类变量,也没有静态代码块时;
    • 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时;
    • 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式 (如果这个static final 不是通过方法或者构造器,则在链接阶段)。
  • static与final的搭配问题(使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行)

public static int a = 1;//在初始化阶段<clinit>()中赋值
public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(123);//在初始化阶段<clinit>()中赋值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1234);//在初始化阶段<clinit>()中赋值

public static final String s0 = "helloworld";//在链接阶段的准备环节赋值
public static final String s1 = new String("helloworld");//在初始化阶段<clinit>()中赋值

public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值
  • clinit()的调用会死锁吗?
    • 虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。
    • 正是因为函数< clinit >()带锁线程安全的,因此,如果在一个类的< clinit >()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。

过程四:类的使用(Using)

  • 任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。
  • 开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法)或者使用new关键字为其创建对象实例。

过程五:类的卸载(Unloading)

  • 类、类的加载器、类的实例之间的引用关系

    • 在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。
    • 一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
  • 方法区的垃圾回收

    方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量不再使用的类型。HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。判定一个常量是否"废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件:

    • 该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。
    • 加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、SP的重加载等,否则通常是很难达成的。
    • 该类对应的java.lang. Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 类的卸载

    • 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)。
    • 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。
    • 开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。

版权声明:本文为weixin_44175122原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。