Java基础:Java类加载器和加载机制

类加载器就是寻找类的字节码文件并构造出类在JVM内部表示对象的组件。

类加载工作由ClassLoader及其子类负责。ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。

类加载器

负责将.class文件加载到内存中,并为之生成对应的 Class 对象。

类加载器层次结构

  1. Bootstrap ClassLoader 根类加载器
    • 也被称为引导类加载器,负责Java核心类的加载。此加载器是C写的,在Java是看不到的。
    • 加载Java jdk核心类JRE/lib/rt.jar,如System,String类。
  2. Extension ClassLoader 扩展类加载器
    • sun.misc.Launcher$ExtClassLoader
    • 负责加载JRE/lib/下的扩展目录ext中的jar类包。
  3. Sysetm ClassLoader 系统类加载器
    • 也称为应用类加载器,sun.misc.Launcher$AppClassLoader
    • 负责在 JVM 启动时加载来自 java 命令的 class 文件,以及 Classpath 环境变量所指定的 jar 包和类路径。

类加载器的流程

在Java中,类加载器把一个类装入JVM,需要经过加载、链接(校验,准备,解析)、初始化步骤。
类加载器的流程

加载

把类的.class文件的字节码加载到内存中,有两种方式:

  1. 隐式加载:不通过代码里调用ClassLoader来加载需要的类。而是通进JVM来自动加载需要的类到内存。
  2. 显式加载:调用ClassLoader类来加载一个类,可以使用以下方法:
    • this.getClass.getClassLoader.loadClass(String name)
    • Class.forName(String className)
    • 或自己实现ClassLoader的 findClass()方法等。

连接

把类的二进制合到到JRE中,分三个阶段

  1. 验证:检验加载的类内部结构是否正确,并和其他类协调一致。
  2. 准备:为类静态变量分配内存,并设置默认值(仅包含类静态变量,不包含实例变量)。
  3. 解析:虚拟机将常量池中的符号引用替换为直接引用。

初始化

类静态变量初正确的初始值,执行静态代码块,执行构造方法等。

  • 类变量赋初始值:
    1. 声明类变量时指定初始化值。
    2. 使用静态初始化块为类变量指定初始值。(静态初始化块都将被当成类的初始化语句)
  • JVM初始化步骤:
    1. 若类没加载和连接,则先加载并连接该类。
    2. 若类的直接父类没有被初始化,则先初始化其直接父类。
    3. 若有初始化语句,则依次执行这些初始化语句
  • 类初始化时机
    1. 创建类的实例。
    2. 调用某个类的静态方法。
    3. 访问某个类或接口的类变量,或为该类变量赋值。
    4. 使用反身来强制创建该类或接口对应的java.lang.Class对象。
    5. 初始化某个类的子类,该子类的直接父类和所有间接父类都被初始化。
    6. 直接使用java.exe来运行某个主类。
  • final修饰的类变量的初始化:
    1. 如果类变量(静态变量)使用final修饰,并且它的值在编译时就确定下来,该变量相当于常量, 不会被初始化。static final String name = “Admin”; 。
    2. 如果使用final修饰类变量的值在不能在编译时确定下来,必须等到运行时才可以确定该类变量的值,则会被初始化。static final String nowtime = System.currentTimeMillis() + “”; 。

使用ClassLoader类的loadClass()方法来加载某个类时,该方法只加载该类;使用Class的**forName()**静态方法会强制初始化该类。

类加载器机制

Java中,一个类用其全限定类名(包括包名和类名)作为唯一标识。
JVM中,一个类用其全限定类名和其类加载器作为唯一标识(类名、包名、加载器的实例)。

  1. 全盘负责制:该类所有依赖的和引用的其他类将由该类的加载器负责载入,除非显式使用另外的加载器载入。
  2. 父类委托:先委托父类加载器加载目标类,找不到时再从自己的类路径中查找并加载目标类,确保类只被加载过一次。
  3. 缓存机制:保证所有加载过的类都会被缓存,当需要使用时,先从缓存区中搜索,缓存不存在该类时,系统才会读取该类对应的二进制数据,将期转换成Class对象,放入缓存区中。

类加载器执行步骤

类加载器加载Class大致经过如下8个步骤:

  1. 检测此类是否载入过(即在缓存区是否存),有进入第8步,没有执行第2步。
  2. 如果父类加载器存在,执行第3步;否则跳到第4步执行。
  3. 请求父类加载器载入目标类,如果成功跳到第8步,否则执行第5步。
  4. 请求根类加载器载入目标类,如果成功跳到第8步,否则执行第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,否则跳到第7步。
  6. 从文件中载入Class,成功载入后跳到第8步。
  7. 抛出ClassNotFoundException异常。
  8. 返回对应的java.lang.class对象

自定义的类加载器

  1. 通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。

    • loadClass(String name,boolean resolve):ClassLoader方法入口,根据指定名称为加载类。
    • findClass(String name)根据指定定名称来查找类。建议使用
    • Class defineClass(String name,byte[] b, int off, int len)
    • defineClass()
    • findSystemClass(String name)
    • static getSystemClassLoader()
    • getParent()
    • findLoadedClass(String name)
  2. 使用自定义类加载器实现如下常见功能:

    • 执行代码前自动验证数字签名。
    • 根据用户提供的密码解密代码,从而可以实现代码混淆来避免反编译*.class文件。
    • 根据用户需求来动态在加载类。
    • 根据应用需求把其他数据以字节码的形式加载到应用中。
  3. URLClassLoader类
    该类也是系统类加载器和扩展类加载器的父类。功能比较强大,可以从本地文件或远程主机获取二进制文件来加载类。

    • **URLClassLoader(URL[] URLS)**:使用默认的父类加载器创建一个ClassLoader,该对象将从urls所指定的系列路径来查询并加载类。
    • **URLClassLoader(URL[] urls, ClassLoader parent)**:使用指定的父类加载器创建一个ClassLoader对象,其他功能与个构造器相同。

一旦得到URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。例如:可以直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接,就无须将驱动添加到CLASSPATH环境变量中。

示例代码

示例一:获取加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo2 {

@Test
public void ClassLoaderTest() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("loader = " + loader);
System.out.println("loader parent = " + loader.getParent());
System.out.println("loader parent`s parent = " + loader.getParent().getParent());
}
}

//----------结果------------------------
loader = sun.misc.Launcher$AppClassLoader@18b4aac2
loader parent = sun.misc.Launcher$ExtClassLoader@2038ae61
loader parent`s parent = null

示例二:URLClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UrlClassLoadTest {
private static Connection conn;

public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://localhost:3306/db_name?useUnicode=true&characterEncoding=utf-8&useSSL=false";
String account = "root";
String key = "123456";
Connection conn = getConn(url, account, key);
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("select * from country limit 10");
System.out.println(conn.getSchema());
}

// 定义一个获取数据库连接的方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 创建一个URL数组
URL[] urls = {new URL("file:///D:/mysql-connector-java-8.0.27.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
@SuppressWarnings("resource")
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.cj.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user", user);
props.setProperty("password", pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
}

相关参考

深入理解Java类加载器(ClassLoader)

Java基础:Java类加载器和加载机制

http://blog.gxitsky.com/2018/03/17/Java-jdk-2-classLoader/

作者

光星

发布于

2018-03-17

更新于

2022-06-17

许可协议

评论