Java类加载机制是技术体系中比较核心的部分,对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解Java虚拟机的连接模型和Java语言的动态性都有很大帮助。
类加载器在jvm中位置
类加载器在JVM中的位置:
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内(1.8就放在了元数据中)的数据结构。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类的加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载:
-
通过一个类的全限定名来获取定义此类的二进制字节流(Class文件);将这个二进制字节流所代表的静态存储结果转化为方法区的运行时数据结构;在内存中生成一个java.lang.Class对象,存放在方法区(1.8为元空间)。
验证:
验证目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;使用纯粹的Java代码无法做到诸如访问数组边界意外的数据、将一个对象转型为它未实现的类型、跳转到不存在的代码之类的事情,如果这样做了,编译器将拒绝编译。
-
文件格式的验证
-
元数据验证
-
字节码验证
-
符号引用验证
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。
准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。首先这时候进行内存分配的仅包括类变量(static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
public static int value = 123;
变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,在类初始化的时候才会将value的值赋为123。如果同时被final和static修饰,才会直接赋值。
解析:
解析阶段是虚拟机将class常量池内的符号引用替换为直接引用的过程。
-
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可;
-
直接引用:是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。有了直接引用,那引用的目标必定已经在内存中存在。
初始化:
类初始化阶段是类加载过程的最后一步;在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源:初始化阶段是执行类构造器( )方法的过程。( )方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static { }块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
类加载器分类
一张图来看一下他们的层次关系
启动(Bootstrap)类加载器:
也叫引导类加载器,是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。
扩展(Extension)类加载器:
扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System)类加载器:
也叫应用程序类加载器,是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库 加载到内存中。开发者可以直接使用系统类加载器。
public class LoaderTest {
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}/* Output:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null
*///:~
最*的启动类加载器就是null。
类加载方式
-
由 new 关键字创建一个类的实例
例:Dog dog = new Dog(); -
调用 Class.forName() 方法
例:Class clazz = Class.forName(“Dog”);Object dog =clazz.newInstance(); -
调用某个 ClassLoader 实例的 loadClass() 方法
例:Class clazz = classLoader.loadClass(“Dog”);Object dog =clazz.newInstance();
比较:
-
通过new关键字实例化类的对象和通过Class.forName()加载类是当前类加载器,即this.getClass.getClassLoader,只能在当前类路径或者导入的类路径下寻找类, new 是静态加载类。
-
我们知道类加载机制的三个过程主要是加载–>链接–>初始化。forName方式和loadClass方式都是动态加载类。Class.forName()实际调用的是Class.forName(className,true,this.getClass.getClassLoader),第二个参数表示加载完后是否立即初始化,第三个参数即前文提到的表示是当前类加载器。classLoader.loadClass()实际调用的是classLoader.loadClass(className,false),第二个参数表示加载完成后是否链接,即用此方法加载类,加载完成后不会去初始化,而用Class.forName()加载类加载完成后可以被初始化。所以有些类如果加载完成后需要立即被初始化则必须使用Class.forName()。例如在加载数据库驱动时,一般用Class.forName(“com.mysql.jdbc.Driver”)。这是因为该驱动有一个在静态代码块中注册驱动的过程,所以需要被初始化。
-
有两个异常:
1)静态加载类时出现的一般是NoClassDefFoundError。(new)
2)动态加载类时出现的一般是ClassNotFoundException。(forName方式和loadClass方式)
区别:
这两者经常被用来比较,其实区别很大。NoClassDefFoundError是错误,不方便被捕捉也不需要被捕捉,不应该尝试从error中恢复程序。他是由于在使用new关键字实例化类的对象时,在内存中找不到对象了,一般比较少见,在运行时发生,即编译时可以找到类运行时却找不到了。而ClassNotFoundException是异常,是可以被捕捉的,应该捕捉并处理尝试恢复程序。这是由于利用类名动态加载类的时候,在外存储器类路径下找不到该类或者其依赖的jar包,还有一个导致其的原因是在同一个包中同一个类被不同的类加载器加载了两遍。
双亲委派机制:
JVM在加载类时默认采用的是双亲委派机制。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形)。
这里我们从系统类加载器和扩展类加载器源码角度来看看怎么实现的。
扩展类加载器类结构:
系统类加载器类结构:
通过这两张图我们可以看出,扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader抽象类。我们来看看它的源码。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
private final ClassLoader parent;
/**
* ClassLoader类中的静态内部类,他可以决定指定的类是否具备并行的能力,
* 若具备并行能力则ClassLoader下面的parallelLockMap便会被初始化,
* 该静态内部类在ClassLoader被加载的时候便被初始化了
*/
private static class ParallelLoaders {
private ParallelLoaders() {}
//存放具备并行能力的加载器类型的Set集合
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}
/**
* 将指定的类加载器注册成为一个具备并行能力的类加载器,注册成功则返回true,否则返回false
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
//只有当该类加载器的父类具备并行能力时,该类加载器方可成功注册
if (loaderTypes.contains(c.getSuperclass())) {
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}
/**
* 判断指定的classloader类加载器是否具备并行的能力
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
//若当前类加载器具备并行能力,则该属性会被初始化
private final ConcurrentHashMap<String, Object> parallelLockMap;
/**
* ClassLoader类的构造函数,在这里主要确定该类加载器是
* 否具备并行能力与其他变量的初始化操作
* @param unused
* @param parent
*/
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
//若该类加载器具备并行能力(如何判断见ClassLoader类中的静态内部类)
if (ParallelLoaders.isRegistered(this.getClass())) {
//初始化parallelLockMap
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
//不具备并行能力,parallelLockMap置为null
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
* 使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:调用findLoadedClass(String)方法
* 检查这个类是否被加载过,使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机
* 内置的加载器调用findClass(String)方法装载类,如果按照以上的步骤成功的找到对应的类,并且该方法接收的resolve
* 参数的值为true,那么就调用resolveClass(Class)方法来处理类,ClassLoader的子类最好覆盖findClass(String)而不是
* 这个方法,除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//这里根据当前类加载器是否具备并行能力而获取对应的锁对象
synchronized (getClassLoadingLock(name)) {
/**
* 在加载类之前先调用findLoadClass方法查找看该类是否加载过,
* 若被加载过则直接返回该对象,无需二次加载,未加载过则返回null,
* 进行下一个流程
* PS:ClassLoader类中的findLoaderClass方法是一个本地方法,
* 自定义的类加载器需要重写该方法
*/
Class c = findLoadedClass(name);
//若该类未被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//如果父类加载器不为空,则调用父类加载器的loadClass方法加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//若父类加载器为空,则调用虚拟机的加载器Bootstrap ClassLoader来加载类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
//若以上的操作都没有成功加载到类
if (c == null) {
long t1 = System.nanoTime();
//则调用该类自己的findClass方法来加载类
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) {
//这个方法用来给ClassLoader链接一个类(Class载入必须link,link指的是把单一的Class加入到有继承关系的类树中)
resolveClass(c);
}
return c;
}
}
/**
* 这个方法是根据当前类加载器是否具备并行能力而决定是否返回锁对象,
* 当该类加载器不具备并行能力,则无需返回一个加锁对象,若具备并行能
* 力,则返回一个新的加锁对象
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
//根据parallelLockMap是否被初始化来判断当前类加载器是否具备并行能力
if (parallelLockMap != null) {
//若该类加载器具备并行能力,则创建新的锁对象返回
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
}
全盘负责委托机制
每个类都有自己的类加载器,那么负责加载当前类的类加载器也会去加载当前类中引用的其他类,前提是引用的类没有被加载过。
例如ClassA中有个变量 ClassB,那么加载ClassA的类加载器会去加载ClassB,如果找不到ClassB,则异常。
线程上下文类加载器以及SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。SPI是java内置的一种服务发现机制,一般在框架设计的时候,将问题抽象成接口,至于服务的实现,由不同的厂家来各自实现,按照指定的规范引入哪个厂家的实现jar包,就可以使用该厂家的服务实现,这种现象个人理解跟java的多态很类似。
jvm推荐我们使用双亲委托机制,主要是保证了相同的类不会被重复加载。但是,在jdk1.2之后,提出了线程上下文类加载器的概念,目的是为了打破双亲委托机制,因为在某些场景下(例如:JNDI,JDBC…)等等SPI场景中。
原生的JDBC的使用,获取数据库连接使用的是 Connection conn = DriverManager.getConnection(xx,xx,xx);很明显,Connection是jdk提供的接口,具体的实现是我们的厂商例如mysql 实现,加入到项目中,那么设想一下,DriverManager.getConnection(xx,xx,xx);该方法肯定是使用的mysql的jar包,返回了mysql实现的Connection对象,那么加载DriverManager类是由启动类加载器加载,根据上面的全盘负责委托机制来说,启动类加载器会去加载MySql的jar包,很明显,找不到。所以使用双亲委托机制来说,无法实现该SPI场景的需求。
线程上下文类加载器和普通类加载器区别
-
双亲委托机制:子加载器对应的命名空间包含了父加载器,所以可以实现子容器访问父容器
-
线程上下文类加载器:使用该类加载器,可以实现 父容器访问子容器场景,主要设置好上下文类加载器即可。
策略模式和SPI的区别
如果从代码接入的级别来看,策略模式还是在原有项目中进行代码修改,只不过它不会修改原有类中的代码,而是新建了一个类。而 SPI 机制则是不会修改原有项目中的代码,其会新建一个项目,最终以 Jar 包引入的方式代码。
从这一点来看,无论策略模式还是 SPI 机制,他们都是将修改与原来的代码隔离开来,从而避免新增代码对原有代码的影响。但策略模式是类层次上的隔离,而 SPI 机制则是项目框架级别的隔离。
从应用领域来说,策略模式更多应用在业务领域,即业务代码书写以及业务代码重构。而 SPI 机制更多则是用于框架的设计领域,通过 SPI 机制提供的灵活性,让框架拥有良好的插件特性,便于扩展。
-
从设计思想来看。策略模式和 SPI 机制其思想是类似的,都是通过一定的设计隔离变化的部分,从而让原有部分更加稳定。
-
从隔离级别来看。策略模式的隔离是类级别的隔离,而 SPI 机制是项目级别的隔离。
-
从应用领域来看。策略模式更多用在业务代码书写,SPI 机制更多用于框架的设计。
借助JDBC源码分析上下文类加载器的使用
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("xxx", "xx", "xx");
将mysql的驱动【com.mysql.jdbc.Driver】注册到jdk的DriverManager上边去,我们跟进一下代码:
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
这会引起【com.mysql.jdbc.Driver】的主动调用,因此会初始化com.mysql.jdbc.Driver:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
//将当前mysql的com.mysql.jdbc.Driver 注册到jdk的java.sql.DriverManager里边去
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
这个时候会引起java.sql.DriverManager的主动调动,导致java.sql.DriverManager初始化(赋予静态变量正确的初始值),因此这个时候java.sql.DriverManager的静态代码块会被执行,我们到java.sql.DriverManager里边看一下:
public class DriverManager {
...
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
//调用loadInitialDrivers方法,继续往里跟进。
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
...
//因为我们没有设置 jdbc.drivers属性,所以这里只展示关键代码,对于其他不影响流程的代码有所删减,具体的可以看源码
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;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//主要的引入各个厂家的Driver类是的服务是在这里加入的
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;
}
});
}
}
通过下面这行代码就可以将mysql依赖加载到内存中了。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
下面来分析 ServiceLaoder.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);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
}
当我们执行 ServiceLaoder.load()的时候,首先会获取当前线程的上下文类加载器。而且在构造方法中也可以看到,如果获取的上下文类加载器为空时,也会使用默认的系统类加载器,而默认设置当前线程的上下文类加载器的时候,默认运行时也是系统类加载器作为上下文类加载器,所以先肯定一点,后续加载类的类加载器肯定是 系统类加载器。
自定义类加载器
当JDK提供的类加载器实现无法满足我们的需求时,才需要自己实现类加载器。
根据上述类加载器的作用,可能有以下几个场景需要自己实现类加载器
-
当需要在自定义的目录中查找class文件时(或网络获取)
-
class被类加载器加载前的加解密(代码加密领域)
实现自己的类加载器:
/**
* 自定义ClassLoader
* 功能:可自定义class文件的扫描路径
* @author zhiminxu
*/
// 继承ClassLoader,获取基础功能
public class TestClassLoader extends ClassLoader {
// 自定义的class扫描路径
private String classPath;
public TestClassLoader(String classPath) {
this.classPath = classPath;
}
// 覆写ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
// getDate方法会根据自定义的路径扫描class,并返回class的字节
byte[] classData = getDate(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 生成class实例
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getDate(String name) {
// 拼接目标class文件路径
String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0 ,num);
}
return stream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
使用自定义的类加载器:
public class MyClassLoader {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 自定义class类路径
String classPath = "/Users/xxx/developer/classloader";
// 自定义的类加载器实现:TestClassLoader
TestClassLoader testClassLoader = new TestClassLoader(classPath);
Class<?> object1 = method1(testClassLoader);
Class<?> object2 = method2(testClassLoader);
// 这里的打印应该是我们自定义的类加载器:TestClassLoader
System.out.println(object1.getClassLoader());
System.out.println(object2.getClassLoader());
}
public static Class<?> method1(ClassLoader classLoader) throws ClassNotFoundException {
return classLoader.loadClass("ClassLoaderTest");
}
public static Class<?> method2(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> clazz = Class.forName("",false,classLoader );
return (Class<?>) clazz.newInstance();
}
}