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中)
这次总该结束了吧,其实本不打算结束的,本来还想继续延伸出关于自定义类加载的内容,但是,由于篇幅已经很长了,所以,关于类加载器的叙述放到单独的一篇文章中去。
本文为原创文章,难免会出现纰漏,甚至错误,,文章中也有一些细节点还没有深入,望大家不吝赐教。。。