【JVM系列笔记】类加载

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。常见的类加载器有启动类加载器,拓展类加载器,应用类加载器以及自定义类加载器。以及类加载机制,双亲委托策略,以及打破双亲委托策略的几种方式。

1. 类加载器

1.1. 概述

类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存这一部分。

类加载器会通过二进制流的方式获取到字节码文件的内容,接下来将获取到的数据交给Java虚拟机,虚拟机会在方法区和堆上生成对应的对象保存字节码信息。

1.2. 分类

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

  • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言与虚拟机底层语言一致,比如Hotspot使用C++。主要目的是保证Java程序运行中基础类被正确地加载,比如java.lang.String,Java虚拟机需要确保其可靠性。
  • JDK中默认提供或者自定义:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制,使用Java语言。所有Java中实现的类加载器都需要继承ClassLoader这个抽象类。

1.2.1. BootStrap ClassLoader(启动/根类加载器)

是由底层虚拟机来加载的类加载器,该类加载器无父加载器。由它来加载Java语言的核心类库,如java.lang等包下的类,因此java.lang.Object也是由该加载器来加载。默认情况下该加载器是根据系统属性sun.boot.class.path来加载对应类库,一般情况下主要是rt.jar中的文件。该类加载器的实现依赖底层操作系统,是虚拟机实现的一部分。它并不是java.lang.ClassLoader的子类,它是由C++编写的。

注意,如果随意修改sun.boot.class.path这个系统属性,可能导致无法加载java.lang.Object这个类从而造成虚拟机启动失败。

  • 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。
  • 默认加载Java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar等。
public class BootstrapClassLoaderDemo {
    public static void main(String[] args) throws IOException {
        ClassLoader classLoader = String.class.getClassLoader();
        System.out.println(classLoader);
        System.in.read();
    }
}
null

这段代码通过String类获取到它的类加载器并且打印,结果是null。这是因为启动类加载器在JDK8中是由C++语言来编写的,在Java代码中去获取既不适合也不安全,所以才返回null。同理,使用Arthas的命令sc -d查看,java.lang.String类的类加载器是空的,Hash值也是null。

用户扩展基础jar包

如果用户想扩展一些比较基础的jar包,让启动类加载器加载,有两种途径:

  • 放入jre/lib下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载。
  • 使用参数进行扩展。推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展,参数中的/a代表新增。

1.2.2. Extension ClassLoader(扩展类加载器)

它是纯Java编写的,是java.lang.ClassLoader的子类。由BootStrap ClassLoader来加载,从层级结构上来看是BootStrap ClassLoader的下级,它从java.ext.dir位置处加载类,或者从JDK安装路径下jre/lib/ext目录下加载类。如果用户将自己的jar放在这个路径下也会由扩展类加载器来加载。

public class AppClassLoaderDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        //当前项目中创建的Student类
        Student student = new Student();
        ClassLoader classLoader = Student.class.getClassLoader();
        System.out.println(classLoader);
        //maven依赖中包含的类
        ClassLoader classLoader1 = FileUtils.class.getClassLoader();
        System.out.println(classLoader1);
        Thread.sleep(1000);
        System.in.read();
    }
}

通过扩展类加载器去加载用户jar包:

  • 放入/jre/lib/ext下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容。
  • 使用参数进行扩展使用参数进行扩展。推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录

1.2.3. App ClassLoader(应用/系统类加载器)

又称系统类加载器,原因是在ClassLoader类中的getSystemClassLoader()方法获取到的就是AppClassLoader,因此也叫系统类加载器。从层级结构上来看,他是Ext ClassLoader的下级,同时默认情况下它也是所有用户自定义类加载器的直接上级(parent)。它从环境变量classpath或java.class.path下加载类。它也是由纯Java编写,是java.lang.ClassLoader的子类。

2. 双亲委托机制

2.1. 定义

当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载。(向上查找是否加载,向下尝试加载)

2.1.1. 案例一

  1. 应用程序类加载器首先判断自己加载过没有,没有加载过就交给父类加载器 - 扩展类加载器。
  2. 扩展类加载器也没加载过,交给他的父类加载器 - 启动类加载器。
  3. 启动类加载器发现已经加载过,直接返回。

2.1.2. 案例二

  1. B类在扩展类加载器加载路径中,同样应用程序类加载器接到了加载任务,按照案例1中的方式一层一层向上查找,发现都没有加载过。那么启动类加载器会首先尝试加载。它发现这类不在它的加载目录中,向下传递给扩展类加载器。
  2. 扩展类加载器发现这个类在它加载路径中,加载成功并返回。

2.2. 注意点

  1. 如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
  • 启动类加载器加载,根据双亲委派机制,它的优先级是最高的
  1. String类能覆盖吗,在自己的项目中去创建一个java.lang.String类,会被加载吗?
  • 不能,会返回启动类加载器加载在rt.jar包中的String类。
  1. 为什么父类加载器会加载失败?
  • 因为加载类不在加载目录下
  1. 双亲委派机制的好处有哪些?
  • 避免恶意代码替换JDK中的核心类库,确保核心类库的完整性和安全性。
  • 避免一个类重复地被加载。

3. 打破双亲委托机制

打破双亲委派机制历史上有三种方式,但本质上只有第一种算是真正的打破了双亲委派机制:

  • 自定义类加载器并且重写loadClass方法。Tomcat通过这种方式实现应用之间类隔离。
  • 线程上下文类加载器。利用上下文类加载器加载类,比如JDBC和JNDI等。
  • Osgi框架的类加载器。历史上Osgi框架实现了一套新的类加载器机制,允许同级之间委托进行类的加载,目前很少使用。

3.1. 自定义类加载器

3.1.1. Tomcat打破双亲委托机制

一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,Tomcat要保证这两个类都能加载并且它们应该是不同的类。如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的MyServlet类就无法被加载了。

Tomcat使用了自定义类加载器来实现应用之间类的隔离。 每一个应用会有一个独立的类加载器加载对应的类。

那么自定义加载器是如何能做到的呢?首先我们需要先了解,双亲委派机制的代码到底在哪里,接下来只需要把这段代码消除即可。

3.1.2. 源码解析

ClassLoader中包含了4个核心方法,双亲委派机制的核心代码就位于loadClass方法中。

public Class<?> loadClass(String name)
类加载的入口,提供了双亲委派机制。内部会调用findClass   
protected Class<?> findClass(String name)
由类加载器子类实现,获取二进制数据调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final void resolveClass(Class<?> c)
执行类生命周期中的连接阶段

1、入口方法:

2、再进入看下:

如果查找都失败,进入加载阶段,首先会由启动类加载器加载,这段代码在findBootstrapClassOrNull中。如果失败会抛出异常,接下来执行下面这段代码:

父类加载器加载失败就会抛出异常,回到子类加载器的这段代码,这样就实现了加载并向下传递。

3、最后根据传入的参数判断是否进入连接阶段:


3.1.3. 代码实现(了解)

Java提供了抽象类java.lang.ClassLoader,所有自定义类加载器都需要继承java.lang.ClassLoader,用户可以自定义加载逻辑。

package classloader.broken;
//package com.system.jvm.chapter02.classloader.broken;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;
/**
 * 打破双亲委派机制 - 自定义类加载器
 */
public class BreakClassLoader1 extends ClassLoader {
    private String basePath;
    private final static String FILE_EXT = ".class";
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }
    private byte[] loadClassData(String name)  {
        try {
            String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
            FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
            try {
                return IOUtils.toByteArray(fis);
            } finally {
                IOUtils.closeQuietly(fis);
            }
        } catch (Exception e) {
            System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
            return null;
        }
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if(name.startsWith("java.")){
            return super.loadClass(name);
        }
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }
    public static void main(String[] args) 
    throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        BreakClassLoader1 classLoader1 = new BreakClassLoader1();
        classLoader1.setBasePath("D:\\lib\\");
        Class<?> clazz1 = classLoader1.loadClass("com.system.my.A");
        BreakClassLoader1 classLoader2 = new BreakClassLoader1();
        classLoader2.setBasePath("D:\\lib\\");
        Class<?> clazz2 = classLoader2.loadClass("com.system.my.A");
        System.out.println(clazz1 == clazz2);
        Thread.currentThread().setContextClassLoader(classLoader1);
        System.out.println(Thread.currentThread().getContextClassLoader());
        System.in.read();
     }
}

3.1.4. 注意点

  1. 自定义类加载器父类怎么是AppClassLoader呢?

以Jdk8为例,ClassLoader类中提供了构造方法设置parent的内容:

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

这个构造方法由另外一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader。

/**
     * Creates a new class loader using the <tt>ClassLoader</tt> returned by
     * the method {@link #getSystemClassLoader()
     * <tt>getSystemClassLoader()</tt>} as the parent class loader.
     *
     * <p> If there is a security manager, its {@link
     * SecurityManager#checkCreateClassLoader()
     * <tt>checkCreateClassLoader</tt>} method is invoked.  This may result in
     * a security exception.  </p>
     *
     * @throws  SecurityException
     *          If a security manager exists and its
     *          <tt>checkCreateClassLoader</tt> method doesn't allow creation
     *          of a new class loader.
     */
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
  1. 两个自定义类加载器加载相同限定名的类,不会冲突吗?

不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

3.2. 线程上下文类加载器

利用上下文类加载器加载类,比如JDBC和JNDI等。

3.2.1. JDBC案例

  1. 启动类加载器加载DriverManager。
  2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
  3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。

3.2.2. SPI


3.2.3. 讨论

最早这个论点提出是在周志明《深入理解Java虚拟机》中,他认为打破了双亲委派机制,这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,所以打破了双亲委派机制。

但是如果我们分别从DriverManager以及驱动类的加载流程上分析,JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制。

所以我认为这里没有打破双亲委派机制,只是用一种巧妙的方法让启动类加载器加载的类,去引发的其他类的加载。

3.2.4. 代码(了解)

1、JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如mysql驱动、oracle驱动。

package classloader.broken;
//package com.itheima.jvm.chapter02.classloader.broken;
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
/**
 * 打破双亲委派机制 - JDBC案例
 */
public class JDBCExample {
    // JDBC driver name and database URL
    static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql:///bank1";
    //  Database credentials
    static final String USER = "root";
    static final String PASS = "123456";
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection(DB_URL, USER, PASS);
            stmt = conn.createStatement();
            String sql;
            sql = "SELECT id, account_name FROM account_info";
            ResultSet rs = stmt.executeQuery(sql);
            //STEP 4: Extract data from result set
            while (rs.next()) {
                //Retrieve by column name
                int id = rs.getInt("id");
                String name = rs.getString("account_name");
                //Display values
                System.out.print("ID: " + id);
                System.out.print(", Name: " + name + "\n");
            }
            //STEP 5: Clean-up environment
            rs.close();
            stmt.close();
            conn.close();
        } catch (SQLException se) {
            //Handle errors for JDBC
            se.printStackTrace();
        } catch (Exception e) {
            //Handle errors for Class.forName
            e.printStackTrace();
        } finally {
            //finally block used to close resources
            try {
                if (stmt != null)
                    stmt.close();
            } catch (SQLException se2) {
            }// nothing we can do
            try {
                if (conn != null)
                    conn.close();
            } catch (SQLException se) {
                se.printStackTrace();
            }//end finally try
        }//end try
    }//end main
}//end FirstExample

2、DriverManager类位于rt.jar包中,由启动类加载器加载。

3、依赖中的mysql驱动对应的类,由应用程序类加载器来加载。

在类中有初始化代码:

DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。(存疑)

那么问题来了,DriverManager怎么知道jar包中要加载的驱动在哪儿?

1、在类的初始化代码中有这么一个方法LoadInitialDrivers:

2、这里使用了SPI机制,去加载所有jar包中实现了Driver接口的实现类。

3、SPI机制就是在这个位置下存放了一个文件,文件名是接口名,文件里包含了实现类的类名。这样SPI机制就可以找到实现类了。

4、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。

3.3. Osgi框架的类加载器

历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的功能。热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。

4. JDK9后加载器

  1. 启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。

Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。

启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。

  1. 扩展类加载器被替换成了平台类加载器(Platform Class Loader)。

平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。

目录
相关文章
|
1月前
|
前端开发 安全 Java
聊聊Java虚拟机(一)—— 类加载子系统
虚拟机就是一款用来执行虚拟计算机指令的计算机软件。它相当于一台虚拟计算机。大体上,虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机就相当于一台物理电脑,里面可以安装操作系统;程序虚拟机是为了执行单个计算机程序而设计出来的虚拟机。其中 Java 虚拟机就是**执行 Java 字节码指令的虚拟机**。
48 2
|
9月前
|
前端开发 安全 Java
JVM类加载和双亲委派机制
JVM类加载和双亲委派机制
111 0
|
3天前
|
存储 安全 前端开发
JVM(二)-类加载子系统
JVM(二)-类加载子系统
7 0
|
24天前
|
存储 Java 程序员
【JVM系列笔记】类生命周期
类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用和卸载五个阶段。加载时,类加载器根据全限定名获取字节码,然后在方法区中创建InstanceKlass对象,并在堆上生成对应的Class对象。连接阶段验证字节码的正确性,准备阶段为静态变量分配内存并赋初始值,解析阶段将符号引用转换为直接引用。初始化阶段执行clinit方法,如静态变量赋值和静态代码块。类的初始化在访问静态成员、使用Class.forName、创建类实例或其子类时触发。
44 1
|
24天前
|
存储 Arthas Java
【JVM系列笔记】字节码
本文介绍了Java虚拟机(JVM)的组成,包括类加载子系统、运行时数据区、执行引擎和本地接口。字节码文件由基础信息(如魔数和版本号)、常量池、字段、方法和属性组成。常量池用于存储字符串等共享信息,方法区则包含字节码指令。执行引擎包含解释器、即时编译器和垃圾回收器,负责字节码的解释和优化。文章还提到了字节码工具,如javap、jclasslib和Arthas,用于查看和分析字节码。
49 0
【JVM系列笔记】字节码
|
1月前
|
存储 算法 Java
JVM内部世界(内存划分,类加载,垃圾回收)(下)
JVM内部世界(内存划分,类加载,垃圾回收)(下)
33 0
|
1月前
|
存储 前端开发 安全
JVM内部世界(内存划分,类加载,垃圾回收)(上)
JVM内部世界(内存划分,类加载,垃圾回收)
58 0
|
1月前
|
安全 Java 程序员
深入理解jvm - 类加载过程
深入理解jvm - 类加载过程
55 0
|
1月前
|
存储 前端开发 安全
浅谈 JVM 类加载过程
浅谈 JVM 类加载过程
47 0
|
1月前
|
存储 安全 Java
JVM类加载(类加载过程、双亲委派模型)
JVM类加载(类加载过程、双亲委派模型)