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

虚拟机类加载机制

作者: ythmilk | 来源:发表于2020-12-20 21:23 被阅读0次

    类文件结构

    Class文件是一组以8字节为基础单位的二进制流,中间没有分隔符。


    类文件结构

    魔数:4个字节CA FE BA BE
    版本号:2个字节的次版本号
    2个字节的主版本号

    常量池

    image.png

    接下来2个字节代表常量池的长度

    常量池的长度是从1开始的,因此真正的大小要减1,比如0x 00 16 则说明一共是22个,从1开始,索引值是1-21

    真内容开始的常量池的偏移量是0x00 00 00 0A
    流程就是,从第一个常量池的偏移量开始开始,首先看tag,tag标识常量池的类型
    下面是CONSTANT_Class_info型常量的结构,07 00 02,07代表tag(CONSTANT_Class_info),00 02 代表name_index,表示常量池的第二个常量。依次类推进行解析

    {
    u1 tag,
    u2 name_index
    }
    二进制字节码过于复杂,使用javap -v 类名 进行字节码分析
    原始类:
    ```java
    package com.yth.demo.demo02;
    public class TestClass {
        private int m;
        public int inc(){
            return m+1;
        }
    }
    

    javap -v 之后

       #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#19         // com/yth/demo/demo02/TestClass.m:I
       #3 = Class              #20            // com/yth/demo/demo02/TestClass
       #4 = Class              #21            // java/lang/Object
       #5 = Utf8               m
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/yth/demo/demo02/TestClass;
      #14 = Utf8               inc
      #15 = Utf8               ()I
      #16 = Utf8               SourceFile
      #17 = Utf8               TestClass.java
      #18 = NameAndType        #7:#8          // "<init>":()V
      #19 = NameAndType        #5:#6          // m:I
      #20 = Utf8               com/yth/demo/demo02/TestClass
      #21 = Utf8               java/lang/Object
    

    字段和方法类似,只有被引用之后才会被编译到常量池中

    字段结构:

    字段结构
    真实字段编译后:
    image.png
    字段的引用格式如下:
    image.png
    即:com/jvm/demo01/TestConstant.m:I

    方法结构

    image.png
    方法的引用格式:
    image.png
    具体jvm中
    image.png
    即:com/jvm/demo01/TestConstant.getM:()I

    访问标志

    常量池之后2字节代表访问标识,用于标识类或者接口的访问信息,包括Class是类还是接口、是否是public,是否是abstract,是否是final等

    类索引、父类索引、接口索引集合

    Class文件用这三项数据来确定该类型的集成关系,类索引、父类索引都是一个u2类型数据,而接口索引集合是一组u2类型的数据集合。
    上述代码的类索引部分
    03 04 00,03代表常量池的偏移量

    字段表集合

    用于描述接口或类中声明的变量


    表字段结构

    access_flags访问标示
    name_index:简单名称(字段名称)
    description_index:描述字段的类型,如下表


    字段类型对应表
    后面两项为属性表

    方法表集合

    描述class中的方法结构,结构与方法表一致。
    name_index:方法名称(没有参数,没有修饰符,没有返回值,上面例子中的inc)
    description_index:描述符,按照(参数列表)返回值 的格式,上面inc方法的描述符就是()V;方法 java.lnag.String.toString()的描述符就是()Ljava/lang/String;方法void test(int a,int b)的描述符是(II)V

    属性表

    Class文件、字段表,方法表,都可以携带属性表,下面是部分属性表项


    image.png

    只介绍Code属性
    方法中的代码经过JVM编译之后,变为字节码指令存入Code属性中。


    image.png
    attribute_name_index:指向常量池偏移量,代表属性名称,此处固定位Code
    attribute_length:属性值的长度

    max_stack:代表操作数栈的最大深度
    max_locals:代表局部变量表所需的存储空间,空间单位是槽,该值不是方法中所有局部变量个数集合,而是各作用域中局部变量数量的最大值
    code_length代表字节码长度,code是用于存储字节码指令的一系列字节
    流。每个指令大小为u1
    code虽然是u4,但其实限制65535条指令

    this:过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方
    法参数的访问,因此任何方法都至少有一个局部变量this,该约定只对实例对象有效,类对象不适用(static方法)

    类的生命周期

    加载、连接(验证、准备、解析)、初始化、使用、卸载


    加载

    负责以下内容

    1. 通过一个类的全限定名获取此类的二进制字节流
    2. 将字节流代码的静态储存结构转化为方法区的运行时数据结构
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类属性的访问入口

    类加载的时机

    jvm没有规定类加载的时机,但是规定了初始化的时机,类的加载必须在初始化之前。

    类初始化时机

    1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
    • 使用new关键字实例化对象
    • 读取或设置一个类型的静态字段 (编译时静态常量除外)

    被 static final 修饰的变量称为常量:编译时常量(static final int A = 1024)、运行时常量(static final int len = "Rhine".length())。

    编译期常量会在编译阶段存入调用类的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

    • 调用一个类的静态方法时
    1. 使用java.lang.reflect包的方法对类型进行反射调用时
    2. 初始化类时,触发父类初始化(接口初始化时,父接口不需要完成初始化)
    3. 包含main函数的类,虚拟机启动时会初始化这个类
    4. java.lang.invoke.MethodHandle调用的方法如果是1.那四种
    5. 接口如果定义了default关键字,如果实现类发生了初始化,则接口也需要

    上述六中是有且仅有。其他情况不会触发初始化,比如数组变量、比如静态变量(只会触发直接定义这个字段的类的初始化,子类并不会)

    验证

    验证阶段保证Class文件的字节流符合要求

    1. 文件格式校验:基于二级制流校验,保证字节流可以正确解析并存入方法区中
    2. 元数据校验:对字节码描述的信息进行语义校验,校验类的元数据符合语义(比如类是否有父类、字段是否与父类矛盾等)
    3. 字节码验证:主要对类的方法进行校验分析(现在很多都移到javac阶段进行)
    4. 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候

    准备阶段

    为类中定义的变量(静态变量,被static修饰的变量)分配内存,并设置类变量初始值(一般是0值,但如果是编译时常量static final,则会直接赋值)。1.8之后类变量会随着Class对象一起放入java堆中。

    解析

    解析阶段是将常量池内的符号引用转化为直接引用的过程
    符号引用:用一组符号来描述引用的目标(见常量池)
    直接引用:可以直接指向目标的指针、偏移量或间接指向目标的句柄

    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引调用进行

    初始化

    在,准备阶段,已经给静态变量初始化过一次零值,在初始化阶段进行真正的赋值。初始化阶段就是执行类构造器<clinit>()方法的过程(不是构造函数)。

    <clinit>()是所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的


    image.png

    <clinit>()不是必须的,如果没有静态变量和静态代码块,则没有该函数

    父类的<clinit>()函数会在子类执行之前执行

    Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,同一个类加载器下,一个类型只会被初始化一次。即:如果多线程环境下初始化类,一个线程退出后,其他线程不会再次进行初始化。

    类加载器

    类在使用的时候才会去加载,加载的时候会使用双亲委派的方式去加载

    类由类本身和加载这个类的类加载器一起确定包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()、instanceof关键字


    image.png

    ·启动类加载器(Bootstrap Class Loader):加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,并且可以被JVM识别的类。比如java、javax、sun等开头的类

    启动类加载器获取的是null

    扩展类加载器(Extension Class Loader):它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。

    只要把自己的jar包放到这下面,也可以被加载的。

    应用程序类加载器(Application Class Loader):getSystem-ClassLoader()方法的返回值,系统默认的加载器,如果没有自定义类加载器,就是这个类加载的

    双亲委派:除了顶层的启动类加载器外,每个类加载器都要有父加载器,父加载器不是继承关系,更像是组合。
    每个类,都先由父类进行加载,如果父类抛出ClassNotFoundException 异常,再由自己类加载(调用自己的findClass方法)。这样可以保证,类和加载器一起有了一个优先级关系。比如Object这种基础类由基础类加载器加载。保证不管哪个类加载器加载类似Object这种类,都是同一种类型。
    即:
    1、保证核心类的安全。防止开发者取了和jdk核心类库中一样的包名和类名,委托给父类加载器能保证JDK类库的类优先加载。
    2、防止已经被加载的类多次加载(一待有父类加载过,子类就不会再次加载了)

    双亲委派的破坏:SPI、OSGI(星状)
    SPI:使用线程线程上下文类加载器解决这个问题(启动类加载JNDI类,然后通过线程上下文类进行加载实现类)
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    SPI中ServiceLoader

        public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    

    数据库驱动加载(SPI方式):DriverManager
    启动的时候进行SPI加载:

        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
    

    然后loadInitialDrivers加载SPI实现类:

      ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    try{
                        while(driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch(Throwable t) {
                    // Do nothing
                    }
    

    其中driversIterator.next()里面最终通过c = Class.forName(cn, false, loader);进行类加载
    其中loader为loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    正常的情况下,ClassLoader.getSystemClassLoader()Thread.currentThread().getContextClassLoader()都是sun.misc.Launcher$AppClassLoader@18b4aac2

    自定义类加载器:

    必要性:

    • 隔离加载类
    • 修改类的加载方式
    • 扩展加载源
    • 防止源码泄漏

    不同的类加载器都去loadClass,那这个返回值会是什么 Class<?> c = findLoadedClass(name)。实验都是null,是和类加载器有关系吧。

    如果一个类是由用户类加载器加载的,JVM会将类加载器的一个引用作为类型信息的一部分保存在方法区中

    image.png

    如何判断这个类是有这个类加载器加载的(双亲委派中),还是由AppClassLoader加载的
    因为他们都是平级的?

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

    用户自定义:
    loadClass(String name)
    findClass+definClass搭配使用

    获取classLoader方式

            System.out.println(Thread.currentThread().getContextClassLoader());
            System.out.println(Main.class.getClassLoader());
            System.out.println(ClassLoader.getSystemClassLoader().getParent());
    

    相关文章

      网友评论

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

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