Skip to content

类的加载篇

JVM加载.png

1、Loading 加载

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

loading 过程

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

什么是类模板对象?

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

二进制流的获取方式

  • 虚拟机通过文件系统读入一个 class 后缀的文件
  • 读入 jar、zip等归档数据包,提取类文件
  • 事先存放在数据库中的类的二进制数据
  • 在运行时生成一段 Class 的二进制信息等

Class 实例的位置在哪?

说明:Class 类的构造方法是私有的,只有 JVM 能够创建

Class实例位置.png

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

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

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 块优先级高于子类

<clinit> 的使用会导致死锁吗?

有可能导致死锁,因为 <clinit> 方法是带锁线程安全的,如果两个类互相引用,就会导致死锁

哪些情况会触发类的加载

Java 程序对类的使用分为 主动使用被动使用

主动使用

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

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

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

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

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

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

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

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

被动使用

被动使用不会引起类的初始化,也就是说,并不是在代码中出现的类就一定会被加载或者初始化,如果不符合主动使用条件,类就不会初始化。

1、当访问一个静态字段时,比如通过子类去访问父类的静态字段,但不会触发子类的初始化,只会触发父类的初始化

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
}

2、通过数组定义类引用,不会触发此类的初始化

java
//定义一个 Parent 的数组,此时并不会触发 Parent 的初始化
Parent[] parents = new Parent[10];

3、引用常量不会触发此类或接口的初始化,因为常量在链接阶段就被显示赋值了

4、调用 ClassLoaderloadClass() 方法不会触发初始化

java
public class T {

    //静态 没有 final 修饰,在 <clinit> 执行
    public static int k = 0;

    public static T t1 = new T("t1");

    public static T t2 = new T("t2");

    public static int i = print("i");

    public static int n = 99;

    static {
        print("静态块");
    }

    //显示赋值、非静态代码块、构造方法 由 <init> 方法来执行
    //=============================
    public int j = print("j");

    {
        print("构造块");
    }

    public T(String str) {
        System.out.println((++k) + ":" + str + "i=" + i + " n=" + n);
        ++n;
        ++i;
    }
    //===============================

    public static int print(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++n;
        return  ++i;
    }

    public static void main(String[] args) {

    }


}

//执行 main()方法,控制台结果
1:j i=0 n=0
2:构造块 i=1 n=1
3:t1i=2 n=2
4:j i=3 n=3
5:构造块 i=4 n=4
6:t2i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99

类的 Using (使用)

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

开发人员可以在程序中访问和调用它的静态类成员信息(静态字段、静态方法),或者使用 new 关键字为其创建对象实例

类的 Unloading 卸载

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

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

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

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

何种情况类会被卸载

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

方法区的垃圾回收

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

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

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

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

类的加载器

何为类的唯一性?

对于任何一个类,都需要由加载它的类加载器和这个类本身一同确认其在 JVM 中的唯一性。每一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。

类加载器的基本特征

  • 双亲委派模型。但不是所有类加载器都遵守这个模型,有时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如 JDK 内部的 ServiceProvider/ServiceLoader 机制,用户可以在标准 API 框架上提供自己的实现。
  • 可见性。子类加载器可以访问父加载器加载的类型,但反过来不允许。
  • 单一性。父加载器加载过的类型不会在子加载器中重复加载;但同一类型可以被邻居加载器加载多次,因为互相并不可见。

类加载器的分类

类加载器总体分为两大类:引导类加载器和自定义类加载器

引导类加载器

  • 引导类加载器 (Bootstrap ClassLoader)使用 C/C++ 语言实现,嵌套在 JVM 内部
  • 它用来加载 Java 的核心库 ( JAVA_HOME/jre/lib/rt.jarsun.boot.class.path 路径下的内容)。用于提供 JVM 自身需要的类
  • 并不继承 java.lang.ClassLoader ,没有父加载器
  • 处于安全考虑, Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类
  • 加载扩展类加载器和应用类加载器,并指定为它们的父类加载器

使用 -XX:+TraceClassLoading 参数可以得到

自定义类加载器

引导类加载器以外的的所有类加载器都归为自定义类加载器,继承于 java.lang.ClassLoader

扩展类加载器
  • Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现
  • 父类加载器为启动类加载器
  • java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载
系统类加载器
  • 父类加载器为扩展类加载器
  • 负责加载环境变量 classpath 或者系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 它是用户自定义类加载器的默认父加载器
  • 通过 ClassLoader.getSystemClassLoader() 可获取
用户自定义类加载器
  • 在 java 日常应用当中,类的加载几乎由上述 3 种类加载器互相配合完成,在必要时,我们可以自定义类加载器
  • 通过类加载器可以实现非常绝妙的插件机制
  • 自定义类加载器可以实现应用隔离

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 方法

java
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }


protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}

当我们重写 loadClass 方法试图去覆盖源码的时候,还有一个机制可以防止源码被篡改

java
private ProtectionDomain preDefineClass(String name,
                                            ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);

        // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
        // relies on the fact that spoofing is impossible if a class has a name
        // of the form "java.*"
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }

        if (name != null) checkCerts(name, pd.getCodeSource());

        return pd;
    }

自定义类加载器实现

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 被篡改

弊端:

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

jdbc 破坏双亲委派机制

破坏双亲委派机制.png

tomcat 破坏双亲委派机制

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

tomcat 类加载器.pngTomcat 类加载.png

tomcat 使用默认的类加载机行不行?

不行,默认的类加载机制无法加载相同类库的不同版本。

tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?

可以使用线程上下文类加载器,让父类加载器请求子类加载器去完成类加载

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

模块类加载器.png