虚拟机类加载机制

2020-12-20  本文已影响0人  ythmilk

类文件结构

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这四条字节码指令时

被 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());
上一篇下一篇

猜你喜欢

热点阅读