Skip to content

类的加载篇

字数: 0 字 时长: 0 分钟

JVM加载.webp

1、Loading 加载

就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型:类模板对象

loading 过程

  • 通过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  • 创建 java.lang.Class 类的实例,作为方法区这个类的各种数据的访问入口

什么是类模板对象?

类模板对象,就是 Class 对象,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样 JVM 在运行期间便能通过类模板而获取到 Java 类中的任何信息;反射的机制也是基于这一基础才能实现。

类模板对象在 JVM 中的生命周期

数组类的加载有什么不同?

数组类本身不是由类加载器加载的,类加载器负责加载数组中存放元素的类型:

1、如果数组的元素类型是引用类型,那么就递归加载和创建数组的元素类型

2、JVM 使用指定的元素类型和数组维度来创建新的数组类

3、如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定;否则数组类的可访问性就是缺省定义为 public

2、Linking 链接

链接过程

  • 验证(verifaication):验证是否符合 JAVA 语法规范
  • 准备(preparation): 为类的静态变量分配内存,并将其赋默认值
  • 解析(resolve): 将类、接口、字段和方法的符号引用转换为直接引用

3、Initialization 初始化

初始化阶段,简言之,为类的静态变量赋上正确的初始值 (显示初始化)。

到了初始化阶段,才开始真正执行类中定义的 Java 程序代码,初始化阶段的重要工作是执行类的初始化方法: <clinit>() 方法

  • <clinit>() 方法只能由 JAVA 编译器生成,并由 JVM 调用;只有在给类中的 static 变量显示赋值或在静态代码块中赋值了,才会生成此方法
  • <init>() 方法一定会出现在 Class 的 method 表中,因为每个 Class 都必定有构造器方法
java
public class InitializationTest {
    //非静态字段不会生成 <clinit>
    public int num = 1;
    //静态字段没有显示赋值也不会生成
    public static int num1;
    //静态常量也不会生成
    public static final int num2 = 1;
    //但这种情况会生成 <clinit> 方法,因为调用了方法需要 <clinit> 支撑
    public static final int num3 = Integer.valueOf(100);
}

子类加载前先加载父类

在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 <clinit> 总是在子类 <clinit> 前被调用,也就是说父类的 static 块优先级高于子类

哪些情况会触发类的加载

Class 只有在必须要首次使用(主动使用)的时候才会被装载,JVM 不会无条件地装载 Class 类型。

  1. 创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反序列化

  2. 调用类的静态方法时,即当使用了字节码 invokestatic 指令

  3. 使用类、接口的静态字段时(final 修饰符特殊考虑),比如,使用 getstaticputstatic 指令

  4. 使用 java.lang.reflect 包中的方法反射类的方法时,比如 Class.forName("com.tt.Test");

  5. 初始化子类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前初始化

  7. 当虚拟机启动时,用户指定的主类 (main() 方法的那个类),虚拟机会先初始化这个主类

被动使用不会触发类的初始化

  • 当访问一个静态字段时,比如通过子类去访问父类的静态字段,但不会触发子类的初始化,只会触发父类的初始化
java
public class Parent{
    static int num = 10;
    static {
        System.out.println("Parent init");
    }
}

public class Child extends Parent {
    static {
        System.out.println("Child init");
    }
}

public static void main(String[] args) {
    //通过子类去访问父类静态字段时,不会触发子类的初始化
    System.out.println(Child.num); // Parent init
}
  • 通过数组定义类引用,不会触发此类的初始化
java
//定义一个 Parent 的数组,此时并不会触发 Parent 的初始化
Parent[] parents = new Parent[10];
  • 引用常量不会触发此类或接口的初始化,因为常量在链接阶段就被显示赋值了
  • 调用 ClassLoaderloadClass() 方法不会触发初始化

类的 Using (使用)

任何一个类型在使用之前都必须经历过完整的加载链接初始化三个阶段。一旦一个类型成功经历这三个阶段后,那么这个类型就进入使用阶段,此时这个类型已经可以正常使用了。

类的 Unloading 卸载

类、类的加载器、类的实例之间的关系

一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass() 方法,这个方法返回代表对象所属类的 Class 对象的引用。

此外,所有 JAVA 类都有一个静态属性 class,它引用代表这个类的 Class 对象。

类、加载器和实例的关系.webp

何种情况类会被卸载

在类的加载器的内部实现中,用一个 Java 集合来存放所加载类的引用,因此要卸载一个类,就得卸载相对应的类的加载器,又得卸载这个类的加载器加载的其他类,因此只有自定义的类的加载器才有可能被卸载

方法区的垃圾回收

方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量不再使用的类型

HotSpot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

不再使用的类型的判定条件很苛刻:

  • 该类的所有实例被回收
  • 加载该类的类加载器被回收
  • 该类对应的 Java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

类的加载器

传统类加载器的层级关系(jdk8及以前)

类加载器的基本特征

  • 双亲委派模型:优先父类加载,但不是所有类加载器都遵守这个模型,有时候,启动类加载器是可能要加载用户代码的,比如 JDK 的 SPI 机制 (ServiceProvider/ServiceLoader),用户可以在标准 API 框架上提供自己的实现。
  • 可见性:指一个类加载器在加载类时能否看到其他类加载器已加载的类的能力,类加载器能看到父加载器加载的类,但反之不可,这是双亲委派机制的核心,确保类加载的安全性和隔离性
  • 单一性:父加载器加载过的类型不会在子加载器中重复加载;但同一类型可以被邻居加载器加载多次,因为互相并不可见。

引导类加载器

  • 引导类加载器 (Bootstrap ClassLoader)使用 C/C++ 语言实现,嵌套在 JVM 内部
  • 它用来加载 Java 的核心库 ( JAVA_HOME/jre/lib/rt.jarsun.boot.class.path 路径下的内容)。用于提供 JVM 自身需要的类
  • 并不继承 java.lang.ClassLoader ,没有父加载器
  • 加载扩展类加载器应用类加载器

扩展类加载器

  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现
  • 父类加载器为Bootstrap ClassLoader
  • java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器 (Application/System ClassLoader)

  • Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现
  • 父类加载器为扩展类加载器
  • 负责加载 classpath 的用户类库,是默认的线程上下文加载器
  • 通过 ClassLoader.getSystemClassLoader() 可获取

用户自定义类加载器

  • 在必要时,我们可以自定义类加载器,用户自定义的类加载器
  • 通过类加载器可以实现非常绝妙的插件机制
  • 自定义类加载器可以实现应用隔离

用户自定义类加载器

java
public class UserDefineClassLoader extends ClassLoader{

    private String rootPath;

    public UserDefineClassLoader(String rootPath) {
        this.rootPath = rootPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //转换以文件路径表示的文件
        String filePath = classToFilePath(name);
        //获取指定路径的 class 文件对应的二进制流数据
        byte[] data = getBytesFromPath(filePath);
        //自定义 classLoader 调用 defineClass 方法
        return defineClass(name, data, 0, data.length);
    }

    private String classToFilePath(String name) {
        return rootPath + "\\" + name.replace(".","\\") + ".class";
    }
    
    //为了代码精简不作异常处理
    private byte[] getBytesFromPath(String filePath) {
        FileInputStream fileInputStream = new FileInputStream(filePath);
        ByteArrayOutputStream   byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;

        while ((len = fileInputStream.read(buffer)) != -1){
            byteArrayOutputStream.write(buffer,0,len);
        }
        return byteArrayOutputStream.toByteArray();
    }
    
    public static void main(String[] args) throws Exception{
        UserDefineClassLoader classLoader = new UserDefineClassLoader("E:\\venti\\venti-java\\src");
        Class<?> aClass = classLoader.findClass("com.ttdxg.jvm.T");
        System.out.println(aClass);
    }
}

双亲委派机制

优势:

  • 避免类的重复加载,确保一个类的全局唯一性,当父加载器加载了时,就没必要子加载器再加载一次
  • 保护程序安全,防止核心 API 被篡改

弊端:

  • 顶层的类加载器无法访问底层的类加载器加载的类,因此应用类访问系统类没问题,但系统类访问应用类就有问题

ClassLoader 双亲委派机制源码

java
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 {
                        //直到最上层,尝试去用 bootstrap 类加载器加载
                        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 方法,否则双亲委派机制就失效了,推荐去重写 findClass 方法,需要调用 defineClass 方法

tomcat 破坏双亲委派机制

  • 一个 web 容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离 (WebApp 类加载器)
  • 部署在同一个 Web 容器中相同的类库相同的版本可以共享 (Shared 类加载器)
  • Web 容器也有自己的依赖的类库,不能与应用程序的类库混淆。(Catalina 类加载器)
  • Web 容器要支持在不重启的情况下对 JSP 的修改 (JSP 类加载器加载) tomcat 类加载器.webpTomcat 类加载.webp

Tomcat 应用需要使用到某个类时的加载顺序

  1. 使用 bootstrap 引导类加载器加载
  2. 使用system 系统类加载器加载
  3. 使用应用类加载器WEB-INF/classes 中加载
  4. 使用应用类加载器WEB-INF/lib 中加载
  5. 使用 common 类加载器CATALINA_HOME/lib 中加载

jdk9 中类加载结构的新变化

1、jdk9 时基于模块化进行构建,此时的 Java 类库天然满足可扩展需求,原来的扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器 PlatformClassLoader

2、启动类加载器、平台类加载器、应用程序类加载器全部继承于 jdk.internal.loader.BuiltinClassLoader

模块类加载器.webp