Java虚拟机--类加载机制

类文件结构

Class文件是一组以8位字节为基础的二进制流,中间没有添加任何分隔符。Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以描述数字、索引引用、数量值或者按照utf-8编码构成的字符串值。
整个Class文件本质上就是一张表,如下:
这里写图片描述

几个名词

全限定名: 描述路径,如com/android/test/TestClass;
简单名:字段的名称或方法的名称,如int a=0;简单名为a void getName(){} 简单名为getName
描述符:描述字段的数据类型 或 方法的参数列表 或 方法返回值

常量池表

eg:13abc 表示:tag=1(CONSTANT_Utf8_info utf8编码字符串),length=3(字符串长度3),bytes=abc

这里写图片描述

访问标志

这里写图片描述

字段表集合

这里写图片描述
字段表集合中还有属性表,后面会讲到

方法表

这里写图片描述
字段表集合中还有属性表,后面会讲到

属性表

这里写图片描述

字节码指令

加载和存储指令:

  • 将一个局部变量加载到操作栈的指令包括有:iload、iload_、lload、lload_、fload、fload_、dload、dload_、aload、aload_
  • 将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_、lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、astore_
  • 将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_
  • 扩充局部变量表的访问索引的指令:wide

运算指令

  • 加法指令:iadd、ladd、fadd、dadd
  • 减法指令:isub、lsub、fsub、dsub
  • 乘法指令:imul、lmul、fmul、dmul
  • 除法指令:idiv、ldiv、fdiv、ddiv
  • 求余指令:irem、lrem、frem、drem
  • 取反指令:ineg、lneg、fneg、dneg
  • 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
  • 按位或指令:ior、lor
  • 按位与指令:iand、land
  • 按位异或指令:ixor、lxor
  • 局部变量自增指令:iinc
  • 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

类型转换指令

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f

对象创建和访问指令

  • 创建类实例的指令:new
  • 创建数组的指令:newarray,anewarray,multianewarray
  • 访问类字段(static 字段,或者称为类变量)和实例字段(非 static 字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic
  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
  • 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
  • 取数组长度的指令:arraylength
  • 检查类实例类型的指令:instanceof、checkcas

操作数栈管理指令

pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap

控制转移指令

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
  • 复合条件分支:tableswitch、lookupswitch
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret

方法调用和返回指令

  • invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
  • invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic 指令用于调用类方法(static 方法)

异常处理指令

在程序中显式抛出异常的操作会由 athrow 指令实现,除了这种情况,还有别的异常会在其它 Java 虚拟机指令检测到异常状况时由虚拟机自动抛出。

同步指令

同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持

类加载机制

类生命周期
这里写图片描述
这几个阶段按顺序开始(解析可以在初始化之后),互相交叉混合运行。

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存(不一定是在Java堆中生成,HotSpot虚拟机的实现就是在方法区里生成的)中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但仍有密切关系。

  • 如果数组的组件类型是引用类型,那就递归采用类加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)
  • 如果数组的组件类型不是引用类型(例如int[]),Java虚拟机将会把数组标记为与引用类加载器关联
  • 数组类的可见性与它的组件类型的可见性一致。如非引用类型,则默认为public

验证

Class文件的来源可以多样化不一定是Java语言编译过来,可能存在安全等问题,验证阶段就是检验这个Class文件是否符合Java虚拟机的要求,是否会危害虚拟机自身的安全。

  • 文件格式验证:主次版本号是否在合理范围,常量池中是否有不被支持的常量类型…
  • 元数据验证:这个类是否有父类(除了Object其他都有父类),非抽象类是否实现了父类或接口中的方法…
  • 字节码验证:保证跳转指令不会跳转到方法体以外的字节码指令上…

设置-Xverify:none 参数可以关闭大部分的类验证措施,以缩短虚拟机类加载的时间

准备

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值(内存置0阶段)的阶段

解析

解析阶段是虚拟机将方法区常量池的符号引用替换为直接引用的过程

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机内存分布无关,引用目标不一定加载到内存中;且各虚拟机内存实现各不相同,但它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
  • 直接引用:直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机内存布局相关;如果有了直接引用,那引用目标必定已经在内存中存在。

解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符(后三种与动态语言息息相关)

  • 类和接口C:
    • 如果C不是数组类型,那虚拟机会把符号引用的全限定名传递给类加载器去加载C
    • 如果C是数组类型,那虚拟机会让类加载器去加载相关类(Integer[] 则去加载Integer类),接着由虚拟机生成一个代表此数维度和元素的数组对象
    • 如果以上无异常,此时C在虚拟机中已经是一个有效的类或接口了,后续还会确认代码所处的类对C的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。
  • 字段:解析字段主要对字段表内class_index项中索引的CONSTANT_Class_info(即字段所属的类或接口)符号引用进行解析。解析后会检验类或接口是否具备字段的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。字段所属的类或接口C查找过程如下:
    • 如C本身包含字段,则返回这个字段的引用,end
    • 否则,如C中实现了接口则按照继承关系从下往上递归搜索各接口和其父接口,找到则返回引用,end
    • 否则,如C不是java.lang.Object,则会按照继承关系从下往上递归搜索父类,找到则返回引用,end
    • 否则,查找失败,抛出java.lang.NoSuchFieldError异常
  • 类方法:解析类方法主要对类方法表的class_index项中索引的方法所属的类或接口的符号引用进行解析。后续还会确认类或接口是否具备此方法的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。类方法所属类C查找过程如下:
    • 类方法和接口方法符合引用的常量类型定义是分开的,如果发现class_index中索引的C是个接口抛出java.lang.IncompatibleClassChangeError异常
    • 在类C中查找是否有简单名称和描述符都与目标匹配的方法,end
    • 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标匹配的方法,end
    • 否则,在类C实现的接口列表以及它们的父接口中递归查找是否有简单名称和描述符与目标匹配的方法,end
    • 否则,查找失败,抛出java.lang.IllegalAccessError
  • 接口方法解析:解析接口方法主要对接口方法表的class_index项中索引的方法所属的类或接口的符号引用进行解析。后续还会确认类或接口是否具备此方法的访问权限,如不具备则抛出java.lang.IllegalAccessError异常。类方法所属接口C查找过程如下:
    • 与类方法解析不同,class_index中的索引C是个类而不是接口则抛出java.lang.IncompatibleClassChangeError异常
    • 在接口C中查找是否有简单名称和描述符都与目标匹配的方法,end
    • 否则,在接口C的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,end
    • 否则,查找失败,抛出java.lang.IllegalAccessError

初始化

初始化阶段是执行类构造器< clinit>() 方法的过程。

< clinit>()介绍

  1. < clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
  2. < clinit>()和类构造函数(或者说实例构造器< init>())不同,< clinit>()不需要显式调用父类构造器,虚拟机会保证子类的< clinit>()方法执行前,父类的< clinit>()已经执行完毕。< init>()则需要我们显式的调用super方法才会执行父类的构造方法
  3. 由于父类的< clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  4. < clinit>()对于类或接口来说并不是必有的,没有类变量和静态语句块的类编译器可以不为这个类生成< clinit>()方法
  5. 接口中不能使用静态语句块,但可以有变量初始化的赋值操作,所以会有< clinit>()方法生成。但接口的< clinit>()执行不需要先执行其父接口的< clinit>()
  6. 虚拟机会保证一个类的< clinit>()方法在多线程的环境下的安全性(被正确的加锁、同步,多线程去初始化一个类其他线程会阻塞,而且同一个类加载器下一个类只会初始化一次,这样第一个线程执行< clinit>()方法释放后其他线程也不会再次进去< clinit>())

此外,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问在它之前已经定义的变量,不能访问在它之后定义的变量但可以赋值

public class Test{
    static {
        i = 0;//正常通过,可以给在它之后的变量赋值
        Log.i(TAG,i);//编译器提示"非法向前引用"
    }
    static int i = 1;//定义i
}

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性(即比较两个类只有在这两个类是同一个类加载器加载的情况下才有意义,否则比较结果必然为否。这里的比较包括Class对象的equals()、isAssignableFrom()、isInstance()、instanceof)

类加载器的种类:

  • 启动类加载器,C++语言实现,虚拟机自身的一部分;放在\lib目录下
  • 其他类加载器,Java语言实现,独立于虚拟机外部
    • 扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,放在\lib\ext目录下
    • 应用程序类加载器/系统类加载器,由sun.misc.Launcher$AppClassLoader实现,getSystemClassLoader()返回的就是这个类加载器,负责加载用户类路径上所指定的类库。
    • 自定义的类加载器

双亲委派:
这里写图片描述
由于不同的类加载器加载同一个Class文件是没有对比意义的,如果java.lang.Object类被不同的类加载器加载很多次,jvm中存在了多个不同的Object类,那么java类型体系中最基础的行为也无从保证,应用程序会一片混乱(we know,all the class extends from java.lang.Object,如果Object存在多个品种,那绝对是灾难)。为了解决这类问题Java设计者推荐给开发者一种类加载器的实现方式,即双亲委派模型

双亲委派模型的工作过程:类加载器之间以组合的方式来复用父类的加载器;当需要加载一个类时,当前类加载器会将它交给父类加载器来执行任务,如果父类完成不了再由自己完成。这样每个java.lang.Object无论是哪个类加载器加载的最终都是由启动类加载器来加载。

字节码执行引擎

虚拟机是相对于物理机的一个概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和系统层面上;虚拟机的执行引擎则可以自行制定。Java虚拟机的执行引擎都是,输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

方法的调用(方法的选择)

回想一下Java虚拟机的内存模型,上面介绍的Java虚拟机的类加载过程,下面介绍方法的调用

方法调用阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法,非方法内部的具体运行过程)。一切的方法调用在Class文件里存储的都只是符号引用,而不是直接引用;而这个过程主要就是把符号引用替换为直接引用(符号引用和直接引用的概念上面类加载过程的解析阶段有讲)。而这个替换有一部分在类加载过程的解析阶段就已经完成,有的则需要在运行期间才能确定。

解析调用:解析调用一定是个静态的过程,在编译期间就完全确定,在类加载过程的解析阶段就会把涉及的符合引用全部替换为直接引用,这部分方法有静态方法、私有方法、实例构造器、父类方法。

分派调用:Java是静态多分派,动态单分派的语言

Human man = new Man();//Human为静态类型,Man为动态类型
  • 静态分派:所有依赖静态类型来定位方法执行版本的分派动作都是静态分派,静态分派的典型应用是重载,静态分派发生在编译阶段。

eg:

public class StaticDispatch {

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }

    static abstract class Human { }

    static class Man extends Human { }

    static class Woman extends Human { }

    public void sayHello(Human guy) {
        System.out.println("hello, guy");
    }

    public void sayHello(Man guy) {
        System.out.println("hello, man");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello, woman");
    }
}
/*
输出
hello, guy
hello, guy
*/

ps:类加载过程解析阶段的方法如果有重载方法,重载方法的选择也是通过静态分派来完成的。
- 动态分派:运行期根据是积累下确定方法执行版本的分派过程称为动态分派。
eg:

public class DynamicDispatch {

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }

    static abstract class Human { 
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        } 
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        } 
    }
}
/*
输出
man say hello
woman say hello
woman say hello
*/

方法的执行(基于栈的字节码解释执行引擎)

Java虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器JIT产生本地代码执行)两种选择,这里只讨论解释执行。
Java虚拟机的指令集是基于栈的,而Dalvik虚拟机则是基于寄存器的(x86也是基于寄存器的)。前者优点是可移植,后者优点速度快。
eg:

//1+1的执行过程
//1.基于栈的指令集
iconst_1
iconst_1
iadd
istore_0
//2.基于寄存器的指令集
mov eax,1
add eax,1
//结果保存在eax寄存器中

最后了解下基于栈的大致的执行过程
ps:这个过程只是一个大致的模型,实际相差甚大,因为会做一些优化操作。

备注:http://blog.csdn.net/xtayfjpk/article/category/2759219/1

猜你喜欢

转载自blog.csdn.net/xiaoru5127/article/details/77151518