根据配置CLASSPATH彻底弄懂AppCLassLoader的加载路径问题

1.前言

  相信任何使用JAVA语言的开发者,都会在一台新的PC上去装上JDK,JRE,用来可以编译我们所写的.java文件,然后让其生成编译后的.class文件,从而能够争取执行。。。有兴趣可以简单了解一下JDK和JRE的作用

  当我们装上JDK的时候,相信大家还会经历非常重要的一步,就是配置环境变量。并且,毫不避讳的说,初次接触java的初学者,总是在配置环境变量的时候,一头雾水,那我们今天就来以配置环境变量为问题切入点,彻底搞懂他究竟涉及到哪些比较核心的问题-----类加载器。。。

2.问题切入点CLASSPATH

  关于如何配置java使用的环境变量,请参考配置环境变量

  

 

上面就是我们再熟悉不过的两个环境变量,,,分别是PATH和CLASSPATH。

2.1 path中配置的两个路径是干嘛的?

首先,我们需要知道这两个路径分别是,,,jdk\bin路径和jre\bin路径。。。想要理解这个,就需要你回上面区看关于JDK和JRE的含义的描述。

我们来看这样的一个操作:DOS命令行编译和执行我们的JAVA代码

  • 第一个红框中是我为了测试,存放一个Hello.java文件的地方,在D盘符下的MyTestCode文件夹下。
  • 第二个红框是我们再熟悉不过的命令行编译(javac)和执行.class文件(java)的操作。

这里就有一个很有意思的问题了,我们的DOS命令让我们进入到了d盘的一个确定的文件夹下,该文件夹下又没有编译和执行java源文件的工具,那么为什么我们可以直接在D:MyTestCode文件夹下,使用javac和java命令的呢???注意,需要明白的是,javac和java是我们正确安装JDK和JRE后,才能使用的命令,是因为在JDK和JRE里面有满足java文件编译和执行的工具。。。 既然如此,我们在DOS命令中想要使用javac和java命令,就必须找到对应的执行工具,而D:MyTestCode文件夹又没有这样的工具。。。

解释了这么多,其实就是为了说明,PATH中的路径就是为了让上述javac和java可以找到对应的编译和执行工具。。。当我们将工具路径注册到Path后,我们可以在任何盘符下,任何位置去使用javac和java命令,当执行这个命令行的时候,windows能够在path中找到对应的工具。。。

2.2 CLASSPATH又是干嘛的?

其实这个CLASSPATH的配置相当简单,但是想要弄清楚这个东西,是一件不容易的是。如果你上网搜索,一定会有一大批答案。但是,我很负责任的告诉你,这些都不是我们想要的。。。

我们来看CLASSPATH的具体配置信息:

.;%JDK_HOME%\lib\dt.jar;%JDK_HOME%\lib\tools.jar;

首先,抛出涉及到的一些问题

  • 或许此时正是你的疑问,CLASSPATH可以不配置啊,我就没有配置
  • CLASSPATH中为什么要添加".;",不添加可不可以?
  • 配置的这两个JAR包路径是干什么的?

上述问题,最终解释为一句话:CLASSPATH中配置的路径是告诉AppClassLoader要去哪里加载.class文件。

这就涉及到了关于类加载器的基本知识,文片不对这些基础知识过多介绍,请参考java中关于类加载器的基础知识

3. 彻底搞清楚CLASSPATH和AppClassLoader的关系

以下的叙述内容全部按照具体实例来阐述。

我们先来看一段代码:

package com.myClassLoaderTest;

import sun.misc.Launcher;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 获取三种JVM类加载器的加载路径
 *
 * @ Author:  liu xuanjie
 * @ Date:    2020/8/6
 */
public class ClassLoaderPathTest 
{
    /**
     * 主函数,程序入口
     */
    public static void main(String[] args)
    {
        //首先是启动类加载器
        System.out.println("启动类加载器的加载路径: ");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for(URL url : urls)
        {
            System.out.println(url);
        }
        System.out.println("=======================================================");


        System.out.println("扩展类加载器的路径: ");
        //然后是扩展类加载器,获取方式是获取系统类加载器(AppClassLoader)的父加载器
        //(注意这就需要开启双亲委派模型)
        URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();
        urls = extClassLoader.getURLs();
        for(URL url : urls)
        {
            System.out.println(url);
        }
        System.out.println("========================================================");


        //应用程序类加载器
        System.out.println("应用程序类加载器的路径: ");
        URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
        urls = appClassLoader.getURLs();
        for(URL url : urls)
        {
            System.out.println(url);
        }
    }
}

 

 

 上述这段代码,很简单,就是获取三种类加载器,然后得到其负责的加载路径。

我们在IDEA下运行一下这个代码,看看与我们的猜测是否一致????

 

 结果似乎跟我们的预期不一致,不是说好AppClassLoader的搜索路径是CLASSPATH中的吗?怎么不一样?

我们先来仔细观察这个结果:

  • 第一个截图,是启动类加载器的搜索路径,很明显,跟我们了解到的一致,是lib目录下的核心类库。多说一句,其实就是我们引入包的时候那些以 java. 开头的类库。
  • 第二个截图,是扩展类加载器的搜索路径,很明显,是lib/ext下的扩展类库,其大部分就是我们引入包的时候那些以 javax. 开头的类库(注意啊,这只是一种笼统的说法)。
  • 第三个截图的结果,可以看到,首先包含了所有第一个截图和第二个截图中出现的类库。。。其次,红框内的那个路径,是当前IDEA下项目文件所生成的.class文件所存放的路径。。。后面的是所有该项目下maven所引入的第三方包。。。

其实,造成AppCLassLoader的加载类库路径与预期不一致的原因很简单,是由于IDEA这个编译器做了一些手脚。

 

其实根据结果可以猜想到,,,IDEA编译器,,,将AppClassLoader所关注的路径显示(仅仅是显示)出为了以下三部分:

(这里需要注意的是,尽管上述结果是我们调用的方法输出结果,但是这只是Idea给打印出来了,并不是说这里所有显示出来的 路径都是由AppClassLoader负责的路径,为了避免这种不直观的现象,我们抛弃Idea。。。实际上,Idea也只是这样打印出来了这些路径,其中由上层加载器控制的路径肯定还是上层加载器负责,不然就不是双亲委派了,,,所以说Idea这里的打印输出结果有点坑,潜规则,容易直接把人带偏了。。。) 

  • 引入JDK的时候的那些基础核心类库和扩展类库。(这肯定不是AppClassLoader负责的)
  • IDEA操作的当前项目文件的.class存放的文件夹路径。
  • 当前项目操作引入的第三方JAR包。 

所以,接下来,我们抛弃IDEA编译器,全部在DOS命令下执行:

 3.1 第一个测试

我们将上述代码拷贝成一份单独的.java源文件,然后去处文件中的包名,放到一个新的单独路径下。

 

在命令行中开始我们的测试:(由于我这里编译好了,就直接执行了,自行测试的时候记得编译。) 

这一次好像验证了我们的结论,AppClassLoader中加载的是我们CLASSPATH中配置的路径。 

 CLASSPATH中配置的路径依次是:

  • 先输出的是E:/CommandLineTest
  •  后续依次是一个JDK中的dt.jar和tools.jar的路径
  • 然后又出现了依次E:/CommandLineTest

我们来分析这个结果::::

  • 按照顺序位置猜测,这个CLASSPATH中的”.;“应该是对应了E:/CommandLineTest,那这个"点分号"为什么会对应这个路径呢?是因为dos命令中当前操作的盘符是这个路径?还是.class文件最终存放在这个路径中?后续我们回去验证。
  • 还有就是CLASSPATH的配置中明明只有三个路径,为什么输出结果中有四个路径,最后一个路径还和第一个路径一样?这两个一样的路径之间有关系吗?

3.2 我们来验证3.1中最后的问题

为了验证这一点,我们不进入.class存放的位置,直接执行java命令。

显然,此时我们就需要更改CLASSPATH,让AppClassLoader能够找到.class文件。

(注意啊,由于配置变量发生了改变,需要重新打开命令行,否则执行结果跟刚才一致) 

首先,我们分别在C盘目录下和D盘目录下直接执行了java ClassLoaderPathTest的命令,分析结果。

  • 首先,值得高兴的是,我们在CLASSPATH中配置了我们要执行的.class文件的路径,然后我们在DOS中操作的时候,没有进入到这个.class文件所在的路径下,也成功执行了。。。这就直接说明了,CLASSPATH确实是一个AppClassLoader搜索加载类库的路径。。。所以说,当我们配置完CLASSPATH后,我们可以在任何位置去执行CLASSPATH路径中存放的.class文件。
  • 我们也知道了"点分号"对应的路径,跟.class无关,就是DOS执行命令行时,当前所操作的路径。第一次我们直接在C盘根目录中操作,输出的就是C:,第二次我们在D盘根目录中操作,输出的就是D:。并且我们操作的.class文件在E:CommandLineTest中。
  • 根据这一次的实验结果,又产生了一个有趣的问题,本次实验中,没有再多出一个路径了,,,输出的路径就恰好是CLASSPATH中所对应的路径。一个不多,一个不少。。。。很有意思,不慌,我们继续实验。
  • 其实这也验证了3.1中多出来的那一个路径与"点分号"无关。
  • 其实,还表明了,3.1中多出来的那个路径与当前操作的盘符路径也无关。

 

3.3 我们把CLASSPATH中的"点分号"去掉

很简单,我们把CLASSPATH中的"点分号"去掉

 再来执行命令行,(注意啊,由于配置变量发生了改变,需要重新打开命令行,否则执行结果跟刚才一致)

继续分析本次的结果:

  • 首先我们在CLASSPATH中去掉了"点分号",结果也确实如此,"点分号"所对应的当前路径消失了。
  • 而我们本次执行是在.class文件存放目录下执行的,因为我们CLASSPATH中干掉了这个.class所存放的路径,如果不进入到这个E:CommandLineTest下,会导致类找不到(这个结论我们下面会验证到)。
  • 哈哈,本次进入到.class文件存放目录后,这个新增的路径又出现了。。。

3.4 为了继续弄懂关系,我们直接把CLASSPATH干掉

很简单,直接不配置ClassPath,删除掉这个环境变量。看看在不同的操作路径下,会有什么不一样

 

 

好了,我们分析两次的执行结果:

  • 首先,第二个结果,表明,CLASSPATH中不存放需要操作的.class文件所在路径的时候,并且如果我们不进入到这个.class文件存放的路径下的时候,AppClassLoader是无法加载的,因为根本找不到。。。
  • 那第一个结果,是我们把CLASSPATH干掉后,然后进入到.class的存放路径下,成功执行了.class文件,并且,执行结果中又新增了这个路径,注意啊,此时的CLASSPATH已经完全没有了。。。
  • 这说明了,CLASSPATH这个系统变量不是必须的,但是如果没有,那就需要手动进入到要执行的.class文件所存放的盘符目录下进行执行。
  • 并且,总和上述三个实验可以得出另一个结论,当我们进入到.class文件存放位置的路径下去执行这个.class文件的时候,AppClassLoader的加载类库路径被默认添加了这个路径,无论我们有没有在CLASSPATH中配置这个路径。。。

3.5 我们验证3.4中的最后一个结论

我们这样做,CLASSPATH中只配置.class文件的存放路径,并且我们进入到这个路径去执行这个.class文件。

结果如图所示:

  •  我们进入到.class文件存放目录去执行的.class文件,为什么这次只打印出来了CLASSPATH中的路径,而没有添加呢?
  • 其实原因很简单,这个路径已经被配置到了CLASSPATH中,也就是如果CLASSPATH中有了这个路径,那么无论如何也都不会再添加一次这个路径了
  • 你可能要问,之前的"点分号"也是当前路径,为什么那个时候还会新增这个.class存放的路径呢???其实这个问题已经解释过了,“点分号”所代表的是DOS命令中当前所操作的盘符,他不是固定的,他跟.class存放的路径毫无关系,只不过是当我们操作路径与.class存放路径一致的时候,产生了巧合而已。。。。
  • 所以说,当我们把.class存放的路径,添加到CLASSPATH的时候,就不会新增了,因为没有意义,即使新增了,也是两个完全一致的路径。

3.6 我们改变.class的位置

为了验证这个问题,我们首先把ClassPath改回去,然后改变ClassLoaderPathTest.class的位置。

我们继续执行命令行;

 

这个实验没什么大用,只是为了再次证明:

  • "点分号"所代表的路径确实就是当前DOS中所操作的盘符路径
  • 当我们在DOS中进入到要执行的.class文件存放路径后执行对应的.class文件,AppClassLoader的类库加载路径会被默认添加上这个路径。

那么,这就完了吗???看到这里是不是又乱又迷,还感觉没一点用。。。哈哈。。。。

不要慌,更乱更迷更没用的还在后面。。。

4.CLASSPATH中严格的顺序问题

看了上述关于CLASSPATH配置问题的介绍,其实感觉真的一点用没有,哈哈,那是因为我们越来越依赖现成的编译器了,而忽略了我们本应该关心的问题。。。

继续看这样一个问题,,,如下配置CLASSPATH

 

 我们分别在D:\MyTestCode和E:\CommandLineTest中放入同一个.class文件,并且同时将这两个路径配置到CLASSPATH中。

执行命令行如下:

分析结果:

  • 我们直接在外部C盘下去执行了这个.class文件,毫无疑问的可以执行成功
  • 但是,我们配置路径中有两个路径都有这个文件,但是dos命令的结果告诉我们,这个.class文件只被执行了一次,当然了,我们也希望他只执行一次。
  • 那么这是为什么呢???并且,到底执行的是哪一个呢???是第一个路径,还是第二个路径????

4.1 来验证上述问题

验证的操作很简单,我们修改某一个对应的ClassLoaderPathTest的内容,也就是当两个.class同名的时候,并且其存在路径还都被放在了CLASSPATH下,我们来看执行结果。。。

我们修改D:\MyTestCode下的文件

 然后编译它,并形成.class文件

 

执行命令行,展示结果如下:

 

果不其然,执行的是CLASSPATH中第一个路径下的.class文件:

  • 这样就是说,AppClassLoader在扫描用户类路径的时候,根据CLASSPATH中的配置信息,是存在相对的位置关系的严格顺序的,他会沿着这个顺序去寻找,当在某一个路径下找到.class文件的时候,他就不会再继续往后找了。。。即便后续的路径中仍旧存在着相同名字的.class文件。。。 

 4.2 从4.1的结果观察中我们有什么启发呢?

  • 首先,我们自己写的用户类的.class文件的存放路径,尽量配置到CLASSPATH中,并且在最前面。
  • 想想我们最初的CLASSPATH的配置,是不是"点分号"在最前面,现在也就理解为什么了,因为,他会首先查找"点分号"路径下的文件,如果找到了,就直接完成了。。。
  • 我们知道,JVM加载类的时候,并不是一股脑的把所有类加载进去,,,他其实是”按需加载“的。。。所有CLASSPATH中的路径配置的前后位置关系,就是AppClassLoader的查找顺序。。。
  • 当多个.class文件同名的时候,且其在CLASSPATH中能够找到加载路径的时候,以最先出现的为执行准则,这仍旧取决于CLASSPATH中的配置顺序。。。

4.3 我们再来进行最终的验证

上面4.1中我们是在C盘根目录下操作的,这次我们改变策略,我们去E:\CommandLineTest下去操作

 

结果分析:

  • 跟我们预想的一样,即便进入到了E:\CommandLineTest的目录下,还是执行的D盘下的那个.class文件。
  • 并且,可以得出一个新的结论,这个搜索的顺序,是先搜索CLASSPATH中配置的路径,如果没有的话,最后才会到当前DOS命令的当前操作的盘符路径下去搜索。 

这就完了吗???不,还没有,哈哈哈哈哈哈哈!!!

5.那dt.jar和tools.jar又是干嘛的嘞??

首先说明,作者精力有限,并没有很深入的研究这个问题,只是提出一个研究方法,和初步的答案。。。。

我们复制这两个jar包到单独的文件,然后去解压(其实jar包就是一种压缩包而已)

 5.1 首先是jdk\jre\lib\dt.jar

 粗略来看这个dt.jar中主要就是javax.swing下的相关类,是一个拓展包,但是没有放在拓展类加载器的搜索类库路径下,而是需要我们自己手动的配置到CLASSPATH中,由AppCLassLoader来负责加载。。。。其实这也可以验证,就是使用这些类,然后CLASSPATH中不配置这个路径,看看能不能成功,这里就不做实验了。。。

5.2 然后再来看tools.jar

 这个里面的类比较多,层次也比较乱,作者也没有深入了解,所以不多做解释,如果有谁研究过的话,可以评论一下,虚心求教。关于这个jar包的作用,大家目前就自行搜索吧,后续深入了解会回来更新这部分内容。。。

这次总算是完了吧,什么???还没有完???

6.另外两个类加载器的路径是怎么配置的呢???

首先来点基础知识,看图:

这个加载类库的路径,其实我们上述的实验中已经验证过了,,,但是仔细想一个问题:

AppClassLoader的路径是通过CLASSPATH来配置的,这很直观,但是我们没有配置启动类加载器和扩展类加载器的路径啊?他们的类库搜索路径是怎么配置的?是什么时候配置的?

我们来看JDK中的源码:

 

 

 

为了理解这个东西,我们先来看这样一个知识:

  • 首先,JVM是”按需加载“的,这个之前已经介绍过了,JVM在启动的时候,不会傻傻的将所有的类和资源都加载进内存
  • 程序在启动的时候,或者说JVM在启动的时候,JVM是会负责先装载所有的ClassLoader的,这一部分,涉及到很底层的JVM指令问题,无需深入,我们只需要知道JVM实例化了sun.misc.Launcher这个类就行,如上图所示。
  • 来看图示为sun.misc.Launcher的构造方法,他加载了扩展类加载器和AppClassLoader,并且注意看图中最后一个标注的地方,他把AppCLassLoader设置为了线程上下文的类加载器,哈哈,是不是长知识了。。。
  • 由于启动类加载器是有C++编写的,不是JAVA语言所写的,所以暂且是需要知道,JVM启动的时候,也会有JVM自己加载这个启动类加载器的。。。

然后我们回到上面的JDK源码,看图中标注的那几个获取系统变量的操作:

 System.out.println(System.getProperty("sun.boot.class.path"));
 System.out.println(System.getProperty("java.ext.dirs"));
 System.out.println(System.getProperty("java.class.path"));

很直观的来看,这三个系统变量就对应了三个类加载器的类库加载路径。。。可以自行验证。QAQ。。。、

这里在简单引申一点基础知识:

  • System.getProperty()一定会对应setProrerty的
  • 然后其实内部是一个Hash逻辑的key-value对应关系

所以说,JVM在最开始实例化sun.misc.Launcher类之前,一定配置好了上述三个系统变量,并且前两个变量的路径是由JVM指定的(当然,正如描述中所说的,我们可以通过JVM启动参数去更改)。原因很简单,这两个类加载器负责的是jdk中的类库,不是用户类库,所以其存放位置根据jdk安装位置,是相对就能够得到的(不信的话,你把lib目录换个位置,立马报错)(这句话的意思是说,jdk中的这种文件结构是有其固定的逻辑的,不要所以更改)。。。。而用户类路径具有不确定性,所以需要我们在CLASSPATH中去配置。。。

前面已经介绍过这两个类加载器所负责的类库,这里再解释一下。

  • 启动类加载器,那个类库搜索路径,主要是基础核心类库,就是java.开头的包内类
  • 扩展类加载器,那个类库搜索路径,主要是拓展类库,就是javax.开头的包内类(这只是很粗的一种便于理解的叙述,我们已经知道dt.jar中有javax.swing,并且其被我们放在了CLASSPATH中

这次总该结束了吧,其实本不打算结束的,本来还想继续延伸出关于自定义类加载的内容,但是,由于篇幅已经很长了,所以,关于类加载器的叙述放到单独的一篇文章中去。

本文为原创文章,难免会出现纰漏,甚至错误,,文章中也有一些细节点还没有深入,望大家不吝赐教。。。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值