前言

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

java虚拟机规范-加载、链接与初始化的更多相关文章

  1. Java类的加载 链接 初始化

    原文地址 Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码到能够 ...

  2. JVM详解之:类的加载链接和初始化

    目录 简介 加载 运行时常量池 类加载器 链接 验证 准备 解析 初始化 总结 简介 有了java class文件之后,为了让class文件转换成为JVM可以真正运行的结构,需要经历加载,链接和初始化 ...

  3. Java类的加载、链接和初始化

    一.Java的类加载机制回顾与总结: 我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象.这样 ...

  4. java 类的加载,链接,初始化

    本篇的话题,讨论Java类的加载.链接和初始化.Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类的对象.一个Java类从字节代码 ...

  5. java类从加载、连接到初始化过程

    类加载器 在了解Java的机制之前,需要先了解类在JVM(Java虚拟机)中是如何加载的,这对后面理解java其它机制将有重要作用. 每个类编译后产生一个Class对象,存储在.class文件中,JV ...

  6. JAVA类的加载、连接与初始化

    JAVA类的加载.连接与初始化 类的声明周期总共分为5个步骤1.加载2.连接3.初始化4.使用5.卸载 当java程序需要某个类的时候,java虚拟机会确保这个类已经被加载.连接和初始化,而连接这个类 ...

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

    Java中类的加载.连接和初始化 类的加载.连接和初始化 先介绍一下JVM和类 JVM和类: 当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多 ...

  8. 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

    目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...

  9. jvm系列(一):java类的加载机制

    java类的加载机制 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装 ...

随机推荐

  1. C++ 14 auto

    C++14标准最近刚被通过,像以前一样,没有给这个语言带来太大变化,C++14标准是想通过改进C++11 来让程序员更加轻松的编程,C++11引入auto关键字(严格来说auto从C++ 03 开始就 ...

  2. Codeforces 1262E Arson In Berland Forest(二维前缀和+二维差分+二分)

     题意是需要求最大的扩散时间,最后输出的是一开始的火源点,那么我们比较容易想到的是二分找最大值,但是我们在这满足这样的点的时候可以发现,在当前扩散时间k下,以这个点为中心的(2k+1)2的正方形块内必 ...

  3. [LeetCode] 47. 全排列 II

    题目链接 : https://leetcode-cn.com/problems/permutations-ii/ 题目描述: 给定一个可包含重复数字的序列,返回所有不重复的全排列. 示例: 输入: [ ...

  4. redis 教程(一)-基础知识

    redis 简介 redis 是高性能的 key-value 数据库,读的速度是110000次/s,写的速度是81000次/s ,它以内存作为主存储 具有以下优点: 1. 支持数据的持久化,将内存中的 ...

  5. MySQL explain,type分析(转)

    问题:explain结果中的type字段代表什么意思? MySQL的官网解释非常简洁,只用了3个单词:连接类型(the join type).它描述了找到所需数据使用的扫描方式. 最为常见的扫描方式有 ...

  6. SharePoint自己定义程序页面部署 不用重新启动IIS

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/dz45693/article/details/30840255 SharePoint的部署方式默认是 ...

  7. 使用axios发送ajax请求

    1.安装 npm install axios 2.在Home.vue中引入 import axios from 'axios' export default {   name: 'Home',   c ...

  8. 富文本编辑器Ueditor

    一.大概使用: 官网:http://ueditor.baidu.com/website/download.html 使用:[参考index.html] 3.1 引入ueditor的js <scr ...

  9. VB中IIF函数

    IIf 函数语法:IIf(表达式, 真值部分, 假值部分)根据表达式的值,表达式为真时,返回真值部分,表达式为假时,返回假部分.如:iif(a>0, "对","错& ...

  10. 去掉windows換行符^M

    在命令模式下运行命令 :%s/^M//g 回车注意:里面的^M 必须是同时按 Ctrl+V+M ,表示回车.不是直接输入 ^M,也不是粘帖复制.命令完成后,用:x 保存退出后,再次用vi打开就全部被替 ...