JVM类加载

一、类加载过程

1、加载

加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常由如下几种来源。

  • 本地文件系统加载class文件,绝大部分程序的类加载方式。
  • JAR包加载class文件,比如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类

2、链接

类被加载之后,系统会为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中,类连接又可分为3个阶段。

(1)验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要有四种验证:

  • 文件格式验证: 主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主、次版本号是否在当前虚拟机处理的范围内。常量池中是否有不被支持的常量类型。
  • 元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
  • 字节码验证:主要针对元数据验证后对方法体进行验证,保证类方法在运行时不会有危害出现。
  • 符号引用验证:主要是在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。

验证阶段对于虚拟机的类加载机制来说,是一个非常重要但不一定是必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,从而缩短虚拟机类加载的时间

(2)准备

负责为类的静态变量分配内存,并设置默认初始值

注意:这个时候进行内存分配的仅包括静态变量(static修饰),而不包括实例变量,实例变量将会在对象实例化时随对象一起被分配在Java堆中

(3)解析

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

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
  • 直接引用:是指向目标的指针、偏移量或者能够直接定位的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,虚拟机可能会对第一次解析的结果进行缓存

对于同一个符号引用可能会出现多次解析,虚拟机可能会对第一次解析的结果进行缓存。

解析动作分为四类:包括类或接口的解析、字段解析、类方法解析、接口方法解析。

3、初始化

初始化是为类的静态变量赋予正确的初始值。

二、类加载时机

JVM虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范严格规定了有且是由四种情况必须立即对类进行初始化,遇到newgetStaticputStaticinvokeStatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的javadiamante场景是:

1、创建类的实例,也就是new一个对象

2、调用类的静态方法

3、访问某个类或接口的静态变量,或者对该静态变量赋值。

4、反射(Class.forName(“com.xxx.xxx”))。

5、JVM启动时标明的启动类,即文件名和类名相同的那个类。

注意:对于final类型的静态变量,如果该变量的值在编译器就可以确定下来,那么这个变量相当于“宏变量”,Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化

宏变量:满足下面三个条件的即是宏变量。

  • 必须是final修饰的变量;
  • 必须在开始时就指定初始值;
  • 该初始值必须在编译器就可以确定;

三、类加载器

类加载负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。一旦一个类被加载到JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

  • 在Java中,一个类用其全限定类名(包名和类名)作为标识,
  • 在JVM中, 一个类用其全限定类名和其类加载器作为其唯一标识

JVM预定义有三种类加载器,当一个JVM启动的时候,Java开始使用如下三种类加载器。

1、根类加载器(启动类)

用来加载Java的核心类,是用原生代码来实现的,并不继承自java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里面所有的class。)

2、扩展类加载器

负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。

调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null,因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器。

3、系统类加载器

它根据在Java应用的类路径(CLASSPATH)来加载Java类,一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它的。

四、类加载机制

1、全盘负责

就是当一个类加载器负责加载某个Class时,该Class所依赖引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

2、双亲委派

就是先让父类加载器试图加载该class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父加载器无法完成此加载任务时,才自己去加载。

使 用 双 亲 委 派 加 载 机 制 的 优 点 \color{green}{使用双亲委派加载机制的优点} 使

  • 1、Java类随着它的类加载器一起具备了一种优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父亲已经加载了该类时,就没必要子ClassLoader再加载一次。

  • 2、防止核心API库被随意篡改,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,就不会重新加载网络传递过来的java.lang.Integer,而直接返回已加载过的Integer.class。

如 何 破 坏 双 亲 委 派 机 制 ? \color{green}{如何破坏双亲委派机制?}

沿用双亲委派机制自定义类加载器很简单,只需继承ClassLoader类并重写findClass方法即可。

定义一个继承ClassLoader的类,除了重写findClass方法外还要重写loadClass方法,这里loadClass方法默认是双亲委派机制,要想打破,必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载。

public class TestClassLoaderN extends ClassLoader {

  private String name;

  public TestClassLoaderN(ClassLoader parent, String name) {
    super(parent);
    this.name = name;
  }

  @Override
  public String toString() {
    return this.name;
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> clazz = null;
    ClassLoader system = getSystemClassLoader();
    try {
      clazz = system.loadClass(name);
    } catch (Exception e) {
      // ignore
    }
    if (clazz != null)
      return clazz;
    clazz = findClass(name);
    return clazz;
  }

  @Override
  public Class<?> findClass(String name) {

    InputStream is = null;
    byte[] data = null;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
      is = new FileInputStream(new File("d:/Test.class"));
      int c = 0;
      while (-1 != (c = is.read())) {
        baos.write(c);
      }
      data = baos.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        is.close();
        baos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return this.defineClass(name, data, 0, data.length);
  }

  public static void main(String[] args) {
    TestClassLoaderN loader = new TestClassLoaderN(
        TestClassLoaderN.class.getClassLoader(), "TestLoaderN");
    Class clazz;
    try {
      clazz = loader.loadClass("test.classloader.Test");
      Object object = clazz.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

3、缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换Class对象,存入缓存区中。

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页