Java中JVM类加载器详细介绍-刘宇
作者:刘宇
CSDN博客地址:https://blog.csdn.net/liuyu973971883
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。
一、什么是类加载器
根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例。并且这个类对应的Class实例在堆区无论你加载多少次只会存在一个,除非使用不同的加载器去加载这个类,则会出现多个的效果。这是因为不同的加载器会出现命名空间的问题。
- 类加载器并不需要等到某个类被“首次主动使用”时再加载它
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果预先加载出现了.class文件丢失,那么会在程序“首次主动使用”时报错异常。
1.1、类加载器的官方解释
- 一个类加载器是一个对象,主要负责来加载类。ClassLoader是一个抽象类,如果给定一个类的二进制名字(如:javax.swing.JSpinner$DefaultEditor),此时类加载器就会去定位(真真实实存在的类)或生成(动态创建的类)这个类定义相应的数据
- 每一个Class对象都包含一个定义这个Class对象的ClassLoader对象的引用,这样我们就可以Class对象中的getClassLoad()获取到ClassLoader对象
- 针对于数组类的Class对象并不是由类加载器创建的,而是根据java运行时期的需要自动创建的。它返回的类加载器和它内部元素类型的ClassLoader是一样的,如果它内部元素是一个原生类型的话,是没有ClassLoader的。
package com.brycen.classloader
public class MyTest15{
public static void main(String[] args){
String[] strings = new String[2];
//输出null,因为是String是由根类加载器加载的
System.out.println(strings.getClass().getClassLoader());
int[] ints = new int[2];
//输出null,因为是原始类型
System.out.println(ints.getClass().getClassLoader());
MyTest15[] tests = new MyTest15[2];
//输出sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(tests.getClass().getClassLoader());
}
}
- ClassLoader使用委托模型来搜索类和资源,就是我们熟知的双亲委托。
- ClassLoader在默认情况下就具有并行加载类的能力,但是其子类需要具有并行能力的话,需要调用registerAsParallelCapable方法来注册。在委托模型不是严格分层的环境中,ClassLoader需要具有并行能力,否则加载类的时候会导致死锁,因为在类加载过程中需持有ClassLoader锁
- 类可能是从网络上获取的或者程序动态创建的,那么此时我们可以通过defineClass方法将其字节数组转换为一个class对象,并调用Class.newInstance方法创建这个新定义的class的实例。
- ClassLoader加载的类的方法或者构造方法可能会引用其他的类,那么此时JVM会调用创建这个类本身的ClassLoader的loadClass方法去加载其他引用的类
1.2、类加载器也是一个类,他是如何加载的呢?
- 内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java类(java.util、java.lang包中的类等),当JVM启动时,会有一块特殊的机器码会执行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码就是启动类加载器(Bootstrap)。
- 启动类加载并不是Java类,而其他的加载器则都是Java类
1.3、获取类加载器的途径
- 获取当前类的ClassLoader
clazz.getClassLoader();
- 获取当前线程上下文的的ClassLoader
Thread.currentThread().getContextClassLoader();
- 获取系统的ClassLoader
ClassLoader.getSystemClassCloader()
- 获取调用者的ClassLoader
DriverManager.getCallerClassCloader()
二、类加载器的分类
- 引导类加载器(Bootstrapclass loader):它用来加载 Java 的核心库,如java.lang.*等,是用C来实现的,它是没有父加载器的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。该加载器没有父加载器,除此之外基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
//获取到根加载器的加载目录
System.getProperty("sun.boot.class.path");
- 扩展类加载器(extensionsclass loader):父是根加载器。从java.ext.dirs系统属性所指定的目录中加载类库,或者从安装目录的jre\lib\ext子目录(扩展目录)下加载类库。如果把用户创建的jar放入这个目录下也会自动由扩展类加载器加载。是java.lang.ClassLoader的子类。
//获取到扩展加载器的加载目录
System.getProperty("java.ext.dirs");
- 系统类加载器(System ClassLoader):也称为应用类加载器或 (App class loader)它根据当前Java 应用的类路径(ClassPath)或者系统属性java.class.path来加载 Java 类。是用户自定义的类加载器的默认父加载器。系统类加载器是纯java类,是java.lang.ClassLoader的子类。
Class<?> class = Class.forName("com.brycen.classloader.SimpleObject");
//获取其默认加载器
System.out.println(class.getClassLoader());
//获取其父加载器
System.out.println(class.getClassLoader().getParent());
//获取其根加载器,此处返回为null,因为根加载器是由C编写的,所有返回null
System.out.println(class.getClassLoader().getParent().getParent());
三、父委托加载机制
一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样一层一层往上抛直到最顶层。如果最顶层没有找到则会交由子类加载器去完成,直至最后一个子类加载器。
优点
提高系统的安全性,可以避免用户恶意破坏结构,如自己定义一个String类、Object类等,用户自定义的类加载器不可能加载应该由父加载器加载可靠类,因此可防止恶意代码的代替父加载器的可靠代码。
名词解释
- 定义类加载器:被加载的这个类是由哪个类加载器加载的。那么这个类加载器就称为定义类加载器
- 初始类加载器:是指这个类的所有父加载器都称之为初始化加载器
四、自定义类加载器
- 继承ClassLoader类
- 实现findClass方法,如果不实现则会抛出找不到类异常
- 创建该类加载器加载的目录
自定义的类加载器:
public class MyClassLoader extends ClassLoader {
private final static String DEFAULT_DIR = "E:\\classloader1";
private String dir = DEFAULT_DIR;
private String classLoaderName;
public MyClassLoader() {
super();
}
public MyClassLoader(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
//指定父加载器,不指定则默认是系统加载器
public MyClassLoader(String classLoaderName, ClassLoader parent) {
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("The class " + name + " not found under " + dir);
}
byte[] classBytes = loadClassBytes(classFile);
if (null == classBytes || classBytes.length == 0)
throw new ClassNotFoundException("load the class " + name + " failed");
return this.defineClass(name, classBytes, 0, classBytes.length);
}
//读取类的二进制文件
private byte[] loadClassBytes(File classFile) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(classFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
public String getClassLoaderName() {
return classLoaderName;
}
}
自定义的类:
public class MyObject {
static {
System.out.println("My object static block");
}
public String hello(){
return "Hello World";
}
}
测试:
1.首先将编译好的MyObject.class的完整包名文件夹以及文件放入我们自定义类加载器加载的目录中
2.删除之前原目录编译好的MyObject.class,不删除的话则类加载器还是会使用系统加载器,而不是使用我们自定义的类加载器,因为父委托机制。
注意:我们自定义的类加载器加载类时中并不会导致初始化,因为类加载不属于类的主动使用,在类实例的时候才会初始化,实例属于类的主动使用。这点类加载器和反射还是有点不同的。关于类的主动使用和被动使用可参考我上一篇博客ClassLoader加载过程
public class MyClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader("MyClassLoader");
//注意在类加载时中并不会导致初始化,因为类加载不属于类的主动使用
Class<?> aClass = classLoader.loadClass("com.brycen.classloader.MyObject");
System.out.println(aClass);
System.out.println(aClass.getClassLoader());
//在类实例的时候才会初始化,实例属于类的主动使用
Object obj = aClass.newInstance();
Method method = aClass.getMethod("hello", new Class<?>[]{});
Object result = method.invoke(obj, new Object[]{});
System.out.println(result);
}
}
输出结果:
class com.brycen.classloader.MyObject
com.brycen.classloader.MyClassLoader@7adf9f5f
My object static block
Hello World
五、类加载器的加密解密
原理:就是先将编译后的class文件进行加密,随后用我们自定义的类加载器对其解密后再加载
前期准备:
- 新建一个自定义类加载的加载目录:E:\classloader2
- 将第四章的MyObject类编译好的class文件及其包名文件夹拷贝到E:\classloader2该目录下
- 删除之前原目录编译好的MyObject.class
- 加密工具类
package com.brycen.classloader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public final class EncryptUtils {
public static final byte ENCRYPT_FACTOR = (byte) 0xff;
private EncryptUtils() {
//empty
}
public static void doEncrypt(String source, String target) {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(target)) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data ^ ENCRYPT_FACTOR);
}
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
doEncrypt("E:\\classloader2\\MyObject.class", "E:\\classloader2\\MyObject2.class");
}
}
- 自定义解密类加载器
其实是在第四章自定义加载器的代码中的读取字节时做了相应的解密工作
package com.brycen.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class DecryptClassLoader extends ClassLoader {
private final static String DEFAULT_DIR = "E:\\classloader2";
private String dir = DEFAULT_DIR;
public DecryptClassLoader() {
super();
}
public DecryptClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("The class " + name + " not found under directory [" + dir + "]");
}
byte[] classBytes = loadClassBytes(classFile);
if (null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("load the class " + name + " failed");
}
return this.defineClass(name, classBytes, 0, classBytes.length);
}
//主要区别在这里,这里做了解密操作
private byte[] loadClassBytes(File classFile) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(classFile)) {
int data;
while ((data = fis.read()) != -1) {
baos.write(data ^ EncryptUtils.ENCRYPT_FACTOR);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
return null;
}
}
public void setDir(String dir) {
this.dir = dir;
}
}
- 测试
package com.brycen.classloader;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
DecryptClassLoader classLoader = new DecryptClassLoader();
//这里别忘了是加载我们加密之后的class文件
Class<?> aClass = classLoader.loadClass("com.brycen.classloader.MyObject2");
System.out.println(aClass);
Object obj = aClass.newInstance();
Method method = aClass.getMethod("hello", new Class<?>[]{});
Object result = method.invoke(obj, new Object[]{});
System.out.println(result);
}
}
输出结果:
class com.brycen.classloader.MyObject
My object static block
Hello World
六、打破类加载器的双亲委托机制
在自定义加载器中只需要重写loadClass这个方法即可,让其子加载器优先于父加载器加载。虽然可以打破但是像java.lang.String等系统的类都是没有办法加载的,会出现Security的安全异常。
前期准备:
- 新建一个自定义类加载的加载目录:E:\classloader3
- 将第四章的MyObject类编译好的class文件及其包名文件夹拷贝到E:\classloader3该目录下
- 注意:这里不删除之前原目录编译好的MyObject.class,按照正常加载顺序及时我们使用了自定义加载器加载该类,也会由系统加载器加载
package com.brycen.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class SimpleClassLoader extends ClassLoader {
private final static String DEFAULT_DIR = "E:\\classloader3";
private String dir = DEFAULT_DIR;
private String classLoaderName;
public SimpleClassLoader() {
super();
}
public SimpleClassLoader(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public SimpleClassLoader(String classLoaderName, ClassLoader parent) {
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String name)
throws ClassNotFoundException {
String classPath = name.replace(".", "/");
File classFile = new File(dir, classPath + ".class");
if (!classFile.exists()) {
throw new ClassNotFoundException("The class " + name + " not found under " + dir);
}
byte[] classBytes = loadClassBytes(classFile);
if (null == classBytes || classBytes.length == 0)
throw new ClassNotFoundException("load the class " + name + " failed");
return this.defineClass(name, classBytes, 0, classBytes.length);
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
//这里要过滤java开头的包名,这些包名必须由系统加载器加载,因为在我们自定义加载器的加载目录中没有这些class文件
if (name.startsWith("java.")) {
try {
ClassLoader system = ClassLoader.getSystemClassLoader();
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (Exception e) {
//ignore
}
}
try {
clazz = findClass(name);
} catch (Exception e) {
e.printStackTrace();
}
if (clazz == null && getParent() != null) {
getParent().loadClass(name);
}
return clazz;
}
private byte[] loadClassBytes(File classFile) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(classFile)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.flush();
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
public String getClassLoaderName() {
return classLoaderName;
}
}
测试
package com.brycen.classloader;
public class SimpleClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
Class<?> aClass = simpleClassLoader.loadClass("com.brycen.classloader.MyObject");
System.out.println(aClass.getClassLoader());
}
}
输出结果:
com.brycen.classloader.SimpleClassLoader@7adf9f5f
正式因为打破了双亲委托机制,所有这里是使用的自定义加载器加载的
七、类加载器的命名空间与运行时包
7.1、命名空间
- 因:类加载器的命名空间是由自身加载器及其所有父加载器的类组成的。(根加载器为null可能不会参与构成)。
- 果:所以在类加载器加载多次相同类时,只会存在一个Class对象分配于堆内存中。如果是两个不同的加载器分别去加载同一个类,则会出现多Class对象分配于堆内存中。这就是因为命名空间的不同。
7.2、运行时包
运行时期一个类的包其实是由其classloader的命名空间及其包名组成的
- 父类加载器看不到子类加载器加载的类
- 不同命名空间下的类加载器之间的类互相不可访问
- 命名空间的组成是由类加载器及其父类加载器组成的
案例:
在第六章的代码基础上进行的实验
package com.brycen.classloader;
public class RuntimePackage {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
SimpleClassLoader simpleClassLoader = new SimpleClassLoader();
//这里的加载器使用的是我们自定义的加载器,即使原MyObject的class及其完整包名没有删除,因为我们使用的是打破双亲委托机制的类加载器。
Class<?> aClass = simpleClassLoader.loadClass("com.brycen.classloader.MyObject");
System.out.println(aClass.getClassLoader());
//根据运行时包名原则,这行就会报错,因为RuntimePackage这个类是由系统类加载器加载的,我们的aClass.newInstance()是由自定义加载器加载的
SimpleObject simpleObject = (SimpleObject) aClass.newInstance();
}
}
运行结果:
com.brycen.classloader.SimpleClassLoader@7adf9f5f
Exception in thread "main" java.lang.ClassCastException:com.brycen.classloader.MyObject cannot be cast to com.brycen.classloader.MyObject
八、类的卸载以及ClassLoader的卸载
8.1、类的卸载
- 由JVM自带的类加载器(根、扩展、系统类加载器)所加载的类始终不会被卸载,因为JVM本身就会引用这些类加载器,而这些类加载器会始终应用他们所加载的类的Class对象。
- 由用户自定义的类加载器所加载的类是可以被卸载的。
我们可以通过java自带的jvisualvm工具来查看当前程序加载的类和卸载的类。
8.2、Class对象的卸载条件
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载。
- 该类所有的实例都已经被GC
- 加载该类的ClassLoad实例已经被GC
- 该类的java.lang.Class对象没有在任何地方被引用
GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。
九、线程上下文加载器
作用
可以通过线程上下文加载器打破加载器的双亲委托机制
获取线程上下文
Thread.currentThread().getContextClassLoader();
数据库驱动案例分析
因为在java中JDBC的规范是由java定义的,都是一些接口,在java.sql包中,是由根加载器去加载的,而我们使用数据库的时候,那些实现类是由系统加载器去加载的。那么就会出现访问不到具体的实现这个问题。这时候就会用到我们的线程上下文加载器去打破这种双亲委托机制。
步骤:
- 首先反射我们具体厂商的实现类
Class.forName("com.mysql.jdbc.Driver");
- 因为反射属于主动使用,那么我们将会执行他下面的静态方法
registerDriver(new Driver());是将自身实例传递进去
- 随后在getConnection的时候就会通过获取线程上下文加载器来加载。因为执行getConnection方法的线程肯定是我们程序中的线程,那么他的加载器也就会是系统加载器。
下面这个代码是getConnection中的源码,将线程上下文加载器赋于callerCL
- 通过指定加载器加载类,这样我们就可以在程序中访问到了