在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

时间:2022-01-16 02:57:46

参考资料

  该文中的内容来源于 Oracle 的官方文档 Java SE Tools Reference 。Oracle 在 Java 方面的文档是非常完善的。对 Java 8 感兴趣的朋友,可以直接找到这个总入口 Java SE 8 Documentation ,想阅读什么就点什么。本博客不定期从 Oracle 官网搬砖。

前言

  在 Linux 中使用 Java,我一般都是直接使用 Linux 发行版自带的软件包,一个命令即可搞定 JDK 的安装。但是 Linux 发行版中自带的 JDK 往往是 OpenJDK,虽说不影响使用,但是如果要按照 Oracle 的文档进行学习,还是安装一个 Oracle JDK 比较好。OpenJDK 和 Oracle JDK 颇有渊源,其内容大部分都是一样的,除了少数 Oracle JDK 中涉及到专利技术或授权因素的部分代码。另外,Oracle JDK 自带的工具要更多一点,如 Java VisualVM 和商业化的 Java Mission Control 等等。最后,就是商标不一样了,请记住,Java 现在是 Oracle 的注册商标哦。至于授权协议的不同,我们普通用户不需要关心,既然 Oracle 提供免费下载,那我们只管用就是了。

  另外,在 Java SE 8 文档中,还有这样一副图片,完整地展示了 Java SE 的组成。在官网中的该图片的每一个小方格都是一个超链接,如果想学习哪一块的知识,点击相应的小方块就可以了。我这里只是一个截图,没有导航功能。把图片放到这里,就是让自己对 Java 的核心技术有一个总揽。如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

下载和安装Oracle JDK

  要下载 Oracle JDK,肯定要去 Oracle官网 ,在官网里面找到 Java 的下载页面是没有什么难度的,我就不多说了。我选择的版本是 JDK 8 For Linux 的 64 位版本。同时,我没有选择 rpm 包,而是选择了 .tar.gz 这个压缩包,如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

  将压缩包下载下来后,放到我自己的主目录,解压后就可以使用。这完全是我自己的私有版本,不会与系统中安装的其它 Java 版本发生冲突。缺点就是当我要调用 javajavac 这样的命令的时候,我必须指定完整的路径。解压的命令为 tar zxf jdk1.8.0u45-linux-x64.tar.gz

  熟悉 Java 的朋友肯定知道,以前安装 JDK 的时候,必须设置 PATH 和 CLASSPATH 两个环境变量。但是在 JDK 8 中,这都不是必须的。不设置 PATH 环境变量是因为在我的系统中存在 OpenJDK,即使将 ~/jdk1.8.0_45/bin 加入到 PATH,调用 java 命令的时候使用的依然是 OpenJDK。所以使用 Oracle JDK 的时候,我就直接输完整路径好了。不设置 CLASSPATH 的原因是 Java SE 8 可以自动探测 Class 所在的路径。当然,设置 CLASSPATH 环境变量也是可以的,只是没有那个必要了。如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

  (截完图后我又想到,其实要想覆盖 OpenJDK 的 java 命令也是可以的,只要这样设置环境变量 export PATH=~/jdk1.8.0_45/bin:$PATH,而不是 export PATH=$PATH:~/jdk1.8.0_45/bin 就可以了。使用 Oracle JDK 的时候就不用每次都输入完整路径了。另外,如果想更改全局的环境变量,可以修改 /etc/environment 配置文件。其实在从 Oracle 官网下载 JDK 的时候,完全可以选择 rpm 包的版本,安装后,可以使用 sudo update-alternative --config java 更改默认的 JDK 配置。)

  然后,写个 HelloWorld 程序测试一下。这个程序实在是太简单了,根本没有必要动用 Eclipse 这样庞大的 IDE,使用 Vim 足以。如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

  写这个测试程序就是为了证明 Oracle JDK 8 可以运行,特别是为了证明 JDK 8 不用设置 CLASSPATH 环境变量也可以运行。这个程序除了输出“Hello, world!”以外,还输出 Java 运行时默认的系统配置。如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

  在这些默认的系统配置中,有几个比较重要的值,我特意用红色的框框将其标记出来了。Java 运行时就是从这些路径中加载 class 的。如下图:
在 Linux 中安装 Oracle JDK 8 以及 JVM 的类加载机制

  下面来看 Java 运行时是如何搜寻并加载 class 的。在以下文字中,我均使用 JDK 代替 ~/jdk1.8.0_45 目录,表示 Oracle JDK 8 的安装路径。

类文件如何被发现并加载

  先来说说关于 Java 的闲话,大家都知道,运行 Java 程序需要一个启动器,比如 java 命令,大家也知道,在 Java 中一切都是类,只有属于类的方法,而没有单独的函数。这一点王垠大神有过批判。确实,Java 是一门纯面向对象的语言,太纯了,这个世界怎么可能所有的东西都是对象呢?本来就应该有一些东西应该存在于对象之外嘛,比如操作多个对象的纯函数,比如程序的入口点 main 函数。所以从语言的角度讲,Java 受到批判是理所当然的。但是, Java 的这种“纯”,简化了它的实现。因为 Java 中一切都是类,所以可以让 Java 代码编译后,每一个类生成一个 .class 文件,然后将 .class 文件放到相应的目录中,或者打包成 .jar 压缩包。程序运行的时候,用一个启动器将相应的 .class 文件加载,并执行其中的字节码即可。这种设计非常的简单、方便。而 C# 并不采用这种组织类和字节码的方式,却学 Java 让所有的函数都成为类的方法,我认为相反是不妥的。

  JVM 按如下顺序加载 .class 文件:

  1. Bootstrap classes。这些类是 Java 平台的基础。其中包含大家都熟悉的 rt.jar,还包含其它一些类或 jar 包。
  2. Extension classes。这些类扩展了 Java 平台,或者说,这些类利用了 Java 的扩展机制。这些类有两个要求:其一是必须打包成 jar,不能是单独的 .class 文件,并且这些 jar 包必须存放于 JDK/jre/lib/ext 目录下;其二是这些类不能引用 Bootstrap classes 和 Extension classes 之外的类。
  3. User classes。也就是用户自定义类。我们自己写的程序已经我们从其它地方下载的第三方库都属于这个范畴。JVM 最后加载这些类,到哪里去找呢?这就需要用到 CLASSPATH 环境变量了,或者在程序启动的时候使用 -classpath 参数。

  在以上三个过程中,用户其实并不需要指定 Bootstrap classes 和 Extension classes 所在的目录,只需要指定 User classes 所在的目录就可以了。这是 JDK 8 和之前版本的区别。Bootstrap classes 和 Extension classes 所在的路径可以自动搜寻。前面的图片中我标出的 Java 系统配置中的 sun.boot.class.path 项就代表了 Bootstrap classes 所在的目录,默认为 JDK/jre/lib 中的 rt.jar 和其它一些 jar 文件。可以在启动程序的时候使用 -Xbootclasspath 选项更改这个路径。而 Extension classes 的路径就是 JDK/jre/lib/ext 目录,在前面的图片中我也标出来了。

  关于 User classes 的搜索路径,如果不指定的话,默认就是当前目录.,我前面图片中标出的就是默认值。如果重新指定 User classes 的搜索路径的话,就不会从当前目录进行搜索了,如果要从当前目录进行搜索,必须将当前目录明确地添加到 CLASSPATH 中。指定 User classes 的搜索路径有以下几种方式:

  1. 设置 CLASSPATH 环境变量;
  2. 程序启动时使用 -cp 或 -classpath 选项;
  3. 如果使用 java -jar 方式启动程序,则从 jar 包中的 manifest 文件中的 Class-Path 配置项中指定的路径搜索。

  搜索路径可以是目录,也可以是 jar 包,也可以是它们之间的任意组合,每一个项之间用:分割。另外,路径中还可以使用通配符*,但是该通配符只能匹配某一个目录下的所有 jar 包,而不能匹配 .class 文件,更加不能匹配子目录。

  那么问题来了,既然 CLASSPATH 中可以包含 jar 包,而 jar 包中又可以指定 Class-Path,而这个 Class-Path 中又可以指定其它的 jar 包,这样会造成无限循环吗?JVM 在搜索 jar 包的时候有什么规则吗?有的,如下三条:

  1. jar包中指定的 Class-Path 会被当成 CLASSPATH 的组成部分,并且放在 jar 包中的其它 class 文件之前,而 class 文件的搜索是按照其路径出现在 CLASSPATH 中的顺序进行的,先找到先得。
  2. 如果之前扫面过的 jar 包又出现了,就不进行重复扫描;
  3. 如果一个 jar 包是作为 Java 的扩展安装的,也就是其在 JDK/jre/lib/ext 目录中,就忽略它的 Class-Path 配置项。

  关于 javacjavadoc 命令和 tools.jar 又有一些例外,这些例外的存在是因为 javacjavadoc 这些程序的运行对 .class 文件的双重需求决定的。作为 JDK 的工具, javacjavadoc 的运行需要 tools.jar 中的类的支持,另外,当 javacjavadoc 编译程序的时候,又需要解析源代码中对其它类的引用。它的基本规则如下:

  1. 运行 javacjavadoc 这些命令时,这些命令本身使用的 Bootstrap classes 、Extension classes 和 tools.jar 是不能改变的,它们只使用它们所在的 JDK 中的版本;
  2. 如果是 javacjavadoc 需要解析的其它的代码中用到了 tools.jar 中的类,则必须将 tools.jar 加入到 User classes 的搜索路径中才能起作用;如果要让它们解析的代码中引用不同版本的 Bootstrap classes 和 Extension classes,可以使用这两个命令的 -bootclasspath 和 -extdirs 选项指定。

  最后,类的加载还受 Class Loader 和 安全策略(Security Policies)的影响。在 Java 中,我们可以使用不同的 Class Loader,也可以编写自己的 Class Loader,但是大部分时候,我们都是使用的内置的 Class Loader。在 Java 中可以启用不同的安全策略,我们信任的类就允许它加载,不信任的就不允许。如果没有开启安全策略,则所有的类都认为是被信任的。但是即使开启了安全策略,也只会影响到 Extension classes 和 User classes,对 Bootstrap classes 是没有影响的。

CLASSPATH和Package的关系

  在 Java 中还有一个让人头疼的地方,就是 .class 文件的存放位置和 package 之间的关系。在 Java 语言中,为了不发生命名冲突,特意引入了 package 机制,将 Java 中的类划分到不同的 package 之中。在前面我展示的 HelloWorld.java 中,我将它的 package 设置为 com.xkland.java_8_study,然后编译这个程序的时候,我特意使用了 javac -d . 选项,设置将当前目录 . 作为编译后的目标位置。编译完成后,我还特意使用 tree 命令查看了一下目录的结构。

  从结果中可以看出,HelloWorld.class 存放的位置为 ./com/xkland/java_8_study/HelloWorld.class。这个目录层次和 package 的命名是完全一致的。而且必须一致,否则将会找不到 .class 文件,也无法运行程序。所以,如果你的 package 的命名是 a.b.c.d 这样的,则只能组织成 a/b/c/d/xx.class 这样的目录树,并且从 a 目录的上一级目录开始搜索这个 class 才找得到,也就是说,必须将 a 目录的上一级目录加入 CLASSPATH。对于源代码没有这个限制,但是使用同样的方式组织源代码是一个好的做法。如果源代码目录是扁平的,就像我前面的例子一样,则调用 javac 的时候一定要指定 -d 参数,它会自动生成 .class 文件的目录结构。

总结

  没什么好总结的。就是看了 Oracle Java SE 8 官方文档后的一点记录。 How classes are found? 还是值得偶尔复习一下的。

(京山游侠于2015-04-19发布于博客园,转载请注明出处。)