Java类的加载、连接和初始化

写在前面:在深度分析Java的ClassLoader机制(源码级别)中,咱们学习了Java的CLassLoader机制,那么,JVM将Java类加载完以后,也就是将二进制代码转换成java.lang.Class对象以后又作了哪些操做?java

 

1、Java的类加载机制回顾与总结:

咱们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象。这样Java就能够对该对象进行一系列操做,装载过程有两个比较重要的特征:层次组织结构和代理模式层次组织结构指的是每一个类加载器都有一个父类加载器,经过getParent()方法能够获取到。类加载器经过这种父亲-后代的方式组织在一块儿,造成树状层次结构。代理模式则 指的是一个类加载器既能够本身完成Java类的定义工做,也能够代理给其它的类加载器来完成。因为代理模式的存在,启动一个类的加载过程的类加载器和最终 定义这个类的类加载器可能并非一个。ClassLoader的加载类过程主要使用loadClass方法,该方法中封装了中加载机制:双亲委派模式安全

通常来讲,父类优先的策略就足够好了。在某些状况下,可能须要采起相反的策略,即先尝试本身加载,找不到的时候再代理给父类加载器。这种作法在Java的Web容器中比较常见,也是Servlet规范推荐的作法。好比,Apache Tomcat为每一个Web应用都提供一个独立的类加载器,使用的就是本身优先加载的策略。IBM WebSphere Application Server则容许Web应用选择类加载器使用的策略。
类加载器的一个重要用途是在JVM中为相同名称的Java类建立隔离空间。在JVM中,判断两个类是否相同,不只是根据该类的二进制名称,还须要根据两个类的定义类加载器。只有二者彻底同样,才认为两个类的是相同的。所以,即使是一样的Java字节代码,被两个不一样的类加载器定义以后,所获得的Java类也是不一样的。若是试图在两个类的对象之间进行赋值操做,会抛出java.lang.ClassCastException。这个特性为一样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求同一名称的Java类的不一样版本在JVM中能够同时存在。经过类加载器就能够知足这种需求。这种技术在OSGi中获得了普遍的应用数据结构

Java类的加载过程:架构

1.经过类的全名产生对应类的二进制数据流。(若是没找到对应类文件,只有在类实际使用时才抛出错误。)
2.分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不一样 JVM 有不一样实现)。这里处理了部分检验,好比类文件的魔数的验证,检查文件是否过长或者太短,肯定是否有父类(除了 Obecjt 类)。
3.建立对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链连接!)。jvm

Java类的连接

Java类的连接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在连接以前,这个类必须被成功加载。
连接的过程比加载过程要复杂不少,这是实现java的动态性的重要一步!分为三部分:verification检测), preparation准备) 和 resolution解析函数

1.verification检测):
验证是用来确保Java类的二进制表示在结构上是彻底正确的。若是验证过程出现错误的话,会抛出java.lang.VerifyError错误。布局

linking的resolve会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这以前,须要检测被引用的类型正确性和接入属 性是否正确(就是public ,private的的问题)诸如,检查final class 没有被继承,检查静态变量的正确性等等。
验证是用来确保Java类的二进制表示在结构上是彻底正确的。若是验证过程出现错误的话,会抛出java.lang.VerifyError错误。学习

2.preparation(准备):spa

准备过程则是建立Java类中的静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。线程

对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(由于这里不会执行任何 Java 代码)。具体以下:
全部原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提升的数据结构分配空间。

3.resolution(解析):

解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会致使其它的Java类被加载。

为类、接口、方法、成员变量的符号引用定位直接引用(若是符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
这一步是可选的。能够在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即便运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
另外,这一步与以后的类初始化是不冲突的,并不是必定要全部的解析结束之后才执行类的初始化。不一样的 JVM 实现不一样。
看下面一段代码:

public class LinkTest { public static void main(String[] args) { ToBeLinked toBeLinked = null; System.out.println("Test link."); } }

LinkTest引用了类ToBeLinked,可是并无真正使用它,只是声明了一个变量,并无建立该类的实例或是访问其中的静态域。若是把编译好的ToBeLinkedJava字节代码删除以后,再运行LinkTest,程序不会抛出错误。这是由于ToBeLinked类没有被真正用到。连接策略使得ToBeLinked类不会被加载,所以也不会发现ToBeLinked的Java字节代码其实是不存在的。若是把代码改为ToBeLinked toBeLinked = new ToBeLinked();以后,再按照相同的方法运行,就会抛出异常了。由于这个时候ToBeLinked这个类被真正使用到了,会须要加载这个类。

3、Java类的初始化

开发 Java 时,接触最多的是对象的初始化。实际上类也是有初始化的。相比对象初始化,类的初始化机制要简单很多。
类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。
当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操做。初始化过程的主要操做是执行静态代码块和初始化静态域。在一个类被初始化之 前,它的直接父类也须要被初始化。可是,一个接口的初始化,不会引发其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块 和初始化静态域。

public class StaticTest { public static int X = 10; public static void main(String[] args) { System.out.println(Y); //输出60  } static { X = 30; } public static int Y = X * 2; }

在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行。所以变量X的值首先初始化成10,后来又被赋值成30;而变量Y的值则被初始化成60。
类的初始化分两步:

1.若是基类没有被初始化,初始化基类。
2.有类构造函数,则执行类构造函数。

类构造函数是由 Java 编译器完成的。它把类成员变量的初始化和 static 区间的代码提取出,放到一个<clinit>方法中。这个方法不能被通常的方法访问(注意,static final 成员变量不会在此执行初始化,它通常被编译器生成 constant 值)。同时,<clinit>中是不会显示的调用基类的<clinit>的,由于 1 中已经执行了基类的初始化。该初始化过程是由 Jvm 保证线程安全的。。

Java类和接口的初始化只有在特定的时机才会发生,这些时机包括:

建立一个Java类的实例。如 MyClass obj = new MyClass() 调用一个Java类中的静态方法。如 MyClass.sayHello() Java类或接口中声明的静态域赋值。如 MyClass.value = 10 访问Java类或接口中声明的静态域,而且该域不是常值变量。如 int value = MyClass.value 在顶层Java类中执行assert语句。

经过Java反射API也可能形成类和接口的初始化。须要注意的是,当访问一个Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑下面的代码:

class B { static int value = 100; static { System.out.println("Class B is initialized."); //输出  } } class A extends B { static { System.out.println("Class A is initialized."); //不会输出  } } public class InitTest { public static void main(String[] args) { System.out.println(A.value); //输出100  } }

在上述代码中,类InitTest经过A.value引用了类B中声明的静态域value。因为value是在类B中声明的,只有类B会被初始化,而类A则不会被初始化。

 

连接:http://www.hollischuang.com/archives/201

相关文章
相关标签/搜索