类加载机制

类加载过程

stateDiagram-v2
    direction LR
    state 链接 {
        direction LR
        验证 --> 准备
        准备 --> 解析
    }
    加载 --> 链接
    链接 --> 初始化
    初始化 --> 使用
    使用 --> 卸载

将.class字节流实例化成Class对象并进行相关初始化的过程

加载

对于数组类的创建:数组类本身不通过类加载器创建,由Java虚拟机直接在 内存中动态构造出来的

验证

目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内

元数据验证

对字节码描述的信息进行语义分析,主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息

字节码验证

对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

符号引用验证

这个阶段检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源

准备

准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存

基本数据类型的初始值:

数据类型 零值
int 0
long 0L
short (short)0
char '\u0000'
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

解析

确保类与类之间的相互引用正确性

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

类或接口的解析

在解析类的时候,如果当前处于类D,则加载其他类的职责会交给类D的类加载器,在加载类时,会检查类D是否有访问权限(修饰符以及模块访问权限)

字段解析

字段解析的过程中会按照继承链从下往上进行查找,当查找到引用时,也会进行检查权限

类方法解析

方法解析首先会判断是否为接口,如果是接口 直接抛出异常

否则跟字段解析一样递归从下往上查找,递归查找不到就会查找父接口等,再找不到就抛出异常,如果找到了同样也会进行权限检查

接口方法解析

如果发现不是接口 抛出异常

递归查找父接口 直到Object 否则就查找失败

同样如果查找到会进行权限检查

初始化

初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程

<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的

static {
    i=1; // 可以赋值
    System.out.println(i); // 无法通过编译
}
static int i =0;
static class Father {
    static int a= 1;
    static {
        a=2;
    }
}
static class Son extends Father{
    static int b = a;
}
public static void main(String[] args) {
    System.out.println(Son.a); // 2
}

类加载时机

主动引用

被动引用

除此主动引用外,所有引用类的方式都不会触发初始化,称为被动引用

static class Father {
    static  {
        System.out.println("father init");
    }
    static int value = 123;
}
static class Son extends Father{
    static {
        System.out.println("son init");
    }
}
public static void main(String[] args) {
    System.out.println(Son.value); // fatcher init ...
}
static class Ref{
    static {
        System.out.println("ref init");
    }
}
public static void main(String[] args) {
    //Ref[]这个类由虚拟机自动生成,包装了对真正数组的访问
    Ref[] refs = new Ref[10]; // print nothing
}
static class Ref {
    static {
        System.out.println("ref init");
    }
    public static final int value = 123;
}
public static void main(String[] args) {
    // 编译期优化掉了,这个123存放在常量池里面
    System.out.println(Ref.value); // only print 123
}

类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载

ClassLoader myLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
          try {
              String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
            InputStream is = getClass().getResourceAsStream(fileName);
          if (is == null) {
              return super.loadClass(name);
          }
          byte[] b = new byte[is.available()];
          is.read(b);
          return defineClass(name, b, 0, b.length);
      } catch (IOException e) {
          throw new ClassNotFoundException(name);
      }
  }
};
Object obj = myLoader.loadClass("wang.ismy.jvm.classload.ClassLoaderTest").newInstance();
System.out.println(obj.getClass()); // obj是使用自定义加载器加载的
System.out.println(obj instanceof wang.ismy.jvm.classload.ClassLoaderTest); // false

分类

双亲委派模型

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,检查请求的类是否已经被加载过了
      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
              // 说明父类和启动类加载器都无法满足需求
          }
          if (c == null) {
            // 依旧找不到,就调用自身的findClass
            long t1 = System.nanoTime();
            c = findClass(name);

            // 用来记录类加载时间等等信息的...
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
          }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
        }
}

2020316101321

批注 2020-07-15 092654

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载

这样就使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一

所以系统中的String类加载优先级就会比在classpath或者用户自定义的String类优先级更高

自定义类加载路径

URL url = new URL("file:~/mysql-connector-java-5.1.44-bin.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class<?> klass = loader.loadClass("com.mysql.jdbc.Driver");
System.out.println(klass);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

自定义类加载器

findInCache -> parent.loadClass -> findClass()

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if ("wang.ismy.Main".equals(name)){
            byte[] bytes = new byte[0];
            try {
                bytes = new FileInputStream("path").readAllBytes();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return defineClass(name,bytes,0,bytes.length);
        }else {
            return super.findClass(name);
        }
    }
}

自定义类加载器的场景:

  1. 隔离加载类 确保中间件应用的jar不会影响到中间件的jar
  2. 修改加载方式 从其他地方获取class字节流
  3. 字节码加解密

打破双亲委派模型

实例

Tomcat 的正统类加载机制

Tomcat6之后 用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用原来完整的加载器架构

屏幕截图 2020-11-08 122329

OSGi 灵活的类加载机制

OSGi(Open Service Gateway Initiative)是OSGi联盟(OSGi Alliance)制订的一个基于Java语言的动态模块化规范

屏幕截图 2020-11-08 123126

在OSGi中,加载器之间的关系不再是双亲委派模型的树形结构,而是已 经进一步发展成一种更为复杂的、运行时才能确定的网状结构