虚拟机类加载机制

虚拟机类加载机制概述

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

Java与编译时需要进行连接工作的语言(C、C++)不同,Java中,类型的加载、连接和初始化过程都是在程序运行期间完成。

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析部分称为连接。

虚拟机规范严格规定有且只有5种情况必须立即对类进行“初始化”:

  1. 遇到new、getstatic、putstatic或invokestatic指令
  2. 使用java.lang.reflect包的方法对类进行反射调用
  3. 初始化一个类,其父类未进行初始化,则先触发其父类的初始化
  4. 虚拟机启动时,指定一个主类(包含main()方法的那个类)会被初始化
  5. 使用java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,则方法句柄对应的类触发初始化

注:已经初始化的类不会再次初始化

以下场景不会触发类的初始化

  1. 引用父类的静态属性
public class SuperClassTest {
    public static String name = "SuperClassTest";

    static {
        System.out.println("init SuperClassTest");
    }
}

public class SubClassTest extends SuperClassTest {
    static {
        System.out.println("init SubClassTest");
    }
}

public class InitClassTest {
    public static void main(String[] args) {
        System.out.println(SubClassTest.name);
    }
}

执行结果:

init SuperClassTest
SuperClassTest

可以看到,引用父类的静态属性,该类是不会触发初始化。

  1. 通过数组定义引用类
public class SuperClassTest {
    public static String name = "SuperClassTest";

    static {
        System.out.println("init SuperClassTest");
    }
}

public class InitClassTest {
    public static void main(String[] args) {
        SuperClassTest[] tests = new SuperClassTest[8];
    }
}

执行结果没有输出init SuperClassTest,说明SuperClassTest类并未被初始化。

  1. 使用类的常量
public class SuperClassTest {
    public final static String name = "SuperClassTest";

    static {
        System.out.println("init SuperClassTest");
    }
}

public class InitClassTest {
    public static void main(String[] args) {
        System.out.println(SuperClassTest.name);
    }
}

执行结果:

SuperClassTest

说明,调用类的常量,final static属性,并不会触发类的初始化。

类加载的过程

指加载、验证、准备、解析、初始化5个阶段。

  • 加载

注意,加载指的是类加载
在加载阶段,虚拟机需要完成以下3件事情:

  1. 通过类的全限定名来获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 生成一个代表该类的Class对象,作为方法区该类的各种数据访问入口

二进制字节流获取途径:

  1. ZIP、JAR、EAR、WAR包
  2. 网络中获取,类似Applet
  3. 动态代理获取,java.lang.reflect
  4. Proxy中获取,使用ProxyGenerator.generateProxyClass为特定接口生成“$Proxy”的代理类的二进制字节流
  5. 从数据库中获取

......

  • 验证

验证是连接阶段的第一步,该阶段目的是确保Class文件的字节流中包含信息符合当前虚拟机的要求。

Class文件的二进制流是可以修改的,假设没有验证环节,执行被修改的Class文件后果是不可预知的。
试验一下,当前目录创建Test.java

public class Test {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

编译,正常运行,会输出hello

$ javac Test.java
$ java Test
hello

vim -b 打开Test.class文件

$ vim -b Test.class
^@^@^@4^@^]
^@^F^@^O        ^@^P^@^Q^H^@^R
^@^S^@^T^G^@^U^G^@^V^A^@^F^A^@^C()V^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
SourceFile^A^@  Test.java^L^@^G^@^H^G^@^W^L^@^X^@^Y^A^@^Ehello^G^@^Z^L^@^[^@^\^A^@^DTest^A^@^Pjava/lang/Object^A^@^Pjava/lang/System^A^@^Cout^A^@^ULjava/io/PrintStream;^A^@^Sjava/io/PrintStream^A^@^Gprintln^A^@^U(Ljava/lang/String;)V^@!^@^E^@^F^@^@^@^@^@^B^@^A^@^G^@^H^@^A^@      ^@^@^@^]^@^A^@^A^@^@^@^E*^@^A^@^@^@^A^@
^@^@^@^F^@^A^@^@^@^A^@  ^@^K^@^L^@^A^@  ^@^@^@%^@^B^@^A^@^@^@   ^@^B^R^C^@^D^@^@^@^A^@
^@^@^@
^@^B^@^@^@^C^@^H^@^D^@^A^@^M^@^@^@^B^@^N

命令模式下输入“:%!xxd”,得出十六进制内容,下面显示前面部分内容

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829  ........()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN

把第二行的0010 0011改成0010 0012

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0012 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829  ........()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN

命令模式下输入“:%!xxd -r”,还原回来,并保持退出“:wq”,再运行java Test

$ java Test
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.ClassFormatError: Invalid constant pool index 18 in class file Test
        at java.lang.ClassLoader.defineClass1(Native Method)

发现执行结果会抛出异常,以同样的步骤,将0010 0012修改回0010 0011,执行java Test,发现执行结果正常。

其实,抛出异常就是在验证阶段进行的。

整体上,验证有4个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

  • 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,变量所使用的内存都将在方法区中进行分配。

  1. 通常情况下,基本数据类型的类变量初始值为零值
    private static int test = 123;

test值初始化时首先为0,直到执行putstatic指令,test被赋值为123,该指令存放于类构造器()方法中。
基本数据类型的零值表

数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char '\u0000' reference null
byte (byte)0
  1. 基本数据类型常量初始值为该常量
    private static final int test = 123;

在准备阶段时,如果字段属性表中存在ConstantValue属性,准备阶段时,test被赋值为该ConstantValue属性。

  • 解析

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

符号引用和直接引用

  1. 符号引用
    符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。与虚拟机实现的内存布局无关。
  2. 直接引用
    直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接锁定位到目标的句柄。与虚拟机实现的内存布局相关。

解析动作主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析
  • 初始化

类初始化阶段是类加载过程的最后一步,初始化阶段会执行类中定义的Java代码。

  1. ()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})
  2. ()方法与类的构造函数不同,它不需要显式调用父类构造函数,虚拟机保证子类()方法执行之前,父类的()方法已经执行完毕。
  3. 类与接口()方法不是必须的。
  4. 接口不能使用静态语句块,但可以有变量初始化赋值操作。执行接口()方法前不需要先执行父接口()方法。
  5. 虚拟机保证一个类的()方法线程安全。

你可能感兴趣的:(虚拟机类加载机制)