类加载机制

来源:互联网 发布:长江证券 mac 编辑:程序博客网 时间:2024/06/17 22:29

类加载机制

类加载的时机

类从被加载到虚拟机内存开始,到卸载到内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分统称为连接。
有且只有5种情况必须立即对类就行“初始化”

  1. 遇到new、getstatic、putstatic、invokestatic这4条指令码时,如果类没有进行过初始化,则需要先触发器初始化。生成4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先触发初始化。
  3. 当初始化一个类的时候,如果发现父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要知道一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. JDK1.7新增,不常用
    这5类触发初始化,称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动初始化

    3个被动引用的例子:

    1. 通过子类引用父类的静态字段,不会导致子类初始化
    2. 通过数组定义来引用类,不会触发此类初始化
    3. 常量的使用,不会触发定义常量类的初始化

类加载过程

加载

完成的事情:

  1. 通过一个类在全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口。

因为加载不一定是从一个Class文件去加载,造就了java的灵活性
在这个过程中可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,可以通过自定义自己的类加载去控制字节流的获取方式,(即重写一个类加载器的loadClass()方法)

验证

验证是连接阶段的一部分,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。Java编译阶段提供了很高的安全性检验,但Class文件不一定由java源代码编译而来,所以这里的安全性验证是非常重要的。验证阶段主要会包括下面4个检验动作:1.文件格式验证。2.元数据验证。3.字节码验证。4.符合引用验证。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中进行分配。
NOTE:初始值一般是0值

public static int value=123;

变量value在准备阶段过后的初始值为0而不是123。
特殊情况需要更改类字段的字段属性表在的ConstantValue的值,上次讲Class结构是说过ConstantValue值需要fianl来指定
所以需要修改为:

public static fianl int value=123;

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无,引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,它们能接收的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才开始真正执行类中定义的Java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器 < clinut >()的过程。

  • < clinut >()方法是由编译器自动收集类中的所有类变量的辅助动作和静态语句块(static()块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
public class Test {    static{        i=0;                    //正常编译通过        System.out.println(i);  //Cannot reference a field before it is defined    }    static int i=1;}
  • < clinit>()方法与类的构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的< clinit>()方法执行之前,父类的< clinit>()方法已经执行完毕。因此在虚拟机在第一个被执行的< clinit>()方法的类肯定是java.lang.Object。
  • 由于父类的< clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
  • < clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit>()方法.
  • 接口中不能使用静态语句块,但仍然有变量初始的赋值操作,因此接口和类一样都会发生< clinit>()方法。但接口与类不同的是,执行接口的< clinit>()方法不行一线执行父接口的< clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化是也一样不会执行接口的< clinit>()方法.
  • < clinit>()方法有同步机制,但只要有一个线程加载,就不会再加载,因为加载是将类信息加载到方法区,而方法区是各个线程共享的内存区域。
0 0
原创粉丝点击