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. 案例一
- 应用程序类加载器首先判断自己加载过没有,没有加载过就交给父类加载器 - 扩展类加载器。
- 扩展类加载器也没加载过,交给他的父类加载器 - 启动类加载器。
- 启动类加载器发现已经加载过,直接返回。
2.1.2. 案例二
- B类在扩展类加载器加载路径中,同样应用程序类加载器接到了加载任务,按照案例1中的方式一层一层向上查找,发现都没有加载过。那么启动类加载器会首先尝试加载。它发现这类不在它的加载目录中,向下传递给扩展类加载器。
- 扩展类加载器发现这个类在它加载路径中,加载成功并返回。
2.2. 注意点
- 如果一个类重复出现在三个类加载器的加载位置,应该由谁来加载?
- 启动类加载器加载,根据双亲委派机制,它的优先级是最高的
- String类能覆盖吗,在自己的项目中去创建一个java.lang.String类,会被加载吗?
- 不能,会返回启动类加载器加载在rt.jar包中的String类。
- 为什么父类加载器会加载失败?
- 因为加载类不在加载目录下
- 双亲委派机制的好处有哪些?
- 避免恶意代码替换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. 注意点
- 自定义类加载器父类怎么是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()); }
- 两个自定义类加载器加载相同限定名的类,不会冲突吗?
不会冲突,在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。
3.2. 线程上下文类加载器
利用上下文类加载器加载类,比如JDBC和JNDI等。
3.2.1. JDBC案例
- 启动类加载器加载DriverManager。
- 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
- 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后加载器
- 启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。
Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
- 扩展类加载器被替换成了平台类加载器(Platform Class Loader)。
平台类加载器遵循模块化方式加载字节码文件,所以继承关系从URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。