JVM类加载机制以及破坏

一、类加载机制

  1. 启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载 JAVA_HOME/lib 目录。
  2. 扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JAVA_HOME/lib/ext 目录或者 java.ext.dirs 系统变量所指定的路径中的类库。开发者可以直接使用扩展类加载器。
  3. 应用类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是ClassLoader 中 getSystemClassLoader方法的返回值,所以也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。这个是程序中默认的类加载器。

双亲委派模型:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此向上委托,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器自己无法加载时,子加载器才会尝试自己去加载。说双亲委派模型实在存在误导性,因为他是一个爸爸的爸爸叫爷爷的过程,是单亲向上委托。并且,父类加载器无法使用由子类加载器加载的类(因为父类加载器不会向下委托给子类加载器),相反,子类加载器可以使用父类加载器加载的类。

为什么要这么做?

1. 保证类的唯一性(如果不是向上委托,则可能出现多个由不同类加载器加载的相同名称的类)。

一个类在同一个类加载器中具有唯一性(Uniqueness),而不同类加载器中是允许同名类存在的,这里的同名是指全限定名相同
但是在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

2. 保证安全,防止类被篡改。

 

代码:java.lang.ClassLoader#loadClass(java.lang.String, boolean)

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 {
                    // 返回一个被启动类加载器加载的类,如果未找到,则返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类无法完成加载,抛出 ClassNotFoundException
            }
            if (c == null) {
                // 如果依然没找到(父类加载器未找到),则调用 findClass 来寻找
                long t1 = System.nanoTime();
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

 

二、怎样才算破坏双亲委派模型?

1. Java SPI

一个典型的例子是MySQL驱动类的加载。

标准的接口由官方指定,不同厂商可以有自己的实现。下面是MySQL的驱动:

其中调用了 java.sql.DriverManager ,这个DriverManager里面有个静态代码块:

这个就是利用SPI来加载驱动的。

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // 如果驱动被打包成Service Provider,则加载它。
    // 利用为java.sql.Driver.class服务的类加载器,加载所有驱动程序

    // java.security.AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许。
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            // SPI的使用
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    // 遍历所有驱动的全限定名
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 用系统类加载器加载
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

SPI的load方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 这里用的是线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

观察得知:

此处用到了两个类加载器:

1. ServiceLoader.load(Driver.class) --线程上下文类加载器

2. Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()) -- 系统类加载器

线程上下文类加载器是一种独立的加载器吗?

最开始也说过,除了自定义的类加载器,JVM中一共有三种类加载器。所以这个加载器应该是个持有者的身份,结合驱动类是放在ClassPath下的,它所持有的肯定不是启动类加载器,因为启动类加载器不能加载ClassPath目录下的类;那么剩下的两个类加载器,它是写在 sun.misc.Launcher 里的:

public class Launcher {
    ...

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            // 系统类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        // 将系统类加载器设置为线程上下文类加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }
    ...
}

那么:

1. ServiceLoader.load(Driver.class) --线程上下文类加载器(默认系统类加载器)

2. Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()) -- 系统类加载器

问题一:ServiceLoader.load 里也调用了Class.forName方法,为什么出来了又要调用一次?我觉得区别在于load里调用的是Class.forName(cn, false, loader),false-只加载类,不初始化类。

问题二:根据双亲委派的可见性原则,启动类加载器() 加载的 DriverManager 不可能拿到 系统类加载器() 加载的实现类。系统类加载器来加载驱动实现类又怎么样?之所以想不通,是因为忽略了一个点:父类加载器能够使用由线程上下文加载器加载的实现类。这就改变了父类加载器不能使用子类加载器或是其他没有直接父子关系的类加载器所加载的类的情况,即破坏了双亲委派模型。破坏双亲委派模型的关键不是重写loadClass,而是引入了线程上下文类加载器。

 

2. Tomcat

Tomcat也是破坏双亲委派模型的例子。对于一些未加载的非核心类库,各个web应用优先用自己的类加载器(WebAppClassLoader)加载,加载不到再交给commonClassLoader走双亲委托。

热部署:JSP文件修改了之后是不需要重启的,当JSP发生修改之后,只需要卸载掉这个JSP的类加载器,然后重新创建类加载器,重新加载这个JSP文件即可。

隔离:一个web容器可能需要部署两个应用程序A和B,A和B可能会依赖同一个第三方类库的不同版本,比如mysql-connector-java.5.X和mysql-connector-java.8.X,不能只加载一个版本,因此要保证每个应用程序的类库都是独立的,保证相互隔离。每个应用都有自己的类加载器,类加载器之间是不可见的,起到了隔离效果。一个类的全限定名和加载该类的加载器二者共同形成了这个类在JVM中的唯一标识

 

posted @ 2021-08-10 16:44  露娜妹  阅读(435)  评论(0编辑  收藏  举报