记录:类加载的双亲委派模型

什么是双亲委派模型,我个人的理解就是:
类加载器加载一个类的时候,不会马上去加载,而是先找他爹(父加载器),他爹要是还有爹就继续向上找爹,直到爹没办法加载(自己的加载范围内找不到类,ClassNotFound),才会由子加载器加载。
或者说的简单一点,在“能够加载到类”的这个范围内,找最“爹”的那个加载器加载。

什么是类加载器:就是根据类全名定位,把class文件加载到JVM转成class对象。
JVM里,类加载器的关系是这样的:(转大佬图)
在这里插入图片描述
一般来讲,我们自己写的java,编译成class文件,应该都是通过Application ClassLoader来加载的,这个加载器就是加载classpath下的class文件。
这个Application ClassLoader上面还有俩爹,加载的就是java_home下的类了。这里摘抄一下大佬的说法:

启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

为什么要用双亲委派模型,这要从一道面试题说起。
大佬发来的一道面试题:

面试官:在项目中自定义java.lang.String类并使用它,会怎样?

于是我自己实践了一下:
在这里插入图片描述
由于双亲委派模型,这里父加载器已经加载了jdk自带的java.lang.String,我们写的这个String类,虽然全名一样,但是不会被加载,所以运行的时候,加载的还是jdk的String类,所以没有main方法。
大佬举了个例子:比如有个黑客,自己写了个java.lang.String类,里面加入了一些恶意代码,双亲委派机制就可以确保这个类不被加载,从而保证安全。

ClassLoader使用loadClass方法来进行类加载,jdk源码如下:

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;
        }
    }

基本过程是:先找已经加载的类,如果没有被加载过,则找父类加载器进行加载,没有父类加载器,则找最根部的bootstrap加载器加载。如果依然没有加载成功,则调用自己的findClass方法进行加载。

所以如果写一个自定义加载器的话,findClass方法需要被重写。
依旧转大佬图,比较清晰地阐述了loadClass方法的过程:
在这里插入图片描述

自己定义一个类加载器:

首先自己写一个类,把编译的class文件单独剪切出来,否则在classpath下就会去走Application ClassLoader,不会走自定义的加载器了。

package com.xx.hello;

public class Hello {

	public void out() {
		System.out.println("类加载器为:"+getClass().getClassLoader().getClass());
	}
}

然后是自定义的加载器代码:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader {
	
	private String path;
	
	

	public MyClassLoader(String path) {
		this.path = path;
	}

	private byte[] loadByte(String path) {
		path = path.replace(".", "/");
		try {
			FileInputStream fileInputStream = new FileInputStream(
					new File(this.path + "/"+path+".class"));
			byte[] clazzData = new byte[fileInputStream.available()];
			fileInputStream.read(clazzData);
			return clazzData;
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		byte[] clazzData = loadByte(name);
		return super.defineClass(name , clazzData, 0, clazzData.length);
	}

	public static void main(String[] args) throws Exception {
		MyClassLoader myClassLoader = new MyClassLoader("D:/zztest");
		Class<?> clazz = myClassLoader.findClass("com.xx.hello.Hello");
		Object o = clazz.newInstance();
		Method m = clazz.getDeclaredMethod("out", null);
		m.invoke(o, null);
//		Class<?> clazz = myClassLoader.findClass("java.lang.String");
//		Object o = clazz.newInstance();
//		Method m = clazz.getDeclaredMethod("hacker", null);
//		m.invoke(o, null);
		
	}

它会去读我在D:/zztest这个路径下存放的class文件并加载,通过反射调用out方法,输出结果为:
在这里插入图片描述
下面那一块被我注释的代码,是我同样把自己刚才写的那个java.lang.String类也放到了D:/zztest这个路径下,运行代码后,会报异常:

Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang

无效报名,查了一下,类全名以java开头的好像都有这样的设置,直接抛出异常,不能加载。这说明jdk在这点上防范的还不错。

最后摘抄一段大佬的话,为什么在tomcat当中舍弃了双亲委派模型:

因为tomcat要解决以下问题
1. 一个web容器可能需要部署多个应用,不同的应用可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的累加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份
2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,很浪费空间 
默认的类加载器是能够实现的,因为他的职责就是保证唯一性
3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。 
和第一个问题类似
4. web容器要支持jsp的修改
但是jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。

参考:https://www.cnblogs.com/wxd0108/p/6681618.html
感谢大佬带飞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值