[置顶] 虚拟机类加载机制

虚拟机类加载机制

类的生命周期

[置顶] 虚拟机类加载机制_第1张图片

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图所示:

  前5个阶段为类加载阶段,加载,验证,准备,初始化(也包括卸载)都是确定的,但是解析阶段不一定,因为为了支持运行时的绑定,解析会在初始化阶段之后进行。

  首先我们要进入类加载第一个阶段,java虚拟机规范做严格规定有且只有5种情况:
>
1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。

>
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

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

4.当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

5.当使用jdk1.7的动态语言支持时。

  除了以上这5种方法以外,所有引用类的方式都不会触发初始化,称为被动引用:

1.通过子类引用父类的静态字段,不会导致子类初始化

2.通过数组定义来引用类,不会触发此类的初始化;

SuperClass[] sca =new SuperClass[10];//不会触发SuperClass中的静态方法,也不会初始化。

3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。后面会介绍

接下来详细介绍类的加载过程,加载,验证,准备,解析,初始化

加载-加载类文件到内存

  在标题上面,我很笼统的把加载归纳为加载类文件到内存,其实细分下来也就三件事

1.获取——通过类的全限定名(绝对路径:)来获取定义此类的二进制流。

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

3.Class生成——在堆区中生成一个代表这个类的java.lang.Class,作为方法区这个类的各种数据的入口。

对于获取阶段,比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取。另外,还有下面几种方式也比较常用:

从网络中获取:比如10年前十分流行的Applet。

根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。

从非class文件中获取,其实这与直接从class文件中获取的方式本质上是一样的,这些非class文件在jvm中运行之前会被转换为可被jvm所识别的字节码文件。

加载阶段,可控性最强的阶段,可以用户自定义类加载器(遵循双亲委托模型)。

注意:

对于数组不同,数组类本身不通过类加载器创建,他是由java虚拟机直接创建的。但是数组类和类加载器仍然有关,数组类的元素类型最终是要靠类加载器去创建,一个数组类创建过程遵循以下规则:

1.如果数组的组件类型(去掉一个维度的类型)是引用类型,那就递归采用类加载器加载。

2.如果数组的组件类型不是引用类型(int[]数组),java虚拟机将会把数组类标记位与引导类加载器关联。

3.数组类的可见性于它的组件类型可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为Public。

对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。

验证-保证Class文件字节流的安全

  当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等,防止一些人用暴力代码,导致系统崩溃

1.文件格式验证-操作字节流

  第一阶段要验证字节流是否符合Class文件格式规范(包括魔数,主,次版本号),目的保证输入字节流能正确地解析并存储于方法区之内,只有通过了这个验证,字节流才会进入内存的方法区中进行存储

2.元数据验证

  基于方法区存储结构进行,对字节码描述的信息进行语义分析,保证信息符合Java语言规范(这个类是否有父类,这个类是否继承了final修饰的类)

3.字节码验证

  第三个阶段也是基于方法区的验证,目的通过数据流和控制流分析,确定程序语义是合法的,符合逻辑,主要对类的方法体进行校验。(变量类型加载是否不同)

4.符号引用验证

  最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作会在解析阶段发生。符号引用验证可以看做是对类自身以外(常量池中的各种符合引用)的信息匹配性校验(符号引用中通过字符串描述的全限定名是否能找到对应的类)

注意

  验证非常重要,但不是一定必要,如果所使用的代码已经被反复使用和验证就可以使用-Xverify:none关闭。

准备-为类变量分配类存并设置值

准备阶段的工作就是为类的静态变量(static修饰)分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:

基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。

引用类型的默认值为null。

对于常量不同,编译时Javac将会为其生成ConstantValue属性,在准备阶段就会复制给他

  public static final int value =123;//准备阶段会赋值123给他

解析——将常量池内的符号引用替换为直接引用

1.符号引用和直接引用区别

符号引用——和内存无关

  符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。

直接引用——内存必须存在

  指向方法区的本地指针。指向实例变量、实例方法的直接引用都是偏移量

解析中又包括了:类或接口解析(已经存在其他地方的类),字段解析(对于内部类,内部接口),类方法(对于内部内种的方法或者自己的方法中),接口方法(对于接口和自己的内部接口),方法类型,方法句柄和调用点限定符7类。

初始化——执行类构造器<clint>

类构造器

类构造器的生成

  
  <clint>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})合并,顺序是源文件中的顺序,同一个代码中从上到下,父子关系中父级优先。

  类构造器对于类或者接口来说,不是必须,没有静态语句块和类变量的赋值动作,就不需要为其生成类构造器方法。

接口中的类构造器——不会执行父接口类构造器方法

  接口不能有静态语句块,但是有变量初始化动作,因此接口与类一样都会生成类构造器方法。但接口与类不同的是,执行接口的类构造器不需要先执行父接口中的类构造器方法,只有当其使用时才会初始化。接口的实现类的初始化时也一样不会执行接口的类构造器方法()。

你可能感兴趣的:(java,jvm,虚拟机,内存,类加载)