类加载器(ClassLoader):是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
类加载器多数是有Java编写的,也有部分是c++编写的,负责接收来自外部的二进制数据,然后执行JNI(也就是本地接口调用)。 JNI:是Java Native Interface的缩写,运行Java调用其他语言编写的方法。
类加载器分为俩类:一类是Java虚拟机底层源码实现,一类是Java代码中实现的(即自定义的classLoader)
虚拟机底层实现:用于加载程序运行时的基础类,保证Java程序运行中基础类被正确加载。
jdk8及以前:有启动类加载器bootstrap(c++编写):加载Java中最核心的类。Java代码实现继承自抽象类ClassLoader。所有的类加载器都需要继承这个类。
package org.example.classLoader;
import java.io.IOException;
public class BootstrapClassLoader {
public static void main(String[] args) throws IOException {
ClassLoader classLoader=String.class.getClassLoader();
System.out.println(classLoader);
System.in.read();
}
}
打印结果为null。
启动类加载器存在于虚拟机中,属于底层。而通过Java代码去获取这个类加载器属于高层,虚拟机会阻止通过代码去获取到启动类加载器并操作的,所有通过代码去获取启动类加载器返回的结果是null。
arthas中有命令:sc -d 类名 就是查看该类的类加载器。
通过启动类去加载用户的jar包的方法:
1、不推荐:将用户的jar包放入jre/lib下进行扩展。尽可能不要更改jdk安装目录的内容。
2、使用参数扩展:使用-Xbootclasspath/a:jar包目录/jar包进行扩展。
扩展类加载器extension:运行扩展Java中比较通用的类。
package org.example.classLoader;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
public class ExtClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
//扩展类加载器
ClassLoader classLoader1= ScriptEnvironment.class.getClassLoader();
System.out.println(classLoader1);
}
}
应用类加载器application:加载应用使用的类。
实体类:student
package org.example.pojo;
import lombok.Builder;
@Builder
public class Student {
public String name;
public int age;
public static void main(String[] args) {
Student student=Student.builder().name("张三").age(18).build();
}
}
package org.example.classLoader;
import org.example.pojo.Student;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
public class AppClassLoader {
public static void main(String[] args) throws IOException {
//Student student = new Student();
ClassLoader classLoader=Student.class.getClassLoader();
System.out.println(classLoader);
//maven依赖中包含的类
ClassLoader classLoader1= FileUtils.class.getClassLoader();
System.out.println(classLoader1);
System.in.read();
}
}
扩展类加载器和应用类加载器都是jdk中提供的、使用Java编写的,它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。
通过扩展类加载器去加载用户jar包方法:
1、放入\jre\lib\ext下进行扩展(不推荐)。
2、使用-Djava.ext.dirs=jar包目录进行扩展,这种方式会覆盖掉原始目录。想扩展多个jar包目录时,多个jar包目录Windows用; mac用: 连接。
类加载器的双亲委派机制:
内容:当一个类加载器接收到加载类的任务时,会自下而上查找是否被加载过,在由顶向下进行加载(根据类的加载路径选择加载到哪个类加载器中)。
由于Java虚拟机中有多个类加载器,双亲委派机制的核心是解决一个类到底是谁加载的问题。
作用:
1、保证类加载的安全性:避免代码替换jdk中的核心类库,确保核心类库完整性和安全性。
2、避免重复加载:双亲委派机制可以避免同一个类被多次加载。
多个类加载器之间有层次变化
虽然类加载器之间是父类加载器关系,但他们之间并不是继承关系而是上下级关系。 自定义加载器父类为应用类加载器。应用类加载器父类为扩展类加载器。虽然扩展类加载器的parent=null,但通过复杂的底层逻辑实现,我们可以认为扩展类加载器的父类是启动类加载器。
双亲委派机制问题:
如果一个类重复出现在三个类加载器的加载位置,英国由启动类加载器加载,根据双亲委派机制,它的优先级最高。
在自己的项目中创建一个java.lang.string类(在启动类加载器中已有相同类名的类被加载),不会被加载,会返回启动类中已被加载的string类。
Java中如何使用代码去主动加载一个类:
1、使用Class.forName方法,使用当前类的类加载器去加载指定的类
2、获取到类加载器,通过类加载器的loadClass方法指定某个类加载器的加载。
打破双亲委派机制的三种方式:
1、自定义类加载器:自定义类加载器并重写loadClass方法,就可以将双亲委派机制的代码去除。如果自定义类加载器未指定父加载器是谁,就默认指向应用类加载器。即使俩个自定义类加载器加载相同限定名的类,也不会冲突。同一个Java虚拟机中只有相同类加载器加载相同的类限定名才会被认为是同一个类。
package org.example.classLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
//打破双亲委派机制
public class BreakClassLoader1 extends ClassLoader{
private String basePath;
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassData(String name){
// 将包名中的"."替换成文件系统的路径分隔符"/"
// 假设类文件存放在某个固定路径下,这里是"classes"目录
String filePath = "/Users/***/Desktop/text1.class";
File classFile = new File(filePath);
long len = classFile.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fis = new FileInputStream(classFile)) {
// 读取类文件的字节码
int r = fis.read(raw);
if (r != len) {
throw new IOException("无法读取全部的类文件:" + name);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return raw;
}
@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();
//设置类加载器的加载目录,我将class文件放到了Desktop桌面上了。
classLoader1.setBasePath("/Users/***/Desktop/");
//加载java.lang.String类。
//Class<?> loadClass = classLoader1.loadClass("java.lang.String");
//loadClass.newInstance();
Class<?> loadClass = classLoader1.loadClass("org.example.text1");
System.out.println(loadClass.getClassLoader());
//加载自定义的父类加载器
ClassLoader parent = classLoader1.getParent();
System.out.println(parent);
//获取系统类加载器
System.out.println(getSystemClassLoader());
//俩个自定义类加载器加载限定名相同的类,不冲突。同一个虚拟机中,只有同一个自定义类加载器加载相同限定名的类时才会冲突。
BreakClassLoader1 classLoader2=new BreakClassLoader1();
Class<?> loadClass1 = classLoader2.loadClass("org.example.text1");
System.out.println(loadClass==loadClass1);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
}
2、线程上下文类加载器:利用上下文类加载器,比如JDBC和JNDI。
3、Osgi(了解即可):历史上Osgi框架实现了一套新的加载器机制,允许同级之间委托进行类的加载。
双亲委派机制的核心代码在Classloader在的4个核心方法之一的loadClass方法中。
打破双亲委派机制的情况:
1、当tomcat中运行俩个限定名相同的类,tomcat需要保证这俩个类都能被加载并且是不同的类。这时就需要打破双亲委派机制,tomcat使用类自定义类加载器来实现应用之间的隔离。每一个应用会有一个独立的类加载器加载对应的类。
package org.example.classLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
//打破双亲委派机制
public class BreakClassLoader1 extends ClassLoader{
private String basePath;
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassData(String name){
// 将包名中的"."替换成文件系统的路径分隔符"/"
// 假设类文件存放在某个固定路径下,这里是"classes"目录
String filePath = "/Users/***/Desktop/text1.class";
File classFile = new File(filePath);
long len = classFile.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fis = new FileInputStream(classFile)) {
// 读取类文件的字节码
int r = fis.read(raw);
if (r != len) {
throw new IOException("无法读取全部的类文件:" + name);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return raw;
}
@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("/Users/***/Desktop/");
//加载java.lang.String类。
//Class<?> loadClass = classLoader1.loadClass("java.lang.String");
//loadClass.newInstance();
Class<?> loadClass = classLoader1.loadClass("org.example.text1");
System.out.println(loadClass.getClassLoader());
//加载自定义的父类加载器
ClassLoader parent = classLoader1.getParent();
System.out.println(parent);
//获取系统类加载器
System.out.println(getSystemClassLoader());
//俩个自定义类加载器加载限定名相同的类,不冲突。同一个虚拟机中,只有同一个自定义类加载器加载相同限定名的类时才会冲突。
BreakClassLoader1 classLoader2=new BreakClassLoader1();
Class<?> loadClass1 = classLoader2.loadClass("org.example.text1");
System.out.println(loadClass==loadClass1);
}
}
2、JDBC案例
Driver Manager属于rt.jar是启动类加载器加载的,而用户jar包的驱动器需要有应用类加载器加载,这就违反了双亲委派机制。
其实JDBC案例可以说打破了,也可以说没有打破。打破了:是启动类加载器委派应用类加载器去加载类跳过了扩展类加载器。没有打破:触发驱动类的加载,驱动类的加载还是自顶向下加载,遵循双亲委派机制。
DriverManager是通过spi机制知道jar包中要加载的驱动在哪的。spi机制是jdk内置的一种服务发现机制。
spi机制:1、在ClassPath路径下的META-INF/services文件夹中,以接口的全限定名来命名文件名,对应文件里面写该接口的实现。2、使用serviceLoader加载实现类。
spi中使用了上下文中保存的类加载器进行类的加载,这个类一般就是应用类加载器,由此spi可以获取到应用类加载器。获取到了线程上下文类加载器,启动类加载器会委托上下文类加载器去驱动类。
package org.example.classLoader;
import com.sun.xml.internal.bind.v2.model.annotation.RuntimeAnnotationReader;
public class NewThreadDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//当线程被创建后,线程中的上下文类加载器就已经是应用类加载器了。
System.out.println(Thread.currentThread().getContextClassLoader());
}
}).start();
}
}