附录A 在Windows系统下编译OpenJDK 6
本附录内容说明
这是本书第1版中介绍如何在Windows下编译OpenJDK 6的例子,里面的部分内容现在已经过时了(例如安装Plug部分),但对在Windows上构建安装环境和进行较老版本的OpenJDK编译还是有一定参考意义的,所以笔者并没有把它删除,而是挪到附录之中。
A.1 获取JDK源码
首先确定要使用的JDK版本,OpenJDK 6和OpenJDK 7都是开源的,源码都可以在它们的主页(http://openjdk.java.net/)上找到。[[OpenJDK]] 6的源码其实是从OpenJDK 7的某个基线中引出的,然后剥离JDK 1.7相关的代码,从而得到一份可以通过TCK 6的JDK 1.6实现,因此直接编译OpenJDK 7会更加“原汁原味”一些,其实这两个版本的编译过程差异并不大。
获取源码有两种方式:
- 通过Mercurial代码版本管理工具从Repository中直接取得源码(Repository地址:http://hg.openjdk.java.net/jdk7/jdk7),这是最直接的方式,从版本管理中看变更轨迹比看什么Release Note都来得实在,不过太麻烦了一些,尤其是Mercurial远不如SVN、ClearCase或CVS之类的版本控制工具那样普及;
- 直接下载官方打包好的源码包,可以从Source Releases页面(地址:http://download.java.net/openjdk/jdk7/)取得打包好的源码,一般来说大概一个月左右会更新一次,虽然不够及时,但的确方便了许多。
笔者下载的是OpenJDK 7 Early Access Source Build b121版,2010年12月9日发布的,大概81.7MB,解压出来约308MB。
A.2 系统需求
推荐平台
如果可能,笔者建议尽量在Linux或Solaris上构建OpenJDK,这要比在Windows平台上轻松许多,而且网上能找到的资料绝大部分都是在Linux上编译的。
如果一定要在Windows平台上编译,建议读者认真阅读一下源码中的README-builds.html文档(无论在OpenJDK网站上还是在下载的源码包里面都有这份文档),因为编译过程中需要注意的细节非常多。虽然不至于像文档上所描述的“Building the source code for the JDK requires a high level of technical expertise. Sun provides the source code primarily for technical experts who want to conduct research(编译JDK需要很高的专业技术,Sun提供JDK源码是为了供技术专家进行研究之用)”那么夸张,但是如果读者是第一次编译,那在上面耗费一整天乃至更多的时间都很正常。
笔者在本次实战中演示的是在32位Windows 7平台下编译x86版的OpenJDK(也就是32位的JDK),如果需要编译x64版,那毫无疑问也需要一个64位的操作系统。另外编译涉及的所有文件都必须存放在NTFS格式的文件系统中,因为FAT32格式无法支持大小写敏感的文件名。
官方文档上写道:编译至少需要512MB的内存和600MB的磁盘空间。如果读者耐心很好的话,512MB的内存也可以凑合使用,不过600MB的磁盘空间仅仅是指存放OpenJDK源码和相关依赖项的空间,要完成编译,600MB肯定是无论如何都不够的。这次实战中所下载的工具、依赖项、源码,全部安装、解压完成最少(“最少”是指只下载C++编译器,不下载VS的IDE)需要1GB的空间。
目录命名限制
对系统的最后一点要求就是所有的文件,包括源码和依赖项目,都不要放在包含中文或空格的目录里面。这样做不是一定不可以,只是这样会为后续建立CYGWIN环境带来很多额外的工作。这是由于Linux和Windows的磁盘路径差别所导致的,我们也没有必要自己给自己找麻烦。
A.3 构建编译环境
准备编译环境的第一步是安装一个CYGWIN[1]。这是一个在Windows平台下模拟Linux运行环境的软件,提供了一系列的Linux命令支持。需要CYGWIN的原因是,在编译中要使用GNU Make来执行Makefile文件(C/C++程序员肯定很熟悉,如果只使用Java,那把这个东西当成C++版本的ANT看待就可以了)。安装CYGWIN时不能直接默认安装,因为表A-1中所示的工具都不会进行默认安装,但是是编译过程中需要的,因此要在图A-1所示的安装界面中进行手工选择。
| 工具名称 | 说明 |
|---|---|
| (表A-1的内容在原始文本中未提供,这里保留占位) | …… |
图A-1 CYGWIN安装界面(CYGWIN安装时的定制包选择界面,原书附图为截图,此处保留文字说明)
表A-1所述的需要手工选择安装的CYGWIN工具(具体列表需参考原书源码或文档)
建立编译环境的第二步是安装编译器。JDK中最核心的代码(Java虚拟机及JDK中Native方法的实现等)是使用C++语言及少量的C语言编写的,官方文档中说它们的内部开发环境是在Microsoft Visual Studio C++ 2003(VS2003)中进行编译的,同时也是在Microsoft Visual Studio C++ 2010(VS2010)中测试过的,所以最好只选择这两个编译器之一进行编译。如果选择VS2010,那么要求在编译器之中已经包含了Windows SDK v 7.0a,否则可能还要自己去下载这个SDK,并且更新PlatformSDK目录。由于笔者没有购买Visual Studio 2010的IDE,所以仅下载了VS2010 Express中提取出来的C++编译器,这部分是免费的,但单独安装好编译器比较麻烦。建议读者选择使用整套Visual Studio C++ 2010或Visual Studio C++ 2010 Express版进行编译。
环境变量路径顺序
准备JDK编译环境的第三步就是下载一个已经编译好的JDK。这听起来也许有点滑稽——要用鸡蛋孵小鸡还真得必须先养一只母鸡呀?但仔细想想,其实这个步骤很合理:因为JDK包含的各个部分(Hotspot、JDK API、JAXWS、JAXP……)有的是使用C++编写的,而更多的代码则是使用Java自身实现的,因此编译这些Java代码需要用到一个可用的JDK,官方称这个JDK为Bootstrap JDK。而编译OpenJDK 7的话,Bootstrap JDK必须使用JDK 6 Update 14或之后的版本,笔者选用的是JDK 6 Update 21。
最后一个步骤是下载一个Apache ANT,JDK中Java代码部分都是使用ANT脚本进行编译的,ANT版本要求在1.6.5以上,这部分是Java的基础知识,对本书的读者来说应该没有难度,笔者不再详述。
A.4 准备依赖项
前面说过,OpenJDK中开放的源码并没有达到100%,还有极少量的无法开源的产权代码存在。OpenJDK承诺日后将逐步使用开源实现来替换掉这部分产权代码,但至少在今天,编译JDK还需要这部分闭源包,官方称之为“JDK Plug”[2],它们从前面的Source Releases页面就可以下载到。Windows平台的JDK Plug是以Jar包的形式提供的,通过下面这条命令可以安装它:
java –jar jdk-7-ea-plug-b121-windows-i586-09_dec_2010.jar运行后将会显示图A-2所示的协议,点击“ACCEPT”接受协议,然后把Plug安装到指定目录即可。安装完毕后建立一个环境变量 ALT_BINARY_PLUGS_PATH,变量值为此JDK Plug的安装路径,后面编译程序时需要用到它。
图A-2 JDK Plug安装协议(原书附图为协议接受界面,此处保留文字说明)
除了要用到JDK Plug外,编译时还需要引用JDK的运行时包,这是编译JDK中用Java代码编写的那部分所需要的,如果仅仅是想编译一个HotSpot虚拟机则可以不用。官方文档把这部分称为Optional Import JDK,可以直接使用前面Bootstrap JDK的运行时包。我们需要建立一个名为 ALT_JDK_IMPORT_PATH 的环境变量指向JDK的安装目录。
然后,安装一个大于2.3版的FreeType[3],这是一个免费的字体渲染库,JDK的Swing部分和JConsole这类工具会用到它。安装好后建立两个环境变量 ALT_FREETYPE_LIB_PATH 和 ALT_FREETYPE_HEADERS_PATH,分别指向FreeType安装目录下的bin目录和include目录。另外还有一点是官方文档没有提到但必须要做的事情,那就是把FreeType的bin目录加入PATH环境变量中。
接着,下载Microsoft DirectX 9.0 SDK(Summer 2004),安装后大约有298MB,在微软官方网站上搜索一下就可以找到下载地址,它是免费的。安装后建立环境变量 ALT_DXSDK_PATH 指向DirectX。
[1] CYGWIN:一个在Windows平台下模拟Linux运行环境的软件,提供了一系列Linux命令支持。
[2] JDK Plug:OpenJDK中少量无法开源的产权代码的闭源包。
[3] FreeType:一个免费的字体渲染库。
附录A 编译OpenJDK
A.5 进行编译
现在需要下载的编译环境和依赖项目都准备齐全了,最后我们还需要对系统做一些设置以便编译能够顺利通过。
首先执行VS2010中的VCVARS32.BAT,这个批处理文件的目的主要是设置INCLUDE、LIB和PATH这几个环境变量,如果和笔者一样只是下载了编译器,则需要手工设置它们。各个环境变量的设置值可以参考下面给出的代码清单A-1中的内容。批处理运行完之后建立ALT_COMPILER_PATH环境变量,让Makefile知道在哪里可以找到编译器。
再建立ALT_BOOTDIR和ALT_JDK_IMPORT_PATH两个环境变量指向前面提到的JDK 1.6的安装目录。建立ANT_HOME指向Apache ANT的安装目录。建立的环境变量很多,为了避免遗漏,笔者写了一个批处理文件以供读者参考,如代码清单A-1所示。
TIP
环境变量的设置是编译前的关键步骤,请务必仔细核对路径。
代码清单A-1 环境变量设置
SET ALT_BOOTDIR=D:/_DevSpace/JDK 1.6.0_21
SET ALT_BINARY_PLUGS_PATH=D:/jdkBuild/jdk7plug/openjdk-binary-plugs
SET ALT_JDK_IMPORT_PATH=D:/_DevSpace/JDK 1.6.0_21
SET ANT_HOME=D:/jdkBuild/apache-ant-1.7.0
SET ALT_MSVCRNN_DLL_PATH=D:/jdkBuild/msvcr100
SET ALT_DXSDK_PATH=D:/jdkBuild/msdxsdk
SET ALT_COMPILER_PATH=D:/jdkBuild/vcpp2010.x86/bin
SET ALT_FREETYPE_HEADERS_PATH=D:/jdkBuild/freetype-2.3.5-1-bin/include
SET ALT_FREETYPE_LIB_PATH=D:/jdkBuild/freetype-2.3.5-1-bin/bin
SET INCLUDE=D:/jdkBuild/vcpp2010.x86/include;D:/jdkBuild/vcpp2010.x86/sdk/Include;%INCLUDE%
SET LIB=D:/jdkBuild/vcpp2010.x86/lib;D:/jdkBuild/vcpp2010.x86/sdk/Lib;%LIB%
SET LIBPATH=D:/jdkBuild/vcpp2010.x86/lib;%LIB%
SET PATH=D:/jdkBuild/vcpp2010.x86/bin;D:/jdkBuild/vcpp2010.x86/dll/x86;D:/Software/OpenSource/cygwin/bin最后还需要进行两项调整,官方文档没有说明这两项,但是必须要做完才能保证编译过程的顺利完成:一是取消环境变量JAVA_HOME,这点很简单;另外一项是尽量在英文的操作系统上编译。估计大部分读者会感到比较为难吧?如果不能在英文的系统上编译就把系统的文字格式调整为“英语(美国)”,在控制面板-区域和语言选项的第一个页签中可以设置。如果这个设置还不能更改就建立一个BUILD_CORBA环境变量,将其值设置为false,取消编译CORBA部分。否则Java IDL(idlj.exe)为*.idl文件生成CORBA适配器代码的时候会产生中文注释,而这些中文注释会因为字符集的问题而导致编译失败。
WARNING
中文系统可能导致CORBA编译失败,建议使用英文系统或设置系统区域为英语(美国)。
完成了上述烦琐的准备工作之后,我们终于可以开始编译了。进入控制台(Cmd.exe)后运行刚才准备好的设置环境变量的批处理文件,然后输入bash进入Bourne Again Shell环境(习惯sh或ksh的读者请自便)。如果JDK的安装源码中存在jdk_generic_profile.sh这个Shell脚本,先执行它,笔者下载的OpenJDK 7 B121版没有这个文件了,所以直接输入make sanity来检查我们前面所做的设置是否全部正确。如果一切顺利,几秒钟之后会有类似代码清单A-2所示的输出。
代码清单A-2 make sanity检查
D:\jdkBuild\openjdk7>bash
bash-3.2$ make sanity
cygwin warning:
MS-DOS style path detected: C:/Windows/system32/wscript.exe
Preferred POSIX equivalent is: /cygdrive/c/Windows/system32/wscript.exe
CYGWIN environment variable option "nodosfilewarning" turns off this warning.
Consult the user's guide for more details about POSIX paths:
http://cygwin.com/cygwin-ug-net/using.html#using-pathnames
( cd ./jdk/make && \
......因篇幅关系,中间省略了大量的输出内容......
OpenJDK-specific settings:
FREETYPE_HEADERS_PATH = D:/jdkBuild/freetype-2.3.5-1-bin/include
ALT_FREETYPE_HEADERS_PATH = D:/jdkBuild/freetype-2.3.5-1-bin/include
FREETYPE_LIB_PATH = D:/jdkBuild/freetype-2.3.5-1-bin/bin
ALT_FREETYPE_LIB_PATH = D:/jdkBuild/freetype-2.3.5-1-bin/bin
OPENJDK Import Binary Plug Settings:
IMPORT_BINARY_PLUGS = true
BINARY_PLUGS_JARFILE = D:/jdkBuild/jdk7plug/openjdk-binary-plugs/jre/lib/rt-closed.jar
ALT_BINARY_PLUGS_JARFILE =
BINARY_PLUGS_PATH = D:/jdkBuild/jdk7plug/openjdk-binary-plugs
ALT_BINARY_PLUGS_PATH = D:/jdkBuild/jdk7plug/openjdk-binary-plugs
BUILD_BINARY_PLUGS_PATH = J:/re/jdk/1.7.0/promoted/latest/openjdk/binaryplugs
ALT_BUILD_BINARY_PLUGS_PATH =
PLUG_LIBRARY_NAMES =
Previous JDK Settings:
PREVIOUS_RELEASE_PATH = USING-PREVIOUS_RELEASE_IMAGE
ALT_PREVIOUS_RELEASE_PATH =
PREVIOUS_JDK_VERSION = 1.6.0
ALT_PREVIOUS_JDK_VERSION =
PREVIOUS_JDK_FILE =
ALT_PREVIOUS_JDK_FILE =
PREVIOUS_JRE_FILE =
ALT_PREVIOUS_JRE_FILE =
PREVIOUS_RELEASE_IMAGE = D:/_DevSpace/JDK 1.6.0_21
ALT_PREVIOUS_RELEASE_IMAGE =
Sanity check passed.Makefile的Sanity检查过程输出了编译所需的所有环境变量,如果看到“Sanity check passed.”则说明检查过程通过了,可以输入“make”执行整个Makefile,然后就去喝个下午茶再回来了。笔者Core i5/4GB RAM的机器编译整个JDK大概需要半个小时的时间。如果失败则需要根据系统输出的失败原因,回头再检查一下对应的设置。最好在下一次编译之前先执行“make clean”来清理掉上次编译遗留的文件。
编译完成之后,打开OpenJDK源码下的build目录,看看是不是已经有一个编译好的JDK在那里等着了?执行一下“java -version”,看到以自己机器命名的JDK了吧?很有成就感吧?
[1] CYGWIN下载地址:http://www.cygwin.com/。 [2] 在2011年,JDK Plug已经不再需要了,但在笔者写本次实战使用的2010年12月9日发布的OpenJDK b121版时还是需要这些JDK Plug的。 [3] FreeType主页:http://www.freetype.org/。
附录B 展望Java技术的未来(2013年版)
本书第1版和2版中的“展望Java技术的未来”分别成文于2011年和2013年,近十年时间已经过去,当时畅想的Java新发展新变化全部如约而至,这部分内容已不再有“展望”的价值。笔者在更新第3版时重写了全部相关内容,并把第2版的“展望”的原文挪到附录之中。假若Java的未来依旧灿烂精彩,假若下一个十年本书还会有第4、第5版,那希望届时能在附录中回首今日,去回溯哪些预测成为现实,哪些改进中途夭折。
在2005年,Java语言诞生10周年的SunOne技术大会上,Java语言之父James Gosling做过题为《Java技术下一个十年》的演讲。笔者不具备James Gosling博士那样高屋建瓴的视角,这里仅从Java平台中几个新生的但已经开始展现出蓬勃之势的技术发展点来看一下后续一至两个JDK版本内的一些很有希望的技术重点。
B.1 模块化
模块化是解决应用系统与技术平台越来越复杂、越来越庞大的一个重要途径。无论是开发人员还是产品最终用户,都不希望为了系统中一小块的功能而不得不下载、安装、部署及维护整套庞大的系统。站在整个软件工业化的高度来看,模块化是建立各种功能标准件的前提。最近几年OSGi技术的迅速发展、各个厂商在JCP中对模块化规范的激烈斗争[1],都能充分说明模块化技术的迫切和重要。
在未来的Java平台中,很可能会对模块化提出语法层面的支持。早在2007年,Sun公司就提出过JSR-277:Java模块系统(Java Module System),试图建立Java平台的模块化标准,但受挫于以IBM为主导的提交的JSR-291:Java SE动态组件支持(Dynamic Component Support for Java SE,实际就是OSGi R4.1)。由于模块化规范主导权的重要性,Sun不能接受一个无法由它控制的规范,在整个Java SE 6期间都拒绝把任何模块化技术内置到JDK之中。在Java SE 7发展初期,Sun公司再次提交了一个新的规范请求文档JSR-294:Java编程语言中的改进模块性支持(Improved Modularity Support in the Java Programming Language),尽管这个JSR仍然没有通过,但是Sun已经独立于JCP专家组在OpenJDK里建立了一个名为Jigsaw(拼图)的子项目来将这个规范在Java平台中转变为具体的实现。Java的模块化之争目前还没有结束,OSGi已经发布到R5.0版本,而Jigsaw从Java 7延迟至Java 8,在2012年7月又不得不宣布推迟到Java 9中发布,从这一点看来,Sun在这场战争中处于劣势,但无论胜利者是哪一方,Java模块化已经成为一股无法阻挡的变革潮流。
B.2 混合语言
当单一的Java开发已经无法满足当前软件复杂的需求时,越来越多基于Java虚拟机的开发语言被应用到软件项目中,Java平台上的多语言混合编程正成为主流,每种语言都可以针对自己擅长的方面更好地解决问题。试想一下:在一个项目之中,并行处理用Clojure语言编写展示层使用JRuby/Rails,中间层则是Java,每个应用层都使用不同的编程语言来完成,而且,接口对每一层的开发者都是透明的,各种语言之间的交互不存在任何困难,就像使用自己语言的原生API一样方便[2],因为他们最终都运行在一个虚拟机之上。
在最近的几年里,Clojure、JRuby、Groovy等新生语言的使用人数如同滚动的雪球一般增长,而运行在Java虚拟机之上的语言数量也在迅速膨胀,图B-1中列举了其中的一部分。这两点证明混合编程在我们身边已经有所应用并被广泛认可。通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。
除了催生大量的新语言外,许多已经有很长历史的程序语言也出现了基于Java虚拟机实现的版本,这样混合编程对许多以前使用其他语言的“老”程序员也具备相当大的吸引力,软件企业投入了大量资本的现有代码资产也能被很好地保护起来。表B-1中列举了常见语言的Java虚拟机实现版本。
图B-1 可以运行在JVM之上的语言[3]
(原图列举了多种语言的Logo,此处以文字表述原图中的语言列表:Java、Groovy、JRuby、Jython、Scala、Clojure、JavaScript(Nashorn/Rhino)、JRuby、Jython、BeanShell、Kotlin、Ceylon、Xtend、Fantom、Jaskell、等等。具体列表请参考原书插图。)
表B-1 常见语言的JVM实现版本
语言 JVM实现版本 JavaScript Nashorn(JDK 8内置)、Rhino Python Jython Ruby JRuby Groovy Groovy(原生运行于JVM) Scala Scala(原生运行于JVM) Clojure Clojure(原生运行于JVM) Java Java(标准JDK) Kotlin Kotlin(JetBrains开发) Ceylon Ceylon(Red Hat开发) … … (完整表格请参考原书,此处仅列举典型示例。)
对这些运行于Java虚拟机之上、Java之外的语言,来自系统级的、底层的支持正在迅速增强,以JSR-292为核心的一系列项目和功能改进(如Da Vinci Machine项目、Nashorn引擎、InvokeDynamic指令、java.lang.invoke包等),推动Java虚拟机从“Java语言的虚拟机”向“多语言虚拟机”的方向发展。
B.3 多核并行
如今,CPU硬件的发展方向已经从高频率转变为多核心,随着多核时代的来临,软件开发越来越关注并行编程的领域。早在JDK 1.5之中就已经引入java.util.concurrent包,实现了一个粗粒度的并发框架。而JDK 1.7中加入的java.util.concurrent.forkjoin包则是对这个框架的一次重要扩充。Fork/Join模式是处理并行编程的一个经典方法,如图B-2所示。虽然不能解决所有的问题,但是在它的适用范围之内,能够轻松地利用多个CPU核心提供的计算资源来协作完成一个复杂的计算任务。通过利用Fork/Join模式,我们能够更加顺畅地过渡到多核时代。
图B-2 Fork/Join模式示意图[4]
(原图展示了Fork/Join的分解与合并过程:一个大任务(Task)被递归拆分为多个子任务(Subtask),每个子任务独立执行,然后结果被合并(Join)为最终结果。常见实现如Java的ForkJoinPool。具体图形请参考原书。)
在Java 8中,将会提供Lambda支持,将会极大改善目前Java语言不适合函数式编程的现状(目前Java语言使用函数式编程并不是不可以,只是会显得很臃肿),函数式编程的一个重要优点就是这样的程序天然地适合并行运行,这样对Java语言在多核时代继续保持主流语言的地位有很大帮助。
另外并行计算中必须提及的还有OpenJDK的子项目Sumatra[5],目前显卡的算术运算能力、并行能力已经远远超过了CPU,在图形领域以外发掘显卡的潜力是近几年计算机发展的方向之一,例如C语言的CUDA。Sumatra项目就是为Java提供使用GPU(Graphics Processing Unit)和APU(Accelerated Processing Unit)运算能力的工具,以后它将会直接提供Java语言层面的API,或者为Lambda和其他JVM语言提供底层的并行运算支持。
在JDK外围,也出现了专为实现并行计算需求的计算框架,如Apache的Hadoop Map/Reduce,这是一个简单易懂的并行框架,能够运行在由上千个商用机器组成的大型集群上,并能以一种可靠的容错方式并行处理TB级别的数据集。另外,还出现了诸如Scala、Clojure及Erlang等天生就具备并行计算能力的语言。
B.4 进一步丰富语法
Java 5曾经对Java语法进行了一次扩充,这次扩充加入了自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环等语法,使得Java语言的精确性和易用性有了很大的进步。在Java 7(由于进度压力,许多改进已被推迟至Java 8)中,对Java语法进行了另一次大规模的扩充。Sun(Oracle)专门为改进Java语法在OpenJDK中建立了Coin子项目[6]来统一处理Java语法的细节修改,如对二进制数的原生支持、在switch语句中支持字符串、“<>”操作符、异常处理的改进、简化变长参数方法调用、面向资源的try-catch-finally语句等都是在Coin项目之中提交的内容。
除了Coin项目之外,JSR-335(Lambda Expressions for the JavaTM Programming Language)中定义的Lambda表达式[7],也将对Java的语法和语言习惯产生很大的影响,面向函数方式的编程可能会成为主流。
B.5 64位虚拟机
几年之前,主流的CPU就开始支持64位架构。Java虚拟机也在很早之前就推出了支持64位系统的版本。但Java程序运行在64位虚拟机上需要付出比较大的额外代价:首先是内存问题,由于指针膨胀和各种数据类型对齐补白的原因,运行于64位系统上的Java应用需要消耗更多的内存,通常要比32位系统额外增加10%~30%的内存消耗;其次是多个机构的测试结果显示,64位虚拟机的运行速度在各个测试项上几乎都全面落后于32位虚拟机,两者大约有15%的性能差距。
但是在Java EE方面,企业级应用经常需要使用超过4GB的内存,对于64位虚拟机的需求是非常迫切的,但由于上述的原因,许多企业应用都仍然选择使用虚拟集群等方式继续在32位虚拟机中进行部署。Sun也注意到了这些问题,并做出了一些改善,在JDK 1.6 Update 14之后,提供了普通对象指针压缩功能(-XX:+UseCompressedOops,这个参数不建议显式设置,建议维持默认由虚拟机的Ergonomics机制自动开启),在执行代码时,动态植入压缩指令以节省内存消耗。但是开启压缩指针会增加执行代码数量,因为所有在Java堆里的、指向Java堆内对象的指针都会被压缩,这些指针的访问就需要更多的代码才可以实现,而且并不仅只是读写字段才受影响,在实例方法调用、子类型检查等操作中也受影响,因为对象实例指向对象类型的引用也被压缩了。随着硬件的进一步发展,计算机终究会完全过渡到64位的时代,这是一件毫无疑问的事情,主流的虚拟机应用也终究会从32位发展至64位,而虚拟机对64位的支持也将会进一步完善。
[1] 如果读者对Java模块化之争感兴趣,可以阅读作者的另外一本书《深入理解OSGi》的第1章。 [2] 在同一个虚拟机上跑的其他语言与Java之间的交互一般都比较容易,但非Java语言之间的交互一般都比较烦琐。dynalang项目(http://dynalang.sourceforge.net/)就是为了解决这个问题而出现的。 [3] 图片来源:http://wikis.sun.com/download/attachments/16418319/OOW-2009+Towards+A+Universal+VM.pdf。 [4] 图片来源:http://www.ibm.com/developerworks/cn/java/j-lo-forkjoin/。 [5] Sumatra项目:http://openjdk.java.net/projects/sumatra/。 [6] Coin项目主页:https://openjdk.java.net/projects/coin。 [7] Lambda项目主页:http://openjdk.java.net/projects/lambda/。
附录C 虚拟机字节码指令表
(此附录为虚拟机字节码指令的完整表格,由于篇幅较长,此处仅列出结构说明。具体指令表请参考原书或JDK官方文档。表格通常包含指令码、助记符、操作数、功能描述等列。)
附录D 对象查询语言(OQL)简介[1]
INFO
OQL(Object Query Language)是用于查询Java堆转储快照的类SQL语言,广泛应用于MAT(Memory Analyzer Tool)等工具中。
D.1 SELECT子句
SELECT子句用于确定查询语句需要从堆转储快照中选择什么内容。如果需要显示堆转储快照中的对象,并且浏览这些对象的引用关系,可以使用“*”,这与传统SQL语句中的习惯是一致的,如:
SELECT * FROM java.lang.String1. 选择特定的显示列
查询也可以选择特定的需要显示的字段,如:
SELECT toString(s), s.count, s.value FROM java.lang.String s查询可以通过“@”符号来使用Java对象的内存属性访问器。MAT提供了一系列的内置函数来获取与分析相关的信息,如:
SELECT toString(s), s.@usedHeapSize, s.@retainedHeapSize FROM java.lang.String s关于对象属性访问器的具体内容,可以参见下文的“属性访问器”。
2. 使用列别名
可以使用AS关键字来对选择的列进行命名,如:
SELECT toString(s) AS Value,
s.@usedHeapSize AS "Shallow Size",
s.@retainedHeapSize AS "Retained Size"
FROM java.lang.String s可以使用AS RETAINED SET关键字来获得与选择对象相关联的对象集合,如:
SELECT AS RETAINED SET * FROM java.lang.String3. 拼合成为一个对象列表选择项目
可以使用OBJECTS关键字把SELECT子句中查找出来的数据项目转换为对象,如:
SELECT OBJECTS dominators(s) FROM java.lang.String s上面例子中,dominators()函数将会返回一个对象数组,所以如果没有OBJECTS关键字,上面的查询将返回一组二维的对象数组的列表。通过使用关键字OBJECTS,迫使OQL把查询结果缩减为一维的对象列表。
4. 排除重复对象
使用DISTINCT关键字可以排除结果集中的重复对象,如:
SELECT DISTINCT classof(s) FROM java.lang.String s上面的例子中,classof()函数的作用是返回对象所属的Java类,当然,所有字符串对象的所属类都是java.lang.String,所以如果上面的查询中没有加入DISTINCT关键字,查询结果就会返回与快照中的字符串数量一样多的行记录,并且每行记录的内容都是java.lang.String类型。
D.2 FROM子句
1. FROM子句指定需要查询的类
OQL查询需要在FROM子句定义的查询范围内进行操作。FROM子句可以接受的查询范围有下列几种描述方式:
(1)通过类名进行查询,如:
SELECT * FROM java.lang.String(2)通过正则表达式匹配一组类名进行查询,如:
SELECT * FROM "java\.lang\..*"(3)通过类对象在堆转储快照中的地址进行查询,如:
SELECT * FROM 0xe14a100(4)通过对象在堆转储快照中的ID进行查询,如:
SELECT * FROM 3022(5)在子查询中的结果集中进行查询,如:
SELECT * FROM (SELECT * FROM java.lang.Class c WHERE c implements org.eclipse.mat.snapshot.model.IClass)上面的查询返回堆转储快照中所有实现了org.eclipse.mat.snapshot.model.IClass接口的类。下面的这句查询语句使用属性访问器达到了同样的效果,它直接调用了ISnapshot对象的方法:
SELECT * FROM $snapshot.getClasses()2. 包含子类
使用INSTANCEOF关键字把指定类的子类列入查询结果集之中,如:
SELECT * FROM INSTANCEOF java.lang.ref.Reference这个查询的结果集中将会包含WeakReference、SoftReference和PhantomReference类型的对象,因为它们都继承自java.lang.ref.Reference。下面这句查询语句也有相同的结果:
SELECT * FROM $snapshot.getClassesByName("java.lang.ref.Reference", true)3. 禁止查询类实例
在FROM子句中使用OBJECTS关键字可以禁止OQL把查询的范围解释为对象实例,如:
SELECT * FROM OBJECTS java.lang.String这个查询的结果不是返回快照中所有的字符串,而是只有一个对象,也就是与java.lang.String类对应的Class对象。
D.3 WHERE子句
1. 关系操作(>=,⇐,>,<,[NOT] LIKE,[NOT] IN)
WHERE子句用于指定搜索的条件,即从查询结果中删除不需要的数据,如:
SELECT * FROM java.lang.String s WHERE s.count >= 100
SELECT * FROM java.lang.String s WHERE toString(s) LIKE ".*day"
SELECT * FROM java.lang.String s WHERE s.value NOT IN dominators(s)2. 等于操作(=,!=)
SELECT * FROM java.lang.String s WHERE toString(s) = "monday"3. 条件与操作(AND)
SELECT * FROM java.lang.String s WHERE s.count > 100 AND s.@retainedHeapSize > s.@usedHeapSize4. 条件或操作(OR)
“条件或操作”可以应用于表达式、常量文本和子查询之中,如:
SELECT * FROM java.lang.String s WHERE s.count > 1000 OR s.value.@length > 10005. 文字表达式
文字表达式包括了布尔值、字符串、整型、长整型和null,如:
SELECT * FROM java.lang.String s
WHERE ( s.count > 1000 ) = true
WHERE toString(s) = "monday"
WHERE dominators(s).size() = 0
WHERE s.@retainedHeapSize > 1024L
WHERE s.@GCRootInfo != nullD.4 属性访问器
1. 访问堆转储快照中对象的字段
对象的内存属性可以通过传统的“点表示法”进行访问,格式为:
[<alias>.] <field>.<field>.<field>...
2. 访问Java Bean属性
格式为:
[<alias>.] @<attribute> ...
使用@符号,OQL可以访问底层Java对象的内存属性。下表列出了一些常用的Java属性。
(常用属性列表:例如 @usedHeapSize(浅堆大小)、@retainedHeapSize(保留堆大小)、@GCRootInfo(GC根信息)、@objectId(对象ID)、@length(数组长度)等。完整列表请参考MAT文档。)
3. 调用OQL Java方法
格式为:
[<alias>.]@<方法>([<表达式>,<表达式>])...
加“()”会将MAT解释为一个OQL Java方法调用。这个方法的调用是通过反射执行的。常见的OQL Java方法如下:
(常见方法:toString(obj)、classof(obj)、dominators(obj)、outboundReferences(obj)、inboundReferences(obj)等。完整列表请参考MAT文档。)
4. OQL的内建函数
格式为:
<function>(<parameter>)
(常见内建函数:如 toString()、classof()、dominators()、outbounds()、inbounds()、referrers()、size()、length() 等。具体函数列表请参考MAT官方文档。)
D.5 OQL语言的BNF范式
(此部分为OQL语言的BNF(巴科斯范式)语法定义,由于内容较理论且多为形式化描述,此处仅作提示。完整BNF请参考原书或MAT帮助文档。)
[1] 此附录内容基于Eclipse Memory Analyzer(MAT)的OQL实现,详情请参考MAT官方文档。
[1] 本文翻译自Eclipse Memory Analyzer Tool(MAT,Eclipse出品的内存分析工具)的OQL帮助文档。
附录E JDK历史版本轨迹
本附录列出了从Java诞生开始直到2019年9月发布的JDK 13为止的Java全部更新版本、发布日期和关键的更新内容。其中提及的绝大部分的JDK历史版本(JDK 1.1.6之后的版本),以及JDK所附带的 各种工具的历史版本,都可以从Oracle公司的归档网站[1]上下载到。
[1] 下载页面地址:http://www.oracle.com/technetwork/java/archive-139210.html
附录E JDK历史版本轨迹
本附录列出了从Java诞生开始直到2019年9月发布的JDK 13为止的Java全部更新版本、发布日期和关键的更新内容。其中提及的绝大部分的JDK历史版本(JDK 1.1.6之后的版本),以及JDK所附带的各种工具的历史版本,都可以从Oracle公司的归档网站[1]上下载到。
[1] 下载页面地址:http://www.oracle.com/technetwork/java/archive-139210.html