java project 显示感叹号_如果说Java类是绝妙创意,JVM是宏大平台,那么ClassLoader就是那座桥...

创建遇不到平台,徒有其里!

平台遇不到创意,徒有其表!

这种无奈在各行各业都会存在,但在Java里,因为有了ClassLoader的存在,这种无奈便迎刃而解了!Java里不缺少平台(其实JVM就是最大的平台),缺少的是绝妙的创意(那就是你的class文件),只要你能构建出让人沸腾的java类,ClassLoader都可以帮你加载到JVM这个最大的平台中!

ClassLoader的定位

如果你问一位JAVA程序员一个问题:“ClassLoader是干啥的?”

多数朋友会回答:“废话!ClassLoader不就是加载类的吗?即使真不知道,通过名字也能猜出来啊!ClassLoder=类加载器!”

没错!这个回答是完全正确的,但不够形象,有一种说了等于白说的感觉。那如何形象的描述出ClassLoader的作用呢?各位看官看看我这段描述是否恰当:

张三的绝妙想法都写在了一个类里,现在他的类缺少的是一个平台! 李四家里有矿,平台很大,唯独缺少精妙的创意! 大刀王五每天走街串巷,他了解了张三的苦,也了解了李四的无奈,最终他大刀一挥: 来!我介绍你们认识!

9b7740b5c8ab0cbd9b003afc77a4489f.png

而大刀王五就是那个加载绝妙创意(类)到大平台(JVM)的ClassLoader!

ClassLoader的作用描述

ClassLoader的作用是在运行时按JVM的需要将类编译之后的字节码文件加载到JVM控制的内存中。

这里要注意两点:

第一:运行时加载,这个很好理解,程序都不运行,没什么好加载的。

第二:按需加载而非一次性加载,这个也好理解,idea虽多,也得结合实际来实施。

到这里,看官们应该清楚ClassLoader是干什么吃的了。

内置的ClassLoader

类加载器从源头来讲可以分两种,一种是内置的即JVM提供的类加载器,另一种就是程序员自定义的。

关于内置的类加载器,咱们先看一个例子:

32051bdaf823386010c44eef93e0f72f.png

上图中的代码将要打印出如下三个类的类加载器:

打印的结果如下图所示:

70c72d35f80d3a16c057b5adeade3fae.png

这份打印的结果正好反应了咱们要讨论的内置的三种类加载器,而且“人如其名”,内置的这三种类加载器就是:

  • application class loader(应用类加载器)
  • extension class loader(扩展类加载器)
  • bootstrap class loader(引导类加载器)

670b0cd19f1575e9c49e7f8c4927bdf3.png

这三种内置的类加载器的职责是什么呢?直白的讲:

  1. 应用类加载器负责加载程序员们编写的类文件,这些类文件必须位于classpath变更指定的目录中,正如上例中我编写的SuzhouOXK002这个类。
  2. 扩展类加载器负责加载标准的Java核心类的扩展类,正如上例中的Logging类,而Logging类正是位于/jre/lib/ext目录中,该目录正是负责存放java核心类的扩展类。
  3. 引导类加载器是最顶级的类加载器,所有的类加载器都是由引导类加载器加载进JVM的,所以引导类加载器可以看作其它所有类加载器的“亲身父亲”。上例中的ArrayList位于/jre/lib目录中,该目录中正是包含了java的标准核心类。
此时有朋友可能会问:“为什么上例子中ArrayList的类加载器输出的结果为null?”
这是因为引导类加载器是用“原生代码”(native code)编写(原生代码就是java字节码被翻译成的适配于当前操作系统的机器语言,所以不同平台的原生代码,都是不相同的)而不是用java编写的,所以java中其实并不存在跟Bootstrap ClassLoader对应的一个类,自然也就不能输出了,不能输出,自然就用null显示了。

再谈Bootstrap Class Loader

为什么要有一个引导类加载器呢?试想,你编写的程序是不是需要一个类加载器将其加载进JVM,而这个类加载器想要加载你编写的类是不是要先实例化,而如果要实例化这个类加载器是不是又需要一个类加载器来加载这个类加载器?(绕口...)如此类推,总归需要一个顶层的类加载器,然后反向一层一层的回归加载,而这个顶层的类加载器,正是Bootstrap Class Loader,这也是为什么它是使用原生代码编写的原因,若不使用原生代码编写之,它无法实例化啊,你的java程序就根本跑不起来啊!!

480ba00fd5178831b39a8eb1dcc658e1.png

通常情况下,Bootstrap ClassLoader负责做两件事:

  • 第一件就是加载JDK的内部核心类,特别是rt.jar,然后目录/jre/lib里的jar也都由它来负责。
  • 第二件就是前面绕口的一堆,负责加载其它的类加载器,这样其它的类加载器才能正常实例化并开始自己的工作。

再谈Extension Class Loader

了解了引导类加载器之后自然就会明白,扩展类加载器是由引导类加载器加载进JVM并实例化出来的,所以扩展类加载器可以看作是引导类加载器的“亲生儿子”。扩展类加载器负责加载JDK的扩展目录,通常是/jre/lib/ext目录,如果你配置了java.ext.dir这个系统变量,其内部的类也由扩展类加载器负责。

截至此时,看官们应该清楚了,JDK内部的类是由引导类加载器和扩展类加载器负责加载的,所以引导类加载器和扩展类加载器适用于运行在当前平台上的任何JAVA程序(简直是废话!)

谈谈Application Class Loader

一言以概之,所有应用级别的类都由Application Class Loader加载进JVM。应用类加载器是由扩展类加载器加载进JVM并实例化的,所以说,应用类加载器就是扩展类加载器的“亲儿子”,如此它也就成了引导类加载器的“亲孙子”。

应用类加载器从哪里定位要加载的类文件呢?这里就不得不提一下classpath了。classpath,顾名思义,就是“类路径”,它是一个由-classpath或-cp命令行选项指定的环境变量,说白了就是一个路径。平时我们在使用ide编程的时候,可能不太关注classpath,因为ide一般都有自己的默认配置。举个例子:

你在D盘创建了一个文件夹叫作project,然后在project创建了一个工程叫作test,此时ide工具默认会在test目录里创建一个java文件夹,里面应该会有一个src文件夹。这些个路径ide工具都是知道的,所以ide工具就会将D:projecttestjavasrc作为classpath,你在使用这个ide创建的任何类文件都会被存放到这个目录中。至于在src目录下如何再划分目录,那就是java包该干的事情了。

另外,还有一种特殊的classpath,那就是jar包!jar包虽然是一个文件,但它本质是一个可移动的目录,你的工程依赖了某个jar包,其实本质上是告诉你的JVM:

“哥!启动时不要忘记我在那个包里的兄弟啊,因为没有它我活不了啊!

类加载器是如何工作的?

想明确了解类加载器是如何工作的,最佳方法就是打开源码一探究竟,下图为java8中ClassLoader类中的loadClass方法:

43fd710dfbec4ae483f6f75c72c26758.png

loadClass方法以要加载的类的全名作为入参,返回类的定义信息,可能抛出ClassNotFoundException,该方法的定义简明扼要言简意赅。

方法体中,首先获取类加载的锁,避免重复加载,拿到锁之后,先检测目标类有没有被加载,如果已被加载,它会谢天谢地,又了却了一桩心愿。如果没有被载,那就得继续干活,但它干活很有门道,喜欢啃老。它会先确认一下自己是不是直系富二代,如果家里有矿的话(parent != null),那就把这活先丢给家里干,如果家里没矿(parent == null),它会直接丢给老祖宗去干!(不孝之子啊!)

当loadClass这斯最终发现没有人能帮它干这件事时(即父加载器都没有找到目标类c==null),那就只能自己干了,至于这里它是怎么干的,我就对这段代码不作深究,但总体上肯定在是是classpath中依据某种匹配策略去定义到目标.class文件,然后读取类的定义信息。

截至到此,懂的朋友自然懂,不懂的朋友可能就会问:

为什么加载类时要先让上层类加载器或导引类加载器(没有上层类加载器时)去加载目标类呢?

其实这就是事先设计的类加载原则,即上层委托模式!简单的来说,上层委托模式就是将加载类的工作委托给直系上层类加载器(如果有的话),而且这个委托过程是一层一层向上委托的,即假如你自定义的类加载器与最顶层的类加载器间隔了十代,那么你自定义的类加载器的加载请求会依次向上委托十次,然后再一层一层的返回下来,下图演示了类加载的委托流程:

9a7d5311793f6574126af91dfcd6c2fd.png

类加载器的三个重要特性

  • 第一:委托模型
  • 第二:加载唯一
  • 第三:向上单向可见

委托模型:

再唠叨一遍:假如目前有一个请求要加载你编写的类A进JVM,首先应用类加载器会将对类A的加载请求委托给扩展类加载器,然后扩展类加载器又会将这个请求委托给引导类加载器。如果引导类加载器成功加载到了类A,那么会将类A的信息返回给扩展类加载器,扩展类加载器再返回给应用类加载器。如果引导类加载器没有加载到类A,那么扩展类加载器会尝试去加载类A,如果扩展类加载器加载到了类A,那么它会将类A的信息返回给应用类加载器,如果扩展类加载器没有加载到类A,那么应用类加载器才会去classpath中定位并尝试加载类A。

采用委托模型的优势之一是为了让JDK的核心类及扩展类优先被加载,因为这些类是应用运行的基石!而优势之二就是下面的”加载唯一“特性。

加载唯一:

这个很好理解,基于委托模型的设计原则,就可以保证被加载类不会出现重复。

向上单向可见:

这个可见性指的是被加载的类之间的可见性,理解起来很简单,举个例子即可,假如:

  • 类A和类B是由应用类加载器加载进JVM的
  • 类C和类D是由扩展类加载器加载进JVM的
  • 类E和类F是由引导类加载器加载进JVM的

那么只会出现以下五种可见性:

  1. 类A和类B之间互相可见
  2. 类A和类B可以访问类C D E F
  3. 类C和类D之间互相可见
  4. 类C和类D可以访问类E F
  5. 类E和类F之间互相可见

小结

第一次在头条写4000多字的文章,从晚上8点到12点!真快累成狗了!

再总结几句吧:

要说针对ClassLoader最需要理解的内容,第一点肯定是每个类加载器负责加载的目录,第二点就是理解类加载时的委托模型,这是顺序问题,最后一点理解被加载类的可见性。只要理解了这三种,遇到类加载相关的错误或异常一般都可以解决。

另外,文章中出现的错误或疑问,还请一定在评论区讨论啊!因为“无兄弟,不编程!”

---------关于作者----------

9f2346e86ca58c2dfe897283b77f86bc.png

Java开发人员,在今日头条开有视频专栏《Java高级加油站》,头条号名称跟知乎名称相同,想看视频专栏的朋友可以去搜索关关注!

-------------------------------------------------------------------------

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值