浅谈JVM类加载器并动手自定义类加载器。什么时候类加载会初始化,什么时候类加载不一定会初始化?

JVM类加载器

类加载的过程

我们用类加载(Class Loading)来表示: 将class/interface名称映射为Class对象的一整个过程。 这个过程还可以划分为更具体的阶段: 加载链接初始化(loading, linking and initializing)。
在这里插入图片描述

  • 加载阶段主要是获取class文件,加载阶段并不会检查class的语法和格式

  • 校验是链接的第一个阶段,确保class文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。校验过程检查 classfile 的语义,判断常量池中的符号,并执行类型检查。

  • 准备阶段将会创建静态字段, 并将其初始化为标准默认值(比如null 或者0值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。请注意,准备阶段并未执行任何Java代码。

    例如:

    public static int i = 1

    在准备阶段i 的值会被初始化为0,后面在类初始化阶段才会执行赋值为1。

    但是下面如果使用final作为静态常量,某些JVM的行为就不一样了:

    public static final int i = 1

    对应常量i,在准备阶段就会被赋值1。例如其他语言(C#)有直接的常量关键字const,让告诉编译器在编译阶段就替换成常量,类似于宏指令,更简单。

  • 解析主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。当一个变量引用某个对象的时候,这个引用在.class 文件中是以符号引用来存储的(相当于做了一个索引记录)。在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。(我是这么理解的,我的脑袋里面装了很多知识,但是这些知识的具体细节我不知道,比如我学了Spring,你问我Spring这个方法是什么,Spring这个类有什么作用的时候,我并不知道,但是由于我系统地学习了Spring,对于知识的框架我大概知道,根据你的问题,我能知道这是哪一块的知识,然后对应去查找就行了)

    加载一个class时, 需要加载所有的super类和super接口口方法解析。

  • 初始化:JVM规范明确规定, 必须在类的首次主动使用时才能执行类初始化。到达了初始化这个阶段,则说明这个类要被进行使用了。类加载并不代表这个类就要被初始化。就像把知识的索引先装进脑海中,只有等到要用的时候才准备拿出来,而不是一股脑的全部拿出来。
    初始化的过程包括执行:类构造器方法static静态变量赋值语句static静态代码块
    如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在java中初始化一个类,那么必然先初始化过 java.lang.Object类,因为所有的java类都继承自java.lang.Object。为了提高性能,HotSpot JVM 通常要等到类初始化时才去装载和链接类。 因此,如果A类引用了B类,那么加载A类并不一定会去加载B类(除非需要进行验证)。主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接。

类加载的时机

在这里插入图片描述

Class.forNameClassLoader的示例代码

首先有一个类,我们分别使用两种方法来加载它。

package classloader;

public class ForName {
  static {
    System.out.println("ForName类静态代码块初始化");
  }

  private static String name = method();

  private static String method() {
    System.out.println("ForName类给静态变量赋值的静态方法执行");
    return "initname";
  }
}

使用Class.forName加载类

package classloader;

public class ClassForNameTest {

  public static void main(String[] args) throws Exception {
    Class<?> aClass = Class.forName("classloader.ForName");
  }
}

输出结果如下,可以看到类不仅加载了,还进行初始化了。
在这里插入图片描述

使用ClassLoader加载类

package classloader;

public class ClassLoaderForNameTest {
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("classloader.ForName");
  }
}

没有任何输出,说明只是加载,但是并未进行初始化

在这里插入图片描述

也可以使用Class.forName只加载不初始化,如下,将初始化参数设置为false。

package classloader;

public class ClassForNameTest {

  public static void main(String[] args) throws Exception {
  //getSystemClassLoader返回的是一个AppClassLoader
    Class<?> aClass = Class.forName("classloader.ForName", false, ClassLoader.getSystemClassLoader());
  }
}

输出如下
在这里插入图片描述

不同的类加载器

Java11中的ClassLoader的类继承关系

从JDK1.8之后的版本提供有一个PlatformClassLoader类加载器,而在JDK1.8以及以前的版本里面提供的加载器为ExtClassLoader,因为在JDK的安装目录里面提供又一个ext的目录,开发者可以将*.jar文件拷贝到此目录里面,这样就可以直接执行了,但是这样的处理开发并不安全,最初的时候也是不提倡使用的,所以从JDK9开始将其彻底废除,同时为了和系统加载器与应用类加载器之间保持设计的平衡,提供有PlatformClassLoader

Java11中的ClassLoader的继承关系
在这里插入图片描述
Java8中的ClassLoader的继承关系
在这里插入图片描述

启动类加载器(BootstrapClassLoader)

启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生C++代码来实现的,并不继承自 java.lang.ClassLoader。它可以看做是JVM自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它。举例来说,java.lang.String是由启动类加载器加载的,所以String.class.getClassLoader()就会返回null。
在Java8中,可以通过如下代码打印出启动类加载器所加载的文件的路径。但是在Java8之后,则不允许了。通过下面的图片可以看出启动类加载器负责加载jre/classes下的class文件以及jre/下的jar包

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("BootStrap ClassLoader");
for (URL url : urls) {
    System.out.println(" ==> " + url.toExternalForm());
}

Java8打印输出结果
在这里插入图片描述

扩展类加载器(ExtClassLoader)

扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,加载jre/lib/ext/或者由java.ext.dirs系统属性指定的目录中的JAR包的类,代码里直接获取它的父类加载器为null(因为无法拿到启动类加载器)。

注意:在Java8之后。提供有一个PlatformClassLoader类加载器,而在JDK1.8以及以前的版本里面提供的加载器为ExtClassLoader。因为在JDK的安装目录里面提供又一个ext的目录,开发者可以将*.jar文件拷贝到此目录里面,这样就可以直接执行了,但是这样的处理开发并不安全,最初的时候也是不提倡使用的,所以从JDK9开始将其彻底废除,同时为了和系统加载器与应用类加载器之间保持设计的平衡,提供有PlatformClassLoader

应用类加载器(AppClassLoader)

应用类加载器(app class loader):它负责在JVM启动时加载来自Java命令的classpath或者cp选项、java.class.path系统属性指定的jar包和类路径。在应用程序代码里可以通过ClassLoader的静态方法getSystemClassLoader()来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。

以我的Maven项目为例:classpath指的是src.main.javasrc.main.resources以及第三方jar包的根路径。我们自己编写的代码都位于classpath中。
在这里插入图片描述

用户自定义类加载器

用户自定义了类加载器,则自定义类加载器都以应用类加载器作为父加载器。应用类加载器的父类加载器为扩展类加载器。这些类加载器是有层次关系的。

层次关系图
在这里插入图片描述
示例代码

首先有一个类Hello.java

package classloader;

public class Hello {
  static {
    System.out.println("Hello Class Initialized!");
  }

  public Hello() {
    System.out.println("Constructor is invoked");
  }

  public void hello() {
    System.out.println("Hello World! Hello Simon!");
  }
}

先将Hello.java编译为class文件,然后使用base64命令对class文件进行加密,然后将加密的文件存入一个hello.txt这个文件。

root@simon-computer:~/IdeaProjects/java-training/jvm/src/main/java/classloader# base64 Hello.class
yv66vgAAADQAIQoACAARCQASABMIABQKABUAFggAFwgAGAcAGQcAGgEABjxpbml0PgEAAygpVgEA
BENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAVoZWxsbwEACDxjbGluaXQ+AQAKU291cmNlRmlsZQEA
CkhlbGxvLmphdmEMAAkACgcAGwwAHAAdAQAWQ29uc3RydWN0b3IgaXMgaW52b2tlZAcAHgwAHwAg
AQAZSGVsbG8gV29ybGQhIEhlbGxvIFNpbW9uIQEAGEhlbGxvIENsYXNzIEluaXRpYWxpemVkIQEA
EWNsYXNzbG9hZGVyL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0B
AANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJp
bnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAcACAAAAAAAAwABAAkACgABAAsAAAAtAAIA
AQAAAA0qtwABsgACEgO2AASxAAAAAQAMAAAADgADAAAACAAEAAkADAAKAAEADQAKAAEACwAAACUA
AgABAAAACbIAAhIFtgAEsQAAAAEADAAAAAoAAgAAAA0ACAAOAAgADgAKAAEACwAAACUAAgAAAAAA
CbIAAhIGtgAEsQAAAAEADAAAAAoAAgAAAAUACAAGAAEADwAAAAIAEA==

在这里插入图片描述
然后自定义classloader读取txt文件,进行解密,并加载对应的class。

package classloader;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;

public class HelloClassLoader extends ClassLoader {

  private String path;

  public HelloClassLoader(String path) {
    this.path = path;
  }

  public static void main(String[] args) {
    //获取系统的当前路径为:D:\dev\Java\java-traning\jvm-learning
    System.out.println(System.getProperty("user.dir"));
    try {
      Class<?> clazz = new HelloClassLoader("src\\main\\java\\classloader\\hello.txt").findClass("classloader.Hello");
      Method declaredMethod = clazz.getDeclaredMethod("hello");
      declaredMethod.invoke(clazz.getConstructor().newInstance());
    } catch (ClassNotFoundException | IllegalAccessException |
            InstantiationException | NoSuchMethodException |
            InvocationTargetException e) {
      e.printStackTrace();
    }
  }

  protected String readBaseClassFromTxt() throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(this.path)));
    String readStr = "";
    String str;
    //一次性读取一行
    while ((str = reader.readLine()) != null) {
      str = str.trim();
      readStr += str;
    }
    readStr = readStr.trim();
    return readStr;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    String helloBase64 = null;
    try {
      helloBase64 = readBaseClassFromTxt();
    } catch (IOException e) {
      e.printStackTrace();
    }
    byte[] bytes = decode(helloBase64);
    return defineClass(name, bytes, 0, bytes.length);
  }

  public byte[] decode(String base64) {
    return Base64.getDecoder().decode(base64);
  }
}

从输出可以看出自定义类加载器加载并初始化相关类。
在这里插入图片描述

类加载机制

类加载机制的特点

  1. 双亲委托:当类加载器需要加载一个类,比如要加载java.lang.String这个类,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直继续往上委托,直至委托到启动类加载器。启动类加载器尝试去加载这个类,如果启动类加载器能加载这个类,比如java.lang.String这个类启动类加载器是能加载的,因此它就会告诉它的子加载器,“我已经加载了,你就不用加载了”。然后子类加载继续告诉它的子类加载器:“这个类已经被加载了,你不用加载了”,这样,所有的子加载器都不需要自己加载了。
    但如果启动类加载器无法加载,那么启动类加载的子加载器就会尝试去加载,如果启动类加载器的子加载器也无法进行加载,则启动类加载器的子加载器的子加载器则尝试去加载。如果所有类加载器都没有加载到指定名称的类,那么会抛出ClassNotFountException异常。
    在这里插入图片描述

  2. 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。

  3. 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。

参考极客时间Java训练营

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值