JAVA中JVM类加载器详细介绍
JVM类加载器详细介绍
一、什么是类加载器
根据一个指定的类的全限定名,找到对应的Class字节码文件,然后加载它转化成一个java.lang.Class类的一个实例。并且这个类对应的Class实例在堆区无论你加载多少次只会存在一个,除非使用不同的加载器去加载这个类,则会出现多个的效果。这是因为不同的加载器会出现命名空间的问题。
二、类加载器的分类
- 引导类加载器(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
正式因为打破了双亲委托机制,所有这里是使用的自定义加载器加载的
七、类加载器的命名空间与运行时包
命名空间
- 因:类加载器的命名空间是由自身加载器及其所有父加载器的类组成的。(根加载器为null可能不会参与构成)。
- 果:所以在类加载器加载多次相同类时,只会存在一个Class对象分配于堆内存中。如果是两个不同的加载器分别去加载同一个类,则会出现多Class对象分配于堆内存中。这就是因为命名空间的不同。
运行时包
运行时期一个类的包其实是由其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的卸载
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
- 通过指定加载器加载类,这样我们就可以在程序中访问到了
本文地址:https://blog.csdn.net/liuyu973971883/article/details/107588334