Java ClassLoader 类加载器详解

35 篇文章 11 订阅

 

本文主要内容:

  • 类加载器简介
  • 双亲委派机制
  • 自定义类加载器
  • Class.forName() VS ClassLoader.loadClass()
  • 类加载器死锁问题
  • 一个类加载器的高级问题

 

ClassLoader 类加载器简介


Java 类加载器主要是用来当程序需要的某个类时,通过类加载器类把类的二进制加载到内存中。

Java 提供了 3 个类加载器,它们分别是:

  • BootstrapClassLoader(C++编写)

       用于加载 JRE/lib/rt.jar 里的 class,JDK系统的类库基本上都在这里

  • ExtClassLoader

       用于加载 JRE/lib/ext/* 文件夹下所有的 class

  • AppClassLoader

       用于加载 CLASSPATH 目录下所有的 class,也就是开发者编写的类


在 Java 中提供了一个抽象类来表示类加载器:ClassLoader

ClassLoader 有一个 parent 属性,表示该 ClassLoader 的父加载器是谁。

在每个 Class 对象中都有一个方法叫做 getClassLoader() 方法用来获取该 Class 是由哪个 ClassLoader 加载的。

我们可以通过下面一个程序来打印

public static void main(String[] args) {
    ClassLoader classLoader = Test.class.getClassLoader();
    while (classLoader != null) {
        System.out.println(classLoader.getClass().getName());
        classLoader = classLoader.getParent();
    }
    System.out.println(classLoader);
}

程序输出结果:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

Test.class 类使我们自定义的类,所以它是由 AppClassLoader 类加载器来加载的。AppClassLoader 的 parent 是 ExtClassLoader,ExtClassLoader 的 parent 是 BootstrapClassLoader,如下图所示:

双亲委派机制

什么是双亲委派机制呢?

就是加载一个类的时候把这个加载任务交给父加载器,父加载器收到这个请求后,也把这个请求交给自己的父加载器,以此类推。所以任何一个类加载操作一开始都会到最顶层的类加载器。如果最顶层的类加载无法去加载,那么这个加载任务再向下逐级传递。如果都无法无加载,则提示找不到类。


下面通过源码理解双亲委派机制:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 检查加载的类是否已经被加载过
        Class<?> c = findLoadedClass(name);
        // 如果没有被加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果有父加载器
                if (parent != null) {
                    // 交给父加载器去加载(递归调用)
                    c = parent.loadClass(name, false);
                } else {
                    // 如果 parent = null,说明当前的类加载器是 Bootstrap ClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                long t1 = System.nanoTime();
                // 如果父类找不到,则调用自己的 findClass 去加载
                c = findClass(name);
                // 省略其他代码...
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

可见类加载的委托机制实际上是一个递归调用。loadClass() 方法触发了这个这个递归,loadClass() 如果没有找到类,那么 loadClass() 方法里面会调用 findClass() 来进行类加载,所以真正的类加载操作要么在 loadClass() 方法里,要么在 findClass() 方法里,所以自定类加载的方式可以通过覆写 loadClass 或 findClass 来实现,它们之间的却别后面会介绍。

 

自定义类加载器


我们知道了类加载就是加载 class 字节码流,然后产生 Class 对象的。我们只要指定了字节码文件不就可以了,所以自定义类加载器很简单。

经过上面对 ClassLoader 的源码分析,我们可以在 loadClass 或 findClass 方法里将字节流转成 Class 对象。Java 官方建议我们通过重载 findClass 方法而不是 loadClass方法来自定义类加载器。下面的自定类加载将采用重载 findClass() 的方式。

类加载机制让开发者可以灵活的去制定加载类的逻辑,如可以将一个 class 文件按照某种加密规则进行加密,然后只有某种特定的类加载器才能正常的解密。下面我们来实现下:

首先我们准备一个简单的类:

package class_load;

public class CipherClass {
    public CipherClass() {
        System.out.println("CipherClass Object was created");
    }
}

将 CipherClass 通过 javac 命令编译成 CipherClass.class 文件,然后按照下面的加密算法将 CipherClass.class 字节码进行加密:

/**
 * 加密方法,同时也是解密方法
 */
private static void cypher(InputStream ips, OutputStream ops) throws Exception {
    int b = -1;
    while ((b = ips.read()) != -1) {
        //1 就变成 0,0 就变成 1
        ops.write(b ^ 0xff);
    }
}


然后我们自定义一个 ClassLoader,在里面可以对其进行解密,然后转成 Class 对象:

public class CipherClassLoader extends ClassLoader {

    private String classDir;
    
    public CipherClassLoader(String classDir) {
        this.classDir = classDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 定位加密后的 class 字节码文件
        String classFileName = classDir + "\\" + name.substring(name.lastIndexOf('.') + 1) + ".class";
        try {
            FileInputStream fis = new FileInputStream(classFileName);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            // 将加密的 class 字节码进行解密
            cypher(fis, bos);
            fis.close();
            byte[] bytes = bos.toByteArray();
            // 将正常的 class 字节流转成 Class 对象
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

// 测试
public static void main(String[] args) throws Exception {
    String dir = "class file dir";
    Class clazz = new CipherClassLoader(dir).loadClass("class_load.CipherClass");
    clazz.newInstance();
}



程序运行输出:

CipherClass Object was created

可见被加密后的 class 字节码文件被我们自定义的 ClassLoader 解密后成功加载。

 


Class.forName() VS ClassLoader.loadClass()


有的时候我们无法直接拿到某个类,但是又需要使用这个类。这个时候可以使用 Class.forName() 和 Classloader.loadClass() 来加载这个类。

这两种方式的区别主要有两个:

1)Class.forName() 会执行类的初始化操作,也就是会执行 static 代码块,而 Classloader.loadClass() 的方式则不会

先来看下 Class.forName() 的源代码:

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize,
                                    ClassLoader loader,
                                    Class<?> caller) throws ClassNotFoundException;


其中 initialize 参数就是表示是否执行初始化操作的,下面看一个例子:

public static void main(String[] args) throws ClassNotFoundException {
    // 会执行初始化操作
    Class clazz1 = Class.forName("class_load.ConstTest");
    // 不会执行初始化操作
    Class clazz2 = new ClassLoader() {}.loadClass("class_load.ConstTest");
}


所以学过 JDBC 的都知道,在操作 MySQL 数据的之前需要通过如下方式注册驱动:

Class.forName("com.mysql.jdbc.Driver");


因为 com.mysql.jdbc.Driver 在其静态代码块中进行注册操作:

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
} 


所以注册驱动只能使用 Class.forName() 而不能使用 Classloader.loadClass()。

使用 Classloader.loadClass() 来加载一个类,哪怕调用它的 class.newInstance() 也不会执行其 static 静态代码块。

2)Class.forName() 可以加载数组类,而 Classloader.loadClass() 则不行:

我们知道,数组类是没有事先定义好的,数组类是由虚拟机中动态创建的。

Class.forName("[Ljava.lang.String;"); // String[]
Class.forName("[I");                  // int[]   基本数据类型数组

// 抛出异常:java.lang.ClassNotFoundException: [Ljava.lang.String;
ClassLoader.getSystemClassLoader().loadClass("[Ljava.lang.String;");

Class.forName 来加载数组类无法对数组类进行实例化和指定数组大小。可以通过 java.lang.reflect.Array.newInstance 反射一个数组对象:

int[] arr = (int[]) java.lang.reflect.Array.newInstance(int.class, 10);
System.out.println(arr.length); // 10


类加载器死锁问题


在 JDK1.7 之前,ClassLoader 是有可能出现死锁的,关于 ClassLoader 死锁的问题可以查看官方对该问题的描述 (点击进入查看)
 
下面是官方对死锁情况的复现描述:

Class Hierarchy:
  class A extends B
  class C extends D

ClassLoader Delegation Hierarchy:

Custom Classloader CL1:
  directly loads class A 
  delegates to custom ClassLoader CL2 for class B

Custom Classloader CL2:
  directly loads class C
  delegates to custom ClassLoader CL1 for class D

Thread 1:
  Use CL1 to load class A (locks CL1)
    defineClass A triggers
      loadClass B (try to lock CL2)

Thread 2:
  Use CL2 to load class C (locks CL2)
    defineClass C triggers
      loadClass D (try to lock CL1)

本来打算在上面改成中文注释的,但是上的描述已经非常简洁明了,所以就不画蛇添足了。

在对死锁情况介绍之前,先来看下 JDK1.6 ClassLoader:

protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

 

可以看到 synchronized 是放在方法上,是整个方法同步,那么 ClassLoader 对象就是同步方法的锁(lock)。

下面可以描述死锁的产生情况了,有两个线程:

  • 线程1:CL1 去 loadClass(A) 获取到了 CL1 对象锁,因为 A 继承了类 B,defineClass(A) 会触发 loadClass(B),尝试获取 CL2 对象锁;
  • 线程2:CL2 去 loadClass(C) 获取到了 CL2 对象锁,因为 C 继承了类 D,defineClass(C) 会触发 loadClass(D),尝试获取 CL1 对象锁
  • 线程1 尝试获取 CL2 对象锁的时候,CL2 对象锁已经被线程2拿到了,那么线程1等待线程2释放 CL2 对象锁。
  • 线程2 尝试获取 CL1 对像锁的时候,CL1 对像锁已经被线程1拿到了,那么线程2等待线程1释放 CL1 对像锁。
  • 然后两个线程一直在互相等中…从而产生了死锁现象。

 

如果你是通过重载 findClass 方法来自定类加载器的,那么将不会有死锁问题,那么也就没有破坏双亲委派机制,这也是官方建议的机制。如果是通过重载 loadClass 方法来实现自定义类加载器就有可能出现死锁的。

那有的人会说那我通过重载 findClass 来实现自定义类加载器不就可以避免了么?是的。

但是有的时候又不得不通过重载 loadClass 方法来实现自定义类加载器,比如我们实现的类加载器不想遵循双亲委派机制(官方称之为 acyclic delegation),那么只能重载 loadClass 了,前面分析 loadClass 方法源码就知道了,是这个方法执行递归操作(双亲委派的逻辑)。

从中可以看出,如果你仅仅是想自定义个类加载器而已,但是不会改变双亲委派机制,那么重载 findClass 方法即可。

如果万不得已要通过重载 loadClass 来实现,在 JDK1.7 中可以在定义类加载器中的静态代码块中添加如下代码来避免死锁的出现:

static {
    ClassLoader.registerAsParallelCapable();
}


其实 JDK 为我们提供的类加载器,如 AppClassLoader 默认就加上了:

static class AppClassLoader extends URLClassLoader {

    // 省略其他代码..

    static {
        ClassLoader.registerAsParallelCapable();
    }
}


一个类加载器的高级问题


我们知道 Tomcat 服务器,是一个大大的 Java 程序,那么它就必须在 JVM 上运行。

这个大大的 Java 程序内部也写了很多类加载器,它用这些类加载器去加载一些特定的类:注入 Servlet 类。

下面我们新建一个 JavaWeb 工程,新建一个 Servlet 程序:

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        ClassLoader classload = this.getClass().getClassLoader();
        while (classload != null) {
            out.println(classload.getClass().getName()+"<br>");
            classload = classload.getParent();
        }
        out.println();
        out.close();
    }

然后配置服务器,部署应用程序,启动 Tomcat 服务器。在页面访问我们这个 Servlet,在页面打印的结果如下图所示:


这是从小到大排序的。现在呢?我想把该 Servlet 打成 jar 包,放在 ExtClassLoader 类加载器加载的路径:

 

 

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chiclaim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值