• java虚拟机规范-加载、链接与初始化


    前言

    java虚拟机是java跨平台的基石,本文的描述以jdk7.0为准,其他版本可能会有一些微调。java代码本身并不能为jvm识别,实际上在jvm中的表现形式为Class对象,一个java类从字节码到能够在jvm中正常运行,需要经过加载-》链接-》初始化三个步骤。

    引用

    虚拟机的启动

    • java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类来完成,这个类是由虚拟机的具体实现指定。紧接着,JAVA虚拟机链接这个初始类,初始化并调用它的main方法。之后整个执行过程都是由对此方法的调用开始。
    • 启动过程如图所示:

    加载

    类加载器层次结构图

    • 在java中,所有的类都是对其第一次使用时,动态加载到JVM中。当程序创建第一个对类的静态成员(方法、变量)的引用时,就会加载这个类。这个证明了构造器也是类的静态方法,即使在构造器之前并没有使用static关键字,使用new操作符创建类的新对象也会被当做对类的静态成员的引用。

    定义

    • 注意加载只是类加载中的一个阶段,在加载阶段虚拟机主要做以下三件事情:
      • 通过一个类的全限定名来获取此类的二进制字节流,(例如class文件中的部分数据、zip包、applet等,执行该步骤的模块称为类加载器
      • 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
      • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    • 在加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区中,然后在内存中实例化一个java.lang.class对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。
    • java类的加载是由类加载器来完成的。类加载器分为两类:
      • 启动类加载器(bootstrap),JVM原生提供,使用C++实现(注意这里特指hotspot虚拟机,有一些不是)
      • 用户自定义类加载器(user-defined),用户自定义实现,继承自java.lang.ClassLoader类。
    • 类的加载方式分为两种:显式加载和隐式加载,这两种方式都是调用classloader类中的loadClass方法来完成类的实际加载工作的。直接调用Classloader中的loadClass方法是另外一种不常用的显式加载类的技术。
      • 显式加载:使用Class.forname的方式就是显式加载
      • 隐式加载:使用new创建实例就是隐式加载。
    • 类加载器有很多用途,例如java热替换技术,jvm中相同类的隔离等。

    链接

    • 链接类或接口包括验证、准备、解析。其中解析是可选的部分。
    • java虚拟机规范允许灵活的选择链接发生的时机,但是必须符合以下规范:
      • 在类或者接口被链接之前,它必须被成功的加载过
      • 在类或者接口初始化之前,它必须被成功的验证及准备过
      • 程序的直接或者间接行为可能会导致链接发生,链接过程中检查到的错误应该在请求链接的程序处被抛出。

    验证

    • 验证(verification)阶段用于确保类或者接口的二进制表示结构是正确的。验证过程中可能会导致某些额外的类或者接口被加载进来,但是不应该导致它们也需要验证或者准备。

    准备

    • 准备(preparation)阶段的任务是为类或者接口的静态字段分配空间,并用默认值初始化这些字段,这个阶段不会执行任何的虚拟机字节码指令。(注意在初始化阶段会有显式的初始化器来初始化这些静态字段,所以准备阶段不做这些事情),注意以下几点:
      • 准备阶段进行内存分配的仅包括类变量(被static修饰),不包括实例变量。实例变量会在对象实例化时随着对象一起分配到java堆中。
    • 示例:
      • 这里的初始值“通常情况下”是数据类型的零值,例如public static int val=23;,那么在准备阶段过后,val的值将设置为0,而不是23。而把val值赋值为23的putstatic指令是程序被编译后,存放于类的构造器<cinit()>方法之中,所以把val值赋值为23的动作将在初始化阶段才会执行,但是如果类的字段属性表中存在ConstantValue属性(final修饰符),那么在准备阶段就会初始化完成,例如public static final int val=23,那么在准备阶段的val值就为23.
    • 具体的代码和字节码参加下图:

    解析

    • 解析(Resolution)是根据运行时常量池的符号引用来动态决定具体值的过程,java虚拟机指令(anewarray,checkcast,getfield,getstatic,instanceof,putstatic,new,invokespecial,invokevirutal)等将符号引用指向运行时常量池。执行上述任何一条指令都需要对它的符号引用进行解析。
    • 本步骤是符号引用,它与直接引用的区别在于:
      • 符号引用只能告诉你怎么无歧义的定位到目标,该值明确规定在class文件中
      • 直接引用是直接指向目标,(指针、偏移量、句柄)。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般都不相同

    初始化

    • 初始化是类加载的最后一步,在前面的类加载过程中,基本上动作都是由虚拟机主导和控制(除了用户自定义类加载器)。到了初始化阶段才开始真正的执行类定义中的JAVA程序代码(或者说是字节码)。初始化阶段可以看做是执行类构造器<cinit()>方法的过程
      • 该方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static)合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到之前定义的变量,但是可以赋值。比如下面的这段代码:
      • 虚拟机会保证父类的<cinit()>方法一定在子类之前执行,因此第一个执行该方法的类一定是java.lang.Object。因此父类的赋值操作一定是在子类之前,即必须把父类中的所有赋值操作执行完毕后才会执行子类的赋值操作
      • 如果一个接口/类中没有静态语句块,也没有对变量的赋值操作,那么编译器就不会为该类生成<cinit()>方法。
      • 多个线程去初始化同一个类,那么只有一个线程去执行该类的<cinit()>方法,其他线程都需要阻塞等待,注意同一个类加载器中,一个类型只会被初始化一次。

    案例

    案例一-关于非法向前引用

    • 仍然以上面的代码为例,非法向前引用这种编译检查,是未了防止静态字段在被初始化之前就被独走默认值,这会导致问题难以诊断。可能有细心的读者会发现在方法体中不会出现该问题,比如下面的这段代码:
    • 这段代码不会抛错,最后执行结果为0.为什么方法和类可以消除向前引用,而变量不可以呢?这个是因为java运行时为了实现向前引用,在初始化所有字段之前会把所有的字段添加到符号表中,以便可以调用这些字段。不过由于还没有初始化这些字段,所以符号表中所有字段都使用默认的值。上图中的代码最后返回的额结果也是0.

    案例二

    • 上述这段代码其实是有问题的,真正运行的时候会抛NPE:
      • 在java虚拟机启动的过程前期,加载和链接过程都是没有问题的。但是在初始化的步骤(执行<cinit()>方法)中,由于编译器收集顺序是语句在源文件中出现的顺序,而res变量的赋值操作在add静态语句块之后。因此在执行add方法的时候,res变量只有一个默认值null。因此会导致该段代码抛NPE
  • 相关阅读:
    Nginx 配置指令location 匹配符优先级和安全问题【转】
    服务器压力测试 ab
    Linux 下绑定域名与IP地址
    nginx 均衡负载配置
    Centos下搭建ftp服务器
    php开发环境搭建
    史上最全Vim快捷键键位图(入门到进阶)
    Linux vi/vim
    CentOS用户权限管理--su与sudo
    Linux基础知识-文件管理
  • 原文地址:https://www.cnblogs.com/editice/p/5420723.html
Copyright © 2020-2023  润新知