类加载和对象创建的过程

虚拟机类加载机制

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

注意:加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,它在某些情况下,可以在初始化阶段之后再开始,这是为了支持JAVA语言的动态绑定。

类加载的时机:

(1)遇到new,getstatic,putstatic或invokestatic这4条字节码指令时。(创建对象或访问类静态成员变量或者方法的时候)。

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

(3)当初始化一个类的时候,发现其父类还没有进行初始化,则需要先触发其父类的初始化。

(4)当虚拟机起动时,用户需要指定一个入口主类,这个类会在程序启动的时候初始化。

(5)当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.methodhandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:以上5种称为主动引用,除了以上五种场景,其他任何引用类的方式都不会触发初始化,称为被动引用。下面3个例子为被动引用

(1)通过子类名访问父类定义的静态成员变量,只会触发父类的初始化,不会触发子类的初始化。

(2)通过数组定义来引用类,不会触发此类的初始化  person[]persons=new person [10];这句代码并没有触发person类的初始化,但是触发了另外一个名为Lorg.fenixsoft.classloading.superClass的初始化,这个类的初始化其实是由字节码指令newarray触发。

(3)调用类的常量的时候,常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。例如:person.a;

public class person{

public static final String a=“sssss”;

}

类加载的过程(加载,验证,准备,解析,初始化)

@@@@@@@@@1,加载

(1)通过一个类的全限定名来获取定义此类的二进制字节流。

通过一个类的全限定名来获取,准确的说是根本没有指明要从哪里获取,怎样获取,由开发人员自己定义。一般有以下途径:

      1,从ZIP包中读取,这个很常见,最终成为日后Jar,Ear,War格式的基础。

      2,从网络中获取,最典型的应用就是Applet。             

      3,运行时计算生成,应用的最多的就是动态代理技术,在java.lang.reflect.proxy中,就是用了proxygenrator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

      4,由其他文件生成,典型场景就是Jsp应用。即由jsp文件生成的对应的class类。

      5,从数据库中读取,这种情况比较少见。

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

(3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口

注意:加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实行自行定义,虚拟机规范未规定此区域的具体数据结构,然后在内存中实例化一个java.lang.Class类的对象(同样存放在方法区),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

@@@@@@@@2,验证

验证是连接(Linking)阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致分为以下几个阶段:

(1)文件格式验证,验证字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理。

(2)元数据验证,对字节码描述信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求。

(3)字节码验证,通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。

(4)符号引用验证,符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.inconmpatibleClassChangeError异常的子类。主要验证以下内容:

    1,符号引用中通过字符串描述的全限定名是否能找到对应的类

    2,在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

    3,符号引用中的类,字段,方法的访问性(private,public,default,protected)是否可被当前类访问。


对于虚拟机的类加载机制来说,验证阶段是一个非常重要,但不是一定必要的阶段,如果代码完全可信,可以在实施阶段通过使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机加载类的时间。

@@@@@@@@@@3,准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中进行分配,基本数据类型初始值为0,引用数据为null,准备阶段只分配静态static的变量,而特殊情况例如字段属性表中存在ConstantValue属性的(final修饰的),会提前进行初始化赋值操作,例如:

public static final int a=“123” 编译时将会为a生成ConstantValue属性,在准备阶段虚拟机就会将a赋值为“123”。

@@@@@@@@@@@4,解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。

符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可,引用的目标不一定已经加载到内存。

直接引用:直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄,直接引用的目标必定已经在内存中。

重点插曲:方法调用(方法调用不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本暂时不涉及到方法内部运行的具体过程)一切方法调用在class文件里面存储的都只是符号引用。而不是方法在实际运行时内存布局中的入口地址。在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。这种解析成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。符合这种要求的方法主要包含5种:1,静态的 2,私有的 3,实例构造器 4,父类方法 5,final修饰的方法。 这种静态调用对应虚拟机的两种指令

(1)invokestatic:调用静态方法

(2)invokespecial:调用实例构造器<init>方法,私有方法,父类方法

另外有三种调用指令属于动态的有

(3)invokevirtual:调用所有虚方法

(4)invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象

(5)invokeDynamic:现在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

符号引用内部包含了自己是否属于静态解析或者动态解析的标志位,虚拟机可以进行判断并在不同的阶段进行解析。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符7类符号引用进行,分别对应常量池的7种常量类型。先讲解前面4种。

(1)类或接口的解析

  假设当前代码所处的类为D,如果要吧一个从未解析过的符号引用n解析为一个类或者接口C的直接引用,需要进行一下步骤:

  1,如果C不是一个数组类型,那么虚拟机会把符号引用n的全限定名传递给D的类加载器去加载这个类c,在加载过程中,如果发现C有父类或者实现的接口,会触发C的父类或者实现的接口的加载,以此类推。

  2,如果C是一个数组类型,并且数组的元素类型为对象,则按照1的模式加载。如果加载的类型是Java。lang。Interger,就由虚拟机生成一个代表此数组维度和元素的数组对象。

  3,如果上面步骤都没出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或者接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限,如果发现不具备访问权限,就抛出异常。

(2)字段解析

要解析一个未被解析的字段符号引用,首先会对字段所属的类或者接口的符号引用进行解析,如果成功解析,将这个字段所属的类或者接口用C表示,那么虚拟机按照如下步骤进行后续字段的搜索。

1,如果C本身就包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用,查找结束。

2,否则,如果C中实现了接口,或者父类,则会按照继承关系从下往上递归搜索各个接口和它的附接口,如果找到了则返回直接引用,查找结束。

3,如果C不是对象,就会按照继承关系从下往上递归搜索其父类,找到了就返回直接引用。

4,如果都没找到 就查找失败抛出异常。

(3)类方法解析

  1,先解析出类方法表中索引的方法所属的类或接口的符号引用,我们依然用C表示这个类。

  2,类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现C是个接口,就直接抛出异常。

  3,否则,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,有就返回直接引用。

  4,否则,在类C的父类中递归查找,有就返回直接引用。

  5,否则,在类C实现的接口列表以及它们的父接口中递归查找,如果找到了说明类C是一个抽象类,抛出异常。

  6,否则查找失败。

(4)接口方法解析

  1,解析出接口方法表中索引的方法所属的类或者接口的符号引用,依然用C表示。

  2,如果在接口方法表中发现索引的C是个类,直接抛出异常。

  3,否则,在接口C中查找,找到了直接返回。

  4,否则,在接口C的父接口中查找,找到了直接返回直接引用。

  5,否则 查找失败。

注意:虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标记为已解析状态),从而避免重复解析。

注意:解析调用时一个静态的过程,在编译期间就完全可以确定,在类装载的解析阶段就会把涉及的符号引用全部转换为直接引用,不会延迟到运行阶段再去完成,而分派调用则可能静态也可能动态。

静态分派:静态分派的典型应用是方法的重载,静态分派发生在编译阶段。编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是在编译器可知的。因此在编译阶段,javac编译器会根据参数静态类型决定使用哪个重载版本。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

动态分派:动态分派和重写有着很密切的关联。

动态分派的原因就是因为虚拟机invokevirtual指令调用所有虚方法的指令。

invokevirtual指令运行时解析过程大致可以分为以下步骤:

(1)找到操作数栈顶的第一个元素所指向的对象实际类型,记做c。

(2)如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行权限校验,校验通过则返回直接引用,查找结束。

(3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

(4)如果始终没有找到合适的方法,就抛出异常。找不到方法。

由于invokevirtual指令执行的第一步就是在运行期确定接受者的实际类型,所以重写传入不同的实际类型 在解析的时候引用地址也会不同,这就是重写的本质。

单分派和多分派

方法的接收者和方法的参数统称为方法的宗量。 根据分派基于宗量多少(接收者是一个宗量,参数是一个宗量),可以将分派分为单分派和多分派。单分派是指根据一个宗量就可以知道调用目标(即应该调用哪个方法),多分派需要根据多个宗量才能确定调用目标。

请看示例:

/**

* Created by fan on 2016/3/29.

*/

public class Dispatcher {

    static class QQ {}

    static class _360 {}

    public static class Father {

        public void hardChoice(QQ arg) {

            System.out.println("father choose QQ");

        }

        public void hardChoice(_360 arg) {

            System.out.println("father choose _360");

        }

    }

    public static class Son extends Father {

        @Override

        public void hardChoice(QQ arg) {

            System.out.println("son choose QQ");

        }

        @Override

        public void hardChoice(_360 arg) {

            System.out.println("son choose 360");

        }

    }

    public static void main(String[] args) {

        Father father = new Father();

        Father son = new Son();

        father.hardChoice(new _360());

        son.hardChoice(new QQ());

    }

}

执行结果如下所示:

这里写图片描述

字节码指令如下所示:

public static void main(java.lang.String[]);

  Code:

  Stack=3, Locals=3, Args_size=1

  0:  new    #2; //class Dispatcher$Father

  3:  dup

  4:  invokespecial  #3; //Method Dispatcher$Father."<init>":()V

  7:  astore_1

  8:  new    #4; //class Dispatcher$Son

  11:  dup

  12:  invokespecial  #5; //Method Dispatcher$Son."<init>":()V

  15:  astore_2

  16:  aload_1

  17:  new    #6; //class Dispatcher$_360

  20:  dup

  21:  invokespecial  #7; //Method Dispatcher$_360."<init>":()V

  24:  invokevirtual  #8; //Method Dispatcher$Father.hardChoice:(LDispatcher$_360;)V

  27:  aload_2

  28:  new    #9; //class Dispatcher$QQ

  31:  dup

  32:  invokespecial  #10; //Method Dispatcher$QQ."<init>":()V

  35:  invokevirtual  #11; //Method Dispatcher$Father.hardChoice:(LDispatcher$QQ;)V

  38:  return

从上面的字节码指令中可以看到,两次方法调用

        father.hardChoice(new _360());

        son.hardChoice(new QQ());

对应的字节码指令都是一样的,只是参数不同而已:

  24:  invokevirtual  #8; //Method Dispatcher$Father.hardChoice:(LDispatcher$_360;)V

  35:  invokevirtual  #11; //Method Dispatcher$Father.hardChoice:(LDispatcher$QQ;)V

由此可见,在class文件中都是调用Father的hardChoice()方法。

解析

在Java源代码进行编译的过程中,发生了这么个事情。

首先确定方法的接收者,发现两个对象变量的静态类型都是Father类型的,因此在class文件中写的Father类中方法的符号引用。

再者,对于方法参数,一个是_360对象,一个是QQ对象,按照静态类型匹配的原则,自然找到各自的方法。

上面的两步都是在编译器中做出的,属于静态分派,在选择目标方法时根据了两个宗量,是多分派的。因此,静态分派属于多分派类型。

当java执行时,当执行到son.hardChoice(new QQ()); 时,发现son的实际类型是Son,因此会调用Son类中的方法。在执行father.hardChoice(new _360()); 时也有这个过程,只不过father的实际类型就是Father而已。发现,在目标选择时只依据了一个宗量,是单分派的。因此,动态分派属于单分派类型。

结论

到目前为止,java语言是一个静态多分派,动态单分派的语言。

@@@@@@@@@@@5,初始化

类初始化阶段是类加载的最后一步,在准备阶段,变量已经赋值过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,这个过程是执行类构造器的

<clinit>()方法的过程。下面讲解此方法的特性

  1,<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产    生的。所谓类变量就是静态变量。

  2,虚拟机在执行子类的<clinit>()方法之前,虚拟机会保证父类的<clinit>()方法已经执行完毕。

  3,如果一个类中没有静态语句块或者这个类是个接口,编译器可以不为这个类生成<clinit>()方法。


  4,如果这个接口中有静态变量赋值操作,也会生成<clinit>()方法,但是与类不同的是,不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

  5,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的枷锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

每一个类加载器,都拥有一个独立的类名称空间,比较两个类是否相等 必须是在同一个类加载器加载的前提下进行比较。

双亲委派模型

从java虚拟机的角度来讲,只存在两种不同的类加载器,一种是启动类加载器,另一种就是其他所有的类加载器。

从开发人员的角度来讲,类加载器还可以划分得更细,分以下3种

1,启动类加载器:这个类加载器负责将存放在lib目录中的,或者被-xbootclasspath参数所制定的路径中的并且被虚拟机识别的类库(如rt。jar)加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。

2,扩展类加载器:这个加载器负责加载java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3,应用程序类加载器:这个类加载器是ClasLoader中的getSystemClassLoader()方法的返回值。它负责加载用户类路径(classPath)上所指定的类库。开发者可以直接使用这个类加载器。

我们自己定义的类加载器可以继承应用程序类加载器。

双亲委派模型的工作过程

如果一个类加载器受到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层级的类加载器都是如此,这样所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己加载。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容