类加载那些事儿

类加载

    • 想必大家都知道类加载的机制是双亲委派模式
    • 但是有些地方需要破坏双亲委派模型
    • 我们再看看类加载的流程
    • 类加载和初始化时机
    • 图解分析加载

想必大家都知道类加载的机制是双亲委派模式

双亲委派如何实现?
类加载那些事儿_第1张图片
当我们自定义加载器必然要继承java.lang.ClassLoader 而它有一个无参构造方法

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
 protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
   private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            assertionLock = this;
        }
    }

getSystemClassLoader() ,想必大家都懂,系统(也称为应用)类加载器,我们parent成员变量就有了系统类加载器的引用。

好了经典委派算法来了~~~~

 if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }

但是有些地方需要破坏双亲委派模型

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第二,加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载
好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

那么,这个问题如何解决呢?按照目前情况来分析,这个mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器。

线程上下文类加载器可以通过Thread.setContextClassLoaser()方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器

很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        //省略代码
        //这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
             while(driversIterator.hasNext()) {
                driversIterator.next();
             }
        } catch(Throwable t) {
                // Do nothing
        }

  //省略代码
    }
}

使用时,我们直接调用DriverManager.getConn()方法自然会触发静态代码块的执行,开始加载驱动
然后我们看下ServiceLoader.load()的具体实现:

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

可以看到核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程,我们不再深入分析,这里只要知道这个ServiceLoader已经拿到了线程上下文类加载器即可。
接下来,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next();,它的具体实现如下:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //此处的cn就是产商在META-INF/services/java.sql.Driver文件中注册的Driver具体实现类的名称
               //此处的loader就是之前构造ServiceLoader时传进去的线程上下文类加载器
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
         //省略部分代码
        }

这里采用的是java.util.ServiceLoader的SPI机制

我们再看看类加载的流程

经典的图

类加载那些事儿_第2张图片

加载
将class文件加载在内存中。
将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构)。
在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。

链接
 链接就是将Java类的二进制代码合并到java的运行状态中的过程。

验证:确保加载的类符合JVM规范与安全。
准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0,其他数据类型参考成员变量声明。
解析:虚拟机将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用 的目标并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

初始化
  初始化阶段是执行类构造器()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并,初始化时机后续再聊。

使用
  正常使用。

卸载
  GC把无用对象从内存中卸载。

类加载和初始化时机

1、类加载时机

当应用程序启动的时候,所有的类会被一次性加载吗?估计你早已知道答案,当然不能,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。那类什么时候被加载呢?例如,A a=new A(),一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。当我们测试的时候,最先加载拥有main方法的主线程所在类。

2、类初始化时机

主动引用(发生类初始化过程)

new一个对象。
调用类的静态成员(除了final常量)和静态方法。
通过反射对类进行调用。
虚拟机启动,main方法所在类被提前初始化。
初始化一个类,如果其父类没有初始化,则先初始化父类。
 被动引用(不会发生类的初始化)

当访问一个静态变量时,只有真正声明这个变量的类才会初始化。(子类调用父类的静态变量,只有父类初始化,子类不初始化)。
通过数组定义类引用,不会触发此类的初始化。
final变量不会触发此类的初始化,因为在编译阶段就存储在常量池中。

图解分析加载

类加载那些事儿_第3张图片
哈哈,博主的分享总结就倒这里,希望大家点赞!

你可能感兴趣的:(JDK)