JVM学习08节-类加载机制

类加载机制

1. 学习目标

  1. 什么是类加载
  2. 类加载的过程
  3. 类加载器
  4. 双亲委派模型

2. 什么是类加载

虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载。

3. 类加载的过程

3.1 生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载,共7个阶段,其中验证、准备、解析,3个部分统称为连接,如下图:

加载、验证、准备、初始化和卸载5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始。而解析则未必,为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。

这里类加载出发的时机、主动引用和被动引用的解释,可以查看《深入理解Java虚拟机》7.3.1 类加载的过程。面向面试学习,整个类加载的知识单单通读了一遍,不做深入学习。重点学习了双亲委派模型

3.2 加载

在加载阶段,虚拟机需要完成以下3件事情:

  • 通过类的全限定名来获取定义此类的二进制字节流
  • 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
  • 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。

3.3 校验

此阶段主要确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。

  1. 文件格式验证:基于字节流验证。
  2. 元数据验证:基于方法区的存储结构验证。
  3. 字节码验证:基于方法区的存储结构验证。
  4. 符号引用验证:基于方法区的存储结构验证。

3.4 准备

为类变量分配内存,并将其初始化为默认值。(此时为默认值,在初始化的时候才会给变量赋值)即在方法区中分配这些变量所使用的内存空间。例如:

public static int value = 123;

此时在准备阶段过后的初始值为0而不是123;将value赋值为1的putstatic指令是程序被编译后,存放于类构造器方法之中。特例:

public static final int value = 123;

此时value的值在准备阶段过后就是123。

3.5 解析

把类型中的符号引用转换为直接引用。

  • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在

主要有以下四种:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

3.6 初始化

初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):

  1. 使用new关键字实例化对象、访问或者设置一个类的静态字段(被final修饰、编译器优化时已经放入常量池的例外)、调用类方法,都会初始化该静态字段或者静态方法所在的类。
  2. 初始化类的时候,如果其父类没有被初始化过,则要先触发其父类初始化。
  3. 使用java.lang.reflect包的方法进行反射调用的时候,如果类没有被初始化,则要先初始化。
  4. 虚拟机启动时,用户会先初始化要执行的主类(含有main)
  5. jdk 1.7后,如果java.lang.invoke.MethodHandle的实例最后对应的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法所在类没有初始化,则先初始化。

4. 类加载器

把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。系统自带的类加载器分为三种:

  1. 启动类加载器(BootStrap ClassLoader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应用程序类加载器(Application ClassLoader)

我们的应用程序都是由这三种类加载器相互配合进行加载的,如果有必要,还可以加入自定义的类加载器,这些类加载器的关系一般如下图所示:

4.1 启动类加载器

这个类加载器是由C++语言实现,是虚拟机自身的一部。负责将存在<JAVA_HOME>\lib 目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且能被虚拟机识别的类库加载到虚拟机内存中。

4.2 扩展类加载器

继承于 java.lang.ClassLoader ,负责加载<JAVA_HOME>\lib\ext 目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

4.3 应用程序类加载器

继承于 java.lang.ClassLoader,负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用这个类库,一般情况是程序默认的类加载器,在 JDK1.2 之后引入。

5. 双亲委派模型

5.1 什么是双亲委派模型?

如上图所展示的类加载器之间的这种层级关系,称之为双亲委派模型,它有两点要求:

  1. 要求顶层是启动类加载器
  2. 要求其余类加载器都应当有自己的父类加载器
  3. 父子关系一般不以继承的关系来实现,而是通过使用组合关系来复用父类加载器的代码

双亲委派并不是一个强制性的约束模型

双亲委派的作用就是:对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性

5.2 工作过程

如果一个类加载器收到了类加载器的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成。

每个层次的类加载器都是如此,因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中,只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系。eg:类java.lang.Object,它存放在rt.jart之中,无论哪一个类加载器都要加载这个类,最终都是双亲委派模型最顶端的Bootstrap类加载器去加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类,并存放在程序的ClassPath中,那系统中将会出现多个不同的Object类。java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。

5.3 代码实现

实现双亲委派的典卖集中在 java.lang.ClassLoader 的 loadClass() 方法中,其代码清单如下:

/**
 * 加载方法
 */
public Class<?> loadClass(String name) throws ClassNotFoundException {
	return loadClass(name, false);
}

/**
 * 真实执行方法
 */
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 {
                    //没有父类加载器,则是顶级的启动类加载器执行
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
				// ClassNotFoundException thrown if class not found
				// from the non-null parent class loader
                //如果父类加载器抛出 ClassNotFoundException 则表示父类加载器无法完成请求
			}

			if (c == null) {
				// 父类加载器无法加载的时候,再调用本身的 findClass 方法进行类加载
				long t1 = System.nanoTime();
				c = findClass(name);

				// 记录统计信息
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		if (resolve) {
            //解析这个类
			resolveClass(c);
		}
		return c;
	}
}

/**
 * 返回类加载操作的锁对象。为了向后兼容,此方法的默认实现行为如下。
 * 如果此ClassLoader对象注册为并行能力,该方法返回一个专用对象关联具有指定的类名。
 * 否则,该方法将返回类加载器对象
 */
protected Object getClassLoadingLock(String className) {
	Object lock = this;
	if (parallelLockMap != null) {
		Object newLock = new Object();
		lock = parallelLockMap.putIfAbsent(className, newLock);
		if (lock == null) {
			lock = newLock;
		}
	}
	return lock;
}

5.4 破坏双亲委派模型

双亲委派模型主要出现3次较大规模的‘被破坏’情况

  • 向前兼容。

    JDK1.2之后引入的双亲委派,为了兼容JDK1.2之前的,已经存在的用户自定义实现类加载器的代码,而做出妥协(之前的用户可能重写了loadClass方法,与双亲委派思想不一致)。

  • 设计缺陷

    这个一开始是不理解的,将书中的话,反复研读,结合双亲委派模型的约定,终于理解啦。下面单开一节,基于 JDBC 如何破坏双亲委派模型。类似的不单单是JDBC,还有JNDI、JCE、JAXB、JBI等。

  • 用户对程序动态性的追求(代码热替换、模块热部署等)

    这部分如果想了解,可以看看OSGI《深入理解OSGi:Equinox原理、应用和实践》

5.5 在日常开发中如何遵循和破坏双亲委派模型

  • 遵循双亲委派模型:Sun 推荐重写 findClass() 而非重写 loadClass() 方法。
  • 破坏双亲委派模型:重写loadClass()

6. 基于JDBC分析,破坏双亲委派模型

6.1 java提供的Driver

java本身提供为数据库操作提供了一个Driver接口,在 rt.jar 中,如下图:

然后提供了一个DriverManager来管理这些Driver的具体实现,代码如下:

public class DriverManager {
	
	// 这里省略了大部分的代码,看关键代码即可
    // List of registered JDBC drivers :保存所有Driver的具体实现
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

}

上述代码中,可以看到,在使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后才能正常使用。

6.2 不破坏双亲委派模型

还记得以前手写数据库连接吗?

// 1.加载数据驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接到数据库
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/train?characterEncoding=UTF-8", "root", "QAZ@wsx123");

可见,由 Class.forName() 触发了mysql驱动的加载,接下来看下mysql对Driver接口的实现:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    //重点在这里
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

Class.forName()其实触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。这个时候,通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了

6.3 破坏双亲委派模型

6.3.1 线程上线文类加载器

JDBC4.0以后,开始支持使用SPI(Service Provider Interface)的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,如下图:

然后使用的时候这样写:

Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/train?characterEncoding=UTF-8", "root", "QAZ@wsx123");

分析下,使用了这种spi服务的模式原本的过程是怎样的:

  • 第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名com.mysql.jdbc.Driver
  • 第二,加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载。

问题来了,com.mysql.jdbc.Driver 是由调用者的 ClassLoader 中 class.forName() 进行加载的,这个调用者DriverManager 是在rt.jar中的,那么其 ClassLoader 是启动类加载器,而 com.mysql.jdbc.Driver 肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类

mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器(Thread Context ClassLoader)

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

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

6.3.2 线程上下文类加载器是如何工作的

DriverManager.getConnection() 会先执行静态代码块中的代码,现在看下DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类。

public class DriverManager {
    
    //省略部分代码
    
    private static void loadInitialDrivers() {
		// 省略代码
        
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				// 重点在这里!! 查找各个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
                }
                return null;
            }
        });
		
        // 省略代码
    }
    
    //静态代码块,它先执行哦~
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
}

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.load() 中拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程,我们不再深入分析,这里只要知道这个ServiceLoader已经拿到了线程上下文类加载器即可。

到这里,只是看到了如何获取线程上线文类加载器,并没有看到启动类加载器通过应用程序类加载器(父类加载器通过子类加载器加载类)的实现啊?别急:DriverManager的 loadInitialDrivers() 方法中有一句driversIterator.next();,在看这个方法之前,先看loadedDrivers.iterator();

public Iterator<S> iterator() {
    return new Iterator<S>() {
        // 已知供应商 迭代器
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            // 查找 迭代器
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            // 查找 迭代器
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

com.mysql.jdbc.Driver 对于启动类加载器肯定是未知的,需要查找的,那在看lookupIterator,代码如下:

driversIterator.next() 会调用 LazyIterator 中的next()方法,进而调用 nextService() 方法,此处使用的线程上线文类加载器应该是应用程序类加载器(默认的),而应用程序类加载器就可以查找到厂商在子级的jar包中注册的驱动具体实现类名,这样就可以在rt.jar包中的DriverManager中成功的加载了第三方应用程序包中的类了,实现了父类加载器能通过调用子类加载器来加载类,也就破坏了双亲委派的原则。

private class LazyIterator implements Iterator<S> {
  
 	// ... 省略代码
    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");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
 	// ... 省略代码
    
    public S next() {
        if (acc == null) {
            // here 这里!!!! 
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
	
    // ... 省略代码
}

Reference:

https://www.tuicool.com/articles/rqaQJj

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值