本文内容
参考
- https://docs.oracle.com/javase/7/docs/technotes/tools/windows/classpath.html
- https://www.cnblogs.com/duanxz/p/3482311.html
- https://www.cnblogs.com/youxia/p/java001.html
classpath 是什么
classpath 就是去寻找我们的 class 文件的路径,这么说当然是没问题的,但是有一个点就是:classpath 并不是一个单独的路径,而是由很多个路径组成的,也就是说,我们可以传入多个路径给 classpath,例如:
java -cp .:/someLib:/anotherLib
这里就包含了 3 个路径:
.:表示当前路径/someLib:第二个路径/anotherLib:第三个路径
此时,如果我们的代码里写了一句:Class.forName("com.example.MyClass") 之类的需要 JVM 去加载某个类的语句,那么 JVM 就会去 classpath 里去找到这个类加载(其实这个行为是 AppClassLoader 做的,如果我们不是使用的这个类加载器,就可能会有别的行为)
寻找这个类的过程就是:
- 去找 ./com/example/MyClass.class,找到了就加载,找不到就下一步
- 去找 /someLib/com/example/MyClass.class,找到了就加载,找不到就下一步
- 去找 /anotherLib/com/example/MyClass.class,找到了就加载,找不到就下一步
- 发现 classpath 里没找到这个类文件,抛出 ClassNotFoundException
使用冒号作为不同的 classpath 的分隔符,在 windows 下使用分号 ; 作为分隔符
AppClassLoader 只去加载一次,并且首先被找到的被加载,因此同一个类名的 class 文件,存在在不同的 classpath 下的话,只有最先被找到的被加载,这也是 jar 包冲突的一种起源了
传入 jar 包或者 zip 包作为 classpath
上面我们传递的都是目录作为 classpath,JVM 会去我们传递的多个目录里寻找我们要加载的 class 文件。当我们传入 jar 包的时候,行为就不一样了,例如:
java -cp .:/someLib:/anotherLib:/libs/myLib.jar
还是以 Class.forName("com.example.MyClass") 为例,寻找的过程如下:
- 去找 ./com/example/MyClass.class,找到了就加载,找不到就下一步
- 去找 /someLib/com/example/MyClass.class,找到了就加载,找不到就下一步
- 去找 /anotherLib/com/example/MyClass.class,找到了就加载,找不到就下一步
- 去到 /libs/myLib.jar 这个 jar 包内部,以 jar 包内部根节点为起始,去寻找 /com/example/MyClass.class,找到了就加载,找不到就下一步
jar 包实际就是 zip 包,把 jar 包解压后的目录看做是根目录,寻找 com.example.MyClass 的时候,就会去这个根目录下寻找 com/example/MyClass.class
因此我们在 classpath 中应该将 jar 包视作为和目录等同的地位,寻找类文件的时候也是把 jar 包当做目录去寻找的。
传入多个 jar 包
有时候我们有多个 jar 包在一个目录下都想作为第三方依赖,比如说我们将项目需要的所有的 jar 包都放到 /lib 目录下面,如果要手动拼写 classpath 将会非常的长,我们可以使用 * 通配符来传入多个 jar 包作为 classpath,假设现在 /lib 下面有 3 个 jar 包分别是 /lib/a.jar、/lib/b.jar、/lib/c.jar,那么我们可以执行
java -cp .:/lib/*
相当于执行了
java -cp .:/lib/a.jar:/lib/b.jar:/lib/c.jar
也就是传入了如下的 classpath
.:当前路径/lib/a.jar/lib/b.jar/lib/c.jar
这种方式只能传递 /lib 下的 jar 包,不包含 lib 下的 class 文件,如果同时还需要 class 文件,需要传入
java -cp /lib:/lib/*这表示将 /lib 作为一个 class 的根目录,同时把 /lib 下所有的 jar 包也作为 classpath 根目录这种通配符并不能递归,也就是说,如果我们有 /lib/child/xxx.jar 那么这个 jar 是不会被加入到 classpath 的
classpath 在系统中的定义
java.class.path
我们使用 System.getProperty("java.class.path") 就可以得到我们定义的 classpath,这些将由 AppClassLoader 去加载。
扩展类 classpath
除了我们在 java -cp 中传入的 classpath 之外,JVM 还会加载一些扩展类 jar 包,JVM 默认会去 $JAVA_HOME/jre/lib/ext 下,将所有的 jar 包都使用 ExtClassLoader 去加载,这个路径我们可以使用
java.ext.dirs 来控制,在我的 IDE 环境中如下:
/Users/czp/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
我们可以使用 java -Djava.ext.dirs=xxx 来修改扩展类的 classpath (一般不建议),或者直接将一些 jar 包丢到 $JAVA_HOME/jre/lib/ext 下,它会被 ExtClassLoader 加载
这个路径是专门加载一些扩展包的,它由 ExtClassLoader 去加载,我们也可以使用自定义类加载器定义别的行为
启动类 classpath
使用 -Xbootclasspath 可以修改启动类加载器,有以下的用法:
java -Xbootclasspath:/aaa/bbb/*:将 /aaa/bbb 下的 jar 包替代原先的 -Xbootclasspath,此时核心类的加载完全被替代了,所以很少用java -Xbootclasspath/a:/aaa/bbb/*:表示加载完了核心类之后,还要去加载 /aaa/bbb 下的 jar 包,也就是追加的关系(用的会多一点)java -Xbootclasspath/p:/aaa/bbb/*:表示加载完了 /aaa/bbb 下的 jar 包之后,再去加载核心类,也就是先加载我们定义的类(用的较少)
这个路径定义的类是被 BootstrapClassLoader 加载的,这是 JVM 实现本身的一部分,也就是 C++ 代码
很少替换这个 classpath,但是如果有需要还是可以这么干的
它对应的 java 系统属性是:
System.getProperty("sun.boot.class.path")
运行 Java 程序的方式
我们运行 java 程序的时候有两种方式:
一种是使用
-cp参数,然后传入一大堆 jar 包和文件目录,接着传入主类,最后传入程序参数。例如:java -Dkey1=val1 -Dkey2=val2 -cp a.jar:b.jar:c.jar:/home/xxxUser/project-java aaa.bbb.ccc.Main arg1 arg2 arg3- 上述的方式中,
-D参数定义的是 Java系统属性,在java中可以通过System.getProperty(key1)的方式读取 arg1 arg2 arg3定义的是程序参数,最后也就是main函数的args数组参数- 运行的主类是
aaa.bbb.ccc.Main,此时 JVM 就会去 a.jar 下寻找 /aaa/bbb/ccc/Main.class 这个 class,找不到就会去 b.jar 下寻找 /aaa/bbb/ccc/Main.class 这个 class,然后是 c.jar,接着是 /home/xxxUser/project-java/aaa/bbb/ccc/Main.class - 主要的要点就是,jar 包视作为一个根目录,要加载的类都是按照包名去这些传递进来的根目录去找
- 因为可以传递多个根路径和 jar 包,那么肯定有可能会出现完全相同的类名,此时,按照 -cp 定义的顺序,最先被找到的那个 class 被首先加载,后面的相同的 class 将不会被加载。这种情况也就是有时候会出现的 jar 包冲突导致 NoSuchMethodError 的问题,因为多个依赖可能也有一些不同版本的底层依赖,然后这个底层依赖的类加载了其中一个版本,导致另一个依赖其他版本的报错了
另一种是使用
-jar参数,然后传入一个jar包,接着就是程序参数。例如:java -Dkey1=val1 -Dkey2=val2 -jar xxx.jar arg1 arg2 arg3- 这种方式只能使用一个jar包,并且 jar 包需要META_INF目录下的manifest.MF文件中定义了Main-Class,会自动去启动这个main类
- 无法添加多个 jar 作为 cp,这种方式只能启动一个 jar
- 可以使用
java -Xbootclasspath/a:/usrhome/thirdlib.jar: -jar yourJarExe.jar的方式,将一些包使用 bootclassLoader 去加载
顺便说一句,
System.getEnv()获取的是环境变量,就是通常意义上的进程的环境变量(jvm实质就是个进程),System.getProperty(key1)获取的是java系统属性,这个是java自己定义的一种参数,arg1 arg2是程序参数,这个对应于main函数接收的参数。