类加载器负责将class文件加载进入虚拟机中。类加载过程中,第一步就是类加载器去加载class,然后在是链接、初始化。
虚拟机自带的类加载器
启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并作为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用程序类加载器(也称为系统类加载器,AppClassLoader)
- Java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
各个加载器之间的关系
类加载器之间存在父子关系,但不是java中的继承关系。其中AppClassLoader的父加载器是Extension ClassLoader,Extension ClassLoader的父加载器是Bootstrap ClassLoader。自定义的类加载器的父加载器是AppClassLoader。
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = Dog.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
//线程上下文加载器
System.out.println(Thread.currentThread().getContextClassLoader());
打印出null是因为引导类加载器是用其他语言写的,所以为空。线程上下文加载器其实就是获取当前线程所用的入口加载器(没有手动设置的情况下),本质上是没有专门的线程上下文加载器,只是一种获取其他加载器的手段。
双亲委托机制
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
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;
}
}
loadClass(String name, boolean resolve)(重载的方法)是加载类的一个方法入口,它有两个参数,一个是string类型,它是要加载的类名。还有一个boolean,代表是否要链接。不链接代表着该类中的静态资源不会立马加载。forName(String name, boolean initialize, ClassLoader loader)这也是一个重载的方法。也可以用来加载类,本质上也是去调用loadClass方法,initialize代表者是否要链接。
loadClass中上了锁,防止一个类被多次加载。findLoadedClass是查找已经被加载过的类,返回它的Class。然后判断是否被加载过,没有加载过就调用父加载器的loadclass去处理,如果到启动类加载器还未找到该类的class文件,就会抛出文件未找到异常,拓展类加载器会捕获这个异常,但不进行异常处理,拓展类加载器再去找,如果未找到就继续抛出异常,下层加载器再捕获,再去找。
如果在入口加载器也未找到,就抛出异常。(这里我将第一个使用loadClass方法的类加载器叫入口加载器,最终使用findClass方法找到类的叫最终加载器。比如我自己编写的类,其中有使用String.那么String的入口加载器是系统类加载器,最终加载器是启动类加载器)
自定义类加载器
public class SelfLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
return defineClass(name, result, 0, result.length);
}
} catch (IOException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(!name.equals("com.tb.jicheng.Benchi")){//确保顶级父类Object用默认的类加载器加载
System.out.println(name);
return getParent().loadClass(name);
}else {
System.out.println(name+"==========");
}
return findClass(name);
}
//自定义流的获取方式
private byte[] getClassFromCustomPath(String name) throws IOException, ClassNotFoundException {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
FileInputStream in = new FileInputStream(new File("G:\\Tbstudy\\baseType\\out\\production\\baseType\\com\\tb\\jicheng\\Benchi.class"));
byte[] by = new byte[in.available()];
in.read(by);
return by;
}
public static void main(String[] args) {
SelfLoader selfLoader = new SelfLoader();
try {
//name参数必须为类路径全称com.tb.jicheng.Dog
// selfLoader.loadClass("com.tb.jicheng.Benchi",true);
Class<?> clazz = Class.forName("com.tb.jicheng.Benchi", true, selfLoader);
Car car= (Car) clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义类加载器可以继承ClassLoader ,重写findClass方法。这个方法就是去寻找class文件,返回一个Class对象。defineClass方法一般和它搭配使用,他可以将一个字节数组转换为一个Class对象。前提是这个数组存的就是class文件。所以不用管class文件的来源、文件是否加密,只要保证最终数组中存的是最初编译生成的class文件就行。
如果你只重写这个方法,想要自定义类加载器加载自己写的类,你就必须将该类的class文件放在其他三个加载器加载不到的地方,不然最终还是由系统类加载器加载。因为还是遵循双亲委托。如果你不想改变class文件的位置,你就必学重写loadClass方法。loadClass方法中是委托逻辑,不过你要确保原先由启动类、拓展类加载器加载的类最终还是他们加载,不然会抛出文件未找到异常。自定义的类就不委托给父加载器,直接自己去调用findClass。
使用自定义加载器加载的类
public class Benchi implements Car {
@Override
public void move() {
System.out.println("嘟嘟");
}
public static void main(String[] args) {
SelfLoader selfLoader = new SelfLoader();
try {
//name参数必须为类路径全称com.tb.jicheng.Dog
Class<?> clazz = Class.forName("com.tb.jicheng.Benchi", true, selfLoader);
Car car= (Car) clazz.newInstance();
car.move();
Method method = clazz.getMethod("skip");
method.invoke(clazz.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
public interface Car {
void move();
default void skip(){
System.out.println("铛铛");
}
}
如果要使用自定义类加载器加载的类,一种是直接用反射调用方法。还有一种是用Class对象生成对象再强转,不过这要借助接口,因为不同类加载器加载的同一类得到的Class,不属于同一类,也不能相互转换。
类的默认类加载器和线程上下文加载器
类的默认类加载器这里指的是入口加载器。假设一个A对象,其中有B对象属性。那么B的入口加载器就是A的最终加载器(可以在自定义类加载器的loadClass方法中输出提示去验证)。如果你加载A时没有让A链接,那么B不会立马加载。如果A链接,也只会加载A中的静态资源。如果非静态B对象要在A加载后就加载,就必须要生成A对象。从这也可以看出资源的初始化顺序。不过最先由子类调用loadClass方法.
线程上下文加载器一般默认是应用类加载器,你不手动设置,线程的上下文加载器就和父线程一样。通常就是由main方法所在的线程就决定了线程上下文加载器是什么。
通常在spi(Service Provider Interface)中就有用到线程上下文加载器,这个spi可以自己去查一下,就是要在核心类中调用自定义的类。正如上面所说,那么B的入口加载器就是A的最终加载器,这里我们核心类是A,自定义的类是B。A的最终类是启动类加载器,而自定义类的入口加载器就是启动类,在它所加载的路径肯定找不到该类,并且它就是最顶级的加载器,也无法向上委托。所以会抛出文件未找到异常。所以你必须找到对应的类加载器去加载这自定义的类。用线程上下文加载器默认就可以获得应用类加载器。这里我是有个疑问的,就是为什么不直接获取应用加载器,想用哪个加载器就获取哪个?要再通过一层去拿,不麻烦吗。我自己的观点是更加灵活,我不管你线程上下文加载器set一个什么加载器,我程序中只管在里面取。我在main线程中设置什么就是取的什么。或者在对应父线程中设置,所有子线程都会取到相应的。