我的世界java版区块加载器_你真的了解java类加载器吗?

Java默认的类加载器通常可以分为三种:

1.启动类加载器(BootstrapClassLoader):jvm内部C++实现,无法获取
2.扩展类加载器(ExtClassLoader)
3.应用程序类加载器(AppClassLoader)

加载路径各不同:

BootstrapClassLoader加载Java核心API,即<JAVA_HOME>jrelib(classes)目录中的jar,或-Xbootclasspath参数指定的。

e49c05d329eefb0789df000032d6fa52.png

启动类加载器加载路径

让BootstrapClassLoader加载自己定义的jar:

1:把jar包解压放在<JAVA_HOME>jreclasses目录中

170ff9ba3bdaa432c7401eb6be5ae59f.png

Test类被启动类加载器加载

2:-Xbootclasspath启动参数指定

014ac4b9f37c5690fe2947ce7cdbd0b4.png

-Xbootclasspath指定

ExtClassLoader加载JAVA_HOME/jre/lib/ext/目录下的,或由java.ext.dirs系统属性指定的目录

0ff0823bbb631dcdac6e200766355851.png

扩展类加载器加载路径

让ExtClassLoader加载自己定义的jar:

1:把jar包在<JAVA_HOME>jrelibext目录中

2: -Djava.ext.dirsVM参数指定目录,如-Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext;D:comcompany

c669f8fe85d0ad3651d9b9efcac51ef0.png

-Djava.ext.dirsVM参数指定加载路径

AppClassLoader加载应用程序classpath目录下的所有jar和class文件(包括BootstrapClassLoader及ExtClassLoader路径上的),或由java.class.path系统属性指定目录

7a4871f409b5cf01d964c86c9d5bb96f.png

应用程序类加载器加载路径

三者的关系图

ffbaed6e47737e3c46bb08968717dfb9.png

加载器类图

由类图可知,保证这种委托逻辑的并不是继承关系,而是组合,即每个加载器在创建或初始化的时候会被设置自己的父类加载器属性

51bf4a42e50f803fcb062a2907893ccb.png

创建AppClassLoader时父加载器为AppClassLoader

67351e1a3d93881f47d545852626e2b7.png

创建ExtClassLoader时父加载器为null

双亲委托模型

5e83934866fdb68544b9a41719d1cd8a.png

委托加载模型

双亲委派说明:

每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个组合包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

主要保证避免重复加载 + 避免核心类篡改

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心JavaAPI发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

实现委托加载的代码

fe0a9450c6c18a3c68048c8256131546.png

委托逻辑

由此可知,委托加载的实现,ClassLoader.loadClass(String name, boolean resolve)方法是关键,所以当自定义类加载器时,为了不破坏委托加载,不应重写loadClass方法,而是重写findClass(String name)方法

自定义类加载器

默认父加载器是应用程序类加载器,也可显式指定

bdcef9e437e95e364971721d6bd1ed36.png

显示指定父加载器

ba0137a6c44493f16f645f19c0bccbfe.png

默认父加载器

一个Class对象是由加载它的加载器实例和这个类的全名描述来确定唯一性的

d1b0d122e314c10e647ea0e0053f9b0d.png

不同加载器实例,findLoadedClass(name)并没有返回之前加载的Test类

假如没有遵循委托逻辑,重写了ClassLoader.loadClass(String name),代码;

e689ec40811b99677fe84b8827ddb8c7.png

重写loadClass方法,破坏双亲委托规则

dc4482be2a97b95d98c4bb86f0b3aad9.png

破坏委托加载,运行后异常

运行后异常了,可以试着分析下这个结果:

代码执行:在D盘目录下加载全名为com.company.Test的类,发现其有父类java.lang.Object,所以要先加载Object类,由于没有委托父加载器加载,则此加载器实例只能自己加载,即在D盘目录下加载全名为java.lang.Object的类,但是D盘下没有这个类文件,所以报java.io.FileNotFoundException: D:javalangObject.class (系统找不到指定的路径。)异常,同时由于通过java/lang/Object符号引用找不到(加载失败)对应的二进制字节流文件,返回java.lang.NoClassDefFoundError: java/lang/Object

其实即使本地有Object的类文件,也不能加载成功,会返回SecurityException异常,因为除 启动类加载器以外,别的加载器是不能加载全名描述以java.开头的类的

948f8d6c415e55bc668ef7fe6ccf7b4b.png

加载前检查

为保证包级别的访问权限的合理性,必须是同一个类加载器加载的同包类(沙箱隔离)

2584f8101a9d00c0c36b20323bb9247d.png

同包不同加载器加载类访问异常

上述代码是在 把test.jar放在JAVA_HOME/jre/lib/ext/目录下,同时在程序目录中创建了com.company相同包的Ru类,main方法运行,引用的Test接口类省缺默认包权限。

可以试着分析下这个结果:

代码执行:AppClassLoader加载Ru类,执行main方法,通过com/company/Test符号引用委托父加载器ExtClassLoader正常加载Test类,但是解析验证阶段,发现不是同命名空间下的同包类(两者的加载器不一致),返回权限异常

注:Java虚拟机默认提供的三种加载器是单例唯一的

显式加载一个类的方式:

1.Class.forName(name)

2.this.getClass().getClassLoader().loadClass()

两者都是用当前类的类加载器加载,不同的是Class.forName(name)会进行初始化的动作

有关双亲委派模型的一些思考:

众所周知,类加载器双亲委派模型可以保证加载的类不会被重复加载即及核心API的安全

其实避免重复加载 + 避免核心类篡,用一种类加载器来处理就可以,伪代码如下:

f59eaedb744950a4467e7902e3a6adac.png

模拟委派

如上述代码,先检查是否已经加载过,没有的话,直接从sun.boot.class.path系统参数目录路径下查询并加载,这样免去了层层委托的复杂性,直奔最终目的路径,然后模拟委托加载,依次从java.ext.dirs,java.class.path所设置的路径下加载,当然loadClass方法内部也可以做一些安全检查,比如只有在sun.boot.class.path所设的路径下才能加载全名描述以java.开头的类等。

既然用一个类加载器就可以实现避免重复加载 + 避免核心类篡,那为什么要默认设计三种类加载器呢?

我们自定义类加载器的父加载器有多种选择,其一个是系统类加载器(AppClassLoader),其另一种是扩展类加载器(ExtClassLoader)。

1.如果父加载器是系统类加载器

当你用自定义类加载器去加载在classpath中的类的时候,会先使用系统类加载器去加载这个类,就直接加载了

2.如果父加载器是扩展类加载器

当用这个自定义类加载器加载某个在classpath中的类时,会出现一种现象,这个类中不能引用由系统类加载器加载的类,否则将加载失败

我们用代码示范下:

85225206d99a277c5501cc6ca28f86c5.png

自定义的类加载器,父加载器为ExtClassLoader

a935d3b004225b57e6c078edde91b8ea.png

自定义类加载器加载路径为classPath下com/yu中的TestImpl类

可以看到自定义的MyClassLoader在加载TestImpl类时,需要先加载其父类Test接口,但是当前类加载器即MyClassLoader通过父类Test的符号引用在classPath/com/yu中找不到其对应的二进制文件(不可见),故返回java.lang.NoClassDefFoundError: com/yy/Test

因为这时候,自定义类加载器和系统类加载器是兄弟关系,他们两个加载器加载的类是互不可见的。

上述代码要想成功加载,其实可以破坏委托规则,如:

a4589d9e143b87d99b787d6678c86201.png

重写loadClass方法,破坏委托规则

或者

15442e088efa76a690a4b2c58826ebae.png

委托兄弟类加载器加载

代码红框处逻辑是,不是自定义加载器加载范围里的类,用向上委托的方式加载或者兄弟类加载器加载,都要打破原有的委托规则

所以设计三种类加载器通过分层实现双亲委派,即可以避免重复加载 + 避免核心类篡,也起到安全隔离的作用

611e38593fe976bf40690c4b7e94a018.png

tomcat类加载器结构

webappclassloader

每个web应用都有一个对应的类加载器实例,每个web应用都相互隔离

双亲委派模型下,子类加载器可以访问父类加载器加载范围中的类,但父类加载器不能访问子类加载器加载范围中的类

那这样又会有个问题:

如果需要在父类加载器加载的类里调用子类加载器所加载范围的类,典型如:SPI, 怎么办?

按上面我们通过破坏委托规则的方式可以实现:

通过指定的加载器去加载某些类

Thread.currentThread().getContextClassLoader()

线程上下类加载器:

65766ef471ce6b439d8c092440f8479a.png

在创建并初始化线程的时候,会从执行者线程赋值一个线程上下文加载器

e7695da751543400cf762da25e65bd1c.png

线程上下文加载器默认是系统类加载器

说明:

类java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源

说说异常:

1.ClassNotFoundException:当用代码直接显式的加载某个类,加载不到时(不在加载的范围内),返回该异常

2.NoClassDefFoundError:当defineClass加载某个已确认在其加载范围内的类或者调用某个存在的方法时,但该类的字节码文件存在父类索引super_class,接口索引集合interfaces 或 该方法表中引用了其他类的符号引用,虚拟机将自动调用ClassLoader(String name)方法,通过该符号引用查询或加载其对应类的二进制文件,在对应加载器加载的范围内查询不到时,返回该异常

自定义可重复加载的加载器

575bbff87ac8c6014e26ffd6c2cf2554.png

HotSwapClassLoader

HotSwapClassLoader加载Ab类,在其加载范围内查询不到,故返回ClassNotFoundException异常

当通过反射调用set(B b)方法时,该方法参数类型为B类,对应字节码文件有引用B的符号引用,虚拟机将自动调用ClassLoader(String name)方法加载B

316bbaaac5f75db1c19bd5474fbfcae2.png

自定义的类加载器,父加载器为ExtClassLoader

a935d3b004225b57e6c078edde91b8ea.png

自定义类加载器加载路径为classPath下com/yu中的TestImpl类

可以看到自定义的MyClassLoader在加载TestImpl类时,需要先加载其父类Test接口,但是当前类加载器即MyClassLoader通过父类Test的符号引用在classPath/com/yu中找不到其对应的二进制文件(不可见),故返回java.lang.NoClassDefFoundError: com/yy/Test

因为这时候,自定义类加载器和系统类加载器是兄弟关系,他们两个加载器加载的类是互不可见的。

上述代码要想成功加载,其实可以破坏委托规则,如:

a4589d9e143b87d99b787d6678c86201.png

重写loadClass方法,破坏委托规则

或者

15442e088efa76a690a4b2c58826ebae.png

委托兄弟类加载器加载

代码红框处逻辑是,不是自定义加载器加载范围里的类,用向上委托的方式加载或者兄弟类加载器加载,都要打破原有的委托规则

所以设计三种类加载器通过分层实现双亲委派,即可以避免重复加载 + 避免核心类篡,也起到安全隔离的作用

611e38593fe976bf40690c4b7e94a018.png

tomcat类加载器结构

webappclassloader

每个web应用都有一个对应的类加载器实例,每个web应用都相互隔离

双亲委派模型下,子类加载器可以访问父类加载器加载范围中的类,但父类加载器不能访问子类加载器加载范围中的类

那这样又会有个问题:

如果需要在父类加载器加载的类里调用子类加载器所加载范围的类,典型如:SPI, 怎么办?

按上面我们通过破坏委托规则的方式可以实现:

通过指定的加载器去加载某些类

Thread.currentThread().getContextClassLoader()

线程上下文类加载器:

65766ef471ce6b439d8c092440f8479a.png

在创建并初始化线程的时候,会从执行者线程赋值一个线程上下文加载器

e7695da751543400cf762da25e65bd1c.png

线程上下文加载器默认是系统类加载器

说明:

类java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源

说说异常:

1.ClassNotFoundException:当用代码直接显式的加载某个类,加载不到时(不在加载的范围内),返回该异常

2.NoClassDefFoundError:当defineClass加载某个已确认在其加载范围内的类或者调用某个存在的方法时,但该类的字节码文件存在父类索引super_class,接口索引集合interfaces 或 该方法表中引用了其他类的符号引用,虚拟机将自动调用ClassLoader(String name)方法,通过该符号引用查询或加载其对应类的二进制文件,在对应加载器加载的范围内查询不到时,返回该异常

57958161814a0c9b41ea8199d7032069.png

自定义可重复加载的加载器

575bbff87ac8c6014e26ffd6c2cf2554.png

HotSwapClassLoader

HotSwapClassLoader加载Ab类,在其加载范围内查询不到,故返回ClassNotFoundException异常

当通过反射调用set(B b)方法时,该方法参数类型为B类,对应字节码文件有引用B的符号引用,虚拟机将自动调用ClassLoader(String name)方法加载B

316bbaaac5f75db1c19bd5474fbfcae2.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值