一、Java语言系统自带有三个类加载器
1、 BootstrapClassLoader 最顶层的加载类,主要加载核心类库。BootstrapClassLoader是由C/C++编写的,它本身是虚拟机的一部分。 Object.class.getClassLoader()的返回值为null,因为Object类由Bootstrap ClassLoader加载,且Bootstrap ClassLoader不是一个java类,所以没有Bootstrap ClassLoader的java实例化对象,因此返回值为null。
2、ExtentionClassLoader 扩展的类加载器。
3、 AppclassLoader也称为SystemAppClass 加载当前应用的classpath的所有类。
二、三个类加载器是分别加载哪些类
类加载器都是有加载范围的,Launcher类中确定了他们的加载范围。
Launcher类(经过适当的改动)
public class Launcher {
private static String bootClassPath = System.getProperty("sun.boot.class.path");
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
}
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader) AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for (int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException) var2.getException();
}
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for (int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
}
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
}
private static class BootClassPathHolder {
static final URLClassPath bcp;
private BootClassPathHolder() {
}
static {
URL[] var0;
if (Launcher.bootClassPath != null) {
var0 = (URL[]) AccessController.doPrivileged(new PrivilegedAction() {
public URL[] run() {
File[] var1 = Launcher.getClassPath(Launcher.bootClassPath);
int var2 = var1.length;
HashSet var3 = new HashSet();
for (int var4 = 0; var4 < var2; ++var4) {
File var5 = var1[var4];
if (!var5.isDirectory()) {
var5 = var5.getParentFile();
}
if (var5 != null && var3.add(var5)) {
MetaIndex.registerDirectory(var5);
}
}
return Launcher.pathToURLs(var1);
}
});
} else {
var0 = new URL[0];
}
bcp = new URLClassPath(var0, Launcher.factory, (AccessControlContext) null);
bcp.initLookupCache((ClassLoader) null);
}
}
}
1、BootstrapClassLoader虽然不是Java写的类加载器,但是它也是要去加载类的,通过BootClassPathHolder类可以发现其加载范围是由System.getProperty("sun.boot.class.path")决定。
2、ExtClassLoader加载范围是由System.getProperty("java.ext.dirs")决定。
3、AppClassLoader的创建过程中传入了ExtClassLoader,这里ExtClassLoader称为AppClassLoader的父加载器,ExtClassLoader的父加载器为null,BootstrapClassLoader这里可以简单粗暴的认为是ExtClassLoader的父加载器,因为BootstrapClassLoader的行为就像是ExtClassLoader的父加载器,如待会要说到的类的加载委托。AppClassLoader加载范围是由System.getProperty("java.class.path")决定。
System.getProperty()中返回了什么数据?
示例:
public static void main(String args[]) {
String[] s = System.getProperty("sun.boot.class.path").split(";");
System.out.println("Bootstrap ClassLoader 加载的类");
System.out.println(" sun.boot.class.path:");
Arrays.stream(s).forEach((i)->System.out.println(" "+i));
s = System.getProperty("java.ext.dirs").split(";");
System.out.println("ExtClassLoader 加载的类");
System.out.println(" java.ext.dirs:");
Arrays.stream(s).forEach((i)->System.out.println(" "+i));
s = System.getProperty("java.class.path").split(";");
System.out.println("AppClassLoader 加载的类");
System.out.println(" java.class.path:");
Arrays.stream(s).forEach((i)->System.out.println(" "+i));
}
结果:
这就是三个类加载器默认的加载范围。
我电脑环境变量配置:
JAVA_HOME:C:\ProgramFiles\Java\jdk1.8.0_161
PATH:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;
CLASSPATH:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar 注:需要注意的是前面的“.;”,“.;”代表当前命令行工作目录。
System.getProperty("java.class.path")在默认情况下获取的是CLASSPATH中的数据。
如果环境变量中CLASSPATH中的“.;”不写会出现什么情况?当类加载器去加载Test类的时候,没有一个类加载器可以找到该类,因此会出现:
三、如何修改类加载器的加载范围
类加载器都有默认的加载范围的,在程序开发过程中难免要去修改。
1、BootstrapClassLoader可以通过启动jvm时指定-Xbootclasspath来改变Bootstrap ClassLoader的加载目录。-Xbootclasspath: 完全取代基本核心的Javaclass 搜索路径,不常用,否则要重新写所有Java 核心class。-Xbootclasspath/a: 后缀在核心class搜索路径后面,常用!!。-Xbootclasspath/p: 前缀在核心class搜索路径前面。不常用,避免引起不必要的冲突。
2、Extention ClassLoader 扩展的类加载器,默认加载%JRE_HOME%\lib\ext目录下的jar包和class文件。可以通过-Djava.ext.dirs修改默认目录。
3、 App classLoader也称为SystemAppClass 加载当前应用的classpath的所有类。可以通过-classpath修改默认目录。
示例:
Person类的全称应是包含包名package的,即:www.com.Person。java中包名的体现是通过文件夹的嵌套的形式实现的。
package www.com;
public class Person {
}
测试类
public class Test {
public static void main(String args[]) throws Exception {
System.out.println("www.com.Person ClassLoader = " + Class.forName("www.com.Person").getClassLoader());
System.out.println("oracle.jdbc.driver.OracleDriver ClassLoader = " + Class.forName("oracle.jdbc.driver.OracleDriver").getClassLoader());
}
}
在c盘建立文件(jodbc14.jar为oracle的驱动包
实例1:修改 App classLoader的加载范围 包含 c:/abc/calsspath中的ojdbc|.jar和Person.class
java -classpath ".;C:\Program Files\Java\jdk1.8.0_161\lib;C:\Program Files\Java\jdk1.8.0_161\lib\tools.jar;C:\abc\classpath\ojdbc14.jar;C:\abc\classpath" Test
结果:
实例2:修改 Extention ClassLoader的加载范围 包含 c:/abc/ext中的ojdbc|.jar和Person.class
java -D"java.ext.dirs=C:\Program Files\Java\jre1.8.0_161\lib\ext;C:\Windows\Sun\Java\lib\ext;C:\abc\ext" Test
结果:
实例3:修改 App classLoader的加载范围 包含 c:/abc/boot中的ojdbc|.jar和Person.class
java -Xbootclasspath/a:"C:\abc\boot\ojdbc14.jar;C:\abc\boot" Test
结果:
四、类加载器的委托模式
三个类加载器的加载范围都是可以修改的,如果三个类加载器的加载范围重叠了,重叠区的类是由哪个类加载器加载呢?即使区域不重叠,如果一类在多个录入都出现了,这些目录被不同的类加载器读取,该类由哪个加载器加载呢?
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class 自己的缓存中是否已经存在,如果存在就直接返回,如果不存在它并不是自己进行查找,而是交给父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrapclassloader的缓存中存在,直接返回,如果不存在,尝试从自己的加载目录中加载,如果成功加载,直接返回,如失败则一级一级返回。这种机制就叫做双亲委托。换句话说:如果一个类可以被多个类加载器扫描到,最终是由最顶层的父加载器加载。
上面已经详细介绍了加载过程,但具体为什么是这样加载。
ClassLoader的继承关系:
在ClassLoader类中:
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) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
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();
//父加载器没有找到,则调用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) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
在loaderClass方法中:
1. 执行findLoadedClass(String)去检测这个class是不是已经加载过了。
2. 执行父加载器的loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。示例:
java -Xbootclasspath/a:"C:\abc\boot\ojdbc14.jar;C:\abc\boot" -D"java.ext.dirs=C:\Program Files\Java\jre1.8.0_161\lib\ext;C:\Windows\Sun\Java\lib\ext;C:\abc\ext" -classpath ".;C:\Program Files\Java\jdk1.8.0_161\lib;C:\Program Files\Java\jdk1.8.0_161\lib\tools.jar;C:\abc\classpath\ojdbc14.jar;C:\abc\classpath" Test
结果:
五、自定义ClassLoader
不知道大家有没有发现,不管是Bootstrap ClassLoader还是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?如果要这样做的话,需要我们自定义一个classloader。
自定义步骤
编写一个类继承自ClassLoader抽象类。
复写它的findClass()方法。
在findClass()方法中调用defineClass()(这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常)。
注意点:
一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。上面说的是,如果自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
假设我们需要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。
我们写编写一个测试用的类文件Test.java。
Test.java
public class Test {
public void say(){
System.out.println("Say Hello");
}
}
然后将它编译过年class文件Test.class放到D:\lib这个路径下。
我们编写DiskClassLoader的代码public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
测试:
public class ClassLoaderTest {
public static void main(String[] args) {
//创建自定义classloader对象。
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
try {
//加载class文件
Class c = diskLoader.loadClass(Test");
if(c != null){
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("say",null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
六、通过类加载器读取资源文件
程序中在读取资源文件时,一般通过类加载器获取当前路径,再加上文的相对路径。
类加载器的路径是由什么决定的?从getResource方法的中可以看到,在资源的获取上同样才取了委托的方式。如果parent不为null调用parent的getResource方法,如果parent为null调用getBootstrapResource方法;如果parent中无法找资源,就调用自己的findResoruce方法。
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
在E:\aa夺下新建一个www.com.Test类。包名是通过文件夹体现的。
package www.com;
import java.util.Arrays;
public class Test {
public static void main(String args[]) throws Exception {
String[] s = System.getProperty("sun.boot.class.path").split(";");
System.out.println("Bootstrap ClassLoader 加载的类");
System.out.println(" sun.boot.class.path:");
Arrays.stream(s).forEach((i) -> System.out.println(" " + i));
s = System.getProperty("java.ext.dirs").split(";");
System.out.println("ExtClassLoader 加载的类");
System.out.println(" java.ext.dirs:");
Arrays.stream(s).forEach((i) -> System.out.println(" " + i));
s = System.getProperty("java.class.path").split(";");
System.out.println("AppClassLoader 加载的类");
System.out.println(" java.class.path:");
Arrays.stream(s).forEach((i) -> System.out.println(" " + i));
System.out.println("====================================================");
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println("classLoader_name = " + classLoader.getClass().getName());
System.out.println("classLoader_path = " + classLoader.getResource("").getPath());
}
}
classLoader.getResource("")中的“”也是一种资源,和写文件名的没有什么区