美文网首页
虚拟机类加载机制

虚拟机类加载机制

作者: YoursBG | 来源:发表于2019-03-17 11:55 被阅读0次

    虚拟机类加载机制概述

    虚拟机把描述类的数据从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
    
    <ca><fe><ba><be>^@^@^@4^@^]
    ^@^F^@^O        ^@^P^@^Q^H^@^R
    ^@^S^@^T^G^@^U^G^@^V^A^@^F<init>^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*<b7>^@^A<b1>^@^@^@^A^@
    ^@^@^@^F^@^A^@^@^@^A^@  ^@^K^@^L^@^A^@  ^@^@^@%^@^B^@^A^@^@^@   <b2>^@^B^R^C<b6>^@^D<b1>^@^@^@^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  .....<init>...()
    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  .....<init>...()
    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,该指令存放于类构造器<clinit>()方法中。
    基本数据类型的零值表

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

    相关文章

      网友评论

          本文标题:虚拟机类加载机制

          本文链接:https://www.haomeiwen.com/subject/tzgjmqtx.html