JVM类加载器子系统

概述

  • Java虚拟机中执行的指令称为字节码指令, 类加载器(Class Loader)是负责将字节码(.class)文件按二进制流的方式装载到方法区的, 然后通过执行引擎(Execution Engine)来解释/ 编译为对应平台的机器指令后执行的
  • 类加载器只负责将字节码文件装载到内存, 至于是否可以运行, 由执行引擎来判断

类加载的过程

在这里插入图片描述

加载阶段(Loading)

  1. 从 .class文件读取字节码按二进制流的方式装载到方法区
  2. 在内存中生成指定类的 java.lang.Class实例对象, 作为此类访问入口

链接阶段(Linking)

  1. 验证(Linking)
  • 主要包括4种验证: 文件格式(如魔数, 版本号等), DNA元数据(如字节码进行语义分析, 检查是否符合 Java语言规范), 字节码(如程序语义合法性, 逻辑等), 符号引用
  1. 准备(Prepare)
  • 为类变量分配内存, 并设置(指定数据类型的)默认初始值, 但不包含 final修饰的 static(因为通过 final修饰后是会在编译的时候已经分配, 然后在此准备阶段时会显式的初始化)
  • 加载器的准备阶段是在方法区进行的, 而不同与类实例对象, 创建实例对象时的内存分配是在堆中进行的
  1. 解析(Resolve)
  • 主要解析类, 方法, 方法类型, 字段, 接口, 接口方法等. 同时将常量池中的符号引用转换为直接引用

初始化阶段(Initialization)

  • 类中有静态变量或静态代码块时, 会在对应类的字节码文件中, 产生针对静态变量的构造器<clinit>(), 如果没有静态变量就不会产生<clinit>(). 在此阶段给每个静态变量显式的赋值 注: 变量赋值顺序是 Java源代码中的代码顺序

public class App {
    static {
        age = 10;
    }
    private static Integer age = 35;

    public static void main(String[] args) {

        System.out.println(age);
    }

}
# 输出: 35

  • 若类有父类, 父类的<clinit>(), 会优先执行后, 执行子类的构造器
  • 虚拟机执行类加载时, 只会调用一次<clinit>(), 所以多线程下是有同步加锁的

public class App {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + "开始");
            Demo demo = new Demo();
            System.out.println(Thread.currentThread().getName() + "结束");
        };
        new Thread(r, "线程01").start();
        new Thread(r, "线程02").start();
    }
}

class Demo {
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "首次初始化!");
            while (true) {}
        }
    }
}
# 输出: 
线程01开始
线程02开始
线程01首次初始化! # 验证在此会被锁住

  • JVM虚拟机规范上, 类加载器有两种分别为引导类加载器和自定义加载器. 规范上(派生于 ClassLoader类的加载器都划分为自定义类加载器)

3种类加载器& 自定义加载器

  1. 启动/引导类加载器(BootStrap ClassLoader)
  • 此加载器是使用 C/C++编写的
  • 没有父级加载器(不继承 ClassLoader类)
  • 主要用于加载核心类库 如: %JRE_HOME%\lib\rt.jar, resources.jar, charsets.jar等
  • 还有 sun.boot.class.path路径下的内容用于加载JVM自身需要用的类
  • 出于安全原因, 引导类加载器只加载包名为 java, javax, sun开头的类
  1. 扩展类加载器(Extension ClassLoader)
  • 此加载器是使用 Java编写的, 类位置在 %JRE_HOME%\lib\rt.jar!\sun\misc\Launcher.class, 是 sun.misc.Launcher.class内的静态内部类 ExtClassLoader. (sun.misc.Launcher是虚拟机的入口)
  • 派生于 ClassLoader类
  • 自动加载扩展目录内的 jar包 %JRE_HOME%\lib\ext, 和通过选项 -Djava.ext.dirs=指定的目录
  1. 应用/系统类加载器(Application ClassLoader)
  • 此加载器是使用 Java编写的, 类位置在 %JRE_HOME%\lib\rt.jar!\sun\misc\Launcher.class, 是 sun.misc.Launcher.class内的静态内部类 AppClassLoader
  • 派生于 ClassLoader类
  • 自动加载环境变量 classpath内属性 java.class.path指定路径下的类库
  • 此加载器是默认类加载器, 自己编写的类默认由它来完成加载的

try {
    System.out.println(
	    Class.forName("org.example.Test").getClassLoader()
    );
} catch (Exception e) {}
# 输出: 
sun.misc.Launcher$AppClassLoader@18b4aac2

public class ClassLoaderApp {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}
# 输出:
sun.misc.Launcher$AppClassLoader@18b4aac2 默认应用类加载器
sun.misc.Launcher$ExtClassLoader@610455d6 之上级是扩展类加载器
null 由于引导类加载器是 native通过C语言编写的, 所以只返回 null

获取 ClassLoader的4种方式

  • ClassLoader类是抽象类, 除启动类加载器外, 其它加载器都继承自它
    1.获取类的 Class.forName(“java.lang.String”).getClassLoader()
    2.获取当前线程上下文的 Thead.currentThread().getContextClassLoader()
    3.获取系统的 ClassLoader.getSystemClassLoader()
    4.获取调用者的 DriverManager.getCallerClassLoader()

自定义类加载器(User Defined ClassLoader)

  • 自定义类加载器的使用场景
  1. 隔离加载类: 当使用中间件时, 为了防止与项目内其它框架包冲突
  2. 修改类加载的方式: 除引导加载器以外的加载器, 可以按需要动态加载
  3. 扩展加载源: 加载方式可以多元化 如 通过网络, 压缩包, 数据库等
  4. 防止源码泄漏: 为了防止反编译, 可以将字节码文件通过自定义加载器做加解密
  • 实现自定义类加载器步骤
  1. 继承抽象类 java.lang.ClassLoader
  2. 在 JDK1.2前继承 ClassLoader类后, 需重写 loadClass()方法, 但之后版本已不再建议重写 loadClass(), 而是建议将自定义类加载逻辑写在 findClass()方法中
  3. 所要编写的自定义类加载器, 如果不复杂(没有加解密等需求)可以直接继承 URLClassLoader类(ClassLoader的子类). 这样可以避免自己写 findClass()方法及其获取字节码流的方式
  • 加载类的2种方式
  1. 通过 Class.forName()方法动态加载, 默认初始化静态变量, 静态变量的初始化也可以可选 Class.forName(name, initialize, loader)中, 通过第二个参数 initialze控制
  2. 通过 ClassLoader.loadClass()方法动态加载, 不初始化静态变量

public class ClassLoaderApp extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException(name);
    }

    private byte[] getClassFromCustomPath(String name) {
        // 指定字节码文件路径通过二进制方式读取, 如果字节码文件有锁, 则在此处进行解密
        return null;
    }

    public static void main(String[] args) {
        ClassLoaderApp loader = new ClassLoaderApp();
        try {
            Class<?> _class = Class.forName("org.example.Test2", true, loader);
            Test test = (Test) _class.newInstance();
            System.out.println(test.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

双亲委派机制

  • 工作原理:
  1. 如果一个类加载器收到加载请求, 首先委托给父类加载器去执行
  2. 如父类还有父类加载器, 则进一步向上委托, 依次递归请求, 最终在顶层启动类加载器
  3. 如父类加载器可以完成加载就成功返回, 若父类无法完成加载, 子加载器才会尝试自己去加载, 这就是双亲委派模型
  • 最后只会被一个加载器所加载
    在这里插入图片描述

双亲委派机制优势

  1. 避免类的重复加载
  2. 防止核心 API被篡改(Java核心源代码保护, 又称为沙箱安全机制)

# 例: 创建包名和名称相同的 String类
package java.lang;
public class String {
    static {
        System.out.println("初始化自定义 String类");
    }

    public static void main(String[] args) {
        System.out.println("自定义的 String.main()");
    }

}
# 输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

- 通过双亲委派机制, 首先会从顶层的引导类加载器(不过包名开头为 java时默认会选择引导类加载器), 尝试查找指定类(包名和名称相同的类), 同时默认核心类库中的 String类内没有 main方法, 因此找不到 main方法

public class ClassLoaderApp {
    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
    }

}
# 输出:
无输出 

- 由于在引导类加载器, 已经加载过了核心类库中的 java.lang.String, 因此自定义 String类未被加载

其它

  • JVM中表示两个对象是否为同一个类的, 判断条件是类名称和包路径相同, 同时加载器的实例也必须相等
  • JVM中记录一些类型信息时, 也会将类加载器的一个引用也作为类型的一部分存在方法区中

类有2种使用方式

  • 分别为主动使用和被动使用, 区别是首次主动使用时会初始化静态变量及静态块, 而被动使用则不会

被动使用例子:

(1) 通过子类引用访问父类的静态变量, 属子类的被动使用, 不会导致子类的初始化


class A {
    static int count = 1;
    static {
        System.out.println("初始化 A类");
    }
}

class B extends A {
    static {
        System.out.println("初始化 B类");
    }
}

public class App {
    public static void main(String[] args) {
        System.out.println(B.count);
    }

}
# 输出:
初始化 A类
1

(2) 在编译阶段, 会将常量存入到, 调用方法所在的类的常量池中, 所以没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化


class D {
    static final int count = 1;
    static {
        System.out.println("初始化 D类");
    }
}

public class ClassLoaderApp {
    public static void main(String[] args) {
        System.out.println(D.count);
    }

}
# 输出:
1

# 但是, 如果将 D类的变量改为以下形式, 就属于主动使用, 所以 D类会被初始化. 因为 UUID.randomUUID().toString()方法是运行期确认的
class D {
    static final String uuid = UUID.randomUUID().toString();
    static {
        System.out.println("初始化 D类");
    }
}

public class ClassLoaderApp {
    public static void main(String[] args) {
        System.out.println(D.uuid);
    }

}
# 输出:
初始化 D类
ac65e308-ea8f-4734-bb72-e29a013954a0

7种主动使用的方式

  1. 通过 new关键字创建类实例
  2. 访问指定类的静态变量
  3. 调用指定类的静态方法
  4. 通过反射加载类 例 Class.forName(“org.example.Test”)
  5. 初始化指定类的子类
  6. 虚拟机启动时被表明为启动类的类
  7. jdk7开始提供的动态语言支持: java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic, REF_putStatic, REF_invokeStatic句柄对应类没有初始化, 则初始化

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

猜你喜欢

转载自blog.csdn.net/qcl108/article/details/108500959
今日推荐