深入理解Java虚拟机(二)-类加载机制
类加载机制
再说类加载机制前,我们自己先思考下类加载机制大概是什么,通过前面解析class文件我们已经知道,class文件包含了很多部分内容,而且要按照一定的格式,不能说我们自己写一堆东西重命名一个.class后缀的就是class文件了,虚拟机在读取class文件之前肯定要有一些校验手段的,验证通过虚拟机将class读取到内存中,成为虚拟机可以使用的类。
什么是类加载机制
类加载机制就是虚拟机把class文件加载到内存,并对数据进行校验,转换解析和初始化,形成虚拟机可以直接使用的Java类型(java.lang.Class)
- 装载(load):查找和导入class文件
- 通过类的全限定名获取到类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成这个类的对象,所为对方法区中这些数据的访问入口
- 链接(link):
- 验证:保证被加载类的正确性 在验证阶段为了保证正确性,会验证文件格式,元数据,字节码,符号引用
- 准备:为类的静态变量分配内存,并将其初始化为默认值
public static int age = 10在准备阶段完成的时候age的值为0 - 解析:把类中的符号引用转为直接引用 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
- 初始化(initializa):对类的静态变量,静态代码块执行初始化操作
- 使用(use)
- 卸载(unload)
类加载器classloader
了解了类加载机制后,链接,初始化等操作都是JVM中进行的,只需要把class文件通过装载加载到虚拟机就可以,那么class文件又是怎么加载进入虚拟机的呢?答案是需要通过类装载器classloader
类加载器分类
bootstrap是最上层的类加载器,级别依次下排
- Bootstrap ClassLoader: 负责加载$JAVA_HOME中jre/lib/rt.jar、resources.jar、charsets.jar和class等或者Xbootclasspath选项指定的jar包。是c++实现的不是ClassLoader的字类。
- Extension ClassLoader: 负责加载Java平台中扩展的一些jar包,包括$JAVA_HOME中jre/lib/ext/目录写的jar和class文件或-Djava.exe.dirs指定目录下的jar包
- App ClassLoader: 负责加载classpath中指定的jar包及-Djava.class.path所指定目录下的类和jar包
- Custom ClassLoader: 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要定义的ClassLoader,如tomcat,jboss会根据j2ee规范自行实现ClassLoader
加载原理(双亲委派)
所谓双亲委派是指:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
当一个类被自下而上的委托给父加载器的时候,最终都会到bootstrap classloader但会因为bootstrap不负责加载该类,在由下一级的ext classloader来进行加载,如果在不符合就继续往下一级,直到被负责该类的加载器来加载。
父子加载器是继承关系吗?怎么AppClassLoader没有继承ExtClassLoader?
在双亲委派中,类加载器之间的父子关系没有继承关系来实现,通过内部成员来定义
public abstract class ClassLoader {
private final ClassLoader parent;
}
接下来通过一个小例子来看一下类加载器
public class TryClassLoader {
public static void main(String[] args){
TryJVM jvm = new TryJVM();
//app classloader
System.out.println("1" + jvm.getClass().getClassLoader());
//ext classloader
System.out.println("2" + jvm.getClass().getClassLoader().getParent());
//bootstrap classloader
System.out.println("3" + jvm.getClass().getClassLoader().getParent().getParent());
//java本身类
System.out.println("4" + new String().getClass().getClassLoader());
System.out.println("4" + new Object().getClass().getClassLoader());
}
}
输出结果
1sun.misc.Launcher$AppClassLoader@73d16e93
2sun.misc.Launcher$ExtClassLoader@15db9742
3null
4null
通过例子可以看到我们自己定义的类使用App ClassLoader,也可以看到App和Ext各自的父加载器,通过String和Object类也可以看到rt.jar下面的类使用的是bootstrap加载器,返回null表示使用bootstrap。
JDK API对于getClassLoader()的描述
Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader. ClassLoader.java源码
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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;
}
}
通过源码可以看到
- 1.先检查类是否已经被加载过
- 2.若没有加载则调用父加载器的loadClass()方法进行加载
- 3.若父加载器为空则默认使用启动类加载器作为父加载器
- 4.如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载 在这里也可以发现JVM通过自上而下加载类到JVM中,通过自下而上的方式检查类是否已经被加载,如果已经被下级的类加载器加载,那么就不需要再次加载
破坏双亲委派
根据ClassLoader的loadClass方法可以知道双亲委派的实现,那么为什么要破坏双亲委派呢?
- 举个例子Tomcat破坏双亲委派 tomcat作为web容器会部署多个应用,不同应用的依赖可能相同可能不同,如果多个应用中依赖同一个依赖的不同版本,这些不同版本中很多类的全路径是是一样的,如果是双亲委派是无法加载多个相同的类的。
tomcat为了实现隔离性为先加载每个web应用自己定义的类,每一个应用自己的类加载器先加载本身应用目录下的class文件,加载不到时候再去Tomcat的CommonClassLoader。
- JDBC,JNDI需要加载SPI接口实现类
在使用JDBC服务的时候,创建数据库连接池一般通过
Connection conn = DriverManager.getConnection("jdbc://mysql://host:3306/mysql","root","123456")代码来创建数据库连接,我们都知道Java提供了JDBC的驱动接口Driver.java各个厂商会自己实现适配自己的Driver。
因此通过查看DriverManager生成conn的源码
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
```
private static void loadInitialDrivers() {
String drivers;
......
// 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();
......
}
}
分析这个代码可以知道DriverManager会被bootstrap类加载器加载,因为他在rt.jar中,但是当加载器加载到这里的时候,会加载所有Driver的实现类,这些实现类都是第三方按照JDBC的标准来实现的,那么按照双亲委派来看,这些第三方的类是不能被bootstrap加载的,但是这些类也明明被加载了,不然我们怎么用呢?
查看ServiceLoader源码可以看到,ServiceLoader中通过引入Thread.currentThread().getContextClassLoader();类加载器来加载Driver的实现类,默认情况下Thread.currentThread().getContextClassLoader()是AppClassLoader这样便破坏了双亲委派原则。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
看下官方对于getContextClassLoader的解释
Returns the context ClassLoader for this Thread. The context ClassLoader is provided by the creator of the thread for use by code running in this thread when loading classes and resources. If not {@linkplain #setContextClassLoader set}, the default is the ClassLoader context of the parent Thread. The context ClassLoader of the primordial thread is typically set to the class loader used to load the application. 重点看最后一句话:原始线程的上下文ClassLoader通常设置为用于加载应用程序的类加载器(App ClassLoader) The context ClassLoader of the primordial thread is typically set to the class loader used to load the application. 到这里我们就明白了当JDBC这样的使用SPI机制,通过第三方来提供实现类的,在最开始通过bootstrap加载DriverManager类,在DriverManager中又使用
Thread.currentThread().getContextClassLoader()也就是App ClassLoader来加载第三方提供的类,从而破坏了双亲委派机制。