signed

QiShunwang

“诚信为本、客户至上”

深入理解JVM之解析以及类加载器 双亲委派

2021/4/26 22:35:27   来源:

前言

小编最近在写精通mybatis的博客,大家有兴趣的可以点开来看一下,如果认为写的可以的话记得三连啊。

解析阶段补充

上次小编讲了类加载机制,那今天稍微补充一下解析阶段
1、解析时机:一般做初始化的时候去解析
2、解析什么: 只要是直接引用都需要解析,方法,接口,类,字段
3、如何避免重复解析:利用缓存,ConstantPoolCache运行时常量池,底层是hashtable,解析之前判断是否已经解析解析完毕后存入缓存

静态变量是如何存储的,大家先看下问题:

public class Test{
    public static void main(String[] args) {
        System.out.printf(TestB.str);
    }
}

class TestA {
    public static String str = "A str";

    static {
        System.out.println("A Static Block");
    }
}

class TestB extends TestA {
    static {
        System.out.println("B Static Block");
    }
}

这边会打印什么呢,答案是

A Static Block
A str

首先静态属性是存储在堆区的 ,静态属性的访问: 1、去缓存去找,如果有直接返回 2、如果没有就触发解析 。其底层实现:1、找到直接引用 2、存储到常量池缓存中,储存的结构是:key为常量池的索引 ,value为ConstantPoolCacheEntry,上面就是将String包装成ConstantPoolCacheEntry。

类加载器

包含启动类加载器,扩展类加载器,应用类加载器,自定义加载器。JVM中有两种类型的类加载器,由C++编写的以及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承⾃类java.lang.ClassLoader。根类加载器通过java代码打印为null,是因为C++编写所以没有能表示的java代码,看源码时扩展类加载器传入父类加载器的时候就传入null。又因为根类加载器为null,所以无法被java程序所调用。
各种类加载器之间存在着逻辑上的⽗⼦关系,但不是真正意义上的⽗⼦关系,因为它们直接没有从属关系。类加载器加载的jar的范围以及双亲委派如下图所示:
在这里插入图片描述
启动类加载器
启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的⼀套逻辑定义为启动类加载器。
查看启动类加载器的加载路径:

public static void main(String[] args) {
        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
        for (URL urL : urLs) {
            System.out.println(urL);
        }
    }

也可以通过-Xbootclasspath指定路径
扩展类加载器
查看类加载器加载的路径:

public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
        URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
        URL[] urls = urlClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }

可以通过java.ext.dirs指定
应用类加载器
默认加载⽤户程序的类加载器 查看类加载器加载的路径:

public static void main(String[] args) {
        String[] urls = System.getProperty("java.class.path").split(":");
        for (String url : urls) {
            System.out.println(url);
        }
        System.out.println("================================");
        URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
        URL[] urls1 = classLoader.getURLs();
        for (URL url : urls1) {
            System.out.println(url);
        }
    }

可以通过java.class.path指定

大家都知道main方法是应用类加载器加载的,但是明明是jvm调用为什么不是根类加载器而是应用类加载器:其实启动类加载器做的事情 是:加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain……启动类、扩展类、 应用类加载器逻辑上的⽗⼦关系就是在这个方法的调用链中生成的。

类加载器加载的类如何存储

上一篇博客中讲到klass模型存储在元空间中,其实各自加载器加载的类都在元空间中开辟一个空间,然后空间内储存自己加载的类信息。如下图所示:
在这里插入图片描述

双亲委派

如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。同时避免了类的重复加载。
源码阅读
java.lang.ClassLoader#loadClass

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

打破双亲委派

在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服 务商来提供,比如mysql的就写了 MySQL Connector ,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载, 只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。 类似这样的情况就需要打破双亲委派。 打破双亲委派的意思其实就是不委派、向下委派。
源码阅读
java.sql.DriverManager#loadInitialDrivers

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;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                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。待会儿小编在spi中写个示例大家就明白了

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

SPI

是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件中所定义的类。这种机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。其实打破双亲委派机制也使用了SPI。
示例代码
pay-center

public class PayMethodTest {
    public static void main(String[] args) {
        ServiceLoader<PayService> load = ServiceLoader.load(PayService.class);
        for (PayService payService : load) {
            payService.pay();
        }
    }
}

pom

<dependencies>

        <dependency>
            <groupId>xxx.xxx.xxx</groupId>
            <artifactId>pay-common</artifactId>
            <version>1.0.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>xxx.xxx.xxx</groupId>
            <artifactId>pay-ali</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>xxx.xxx.xxx</groupId>
            <artifactId>pay-wx</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

pay-common
共同引入的jar包

public interface PayService {
    /**
     * 支付方法
     */
    void pay();
}

pay-ali

public class AliPayService implements PayService {
    @Override
    public void pay() {
        System.out.println("支付宝支付");
    }
}

配置文件,前面为接口的包路径xxx.xxx.xxx
在这里插入图片描述
内容为实现类的路径:

xxx.xxx.xxx.AliPayService

这样就是spi的用法了

反射底层原理

这边小编简单说明一下。反射forName,getField,getMethod原理,大家其实知道了底层如何存储的,那基本也就知道它是如何获取的,类的存储,底层叫Directory存储是hashtable,key是根据类全限定名加类加载器计算得出index,然后value为klass模型。然后反射的时候根据类全限定名以及类加载器计算index然后我们就可以找到klass模型,klass模型中什么都有了(还需要找到他的字段和方法),之后可以根据权限拿到对应的值即可。

总结

今天小编主要补充了解析阶段,然后是类加载器,加载类的存储以及双亲委派打破双亲委派,反射底层原理。小编这边其实还没完整说明,类加载器的整个流程,也没说明源码,其实这边需要大家搭建一个openjdk的环境,启动类加载器在hotspot里面就是一个逻辑块命名为classLoader,里面的方法都是静态方法,然后是类加载器创建,创建链中何时赋值线程上下文类加载器等等。小伙伴得自己看代码然后总结。加油努力吧。