第七章 虚拟机类加载机制

   虚拟机把描述类的数据从Class文件加载到内存 , 并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

启动类加载器:主要加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的类库加载到内存中.
扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器.
应用程序类加载器:它负责加载用户路径(classPath)上所指定的类库,如果应用程序中没有指定自己的加载器,则使用应用程序类加载器.

    一个类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段,其中验证、准备、解析3个部分统称为连接
    加载,验证,准备,初始化和卸载这5个阶段是顺序的.
    虚拟机规范严格规定了有且只有5种情况必须立即对类进行"初始化"
  •         遇到new,getstatic , putstatic , invokestatic这4个字节码指令时,如果类没有初始化
  •         反射时,如果类没有被初始化
  •         当初始化一个类,发现其父类没有初始化时,初始化父类
  •         当虚拟机启动时执行的主类
  •         1.7动态语言支持时
    加载
        1.通过一个类的全限定名来获取定义此类的二进制字节流
        2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
        3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口.
    验证
        这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.
        验证阶段大致会完成4个校验动作
            文件格式验证
                是否以魔数开头
                主次版本号是否在虚拟机的处理范围之内
                常量的tag
                指向常量的索引
                CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
                Class文件是否有改动
                ......
            元数据验证
                主要目的是对类的元数据信息进行语义校验 , 保证不存在不符合java语言规范的元数据信息.例如检查是否有父类....
            字节码验证
                是验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的 , 符合逻辑的.例如保证跳转指令不会跳转到方法体以外的字节码指令上去....
            符号引用验证
                可以看做是对类自身以外的信息进行匹配性校验,例如:符号引用中通过字符串描述的全限定名是否能找到对应的类.
    准备
        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存豆浆在方法区中进行分配.(仅包括类变量即static修饰的,实例变量会在初始化的时候在堆中分配)
        另外,如果字段属性值存在ConstantValue属性,则会初始化为ConstantValue属性指定的值,而不是初始值.(例如final修饰的类变量)
    解析
          解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程.
                符号引用:用一组符号来描述所引用的目标.引用的目标不一定已经加载到内存中.符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中.
                直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能直接定位到目标的句柄.如果直接引用存在,那么目标对象必定在内存中存在.
          16个操作符引用的字节码指令之前,先对他们所使用的符号引用进行解析.
          除了invokedynamic(用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法)指令之外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用
            并把常量标识为已解析状态)从而避免解析动作重复解析.invokedynamic指令的目的本来就是用于动态语言支持,它所对应的引用称为"动态调用点限定符",这里"动态"

第八章 虚拟机字节码执行引擎
    栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素.每一个方法从调用到执行完成的过程都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程
一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,大小存储在方法表的Code属性中.而仅仅取决于具体的虚拟机实现.
    在活动线程中,只有位于栈顶的栈帧才是有效的, 称为当前栈帧
栈帧的结构:
    局部变量表
        局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量.
        局部变量表的容量以变量槽(Slot)为最小单位,Solt可以存放32位以内的数据结构8种,但不表示Slot占32位长度的内存空间,对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间
     (和"long和double的非原子性协定"中把一次long和double数据类型读写分割为两次32位读写的做法类似),因为线程私有,所以无论是否两个连续的Slot都不会出现安全性问题.但是当单独访问一个连续的Slot
    时会报错.
        为了尽可能的节省栈空间,局部变量表中的Slot是可以重用的,Slot可以在变量的作用域外复用,但是会导致GCRoots一部分的局部变量表会保留其引用,即会影响垃圾回收.
添加运行参数:
-verbose:gc  //可以输出垃圾回收的信息
    
{
    byte[] placeholder =new byte[64* 1024 * 1024];
}
inta = 0;//如果不加此条代码,局部变量表中会保留placeholder的Slot值,从而导致回收失败
System.gc();
但也不应对置null操作有过多依赖.
    操作数栈
        同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中,操作数栈的每一个元素可以是任意的java数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量是2
        例如一个整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了int型整数,在执行时会将这两个int值出栈并相加,然后将相加的结果入栈.
        在大多数虚拟机实习那种,会令两个栈帧出现一部分重叠,让下面栈帧的部分操作数栈和上面栈帧部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递.
    动态连接
        每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.
    方法返回地址
        当一个方法执行完成之后,有两种方法可以退出这个方法
            1,遇到方法返回的指令.是否有返回值根据何种返回指令来决定.(正常完成出口)(可能是保存PC计数器的值)
            2.遇到未做处理的异常(返回地址是要通过异常处理器表来确定,栈帧一般不会保存)
        退出之后的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调整PC计数器的值.
    附加信息
        虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中.(与动态连接,方法返回地址合称栈帧信息)
    方法调用
        方法调用不等同于方法执行,一切方法调用在Class文件里面存储的都只是符号引用.
解析
     能在类加载的时候就能将符号引用转化为直接引用的方法叫非虚方法,反之则叫虚方法
        知道能被invokestatic和invokespecial指令调用的方法都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类.
    另外使用invokevirtual(调用虚方法指令)调用的final方法也属于非虚方法.
分派(多态的一些基本体现)
    静态分派(重载)(发生在编译阶段)
    所有依赖静态类型来定位方法执行版本的分派动作称为静态分派.静态分派的典型应用是方法重载.静态分派发生在编译阶段.
    例如:传参为'a',其会根据char->int->long->float->double的顺序进行转型匹配,去查找相应参数类型的方法.
    动态分派(重写)
    例如:创建一个接口Human,构造sayHello方法,然后man对象和woman对象实现Human,
            分别创建man和woman对象,调用sayHello方法
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
虚拟机是如何找到对应的方法的?
利用javap查看代码的字节码会发现
调用方法时,会将对象压入栈顶,然后通过invokevirtual来调用方法
执行invokevirtual时会大致执行几个步骤:
    1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C.(类型分为实际类型man和静态类型,静态类型也叫外观类型Human)
    2)如果在C中找到与常量中的描述符和简单名称都相符的方法,则进行权限校验,如果通过则返回直接引用,不通过抛异常
    3)否则,按照继承关系从下往上对C的各个父类进行2)操作
    4)如果始终没有找到合适的方法则抛AbstractMethodError异常
这个过程就是java重写的本质.
    单分派与多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种总量可以分为单分派和多分派.
因为静态分派时,需要考虑参数类型而有可能有多个总量选择.所以为多分派.
而运行阶段,只需要考虑接收者,方法的参数已经在静态分派的编译阶段处理过,所以只有单宗量,属于单分派
即,java语言直至1.8都属于"静态多分派,动态单分派的语言"
    虚拟机动态分派的实现
在虚拟机的实际实现中基于性能的考虑,大部分实现最常用的手段就是为类在方法区中建立一个虚方法表,使用虚方法表索引来替代元数据查找以提高性能.
    虚方法存放着各个方法的实际入口地址,当子类重写了父类方法时,存放的是子类的方法入口,如果没有重写,则存放的是父类的方法入口.
动态类型语言的支持
类型检查的主体过程是在运行期而不是编译期,就是动态类型语言.相对的,在编译期就进行类型检查过程的语言就是最常用的静态类型语言.
基于栈的字节码解释执行引擎
    许多java虚拟机的执行引擎在执行java代码的时候都有解释执行编译执行两种选择.
    解释执行
        程序源码->词法分析->单词流->语法分析->抽象语法树
        指令流(可选)->解释器->解释执行
        基于栈的指令集与基于寄存器的指令集
            基于栈的指令集优点:
                    可移植(寄存器由硬件直接提供,会受到硬件的约束)
                    代码更加紧凑
                    编译期实现更加简单
            缺点:
                    执行速度相对来说会稍慢一些
            因为完成相同功能所需的指令数量相对较多(出栈和入栈本身就产生了相当多的指令数量),更重要的是栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,
            相对于处理器来说,内存始终是执行速度的瓶颈.
        基于栈的解释器执行过程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值