本文目录#

引言#

众所周知,Java的slogan就是”Write once, run anywhere.”,这也就意味着无论我们在什么平台的机器上用Java去做实现,都可以在任何支持Java的系统上直接运行,无需做任何额外操作。
Java是如何做到这些的呢?答案是JRE。

  • 那么什么是JRE?为什么叫JRE?
    • JRE(Java Runtime Environment),是一个Java代码的运行时环境,属于软件层,运行在操作系统软件之上,属于JDK的一部分。
  • 什么是JDK?
    • JDK(Java Development Kit),每一个JDK都包含了一个兼容的JRE和一个JVM,并且JDK包含了许多Java开发人员常用的工具以及类库,比如javac、java、jar、jmap、jstat、jstack、jinfo、rt.jar等。
  • 什么是JVM?
    • JVM(Java Virtual Machine),JVM可以理解为是一个运行在操作系统之上的虚拟电脑,当我们通过javac*.java编译成JVM可识别*.class字节码文件后,再执行java,此时JVM会将*.class字节码文件解释成当前操作系统平台可识别的机器码去执行。这样的话就实现了”Write once, run anywhere.”。
  • 整体流程如下所示

JVM类加载的过程#

加载阶段:#

  1. 通过类的全限定名来读取class字节码文件的二进制流
  2. 将字节流中的静态数据结构转化为方法区的运行时数据结构
  3. 在内存中生成代表这个类的java.lang.Class对象,作为方法区中这个类中的各种数据结构的访问入口
    • 注意:
      • 这只是类加载的其中一个阶段,不要和类加载混淆
      • 加载阶段和链接阶段中的部分动作是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始

链接阶段(linking)#

  1. 验证
    1. 文件格式验证:确保class文件的字节流中包含的信息符合虚拟机规范,并且不会危害虚拟机自身的安全
    2. 元数据验证:语义分析
      1. 是否有父类(除java.lang.Object外,所有的类都必须有父类)
      2. 是否继承了不该继承的类
      3. 如果不是抽象类,是否实现了父类或接口中要求实现的所有方法
      4. 字段、方法是否与父类冲突
    3. 字节码验证:
      1. 确定语义合法、符合逻辑
      2. 类的方法不会做出危害虚拟机的事件
    4. 符号引用验证:发生将符号引用转化为直接引用的时候 -> 解析时
      1. 全限定名是否能找到对应的类
      2. 指定类中是否存在被引用的方法和字段
      3. 符号引用中的类、字段、方法的访问性是否可以被当前类访问(private、protected、public、default)
  2. 准备:
    1. 为类变量(被static修饰的)在方法区中分配内存,实例变量在对象实例化时分配在Java堆中
    2. 设置初始值,此时是赋零值,真正的值在初始化时完成赋值;finnal例外,直接赋值;
      1
      2
      3
      4
      5
      public static int value = 99;
      public static final int value = 99;
      准备阶段结束后
      public static int value = 0;
      public static final int value = 99;
  3. 解析:将常量池中的符号引用替换为直接引用
    //TODO 什么是符号引用和直接引用?
    1. 类或接口解析
    2. 字段解析
    3. 类方法解析
    4. 接口方法解析

初始化#

  1. 这是类加载过程的最后一步,除了在加载阶段用户可以通过自定义类加载器参与,其他阶段皆由虚拟机主导和控制
  2. 在该阶段开始真正初始化类中定义的Java程序代码(或者说是字节码),调用<clinit>()
    在遇到以下几种情况时,触发初始化:
    • 当虚拟机启动时,初始化用户指定的主类;
    • 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
    • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
    • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
    • 子类的初始化会触发父类的初始化;
    • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
    • 使用反射 API 对某个类进行反射调用时,初始化这个类;
    • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

至此,一个字节码文件便已经初始化完成。

本文总结#

加载一个class类的过程总体分三个步骤,加载、链接、初始化,其中链接阶段分为验证、准备、解析三个阶段,加载阶段通过类的全限定名来读取class字节码文件的二进制流,并将字节码数据转化为方法区的运行时数据结构。链接阶段中的验证阶段对字节码文件进行格式和安全校验,准备阶段为类中的部分变量(被static修饰的变量)分配内存和初始值的赋值,解析阶段将常量池中的符号引用替换为直接引用。初始化阶段会将静态代码的赋值操作和静态代码块中的代码交给<clinit>()方法进行初始化,完成变量的赋值以及资源的分配。

相关问题#

虚拟机是如何做到通过类的全限定名去找到对应的Class文件的?#


知识共享许可协议

本作品系原创,采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,转载请注明出处。