Java 类加载器与双亲委派机制
类加载过程
java在运行一段程序的时候,首先会通过类加载器把要运行的程序对应的类加载到JVM中去。
package jvm;
/**
* @Program : Test
* @Description : TODO
* @CreateDate : 2022/4/17 13:54
* @Version : 1.0
* @Author : lyg
*/
public class Test {
static {
System.out.println("********************** load Test.class ********************************");
}
public static void main(String[] args) {
new A();
System.out.println("********************** main method ********************************");
B b = null;
}
}
class A{
static {
System.out.println("********************** load A.class ********************************");
}
public A(){
System.out.println("********************** A constructor ********************************");
}
}
class B{
static {
System.out.println("********************** load B.class ********************************");
}
public B(){
System.out.println("********************** B constructor ********************************");
}
}
通过java程序执行的大致流程如下
其中loadClass的类加载过程分为这几个步骤:
加载->验证->准备->解析->初始化->使用->卸载
·加载
在硬盘上查找并通过IO读入字节码文件,使用到对应的类时才会加载。
·验证
校验字节码文件的正确性
·准备
给静态变量分配内存,并赋默认值
·解析
将符号引用替换为直接引用,在这个阶段会把一些静态方法替换为指向数据所在内容的指针或句柄
·初始化
对类的静态变量初始化为指定的值,执行静态代码块
注:静态代码是在类加载的时候执行的,构造方法是在类加载完成之后,创建对象的时候执行的。上述代码执行结果可见
B的方法未被执行是由于B未被使用,主类在运行过程中如果使用到其他的类,会逐步加载这些类,只有用到了才会被加载。
类加载器
上面的类加载过程主要是通过类加载器来实现的,java中有如下几种类加载器
- 引导类加载器:(bootstrap class loader) 它用来加载 Java 的核心库(jre/lib/rt.jar),是用原生C++代码来实现的,并不继承自java.lang.ClassLoader。
加载扩展类和应用程序类加载器,并指定他们的父类加载器,在java中获取不到; - 扩展类加载器:(extensions class loader) 它用来加载 Java 的扩展库(jre/ext/*.jar)。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类;
- 应用类加载器:(system class loader) 它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它;
- 自定义类加载器:(custom class loader) 除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求;
在Java源码中,Launcher内部创建了两个类加载器,分别是ExtClassLoader(扩展类加载器)和AppClassLoader(应用类加载器),并且JVM默认使用的Launcher的getClassLoader()方法返回的默认类加载器是AppClassLoader的实例。
双亲委派机制
这里的类加载实际上就是双亲委派机制,加载一个类时,会先委托他的父类去加载,父类找不到目标类时,会再委托上层的父加载器去加载。如果所有的父类加载器在自己的加载路径下都找不到目标类,才会在自己的类加载路径去查找并加载目标类。
双亲委派机制简单解释就是先找父类加载,父类无法加载就由子类加载
在java源码中,主要是由ClassLoader类的loadClass()方法来实现双亲委派机制,源码如下:
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;
}
}
方便查看放一下图片版的
- ① 类记载的时候,首先会检查当前的类加载器是否已经加载过这个类,如果加载过的话,会直接返回。
- ②③④ 类未加载过之后,会去调用他的父类去加载,父类不为null,会调用父类的loadClass方法,父类为null,会调用引导类加载器去加载该类
- ⑤⑥ 如果经过父类加载器加载之后,还是未加载到指定的类,会调用URLClassLoader的findClass方法在加载器的类路径下去查找并加载该类
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
URLClassLoader类中的findClass方法,相当于在该加载器的类路径下去查找指定的类。
设计双亲委派机制的意义
- 防止类的重复加载 通过委托父类的方式,父类加载过的,子类就不会再加载了,保证了被加载类的唯一性。
- 安全 对于系统的核心类,他们都通过上级的类加载器去加载,这样可以防止核心的API被篡改。
如以下代码
package java.lang;
/**
* @Program : String
* @Description : TODO
* @CreateDate : 2022/4/17 15:12
* @Version : 1.0
* @Author : lyg
*/
public class String {
public static void main(String[] args) {
System.out.println("hello world");
}
}
自定义的String类跟系统的String类在同一个包下,运行之后会报如下错误
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
他实际上就是通过BootStrapClassLoader加载的,加载的系统核心类中没有对应的方法,所以才报错。
如何打破双亲委派机制
上面可以知道,双亲委派是在通过ClassLoader类的loadClass()方法实现的,那只要我们通过自定义的类加载器,重写loadClass方法的逻辑,不委派给他的父类就可以了。
代码如下
package jvm;
import java.io.FileInputStream;
/**
* @Program : CustomClassLoader
* @Description : TODO
* @CreateDate : 2022/4/17 15:18
* @Version : 1.0
* @Author : lyg
*/
public class CustomClassLoader extends ClassLoader {
private String classPath;
public Class<?> defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
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();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if (!name.startsWith("jvm")) {
c = this.getParent().loadClass(name);
}else {
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;
}
}
private byte[] loadByte(String name) throws Exception {
name = name.replace(".", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] b = new byte[len];
fis.read(b);
fis.close();
return b;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = null;
try {
b = loadByte(name);
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, b);
}
}
输出
jvm.CustomClassLoader@4554617c
Process finished with exit code 0