JVM 类加载器

什么是类加载器

类加载器负责在运行时将Java类动态加载到Java虚拟机,他们也是JRE(Java运行时环境)的一部分。因此,借助类加载器,JVM无需了解底层文件或文件系统即可运行Java程序。此外,这些Java类不会一次全部加载到内存中,而是在应用程序需要他们时才会进行加载,这就是类加载器发挥作用的地方,他们负责将类加载到内存中。

类加载器是有一定的层级关系,以JDK8为例:

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Excension ClassLoaderJAVA_HOME/jre/lib/ext上级为Bootstrap,显示为null
Application ClassLoaderclasspath上级为Excension
自定义的类加载器自定义上级为Application

分为了这几个层级,最顶层的叫Bootstrap ClassLoader(启动类加载器),下一级是Excension ClassLoader(扩展类加载器),再下一级是Application ClassLoader(应用程序类加载器),最后一级是自定义的类加载器

这几个不同层级的类加载器到底有什么关系呢?实际上,每个类加载器各管一块儿,比如Bootstrap ClassLoader只负责加载JAVA_HOME/jre/lib目录下的所有的类,不在这个目录下的类他就不闻不问。类似的,Excension ClassLoader只负责去加载JAVA_HOME/jre/lib/ext扩展目录里面的这些类,除了这个目录,他也不认。最常见的是Application ClassLoader,他负责加载classpath,即类路径下的所有的类。当然,自定义类加载器要加载的类自己可以去定义的。这些是他们各自管的区域。

除此以外,他还有层级关系,比如Application ClassLoader去加载类的时候,他首先会问一问,这些类是不是由它的上级(Excension ClassLoader)加载过了,即看Excension ClassLoader有没有加载过这个类,如果没有,他还会让这个Excension ClassLoader再委托他的上上级即Bootstrap ClassLoader看看有没有加载过这个类,如果他们这两个上级都没有加载,那才轮得到Application ClassLoader去加载这个类。

比如,我们想加载String类,我通过Application ClassLoader去调用他的loadClass方法去加载字符串类,结果他就会去看Excension有没有加载过这个类,若Excension说没有,Excension就会继续去问更上级的Bootstrap有没有记载过string类,结果Bootstrap加载了这个类,因为string类属于JAVA_HOME/jre/lib目录下的一个类,所以他肯定是又这个Bootstrap已经加载过了,这样的话Excension也好还是Application加载器也好,都不用操心这个string类的加载了。

那么再比如说我们自定义一个Student类,同样的也会先让Excension加载器去看有没有加载过,如果没有再去委托Bootstrap加载器有没有加载,当然,由于自定义的student类是不会出现在Java的核心工作目录中(JAVA_HOME/jre/lib),所以Bootstrap就会说没有加载,然后再让Excension加载器去看他自己在JAVA_HOME/jre/lib/ext目录里去找student类,也肯定找不到,所以student类就会交给你Application ClassLoader去加载这个类。

其实上面所介绍的类的这种委托方式,在JVM的领域把他叫做“双亲委派类加载模式”。

所以这个层级关系就是自定义类加载器的上级是应用程序类加载器应用程序类加载器的上级是扩展类加载器扩展类加载器的上级是启动类加载器,需要注意的是,扩展类加载器getParent的时候打印的是null,因为Bootstrap类加载器他不是Java代码写的,而是c++代码写的,所以他不会让我们的Java代码所直接访问。

public void test() throws ClassNotFoundException {
	System.out.println("Classloader of this class:" + Test.class.getClassLoader());
	System.out.println("Classloader of Logging:" + Logging.class.getClassLoader());
	System.out.println("Classloader of ArrayList:" + ArrayList.class.getClassLoader());
}

// 运行结果如下:
/*
Classloader of this class:sun.misc.Launcher$AppClassLoader@18a4bbc2 // 应用类加载器(加载classpath中我们自己写的文件)
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@2escaf27 // 扩展类加载器
Classloader of ArrayList:null // 启动类加载器(之所以Null是因为启动类加载器是用native code写的,而不是Java写的,因此他不会显示为Java类)
*/
启动类加载器

Bootstrap加载器通常都是去加载JAVA_HOME/jre/lib目录下的这些类,但是我们也可以通过一些特殊的虚拟机参数把我们自己编写的一些类交由Bootstrap加载器去进行加载。先定义一个F类

public class F {
	static {
		System.out.println("bootstrap F init");
	}
}

然后执行代码如下:

public class Load {
	public static void main(String[] args) throws ClassNotFoundException {
		// 通过Class.forName去加载类。Class.forName既可以完成类的加载,也可以顺便做类的链接、初始化操作。
		Class<?> aClass = Class.forName("com.cnm.F");
		// 如何知道F类是被哪个类加载器加载了呢?所有的Class都有getClassLoader()方法,获得这个类他对应的类加载器。所以
		// 打印出来就能知道是哪个类加载器加载了F类。
		// 如果是应用程序类加载器,就睡输出AppClassLoader,如果是扩展类加载器,就打印ExtClassLoader。而启动类加载器,由于
		// 是c++程序编写的,所以他不能够通过Java代码直接访问,如果打印出null,就说明他是启动类加载器。
		System.out.println(aClass.getClassLoader());
	}
}

执行类加载前,需要注意的是为了避免idea对类路径的一些干扰,可以在idea下面的Terminal命令行下执行这段代码,首先找到输出目录:cd D:\cnm\jvm

接着执行下面的命令,通过java命令去执行Load类(参数-Xbootclasspath是去指定启动类加载的路径,/a是启动类路径追加一些信息,即他不会去改变原本启动类加载起要去加载的那个JAVA_HOME/jre/lib路径,只是在原有基础上追加,后面的.是指把当前目录追加上去):java -Xbootclasspath/a:. com.cnm.Load

  • -Xbootclasspath 表示设置bootclasspath(启动类加载器的类路径),他又几种方式:
    • java -Xbootclasspath:<new bootclasspath>,即写个新路径,就相当于用新路径完全替换掉JAVA_HOME/jre/lib
    • java -Xbootclasspath/a:<追加路径>,即在原有的基础上后面追加。
    • java -Xbootclasspath/p:<追加路径>,这是前追加。(这个和上面的是开发JVM的人去考虑的,对于我们普通用户没必要去考虑,了解即可
  • 其中/a:.表示将当前目录追加至bootclasspath之后
输出如下:
bootstrap F init
null

打印null说明F类是由启动类加载器去加载的。

Java类由java.lang.ClassLoader的对象/实例加载。但是类加载器本身就是一个类,所以谁加载这个ClassLoader类呢?答案是启动类加载器。它主要负责JDK内部类,通常是rt.jar和其他位于$JAVA_HOME/jre/lib目录下的核心类库,此外,启动类加载器是所有其他ClassLoader实例的parent。启动类加载器是核心JVM的一部分,并且是用本机代码(native code)编写的,不同的JVM平台可能有这个特定类加载器的不同实现。

扩展类加载器
public class G {
	static {
		System.out.println("classpath G init");
	}
}

public class Load {
	public static void main(String[] args) throws ClassNotFoundException {
		Class<?> aClass = Class.forName("com.cnm.G");
		System.out.println(aClass.getClassLoader());
	}
}

默认情况下,G肯定在classpath下,那么这里打印出aClass.getClassLoader()肯定是应用程序类加载器,运行如下:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

AppClassLoader就能知道这确实是由应用程序类加载器去加载的,因为它不可能在启动类加载器的路径下以及扩展类加载器的路径下找到G类

那如果再写一个同名的G类,并放在扩展类加载器路径下,那么,此时G类是会被哪个类加载器加载呢?由于扩展类加载器路径下的类必须是以jar包形式存在,所以,可以把G类打包(为了区分先把输出语句改成"ext G init"),命令行里输入 jar -cvf my.jar com.cnm.G.class,即把G类打包成my.jar,然后把这个jar包放到C:\ProgramFiles\Java\jdk1.8.0_91\jre\lib\ext目录中,这样的话扩展类路径下有了my.jar。然后把刚才改的输出语句恢复原来的"classpath G init"后,再重新运行下Load类,输出如下:

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

可以发现,这回打印的是ext G init,即说明加载的是扩展类路径下的G类my.jar),而不是应用程序路径下的G类,从打印出ExtClassLoader能知道确实是扩展类加载器加载了他。

那如何解释这个现象呢?当我们的应用程序类加载器想去加载这个类的时候,他的先问问他的上级同不同意,上级就是扩展类加载器,结果人家扩展类加载器已经在他的类路径下找到了一个同名的G这个类,所以扩展类加载器就把G加载了,加载以后,应用程序类加载器他就没有机会再加载了。相当于优先级最高的是启动类加载器,其次是扩展类加载器,第三才会轮到应用程序类加载器
网友1:所以咱们要是写了一个和JDK同名的类,是完全用不了滴。

扩展类加载器是启动类加载器的子类(?),负责加载核心Java类的扩展,以便平台上运行的所有应用程序都可以使用他们。扩展类加载器从JDK扩展目录加载,通常是$JAVA_HOME/lib/ext目录,或java.ext.dirs系统属性中存在的任何其他目录。

应用/系统类加载器

负责将所有的程序级别的类加载到JVM中。他会加载在类路径环境变量(classpath)中找到的文件,他也是扩展类加载器的子类(?)。

双新委派模式

所谓的双亲委派,就是指调用类加载器的loadClass方法时,查找类的规则。总的来说,就是委派上级优先来做这个类的加载,上级没有的话,再由我本级的类加载器来完成加载。(需要注意翻译的方式,通常国内文献都把他翻译为双亲委派模式,但翻译为上级似乎更为合适,因为比如启动类加载器、扩展类加载器、应用程序类加载器他们其实并没有一种继承上的父子关系,他只是级别不一样

双新委派模式工作过程

比如,下面的Java代码要通过类加载器的loadClass方法去加载H类

public class Load {
	public static void main(String[] args) throws ClassNotFoundException {
		System.out.println(Load.class.getClassLoader());
		Class<?> aClass = Load.class.getClassLoader().loadClass("com.cnm.H");// 当然H类在classpath下
		System.out.println(aClass.getClassLoader());
	}
}

debug模式运行代码,在第二行(Class<?>…省略)暂停后,点进去loadClass方法,去观察他内部的实现。点进去后:

ClassLoader.java
...
public class<?> loadClass(String name) throws ClassNotFoundException {Ereturn loadClass(name, false);// 然后再点击loadClass进去源码
}
...

源码如下:

protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {
	// 【断点执行到这里时(A)】当前类加载器是谁呢?此时this是AppClassLoader对象,即应用程序类加载器。
	synchronized (getClassLoadingLock(name)) {
		// 【断点执行到这里时】调用findLoadedClass,在应用程序类加载器自己的一个缓存中看看有没有已经加载过一个H这样一个类。
		Class<?> c = findLoadedClass(name);
		// 【断点执行到这里时】当然,首次肯定是没有的,即c是null。
		if (c == null) {
			
			long t0 = System.nanoTime();
			try {
				// 【断点执行到这里时】由于没有找到已加载的类,那他要委托他的上级parent(ExtClassLoader)去查找。
				if (parent != null) {
					// 【断点执行到这里时】如果不为空,就去调用扩展类调用器的loadClass方法。
					// 由于要调用loadClass方法,所以再次鼠标点击该方法进去看看,进去后,再次会跳到上面的(A)处。
					/*
					(从头部开始执行,第一次轮回)
					1,那么此时,当前的this就是扩展类加载器了。
					2,当然,扩展类加载器也有自己的类的缓存。即调用findLoadedClass函数。先到自己的缓存里去找看看有没有加载过的H。
					3,当然,也是没有的。所以c是null。
					4,接下来,扩展类加载器会委托他的上级parent,但此时parent是null,就像上面所说,上级如果是null的话
					   那就表示parent是启动类加载器了,所以这回parent!=null条件不成立,所以会进入(B)处。
					5,(B)处发现启动类加载器下,还是没有找到H。所以在第一次轮回时,会走到(C)处。
					*/
					c = parent.loadClass(name,false);//(相当于递归调用了上级类加载器的loadclass来完成类加载)
				}else {
					// (B)parent为启动类加载器时,就会执行该函数,其内部就是去委托启动类加载器到JAVA_HOME\jre\lib下去找H,
					// 当然肯定是没有的。值得注意的是,该方法内调用的是本地方法,即c++实现的功能,所以看不到源码。
					c = findBootstrapClassOrNull(name);
				}
			}catch(ClassNotFoundException e) {
				// 如果是调用扩展类加载器的loadClass时,当执行findClass时找不到类的话,就会跑到这里,但这里没有
				// 做任何处理。接着执行下去的话,又会来到(C)处【D】。
			}
			
			// (C)如果c还是为null,就调用findClass函数。第一次轮回时,此时this是扩展类加载器,所以在这里去找扩展类
			// 加载器的findClass方法,其内部实际上就是到JAVA_HOME\jre\lib\ext目录下去找H类。(这里findClass会报异常,
			// 因为他在ext下找不到H类。由于出现异常,所以此时直接跑到catch里面,而不会执行下面的部分。因为这是在扩展
			// 类加载器执行loadClass时被应用程序加载器的catch给捉住了。)
			// 【D】由于catch后,从loadClass中跳出来了,所以这回执行时调用的是应用程序类加载器(即自己)的findClass方法。
			// 应用程序类加载器的findClass内部就是到类路径下去找H类。当然,这个例子中是有的[接下D2]。
			if (c == null) {
				long t1 = System.nanoTime();
				c = findClass(name);
				// 【D2】此时,c就会有值了,因为类路径下有H类。[接下D3]
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		if (resolve) {
			resolveClass(c);
		}
		return c;// 【D3】return。这样的话,就跳出了自己的loadClass方法,所以回到了【E】
	}
}

类加载器是Java运行时环境的一部分,当JVM请求访问一个类时,类加载器会尝试定位该类并使用类的全名将类加载到运行时环境。java.lang.ClassLoader.loadClass()方法负责将类定义加载到运行时环境。他尝试根据类的全名称加载类。如果该类还没有被加载过,他会将请求委托给父类加载器,这个过程是递归发生的。最终,如果父类加载器没有找到该类,那么子类加载器将调用java.net.URLClassLoader.findClass()方法在文件系统本身中查找类。如果最有一个子类加载器也无法加载该类,他会抛出java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException异常。

线程上下文类加载器

这是一个比较特殊的类加载器。我们在使用JDBC时,都需要加载Driver驱动,不知道你注意到没有,不写Class.forName("com.mysql.jdbc.Driver");也是可以让com.mysql.jdbc.Driver正确加载的,你知道是怎么做的吗?

可以看一下DriverManager的源码:

public class DriverManager {
	// 注册驱动的集合
	private final static CopyOnWriteArrayList<DriverInfo> registerdDrivers = new CopyOnWriteArrayList<>();
	
	// 初始化驱动
	static {
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}
}

可以看到DriverManager源码里,静态代码块儿中似乎也有一个方法(loadInitialDrivers)是用来加载和初始化驱动类的,看起来好像是
loadInitialDrivers来完成这件事。

但是这样矛盾就来了,可以想一下,DriverManager类所在包是在启动类路径下的,所以他的类加载器实际上就是Bootstrap ClassLoader。如果打印System.out.println(DriverManager.calss.getClassLoader());就能看到打印了null。打印null表示他的类加载器是Bootstrap ClassLoader,会到JAVA_HOME/jre/lib下搜索类,但JAVA_HOME/jre/lib下显然没有mysql-connector-java-5.1.47.jar包,这样问题来了,在DriverManager静态代码块儿中,怎么能正确加载com.mysql.jdbc.Driver驱动类呢?可以继续去查看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;
	}

	// 1)使用ServiceLoader机制加载驱动,即SPI
	AccessController.doPrivileged(
		new PrivilegedAction<Void> {
			public void run() {
				ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
				Iterator<Driver> driversIterator = loadedDrivers.iterator();
				try {
					while(driversIterator.hasNext()) {
						driversIterator.next();
					}
				}catch(Throwable t) {
					// Do noting
				}
				return null;
			}
		}
	);

	// 2)使用Java的系统环境变量的叫jdbc.drivers来找到驱动的类名,并加载驱动
	if (drivers == null || drivers.equals("")) {
		return;
	}
	String[] driversList = drivers.split(":");
	println("number of Drivers:" + driversList.length);
	for(String aDriver : driverList) {
		try {
			println("DriverManager.Initialize: loading " +  aDriver);
			// 这里的ClassLoader.getSystemClassLoader() 就是应用程序类加载器
			Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
		}catch(Exception ex) {
			println("DriverManager.Initialize: load failed:" + ex);
		}
	}
}

这个方法里有两个关键的地方,即尝试使用ServiceLoader机制加载驱动,即SPI;然后还有尝试使用Java的系统环境变量的叫jdbc.drivers来找到驱动的类名,并加载驱动。

先看简单的2),在2)里可以发现他实际上是调用了Class.forName来完成了驱动类(aDriver)的类加载,这里他用的是
ClassLoader.getSystemClassLoader(),这里的getSystemClassLoader就是AppClassLoader,也就是应用程序类加载器。所以这块儿他JDK其实
就是打破了这个双亲委派模式,按理来讲,我们在DriverManager这个类初始化的时候,他理应(网友1:理应?网友2:因为这是JVM虚拟机的规范)是用启动类加载器来完成所有与DriverManager相关联的类的加载,但是可以看到这里违反了这一约定,他是使用了应用程序类加载器来加载了驱动类。

其实也可以想象到,在我们用ServiceLoader1)来类加载时,他内部肯定也不能用启动类加载器去加载驱动类,因为根本找不到,他必须还是要用到应用程序类加载器来完成mysql的驱动加载。所以这个问题到这里就相对比较明朗了,那是因为JDK在某些情况下需要打破这个双亲委派模式,那么有时候他会调用应用程序类加载器来完成类加载,否则的话有些类他是找不到的。
网友1:由于JDBC在核心类库中,他有启动类加载器加载。由于驱动是在他的类初始化方法中加载的,所以驱动是DriverManager的依赖,默认是由启动类加载器加载,但找不到,不可能加载到驱动,于是要显式的调用Class的forName方法使用一个能加载驱动的加载器去加载驱动。

那再看1)ServiceLoader,他就是实际上他就是JDK中实现的一种大名鼎鼎的Service Provider Interface(SPI接口)。它主要是为了解耦,他的具体使用规则如下,他要求我们在jar包里加一个特殊的目录,叫META-INF,并且在该目录下有一个services包,在services包下,以你的接口的全限定名名为文件的名称,这个文件的内容就是普通的文本文件,文本文件内部内容就是这个接口的所有的实现类,你只要按照这个约定去设计了这个jar包,那么将来我们就可以配合ServiceLoader来根据接口找到他的实现类,并加以实例化。这样就实现了解耦。(网友1:根据接口找到文件,文件内容是要加载类的类名

在这里插入图片描述
[图片转自黑马]

具体使用如下:

ServiceLoader<接口类型> allTmpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allTmpls.iterator();
while(iter.hasNext()) {
	iter.next();
}

接口类型一般都是JDK里定义好的,比如上图中的java.sql.Driver这种已经预定义好的接口,可以根据这个接口去进行load,这个load方法就可以找到这个接口的所有的实现类,其实就是根据刚才这个约定找到文件,并找到文件里这些内容,把它们的实现类都拿出来。他是一个集合,可以调用他的迭代器(iterator)然后遍历,每循环一次就调用next方法就可以得到具体的某一个实现类的实例对象了。(这个跟spring容器也有点像,可以根据接口去得到他实现类的实例对象

很多的框架中都运用了这个SPI思想来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet初始化器
  • spring容器
  • Dubbo(对SPI进行了扩展)

那他和类加载有什么关系呢?比如:

// 1)使用ServiceLoader机制加载驱动,即SPI
AccessController.doPrivileged(
	new PrivilegedAction<Void> {
		public void run() {
			ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
			Iterator<Driver> driversIterator = loadedDrivers.iterator();
			try {
				while(driversIterator.hasNext()) {
					driversIterator.next();
				}
			}catch(Throwable t) {
				// Do noting
			}
			return null;
		}
	}
);
...

比如这里可以看到他调用了ServiceLoader.load(Driver.class),他的内部如下:

public static <S> ServiceLoader<S> load(Class<S> service) {
	// 获取线程上下文类加载器,Thread.currentThread()是当前线程,然后通过他的getContextClassLoader获取类加载器。
	ClassLoader c1 = Thread.currentThread().getContextClassLoader();// 通过这种方法获取的类加载器称之为线程上下文(Context)类加载器。
	return ServiceLoader.load(service,c1);// 然后调用内部方法,把c1(类加载器)传递进去
}

线程类加载器是怎么来的呢,他是在每个线程启动的时候,会由JVM他默认是把应用程序类加载器赋值给当前线程,将来当前线程去调用
getContextClassLoader()方法时,就会拿到应用程序类加载器了。

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,他[ServiceLoader.load(service,c1)]内部又是由Class.forName调用了线程上下文类加载器完成JDBC驱动类的类加载,具体代码在ServiceLoader的内部类LazyInterator中:

private S nextService() {
	if(!hasNextService())
		throw new NoSuchElementException();
	String cn = nextName;
	nextTime = null;
	Class<?> c = null;
	try {
		/*
		他又是利用了Class.forName然后去加载按照一个字符串类名(cn)去进行类加载。
		loader就是上面传递过来的c1(线程上下文类加载器,其实就是应用程序类加载器)。
		*/
		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);
	}
}

所以回过头来看,当DriverManager的静态代码块儿被执行时,虽然DriverManager本身是启动类加载器去加载的,但由于ServiceLoader他内部用的是应用程序类加载器,所以他每调用一次next,他实际上内部其实就是用的线程上下文类加载器完成了类加载,也是破坏了双亲委派的机制,并没有利用启动类加载器去找mysql驱动

自定义类加载器

哪些情况下需要用到自定义类加载器的几种场景:

1)想加载任意路径下的类文件时,此时需要自定义类加载器。比如我们要记载的类文件即不在启动、扩展、classpath下。

2)比如做框架设计的时候,那么需要通过接口来使用不同的实现,这样实现软件的解耦。

3)可能有一个类有多种不同的版本,比如有旧版本有新版本,我希望新旧版本同时工作,虽然这些类的包名、类名都是一样的,但是他里面的字节码有新旧之分,我还希望他同时工作,此时也需要用到自定义的类加载器。这种情况经常是可以在tomcat这样的容器里看到,由于tomcat他就是使用了这种自定义类加载器的方式对不同的应用程序进行了隔离,将来你即使是同名同包的类,也可以在同一个tomcat上运行。

实现自定义加载器的步骤:

1)继承ClassLoader父类

2)要遵从双新委派机制,重写findClass方法,因为只有重写了findClass方法,才会委托上级的类加载器优先去进行类的加载,只有在上级类加载器没有找到该类时,才会调用findClass在本身的类加载器里进行查找,这也是符合双新委派原则。(注意:不要重写loadClass方法,否则不会走双亲委派机制

3)在findClass里读取类文件的字节码,一般都是byte数组

4)读完byte数组后,下一步是调用父类的difineClass方法来加载类,即把byte数组真正完成类加载。

5)对于类加载器的使用者,就让他去调用你类记载其的loadClass方法就可以实现类的加载了。

例子:记载自定义路径下的两个class,比如,D盘中有两个class,分别是MapImpl1.classMapImpl2.class。若反编译的话,大概如下:

MapImpl1.java
public class MapImpl1 extends java.util.AbstractMap implements java.util.Map {
	public MapImpl1();
	public java.util.Set<Java.util.Map$Entry> entrySet();
	public java.lang.String toString();
	static {};
}

MapImpl2.java
public class MapImpl2 extends java.util.AbstractMap implements java.util.Map {
	public MapImpl2();
	public java.util.Set<Java.util.Map$Entry> entrySet();
	public java.lang.String toString();
	static {};
}

可以看到重写了toString方法,还有static代码块儿。然后写一个自定义的类加载器把他们都加载进来。

Load.java

// 测试类
public class Load {
	public static void main(String[] args) throws Exception{
		MyClassLoader classLoader = new MyClassLoader();
		classLoader.loadClass("MapImpl1");

		// 你即使多次执行loadClass方法,但实际上类文件只会加载一次,因为第一次加载以后,就会放在自定义类加载器的缓存当中,
		// 下次再调用loadClass的时候,缓存里已经能找到了,就不会重复的类加载了。
		Class<?> c1 = classLoader.loadClass("MapImpl1");
		Class<?> c2 = classLoader.loadClass("MapImpl1");
		System.out.println(c1 == c2);// true,两个内存地址一样

		// 那假如用不同的类加载器对象去加载同一个类
		MyClassLoader classLoader2 = new MyClassLoader();
		Class<?> c3 = classLoader2.loadClass("MapImpl1");
		System.out.println(c1 == c3);// false,其实唯一确定类的方式是他的包名类名相同,而且类加载器也要是同一个,他才认为这个类是完全一致的。所以会加载两次,会认为他两是相互隔离的,不会产生冲突。

		// 最后用反射去调用他
		// 去创建MapImpl1的对象
		c1.newInstance();// 打印Map impl1 init,这是静态代码块儿里的内容,所以创建他的实例对象时,就会触发类的静态代码块儿的执行
	}
}

// 自定义的类加载器(要继承ClassLoader,并且要重写findClass方法)
class MyClassLoader extends ClassLoader {

	// name参数是类名称,要根据这个名称去找到真正的类文件
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		String path = "d:\\" + name + ".class";// 文件的真实路径
		
		try {
			// 读取他,形成一个二进制的字节数组
			// 把path路径的文件拷贝到输出流os里
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			Files.copy(Paths.get(path), os);

			// 得到字节数组
			byte[] bytes = os.toByteArray();

			// 那么字节数组如何变成class类对象呢
			// 需要调用父类的defineClass方法,他可以把byte[]变成*.class这种类对象
			// 0是byte数组的第一个开始读取,要读取的长度是bytes.length
			return defineClass(name, bytes, 0, bytes.length);
		}catch(IOException e) {
			e.printStackTrace();
			// 网友1:这里为什么要throw?不是已经Print了吗?网友2:自定义异常信息
			throw new ClassNotFoundException("类文件未找到", e);
		}
	}
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值