ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,在堆中创建实例,在方法区中创建Class文件的数据结构,把堆中的实例链接到方法区的数据结构上。(如下图所示)然后交给Java虚拟机进行链接、初始化等操作、因此,ClassLoader在整个装载(加载)阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。
因为Java虚拟机的设计者当初在设计类加载器的时候,并没有考虑将它绑定在Jvm内部,这样做的好处就是能够更加灵活和动态地执行类加载操作,所以如今类加载器却在OSGI(热部署)、字节码加密解密领域、版本隔离(Tomcat)等可以得到应用。
1 类的加载方式
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式(在日常开发以上两种方式一般会混合使用)
- 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
- 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。比如 new User()。
举例如下:
public class UserTest {
public static void main(String[] args) {
User user = new User(); //隐式加载
try {
Class clazz = Class.forName("com.atguigu.java.User"); //显式加载
ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java.User");//显式加载
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
2 命名空间
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
对于任意一个类,都需要由加载它的类加载器和这个本身一同确认其在JAVA虚拟机中的唯一性。每一个加载器,都拥有一个独立的类命名空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
解释代码如下,实例中在程序中通过不同类加载器加载同一个User对象,对象地址不相等。
/**
* @Author jlu
* @Date 9:44 2022/5/22
* @Description: 自定义加载器实践
* 注意:使用之前在目录下编译User对象 javac User.java 获得User的class文件
**/
public class UserClassLoader extends ClassLoader {
private String rootDir;
public UserClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 编写findClass方法的逻辑
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 编写获取class文件并转换为字节码流的逻辑 * @param className * @return
*/
private byte[] getClassData(String className) {
// 读取类文件的字节
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
// 读取类文件的字节码
while ((len = ins.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的完全路径
*/
private String classNameToPath(String className) {
return rootDir + "\\" + className.replace('.', '\\') + ".class";
}
public static void main(String[] args) {
String rootDir = "C:\\code\\java\\java-tutorial\\tutorials-in-java\\tutorial-java-jvm\\src\\main\\java";
System.out.println("不同的类加载器加载同一个对象User,通过==进行比较,结果为:");
try {
//创建自定义的类的加载器1
UserClassLoader loader1 = new UserClassLoader(rootDir);
Class clazz1 = loader1.findClass("jvm.User");
//创建自定义的类的加载器2
UserClassLoader loader2 = new UserClassLoader(rootDir);
Class clazz2 = loader2.findClass("jvm.User");
System.out.println(clazz1 == clazz2); //clazz1与clazz2对应了不同的类模板结构。
System.out.println("创建自定义的类的加载器1加载User,所生成的地址:");
System.out.println(clazz1.getClassLoader());
System.out.println("创建自定义的类的加载器2加载User,所生成的地址:");
System.out.println(clazz2.getClassLoader());
//系统类加载器加载User对象
System.out.println("系统类加载器加载User对象所得到的地址:");
Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("jvm.User");
System.out.println(clazz3.getClassLoader());
System.out.println("自定义对象的父加载器,即系统类加载器加载User对象所得到的地址:");
System.out.println(clazz1.getClassLoader().getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
不同的类加载器加载同一个对象User,通过==进行比较,结果为:
false
创建自定义的类的加载器1加载User,所生成的地址:
jvm.UserClassLoader@1b6d3586
创建自定义的类的加载器2加载User,所生成的地址:
jvm.UserClassLoader@74a14482
系统类加载器加载User对象所得到的地址:
sun.misc.Launcher$AppClassLoader@18b4aac2
自定义对象的父加载器,即系统类加载器加载User对象所得到的地址:
sun.misc.Launcher$AppClassLoader@18b4aac2
3 SecureClassLoader与URLClassLoader
1) 接着SecureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联
2) ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
4 Class.forName()与ClassLoader.loadClass()(显示加载)
1) Class.forName():是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个 Class 对象。该方法在将 Class 文件加载到内存的同时,会执行类的初始化。如:Class.forName(“java.HelloWorld”);
2)ClassLoader.loadClass():这是一个实例方法,需要一个 ClassLoader 对象来调用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器。