《Java虚拟机》之类加载机制

一.类加载的时机 类从被加载到虚拟机内存开始,到卸载出内存为止,大致的生命周期为:加载(loading)->验证(Verification)->准备(Preparation)-》解析(Resolution)-》初始化(Initialization)->使用(Using)-》卸载(Unloading)等七个阶段。
在这当中,加载,验证,准备,初始化和卸载五大阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定,为了支持Java的运行时动态绑定,在某些情况下可以在初始化阶段之后再开始。
针对于初始化阶段,虚拟机规范严格规定了有且只有5中情况下必须立即对类进行“初始化”:

  1. 在遇到new ,getstatic,putstatic或者是invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。最常见的场景是:使用new关键字实例化对象的时候,读取或者是设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化。则需要先触发初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化.
二·类加载过程(第一次使用该类)
Java是使用双亲委派模型来进行类的加载,所以再描述类的加载过程前,我们先看一下它的工作流程:
双亲委派模型的工作流程:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈回来自己无法完成这个加载请求(即再它的搜索范围内没有找到所要加载的类)时,子类加载器才会尝试自己去加载。
使用双亲委派模型的好处在于,能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类,类加载器在执行加载时,始终只会加载其中的一个类。
1.加载
“加载”是“类加载”的一个阶段,在该阶段,虚拟机需要完成3件事情:
  1. 通过一个类的全限定名来获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个了的各种数据的访问入口
2.验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  1. 文件格式验证:验证字节流是否符合Class文件格式的规范,并且可以被当前版本的虚拟机处理:
  • 是否以魔数0xCAFEBABE开头
  • 主,次版本号是否在当前虚拟机处理范围内
  • 常量池的常量是否有不被支持的常量类型(检查常量tag标志)
  • 指向常量的各自索引值是否有指向不存在的常量或者是不符合类型的常量-------
2.元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范,其验证点有:
  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
  • 这个类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或者是接口之中要求实现的所有方法
  • 类中的字段,方法是否与父类产生矛盾--------
3.字节码验证
这个阶段是最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。继对元数据信息的数据类型做完校验,这个阶段对类的方法体进行校验分析,保证安全性:
  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 保证方法体的类型转换是有效的-----
3.准备
准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。在这里,要明确的是这时候进行内存分配的仅仅包括类变量(被static修饰的变量),而不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在Java堆中。
4.解析
将常量池中的符号引用转换为直接引用(得到类或者是字段,方法在内存中的指针或者是偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
【《Java虚拟机》之类加载机制】解析需要静态绑定的内容 //所有不会被重写的方法和域都会被静态绑定。
以上2,3,4三个阶段又合称为链接阶段,链接阶段要做的就是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中
5.初始化(先父后子)
(1)为静态变量赋值
(2)执行static代码块 (static代码块只能有JVM调用)
如果是多线程需要同时初始化一个类,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。因为子类存在着对父类的依赖关系,所以类的加载顺序是先加载父类后加载子类,初始化也一样。不过,父类初始化时,子类静态变量的值也是有的,为默认值。最终,方法区会存储当前类的类信息,包含类的静态变量,类初始化代码(定义静态变量时的复制语句和 静态初始化代码块),实例变量定义。实例初始化代码(定义实例变量时的赋值语句,实例代码块和构造方法)和实例方法,还有父类的类信息引用。
类初始化阶段是类加载过程的最后一步,这也是真正开始执行类中定义的Java字节码。初始化阶段是执行类构造器()方法的过程。
  • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{ })中的语句合并而成,编译器收集的顺序是由语句再源文件出现的顺序所决定的。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
  • ()方法与类的构造函数(或者说是实例构造器()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的client>()方法执行之前,父类的()方法已经执行完毕了。因此在虚拟机中到一个被执行的()方法的类肯定是java.lang.Object。
  • 因为父类的()方法先执行完毕,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作
三.创建对象 Java在new一个对象的时候,首先会查看对象所属的类有没有被加载到内存,如果没有的话,将会通过类的全限定名来加载。加载并初始化类完成后,再进行对象的创建工作。
1.在堆区分配对象所需要的内存
分配的内存包括本类和父类的所有实例变量,但不包括任何静态便变量
2.对所有的实例变量赋予默认值
将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值
3.执行实例初始化代码
初始化顺序是 先初始化父类再子类。初始化时先执行实例代码块然后是构造方法
4,如果有类似于Children c=new Children()形式的c引用的话,在栈区定义Children类型引用变量C,然后将堆区对象的地址赋值给它。
需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super()关键字来调用父类对象,但在外部不访问。
补充:
通过实例引用调用实例方法的时候,先从方法区中对象的实际类型信息中找,找不到的话再去父类类型信息中找。
如果继承的层次比较深,要调用的方法位于较上层的父类,则调用的效率是比较低下的,因为每次调用都要经过多次查找。这个时候大多系统会采用一种称为虚方法表的方法来优化调用效率。
所谓虚方法表,就是在类加载的时候,为每个类创建一个表,这个表包含了该类的对象所有动态绑定的方法以及其地址,包括父类的方法,但是一个方法只有一条记录,子类重写了父类方法后只会保留子类的。当通过对象动态绑定方法的时候,只需要查找这个表就可以了,而不需要挨个查找每个父类。
>参考《深入理解Java虚拟机》
>争渡争渡,惊起一滩欧鹭。
==欲知后事如何,请见下回分解==

    推荐阅读