类的加载机制2

来源:互联网 发布:罗马2 兵种数据修改 编辑:程序博客网 时间:2024/06/18 09:28

一、加载

JVM主要完成三件事:

1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,通过jar包、war包、网络中获取、JSP文件生成等方式。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,但最终的元素要靠类加载器创建。一个类与类加载器必须一起确定唯一性

二、类的连接

类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE中。类的连接大致分三个阶段。

1、验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。四个阶段的验证动作:元数据验证、字节码验证、符号引用验证、文件格式验证。(还记得class文件结构中的头4个字节的魔数概念吗?

文件格式验证:字节流是否符合Class文件格式,比如前四个字节是否以魔数0xCAFEBABY开头

元数据验证:比如一个类是否有父类,这个父类是否合法,如果是final的那就不合法。

字节码验证:最为复杂,逻辑是否正确,例如跳转指令不会跳到方法体以外的字节码。

符号验证:符号引用--》直接引用(解析)。该阶段确保解析的正常运行,比如能否根据全限定名访问到类

验证是重要的,但不是必要的。

2、准备:为类的静态变量在方法区分配内存,并赋默认初值(0值或null值)。如static int a = 100;静态变量a就会在准备阶段被赋默认值0,该赋值阶段不包括实例变量对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。

另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666;  静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。

3、解析:将类的二进制数据中的符号引用换为直接引用。

我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

 

三、初始化

类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码初始化阶段就是执行类构造器clinit()的过程。 
clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化

初始化过程的注意点******************

clinit()方法中静态成员变量的赋值顺序是根据Java代码中成员变量的出现的顺序决定的。

static{}能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量;能给出现在静态代码块之后的静态成员变量赋值。

构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法。

如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法。

接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。

接口中不能使用静态代码块。

接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法。

虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其它的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次


四、类加载器

对于任何一个类都需要它和它的类加载器一起确立唯一性。比较两个类是否相等,要在同一个类加载器前提下才有意义。

(1)启动类加载器:lib目录中的类库加载到jvm中

(2)扩展类加载器:lib/ext目录中的类库加载到jvm中

(3)应用程序类加载器:

双亲委派模型:

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。
   双亲委派模型的式作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完全这个加载请求时,子加载器才会尝试自己去加载。

比如:java.lang.Object类在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派到顶层的启动类加载器,所以Object类在程序的各种类加载器环境中都是同一个类。否则不是双亲委派模型,由个各类自己自行加载的话,我自己编写了java.lang.Object类放到classpath中,那系统就有多个不同的Object类了。


原创粉丝点击