JVM学习笔记(十一)———Java类加载器详解

前言

本文主要介绍了类的加载机制和类加载器,通过流程图展示类的加载过程和类加载的详细流程,对其中的一些概念性名词进行了解释;介绍分析了双亲委派机制;通过分析源码填补文字描述中存疑的点,并通过源码展示双亲委派机制底层是如何实现的;展示了如何自定义类加载器,如何用自定义类加载器打破双亲委派机制。

作者能力欠佳,如文中有描述不清或有存在纰漏的地方,欢迎留言讨论。

一、类的加载过程

首先看一张类加载以及前后过程的流程图
类加载过程

流程图中可以看到一个类的生命周期分为:加载→验证→准备→解析→初始化→使用→卸载七个阶段,其中标绿的五个阶段为一个类的完整加载过程,这五个步骤统称为类的加载,以下着重对这五个步骤进行简单介绍

  • 加载:通过类的全名在硬盘上搜索并获取其二进制字节流,并将其读入JVM方法区,同时在堆内存中创建该类的Class对象。
  • 验证:通过文件格式验证、元数据验证、字节码验证和符号引用验证四个验证来确保被加载类的正确性,保证.class文件中的二进制字节流符合JVM要求且不会危害JVM。
  • 准备:仅为类中的静态变量(被static修饰的变量)分配内存并初始化为默认值,不会为普通成员变量分配内存,其中用被final static修饰的变量在编译阶段就已经完成分配。
  • 解析:将常量池中的符号引用转换为直接引用。
  • 初始化:若有该类有父类未被初始化,则先初始化其父类,随后为类的静态变量赋初始设定值,执行静态代码块。

二、类加载器

类加载器负责将.class文件加载到JVM内存中,并生成与之相对应的Class对象。
类加载器一共分为三种:启动类加载器、扩展类加载器、应用类加载器。

  • 启动类加载器(BootstrapClassLoader):也叫引导类加载器,本加载器由C++实现,负责加载jre/lib目录下的核心类库,比如rt.jar、charsets.jar等。
    :JAVA的沙箱安全机制严格限制代码对本地资源的访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱安全机制体现在启动类加载器上就是该启动类只会加载指定包(java、sun等包)下的类,且会优先加载JDK自带的类,同包同名外部类也不会被加载,保护了JAVA核心源代码。
  • 扩展类加载器(ExtClassLoader):负责加载jre/lib/ext目录中的JAR类包(通常为非原生被引用的jar包)。
  • 应用类加载器(AppClassLoader):也叫系统类加载器,负责加载用户自定义路径下的class字节码文件,通常该加载器为程序默认加载器。

除了以上三种类加载器,我们还可以自定义类加载器,根据需求扩展类的加载方式,后文中会进行演示。

三、双亲委派机制

双亲委派机制如下图所示、
在这里插入图片描述
可以简单吧双亲委托机制看成两个部分:委托和加载(非官方,作者个人理解)

  • 委托:当收到类加载请求时,最先调用应用类加载器(前文中有提及,通常应用类加载器为程序中的默认加载器),判断此前应用类加载器是否加载过该类,若没有加载过,则向上委托给其父类扩展类加载器,扩展类加载器执行同样的操作,若扩展类加载器也没有加载过,则向上委托给其父类启动类加载器,同样先执行判断操作。在以上三次判断操作中,若发现曾加载过此类,则直接返回,不再向上进行委托;若直到启动类加载器也没有加载过该类,则进行加载操作。
  • 加载:加载操作由启动类加载器先开始,启动类先搜索jre/lib目录下的核心类库中是否有该类,若有,加载后返回,若没有,则由其子类继续执行加载操作,扩展类加载器和应用类加载器同理。

:启动类加载器、扩展类加载器以及应用类加载器之间并无继承关系,提及父类子类是因为在ClassLoader类中存在一个parent变量,后文中会提及。
使用双亲委派机制的优点

  1. 先判断再加载的机制避免了类的重复加载问题
  2. 加载时,加载顺序为启动类加载器→扩展类加载器→应用类加载器,实质上是从核心类库到自定义类的加载顺序,这样的顺序解决了一些安全问题,比如用户自定义了一个String类,这与java.lang包下的String类同名,使用双亲委派机制就会优先加载JDK自带的String类而忽视用户自定义的String类,防止JAVA核心代码被篡改

四、类加载的详细流程

作者在测试项目中创建了如下测试类ClassLoaderTest1
在这里插入图片描述
当想要运行这个类时,详细类加载流程如下,后文会对流程要点进行说明。
类加载详细流程

五、类加载源码分析

本部分主要通过查看源码来解决一些前文中可能存在的疑问,并进行梳理
后文中的源码大都进行了部分省略和注释添加,只保留讲解需要部分,下文不再赘述

1. Launcher启动类与扩展/应用类加载器的关系

在这里插入图片描述
在这里插入图片描述
如上两图所示,ExtClassLoader和AppClassLoader本质上就是Launcher类下的两个静态内部类。

2. 创建Launcher类实例时同时创建其他两个类加载器/应用类加载器通常为程序中默认类加载器

Launcher源码如下

public class Launcher {

    private static Launcher launcher = new Launcher();
    //ClassLoader变量用于存放启动类中默认的类加载器
    private ClassLoader loader;

    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
    	//创建扩展类加载器实例
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
		//创建启动类加载器实例,同时赋值给私有成员变量loader
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
    }
}

如上图所示,在Launch启动类中,存在一个Classloader私有类加载器,在构造方法中分别创建了两个成员内部类(扩展类加载器、应用类加载器)的实例,这对应了类加载流程图中的"创建Launcher实例的同时创建其他两个类加载器"。其中用loader接收应用类加载器实例,相当于将应用类加载器设为了程序中的默认类加载器。

3. 各类加载器的关系

3.1 各类加载器的继承关系

前文提到过启动类加载器、扩展类加载器以及应用类加载器之间并无实际继承关系,但存在一个parent变量表述他们的委托关系,下面来进行验证。
在这里插入图片描述
在继承关系中可以看到应用类加载器和扩展类加载器都是之间继承URLClassLoader,没有启动类加载器是因为启动类加载器由C++实现。

3.2 各类加载器之间的委托父子关系是如何实现的

在这里插入图片描述
在所有类加载器的父类ClassLoader类中可以找到parent变量,源码对该变量的描述为"The parent class loader for delegation-委托关系中的父类加载器",委托关系就靠这个变量表述。

3.3 各类加载器的parent变量是怎么设置的

首先先关注一下应用类加载器,源码如下。在前文中可以了解到,在Launcher的构造方法中创建了应用类加载器的实例,创建实例使用的是getAppClassLoader(var1)方法,并传入var1(var1为先前创建的扩展类加载器实例)。

    static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
		//getAppClassLoader方法为Launcher构造方法中创建应用类加载器时使用的方法
        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
        	//下面主要获取了系统变量(java.class.path为系统类路径)并进行解析 非关注重点
            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<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                	//var1x变量为若干jar包路径
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    //此处创建并返回的一个应用类加载器实例
                    //传入两个参数,分别为(若干jar包的路径)URL数组和由Launcher构造方法传入的扩展类加载器实例。
                    //创建AppClassLoader实例时调用下方的构造方法
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
		//创建时调用构造方法
        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
    }

以上部分的代码进行到了调用应用类加载器的构造方法,主要关注点在super和参数var2上,后续源码如下:

		//AppClassLoader构造方法
		AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
        //关注super调用的父类构造方法 ↓ ↓ ↓
        -------------------------------
        //URLClassLoader类构造方法(AppClassLoader父类)
        public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
            //此处参数名变为parent,继续调用父类构造方法
	        super(parent);
	        //本构造方法其余代码省略
    	}
    	//关注super调用的父类构造方法 ↓ ↓ ↓
    	-------------------------------
    	//SecureClassLoader类构造方法(URLClassLoader父类)
    	protected SecureClassLoader(ClassLoader parent) {
	        super(parent);
	        //本构造方法其余代码省略
   		}
   		//继续关注super调用的父类构造方法 ↓ ↓ ↓
    	-------------------------------
    	//ClassLoader类构造方法(SecureClassLoader父类)
    	
    	protected ClassLoader(ClassLoader parent) {
    		//调用本类其他构造方法
        	this(checkCreateClassLoader(), parent);
    	}
    	
    	private ClassLoader(Void unused, ClassLoader parent) {
    		//在此处将扩展类加载器赋值给parent变量
	        this.parent = parent;
        	//本构造方法其余代码省略
   		}
    	

以上就是应用类加载器中parent变量的赋值过程,扩展类加载器同理,但注意,由于启动类加载器是由C++实现,JAVA中没有对应的类,因此扩展类加载器的构造方法中将父类指定为null,源码如下

		public ExtClassLoader(File[] var1) throws IOException {
			//此处设置为null 其余流程同应用类加载器
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

下面编写一小段代码进行验证

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        Launcher launcher = new Launcher();
        ClassLoader c1 = launcher.getClassLoader();//获取Launcher类的默认类加载器 也就是启动类加载器AppClassLoader
        ClassLoader c2 = c1.getParent();//getParent()方法返回父类加载器
        ClassLoader c3 = c2.getParent();

        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
    }
}

输出结果如下,符合预期结果。

sun.misc.Launcher$AppClassLoader@7ef20235
sun.misc.Launcher$ExtClassLoader@27d6c5e0
null

4.双亲委派机制的实现

双亲委派机制的实现主要关注两个方法,分别是Classloader类中的loadClass方法和findClass方法,具体流程见源码中标注的注释
首先看应用类加载器对loadClass方法进行的重写,源码如下

		//AppClassLoader的loadClass方法重写
		//其中参数var1为例如"com.example.Test.classLoaderTest1"的全路径
		//参数var2表示该类是否需要被解析 和ClassLoader中的resolveClass方法相关 类的解析非本文重点 后文中仅在源码注释提及
		public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
			//进行安全检查
            int var3 = var1.lastIndexOf(46);
            if (var3 != -1) {
                SecurityManager var4 = System.getSecurityManager();
                if (var4 != null) {
                	//此方法用于检查传入包名是否与受限包名相等
                    var4.checkPackageAccess(var1.substring(0, var3));
                }
            }
			//此处省略无关代码
            } else {
            	//调用父类的loadClass方法
            	//AppClassLoader的直接父类没有重写该方法
            	//此处跳转至ClassLoader类中的loadClass方法
                return super.loadClass(var1, var2);
            }
        }

AppClassLoader重写的loadClass方法主要增加了包名安全检查等功能,核心部分代码仍在ClassLoader下的loadClass方法内,ClassLoader内相关源码如下。

	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
        	//首先通过findLoadedClass方法检查此前是否以及读取过该类
            Class<?> c = findLoadedClass(name);
            //如果 c!=null 表明目标类此前已经由该类加载器加载过
            //直接跳出 检查是否需要解析后返回
            if (c == null) {
                long t0 = System.nanoTime();
                //向上委托(查找)
                try {
                	//由于启动类加载器由C++实现,所以此处进行判断
                	//当委托父类不为启动类加载器时
                	//直接调用父类加载器的loadClass方法 完成向上委托
                    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
                }
				//向下委托(加载)
				//到此步骤如过c仍为null,表明目标类此前未被任何加载器加载过
				//开始向下委托进行加载
                if (c == null) {
                    //关键方法findClass
                    c = findClass(name);
					
					//部分无关代码省略
                }
            }
            if (resolve) {
            	//resolveClass方法使类的Class对象创建完成也同时被解析
                resolveClass(c);
            }
            return c;
        }
    }

    //此方法可以理解为启动类加载器的loadClass方法(实际不存在)
    private Class<?> findBootstrapClassOrNull(String name)
    {
    	//checkName方法判断如果name为null或可能是有效的二进制名称,则返回true
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

    // 此方法为native方法,非JAVA实现,不关注具体是如何实现的
    private native Class<?> findBootstrapClass(String name);
	
	//ClassLoader中的findClass方法无实质内容,由子类重写
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

以上源码主要涉及双亲委派机制的关键方法loadClass,findClass方法主要在UrlClassLoader类中进行的重写,源码如下。

protected Class<?> findClass(final String name) throws ClassNotFoundException{
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                    	//首先是对路径进行解析,将.替换为/,并且在字符串末尾追加.class
                    	//例如将com.example.Test.classLoaderTest1
                    	//转换为com/example/Test/classLoaderTest1.class
                        String path = name.replace('.', '/').concat(".class");
                        //res为从.class文件重新创建的源代码
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                            	//将byte字节流转换为JVM可以解析的Class对象
                            	//可以将class文件实例化为Class对象
                            	//若成功加载,则加载完成,不再向下委托
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                        	// 返回null则继续由子类加载器进行尝试加载
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

六、自定义类加载器及测试

前文提到过,双亲委派机制实现的核心方法是loadClass,类加载实现的核心方法是findClass,所以自定义类加载器只需要把关注点放在这两个方法上即可

1. 不打破双亲委派机制

双亲委派机制的主要实现在loadClass方法中,既然不想要打破,那就只需要去重写findClass方法就好了

1-1 首先重写findClass方法

下面代码中的CustomClassLoader的委托父类为AppClassLoader

public class ClassLoaderTest1 {
	//模仿应用类加载器和扩展类加载器 写一个静态内部类
	//此处直接继承ClassLoader类
	//在双亲委派机制中CustomClassLoader的委托父类为AppClassLoader
    static class CustomClassLoader extends ClassLoader{
		//类路径
        private String classPath;


        public CustomClassLoader(String classPath){
        	//用于获取Java类路径 不需要加载外部类可以用这个
            //this.classPath = System.getProperty("java.class.path");
            this.classPath = classPath;
        }
		//路径转换方法 返回一个字节数组
        public byte[] getByte(String name) throws IOException {
            String path = name.replace('.', '/').concat(".class");
            FileInputStream fileInputStream = new FileInputStream(this.classPath + "/" + path);
            byte[] b = new byte[fileInputStream.available()];
            fileInputStream.read(b);
            fileInputStream.close();
            return b;
        }

        @Override
        protected Class<?> findClass(final String name) throws ClassNotFoundException {
            try {
            	//获取class类路径
                byte[] b = getByte(name);
                //将字节数组转化为一个Class对象
                return defineClass(name,b,0,b.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    }
}

1-2 准备一个用于测试的.class文件

按图中操作即可
在这里插入图片描述
在这里插入图片描述

1-3 生成测试方法进行测试

我这里的测试方法为本机ClassLoaderTest1类下的main方法,代码如下

	//ClassLoaderTest1类下的main方法
	public static void main(String[] args) throws Exception {
        CustomClassLoader customClassLoader = new CustomClassLoader("E:/ClassLoaderTest");

        Class c1 = customClassLoader.loadClass("com.example.Test.Test");
		//输出是哪个类加载器读取了Test类
        System.out.println(c1.getClassLoader());
		//反射调用test方法
        Method m = c1.getMethod("test");
        m.invoke(c1.newInstance());
    }

对下面三种情况进行测试

1. target包下和外部包下的Test.class文件同时存在

输出结果如下,可以看见是由应用类加载器加载了Test类,这是因为我们没有打破双亲委派机制,向下委托过程中先委托到了应用类加载器,成功加载

sun.misc.Launcher$AppClassLoader@18b4aac2
HelloWorld!
2.只有target包下存在Test.class文件

输出结果如下,跟第一种情况相同,外部包的.class文件是否存在不影响结果

sun.misc.Launcher$AppClassLoader@18b4aac2
HelloWorld!
3.只有外部包下存在Test.class文件

输出结果如下,委托到应用类加载器时,应用类加载器不能找到Test.class,因此继续向下委托给我们的自定义类加载器

com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!

2. 打破双亲委派机制

前文中多次提及,实现双亲委派机制的核心方法是loadClass方法,因此想要在自定义类加载器中打破双亲委派机制有两种方法

2-1 直接使用findClass方法加载类

直接将loadClass方法替换为findClass方法

    public static void main(String[] args) throws Exception {
        CustomClassLoader customClassLoader = new CustomClassLoader("E:/ClassLoaderTest");
		//此处将loadClass方法直接替换为我们重写过的findClass方法
        Class c1 = customClassLoader.findClass("com.example.Test.Test");

        System.out.println(c1.getClassLoader());

        Method m = c1.getMethod("test");
        m.invoke(c1.newInstance());
    }

此时进行测试,三种.class文件的存在情况输出结果均如下,成功打破双亲委派机制

com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!
2-2 重写loadClass方法

代码如下

		@Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
            synchronized (getClassLoadingLock(name)) {
            	//首先依然是先判断是否加载过
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                	//关键
                	//此处相当于单独把com.example.Test包抽离出双亲委派机制
                	//在com.example.Test包下的类都会由我们的自定义类加载器进行加载
                	//这么做一方面在加载Test类时打破了双亲委派机制
                	//另一方面让其他的类(比如Object等原生类)正常加载 (因为沙箱安全机制,无法使用自定义类加载器完成这项工作)
                    if(!name.startsWith("com.example.Test")){
                        c = this.getParent().loadClass(name);
                    }else{
                        c = this.findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }

target包下和外部包下的Test.class文件同时存在时运行结果如下,成功打破双亲委派机制

com.example.Test.ClassLoaderTest1$CustomClassLoader@27d6c5e0
HelloWorld!

3. 沙箱安全机制的体现

这部分的验证比较简单,代码也有重复,直接上截图
首先自定义了一个在java.lang包下的String类
在这里插入图片描述
之后将.class文件丢到外部包中
在这里插入图片描述
之后打破双亲委派机制,使用自定义类加载器直接尝试进行加载自定义的String类
结果如下,果不其然加载失败,体现了java对于恶意代码的防护机制
在这里插入图片描述


本篇内容到此结束
作者才疏学浅,如文中出现纰漏,还望指正

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7rulyL1ar

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值