1.4 Java虚拟机家族

上一节我们以JDK版本演进过程为线索,回顾了Java技术的发展历史,体会过其中企业与技术的 成败兴衰,现在,我们将聚焦到本书的主题“Java虚拟机”。许多Java程序员都会潜意识地把Java虚拟机 与OracleJDK的HotSpot虚拟机等同看待,也许还有一些程序员会注意到BEA JRockit和IBM J9虚拟机, 但绝大多数人对Java虚拟机的认识就仅限于此了。从1996年初Sun发布的JDK 1.0中包含的Sun Classic虚 拟机到今天,曾经涌现、湮灭过许多或经典,或优秀,或有特色,或有争议的虚拟机实现,在这一节 中,我们仍先把代码与技术放下,一起来回顾Java虚拟机家族的发展轨迹和历史变迁。

1.4.1 虚拟机始祖:Sun Classic/Exact VM

以今天的视角来看,Sun Classic虚拟机的技术已经相当原始,这款虚拟机的使命也早已终结。但 仅凭它“世界上第一款商用Java虚拟机”的头衔,就足够有令历史记住它的理由。

1996年1月23日,Sun发布JDK 1.0,Java语言首次拥有了商用的正式运行环境,这个JDK中所带的 虚拟机就是Classic VM。这款虚拟机只能使用纯解释器方式来执行Java代码,如果要使用即时编译器那 就必须进行外挂,但是假如外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解 释器便不能再工作了。在JDK 1.2及之前,用户用Classic虚拟机执行java-version命令,将会看到类似下 面这行的输出:

1
2
java version "1.2.2"
Classic VM (build JDK-1.2.2-001, green threads, sunwjit)

其中的“sunwjit”(Sun Workshop JIT)就是Sun提供的外挂编译器,其他类似的外挂编译器还有 Symantec JIT和shuJIT等。由于解释器和编译器不能配合工作,这就意味着如果要使用编译执行,编译 器就不得不对每一个方法、每一行代码都进行编译,而无论它们执行的频率是否具有编译的价值。基 于程序响应时间的压力,这些编译器根本不敢应用编译耗时稍高的优化技术,因此这个阶段的虚拟机 虽然用了即时编译器输出本地代码,其执行效率也和传统的C/C++程序有很大差距,“Java语言很慢”的 印象就是在这阶段开始在用户心中树立起来的。

Sun的虚拟机团队努力去解决Classic虚拟机所面临的各种问题,提升运行效率,在JDK 1.2时,曾 在Solaris平台上发布过一款名为Exact VM的虚拟机,它的编译执行系统已经具备现代高性能虚拟机雏 形,如热点探测、两级即时编译器、编译器与解释器混合工作模式等。

Exact VM因它使用准确式内存管理(Exact Memory Management,也可以叫Non-Con- servative/Accurate Memory Management)而得名。准确式内存管理是指虚拟机可以知道内存中某个位 置的数据具体是什么类型。譬如内存中有一个32bit的整数123456,虚拟机将有能力分辨出它到底是一 个指向了123456的内存地址的引用类型还是一个数值为123456的整数,准确分辨出哪些内存是引用类 型,这也是在垃圾收集时准确判断堆上的数据是否还可能被使用的前提。由于使用了准确式内存管 理,Exact VM可以抛弃掉以前Classic VM基于句柄(Handle)的对象查找方式(原因是垃圾收集后对 象将可能会被移动位置,如果地址为123456的对象移动到654321,在没有明确信息表明内存中哪些数 据是引用类型的前提下,那虚拟机肯定是不敢把内存中所有为123456的值改成654321的,所以要使用 句柄来保持引用值的稳定),这样每次定位对象都少了一次间接查找的开销,显著提升执行性能。

虽然Exact VM的技术相对Classic VM来说先进了许多,但是它的命运显得十分英雄气短,在商业 应用上只存在了很短暂的时间就被外部引进的HotSpot VM所取代,甚至还没有来得及发布Windows和 Linux平台下的商用版本。而Classic VM的生命周期则相对要长不少,它在JDK 1.2之前是JDK中唯一的 虚拟机,在JDK 1.2时,它与HotSpot VM并存,但默认是使用Classic VM(用户可用java-hotspot参数 切换至HotSpot VM),而在JDK 1.3时,HotSpot VM成为默认虚拟机,它仍作为虚拟机的“备用选 择”发布(使用java-classic参数切换),直到JDK 1.4的时候,Classic VM才完全退出商用虚拟机的历史 舞台,与Exact VM一起进入了Sun Labs Research VM之中。

1.3 Java发展史

从Java的第一个版本诞生到现在已经有二十余年的时间,白驹过隙,沧海桑田,转眼已过了四分 之一个世纪,在图1-3所示的时间线里,我们看到JDK的版本已经发展到了JDK 13。这二十多年里诞生 过无数与Java相关的产品、技术与标准。现在让我们走入时间隧道,从孕育Java语言的时代开始,再来 回顾一下Java的发展轨迹和历史变迁。

1991年4月,由James Gosling博士领导的绿色计划(Green Project)开始启动,此计划最初的目标 是开发一种能够在各种消费性电子产品(如机顶盒、冰箱、收音机等)上运行的程序架构。这个计划 的产品就是Java语言的前身:Oak(得名于James Gosling办公室外的一棵橡树)。Oak当时在消费品市 场上并不算成功,但随着1995年互联网潮流的兴起,Oak迅速找到了最适合自己发展的市场定位并蜕 变成为Java语言。

1995年5月23日,Oak语言改名为Java,并且在SunWorld大会上正式发布Java 1.0版本。Java语言第 一次提出了“Write Once,Run Anywhere”的口号。

1996年1月23日,JDK 1.0发布,Java语言有了第一个正式版本的运行环境。JDK 1.0提供了一个纯 解释执行的Java虚拟机实现(Sun Classic VM)。JDK 1.0版本的代表技术包括:Java虚拟机、Applet、 AWT等。

1996年4月,十个最主要的操作系统和计算机供应商声明将在其产品中嵌入Java技术。同年9月, 已有大约8.3万个网页应用了Java技术来制作。在1996年5月底,Sun于美国旧金山举行了首届JavaOne大 会,从此JavaOne成为全世界数百万Java语言开发者每年一度的技术盛会。

1997年2月19日,Sun公司发布了JDK 1.1,Java里许多最基础的技术支撑点(如JDBC等)都是在 JDK 1.1版本中提出的,JDK 1.1版的技术代表有:JAR文件格式、JDBC、JavaBeans、RMI等。Java语 言的语法也有了一定的增强,如内部类(Inner Class)和反射(Reflection)都是在这时候出现的。

直到1999年4月8日,JDK 1.1一共发布了1.1.0至1.1.8这9个版本。从1.1.4以后,每个JDK版本都有 一个属于自己的名字(工程代号),分别为:JDK 1.1.4-Sparkler(宝石)、JDK 1.1.5-Pumpkin(南瓜)、JDK 1.1.6-Abigail(阿比盖尔,女子名)、JDK 1.1.7-Brutus(布鲁图,古罗马政治家和将军) 和JDK 1.1.8-Chelsea(切尔西,城市名)。

1998年12月4日,JDK迎来了一个里程碑式的重要版本:工程代号为Playground(竞技场)的JDK 1.2,Sun在这个版本中把Java技术体系拆分为三个方向,分别是面向桌面应用开发的J2SE(Java 2 Platform,Standard Edition)、面向企业级开发的J2EE(Java 2 Platform,Enterprise Edition)和面向手 机等移动终端开发的J2ME(Java 2 Platform,Micro Edition)。在这个版本中出现的代表性技术非常 多,如EJB、Java Plug-in、Java IDL、Swing等,并且这个版本中Java虚拟机第一次内置了JIT(Just In Time)即时编译器(JDK 1.2中曾并存过三个虚拟机,Classic VM、HotSpot VM和Exact VM,其中 Exact VM只在Solaris平台出现过;后面两款虚拟机都是内置了JIT即时编译器的,而之前版本所带的 Classic VM只能以外挂的形式使用即时编译器)。在语言和API层面上,Java添加了strictfp关键字, Java类库添加了现在Java编码之中极为常用的一系列Collections集合类等。在1999年3月和7月,分别有 JDK 1.2.1和JDK 1.2.2两个小升级版本发布。

1999年4月27日,HotSpot虚拟机诞生。HotSpot最初由一家名为“Longview Techno-logies”的小公司 开发,由于HotSpot的优异表现,这家公司在1997年被Sun公司收购。Hot-Spot虚拟机刚发布时是作为 JDK 1.2的附加程序提供的,后来它成为JDK 1.3及之后所有JDK版本的默认Java虚拟机。

2000年5月8日,工程代号为Kestrel(美洲红隼)的JDK 1.3发布。相对于JDK 1.2,JDK 1.3的改进 主要体现在Java类库上(如数学运算和新的Timer API等),JNDI服务从JDK 1.3开始被作为一项平台 级服务提供(以前JNDI仅仅是一项扩展服务),使用CORBA IIOP来实现RMI的通信协议,等等。这 个版本还对Java 2D做了很多改进,提供了大量新的Java 2D API,并且新添加了JavaSound类库。JDK 1.3有1个修正版本JDK 1.3.1,工程代号为Ladybird(瓢虫),于2001年5月17日发布。

自从JDK 1.3开始,Sun公司维持着稳定的研发节奏:大约每隔两年发布一个JDK的主版本,以动 物命名,期间发布的各个修正版本则以昆虫作为工程代号。

2002年2月13日,JDK 1.4发布,工程代号为Merlin(灰背隼)。JDK 1.4是标志着Java真正走向成 熟的一个版本,Compaq、Fujitsu、SAS、Symbian、IBM等著名公司都有参与功能规划,甚至实现自己 独立发行的JDK 1.4。哪怕是在近二十年后的今天,仍然有一些主流应用能直接运行在JDK 1.4之上, 或者继续发布能运行在1.4上的版本。JDK 1.4同样带来了很多新的技术特性,如正则表达式、异常 链、NIO、日志类、XML解析器和XSLT转换器,等等。JDK 1.4有两个后续修正版:2002年9月16日发 布的工程代号为Grasshopper(蚱蜢)的JDK 1.4.1与2003年6月26日发布的工程代号为Mantis(螳螂) 的JDK 1.4.2。

2002年前后还发生了一件与Java没有直接关系,但事实上对Java的发展进程影响很大的事件,就是 微软的.NET Framework发布。这个无论是技术实现还是目标用户上都与Java有很多相近之处的技术平 台给Java带来了很多讨论、比较与竞争,.NET平台和Java平台之间声势浩大的孰优孰劣的论战到今天 为止都仍然没有完全平息。

2004年9月30日,JDK 5发布,工程代号为Tiger(老虎)。Sun公司从这个版本开始放弃了谦逊 的“JDK 1.x”的命名方式,将产品版本号修改成了“JDK x”[^1]。从JDK 1.2以来,Java在语法层面上的变 动一直很小,而JDK 5在Java语法易用性上做出了非常大的改进。如:自动装箱、泛型、动态注解、枚 举、可变长参数、遍历循环(foreach循环)等语法特性都是在JDK 5中加入的。在虚拟机和API层面上,这个版本改进了Java的内存模型(Java Memory Model,JMM)、提供了java.util.concurrent并发包 等。另外,JDK 5是官方声明可以支持Windows 9x操作系统的最后一个JDK版本。

2006年12月11日,JDK 6发布,工程代号为Mustang(野马)。在这个版本中,Sun公司终结了从 JDK 1.2开始已经有八年历史的J2EE、J2SE、J2ME的产品线命名方式,启用Java EE 6、Java SE 6、Java ME 6的新命名来代替。JDK 6的改进包括:提供初步的动态语言支持(通过内置Mozilla JavaScript Rhino引擎实现)、提供编译期注解处理器和微型HTTP服务器API,等等。同时,这个版本对Java虚拟 机内部做了大量改进,包括锁与同步、垃圾收集、类加载等方面的实现都有相当多的改动。

在2006年11月13日的JavaOne大会上,Sun公司宣布计划要把Java开源,在随后的一年多时间内, 它陆续地将JDK的各个部分在GPLv2(GNU General Public License v2)协议下公开了源码,并建立了 OpenJDK组织对这些源码进行独立管理。除了极少量的产权代码(Encumbered Code,这部分代码所有 权不属于Sun公司,Sun本身也无权进行开源处理)外,OpenJDK几乎拥有了当时SunJDK 7的全部代 码,OpenJDK的质量主管曾经表示在JDK 7中,SunJDK和OpenJDK除了代码文件头的版权注释之外, 代码几乎是完全一样的,所以OpenJDK 7与SunJDK 7本质上就是同一套代码库出来的产品。

JDK 6发布以后,由于代码复杂性的增加、Java开源、开发JavaFX、世界经济危机及Oracle对Sun 的收购案等原因,Sun公司在发展Java以外的事情上耗费了太多精力和资源,JDK的更新没有能够继续 维持两年发布一个主版本的研发速度,这导致了JDK 6的生命周期异常的长,一共发布了211个更新升 级补丁,最后的版本为Java SE 6 Update 211,于2018年10月18日发布。

2009年2月19日,工程代号为Dolphin(海豚)的JDK 7完成了其第一个里程碑版本。按照JDK 7最 初的功能规划,一共会设置十个里程碑。最后一个里程碑版本原计划定于2010年9月9日结束,但由于 各种原因,JDK 7最终无法按计划完成。

从JDK 7最原始的功能清单来看,它本应是一个包含许多重要改进的JDK版本,其中规划的子项 目都为Java业界翘首以盼,包括:

  • Lambda项目:支持Lambda表达式,支持函数式编程。
  • Jigsaw项目:虚拟机层面的模块化支持。
  • 动态语言支持:Java是静态语言,为其他运行在Java虚拟机上的动态语言提供支持。
  • Garbage-First收集器。
  • Coin项目:Java语法细节进化。

令人惋惜的是,在JDK 7开发期间,Sun公司相继在技术竞争和商业竞争中陷入泥潭,公司的股票 市值跌至仅有高峰时期的3%,已无力推动JDK 7的研发工作按计划继续进行。为了尽快结束JDK 7长 期跳票的问题,Oracle收购Sun公司后随即宣布马上实行“B计划”,大幅裁剪了JDK 7预定目标,以保证 JDK 7的正式版能够于2011年7月28日准时发布。“B计划”的主要措施是把不能按时完成的Lambda项 目、Jigsaw项目和Coin项目的部分改进延迟到JDK 8之中。最终,JDK 7包含的改进有:提供新的G1收 集器(G1在发布时依然处于Experimental状态,直至2012年4月的Update 4中才正式商用)、加强对非 Java语言的调用支持(JSR-292,这项特性在到JDK 11还有改动)、可并行的类加载架构等。

Oracle公司接手了JDK开发工作以后,迅速展现出了完全不同于Sun时期的、极具商业化的处事风 格。面对Java中使用最广泛而又一直免费的Java SE产品线,Oracle很快定义了一套新的Java SE Support[^2]产品计划,把JDK的更新支持作为一项商业服务。JDK 7发布的前80个更新仍然免费面向所 有用户提供,但后续的其他更新包,用户[^3]只能从“将Java SE升级到Java SE Support”与“将JDK 7升级 到最新版本”两个选项里挑一个。JDK 7计划维护至2022年,迄今(面向付费用户)已发布了超过两百 个更新补丁,最新版本为JDK 7 Update 221。

对于JDK 7,还有一点值得提起的是,从JDK 7 Update 4起,Java SE的核心功能正式开始为Mac OS X操作系统提供支持,并在JDK 7 Update 6中达到所有功能与Mac OS X完全兼容的程度;同时, JDK 7 Update 6还对ARM指令集架构提供了支持。至此,官方提供的JDK可以运行于Windows(不含 Windows 9x)、Linux、Solaris和Mac OS X操作系统上,支持ARM、x86、x86-64和SPARC指令集架 构,JDK 7也是可以支持Windows XP操作系统的最后一个版本[^4]。

2009年4月20日,Oracle宣布正式以74亿美元的价格收购市值曾超过2000亿美元的Sun公司,传奇 的Sun Microsystems从此落幕成为历史,Java商标正式划归Oracle所有(Java语言本身并不属于哪间公 司所有,它由JCP组织进行管理,尽管在JCP中Sun及后来的Oracle的话语权很大)。由于此前Oracle已 经收购了另外一家大型的中间件企业BEA公司,当完成对Sun公司的收购之后,Oracle分别从BEA和 Sun手中取得了世界三大商用虚拟机的其中两个:JRockit和HotSpot。当时Oracle宣布要在未来一至两 年的时间内,把这两个优秀的Java虚拟机合二为一[^5]。两者合并的结果只能说差强人意,JRockit的监 控工具Java Mission Control被移植到了HotSpot,作为收费功能提供给购买了Java SE Advanced产品计划 的用户,其他功能由于两者架构的差异性明显,HotSpot能够直接借鉴融合的功能寥寥无几[^6]。

JDK 8的第一个正式版本原定于2013年9月发布,最终还是跳票到了2014年3月18日,尽管仍然是 没有赶上正点,但比起JDK 7那种以年作为计时单位、直接把公司跳崩的研发状况已是大有改善。为 了保证日后JDK研发能更顺利地进行,从JDK 8开始,Oracle启用JEP(JDK Enhancement Proposals)来 定义和管理纳入新版JDK发布范围的功能特性。JDK 8提供了那些曾在JDK 7中规划过,但最终未能在 JDK 7中完成的功能,主要包括:

  • JEP 126:对Lambda表达式的支持,这让Java语言拥有了流畅的函数式表达能力。
  • JEP 104:内置Nashorn JavaScript引擎的支持。
  • JEP 150:新的时间、日期API。
  • JEP 122:彻底移除HotSpot的永久代。
  • ……

“B计划”中原本说好的会在JDK 8提供的Jigsaw模块化功能再次被延期到了JDK 9,不得不说,即 使放到整个Java发展史里看,Jigsaw都能算是天字第一号的大坑。Java的模块化系统本身面临的技术挑 战就很艰巨,从微软的DLL技术开始,到Java自己的JAR,再到.NET的Assembly,工程庞大起来都无 一例外会陷入“模块地狱”[^7]的困境之中,而Jigsaw面临的更大困难是厂商之间以标准话语权为目的, 以技术为“找茬”手段的激烈竞争。

原本JDK 9是计划在2016年发布的,但在2016年伊始,Oracle就宣布JDK 9肯定要延期至2017年, 后来又连续经过了两次短时间的跳票,最终到2017年9月21日才得以艰难面世。后两次跳票的原因是以 IBM和RedHat为首[^8]的十三家企业在JCP执行委员会上联手否决了Oracle提出的Jigsaw作为Java模块化 规范进入JDK 9发布范围的提案[^9]。凭良心说,Java确实有模块化的刚需,不论是JDK自身(例如拆分 出Java SE Embedded这样规模较小的产品)抑或是Java应用都需要用到模块化。这方面IBM本身就是各 大Java发行厂商中做得最好的,它不仅让自家的JDK实现了高度模块化,还带头成立了OSGi联盟,制 订了Java框架层面模块化的事实标准,所以它当然会想把OSGi推到Java规范里去争个“名份”,而不是 被Jigsaw革掉“性命”。可是Oracle对此没有丝毫退让,不惜向JCP发去公开信[^10],直言如果提案最后无 法通过,那Oracle将摒弃JSR专家组,独立发展带Jigsaw的Java版本,Java顿时面临如Python 2与Python 3那般分裂的危机。

不论如何,经过前后六轮投票,经历桌上桌下的斗争与妥协,Java没有分裂,JDK 9总算是带着 Jigsaw最终发布了,除了Jigsaw外,JDK 9还增强了若干工具(JS Shell、JLink、JHSDB等),整顿了 HotSpot各个模块各自为战的日志系统,支持HTTP 2客户单API等91个JEP。

JDK 9发布后,Oracle随即宣布Java将会以持续交付的形式和更加敏捷的研发节奏向前推进,以后 JDK将会在每年的3月和9月各发布一个大版本[^11],目的就是为避免众多功能特性被集中捆绑到一个 JDK版本上而引发交付风险。这次改革确实从根源上解决了跳票问题,但也为Java的用户和发行商带 来了颇大的压力,不仅程序员感慨“Java新版本还没开始用就已经过时了”,Oracle自己对着一堆JDK版 本分支也在挠头,不知道该如何维护更新,该如何提供技术支持。Oracle的解决方案是顺理成章地终 结掉“每个JDK版本最少维护三年”的优良传统,从此以后,每六个JDK大版本中才会被划出一个长期 支持(Long Term Support,LTS)版,只有LTS版的JDK能够获得为期三年的支持和更新,普通版的 JDK就只有短短六个月的生命周期。JDK 8和JDK 11会是LTS版,再下一个就到2021年发布的JDK 17 了。

2018年3月20日,JDK 10如期发布,这版本的主要研发目标是内部重构,诸如统一源仓库、统一 垃圾收集器接口、统一即时编译器接口(JVMCI在JDK 9已经有了,这里是引入新的Graal即时编译 器)等,这些都将会是对未来Java发展大有裨益的改进,但对普通用户来说JDK 10的新特性就显得乏 善可陈,毕竟它只包含了12个JEP,而且其中只有本地类型推断这一个编码端可见的改进。尽管JDK 10可见的改进有限,但2018这一年Java圈丝毫不缺乏谈资,相继发生了几件与“金钱”相关的历史性大 事件。

首先是2018年3月27日,Android的Java侵权案有了最终判决,法庭裁定Google赔偿Oracle合计88亿 美元,要知道2009年Oracle收购Sun也就只花了74亿,收购完成后随即就用Sun的专利把Google告上了法 庭,经过Oracle法务部的几轮神操作,一场官司的赔偿让收购Sun公司等同免费。对此事Java技术圈多 数吃瓜群众是站在Google这边的,认为Oracle这样做是自绝Java的发展前景,毕竟当年Android刚刚起步 的时候可是Sun向Google抛去的橄榄枝,Android的流行也巩固了Java“第一编程语言”的行业地位。摒弃 对企业的好恶情感,就事论事,Google采用Java的语法和API类库,开发出来的程序却不能运行在其他 Java虚拟机之上,这事情无论怎样都是有违Java技术的精神原旨的,也肯定违反了Java的使用协议[^12]。如果说Oracle控告Google“不厚道”,那当年微软用J++做了同样的事情(借用语法和API,但程序 不兼容标准Java虚拟机),被Sun告到登报道歉,一边赔款一边割地,声明放弃J++语言和Windows平 台上的内置虚拟机,这又该找谁说理去?

按常理说Java刚给Oracle赚了88亿美金,该颇为受宠才对,可Oracle是典型只谈利益不讲情怀的公 司,InfoWorld披露的一封Oracle高管邮件表明[^13],Java体系中被认为无法盈利也没有太多战略前景的 部分会逐渐被“按计划报废”(Planned Obsolescence)。这事的第一刀落下是在2018年3月,Oracle正式 宣告Java EE成为历史名词。虽然Java SE、Java EE和Java ME三条产品线里确实只有Java SE称得上成 功,但Java EE毕竟无比辉煌过,现在其中还持有着JDBC、JMS、Servlet等使用极为广泛的基础组件, 然而Oracle仍选择把它“扫地出门”,所有权直接赠送给Eclipse基金会,唯一的条件是以后不准再使 用“Java”这个商标[^14],所以取而代之的将是Jakarta EE。

2018年10月,JavaOne 2018在旧金山举行,此前没有人想过这会是最后一届JavaOne大会,这个在 1996年伴随着Java一同诞生、成长的开发者年度盛会,竟是Oracle下一个裁撤的对象[^15],此外还有 Java Mission Control的开发团队,也在2018年6月被Oracle解散。

2018年9月25日,JDK 11发布,这是一个LTS版本的JDK,包含17个JEP,其中有ZGC这样的革命 性的垃圾收集器出现,也有把JDK 10中的类型推断加入Lambda语法这种可见的改进,但都比不过它发 布时爆出来的谣言轰动:“Java要开始收费啦!”

随着JDK 11发布,Oracle同时调整了JDK的授权许可证,里面包含了好几个动作。首先,Oracle从 JDK 11起把以前的商业特性[^16]全部开源给OpenJDK,这样OpenJDK 11和OracleJDK 11的代码和功 能,在本质上就是完全相同的(官方原文是Essentially Identical)[^17]。然后,Oracle宣布以后将会同时 发行两个JDK:一个是以GPLv2+CE协议下由Oracle发行的OpenJDK(本书后面章节称其为Oracle OpenJDK),另一个是在新的OTN协议下发行的传统的OracleJDK,这两个JDK共享绝大部分源码, 在功能上是几乎一样的[^18],核心差异是前者可以免费在开发、测试或生产环境中使用,但是只有半 年时间的更新支持;后者个人依然可以免费使用,但若在生产环境中商用就必须付费,可以有三年时 间的更新支持。如果说由此能得出“Java要收费”的结论,那是纯属标题党,最多只能说Oracle在迫使商 业用户要么不断升级JDK的版本,要么就去购买商业支持[^19]。

2019年2月,在JDK 12发布前夕,Oracle果然如之前宣布那样在六个月之后就放弃了对上一个版本 OpenJDK的维护,RedHat同时从Oracle手上接过OpenJDK 8和OpenJDK 11的管理权利和维护职责 [^20]。Oracle不愿意在旧版本上继续耗费资源,而RedHat或者说它背后的IBM又乐意扩大自己在Java社 区的影响力,这是一笔双赢的交易。RedHat代替Oracle成为JDK历史版本的维护者,应该有利于Java的 持续稳定,但从技术发展角度来看,这并不能为Oracle领导Java社区的局面带来根本性的改变,毕竟要 添加新的或实验性的功能,仅会针对Java的最新版本,而不会在旧版本上动手。

2019年3月20日,JDK 12发布,只包含8个JEP,其中主要有Switch表达式、Java微测试套件 (JMH)等新功能,最引人注目的特性无疑是加入了由RedHat领导开发的Shen-andoah垃圾收集器。 Shenandoah作为首个由非Oracle开发的垃圾收集器,其目标又与Oracle在JDK 11中发布的ZGC几乎完全 一致,两者天生就存在竞争。Oracle马上用实际行动抵制了这个新收集器,在JDK 11发布时才说应尽 可能保证OracleJDK和OpenJDK的兼容一致,转眼就在OracleJDK 12里把Shenandoah的代码通过条件编 译强行剔除掉,使其成为历史上唯一进入了OpenJDK发布清单,但在OracleJDK中无法使用的功能。

Oracle收购Sun是Java发展历史上一道明显的分界线。在Sun掌舵的前十几年里,Java获得巨大成 功,同时也渐渐显露出来语言演进的缓慢与社区决策的老朽;而在Oracle主导Java后,引起竞争的同时也带来新的活力,Java发展的速度要显著高于Sun时代。Java的未来是继续向前、再攀高峰,还是由盛 转衰、锋芒挫缩,你我拭目以待。

Java面临的危机挑战前所未有的艰巨,属于Java的未来也从未如此充满想象与可能。

[^1]: Java从1.5版本开始,官方在正式文档与宣传上已经不再使用类似“JDK 1.5”的命名,只有程序员内 部使用的开发版本号(Developer Version,例如java-version的输出)中才继续沿用1.5、1.6、1.7这样的 版本号,而公开版本号(Product Version)则是改为JDK 5.0、JDK 6、JDK 7的命名方式,JDK 5.0 中“.0”的后缀从JDK 6起也被移除掉,本书为了行文统一,同样以JDK 5来指代JDK 5.0。
[^2]: 除了Java SE Support外,还有面向独立软件提供商的Java SE Advanced & Suite产品线,差别是后者 带有JMC等监控工具,详细内容可以参见本书第4章。
[^3]: 特指商业用户,个人使用仍然是可以免费获得这些更新包的。
[^4]: 这是官方的声明,而事实上直到JDK 8 Update 21之前在Windows XP上仍可正常运行。
[^5]: “HotRockit”项目的相关介绍:http://hirt.se/presentations/WhatToExpect.ppt。
[^6]: 除了JMC和JFR,HotSpot用本地内存代替永久代实现方法区,支持本地内存使用情况追踪 (NMT)等功能是从JRockit借鉴过来的。
[^7]: 来自于以前的“DLL Hell”,如果读者不清楚什么是模块地狱的话,打开你计算机的windows目录或 者windows\system32目录就明白了。
[^8]: 其实就是以IBM为首,IBM一直与RedHat有密切合作,2018年IBM以340亿美元天价收购了 RedHat。
[^9]: 投票记录:https://jcp.org/en/jsr/results?id=5959。
[^10]: 公开信:https://www.infoq.cn/article/2017/05/jigsaw-open-letter。
[^11]: 也改掉了在开发版号中1.7、1.8的命名,从JDK 10后将是年份加月份作为开发版本号,譬如18.3, 即表示2018年3月的大版本。
[^12]: Oracle与Google的官司主要焦点在于Java API的版权问题,而不在程序是否能运行在标准Java虚拟 机上。
[^13]: 资料来源:https://www.infoworld.com/article/2987529/insider-oracle-lost-interest-in-java.html。
[^14]: 最大的争议点是Oracle要求包名中不能出现java字样,导致一堆javax.*开头的包一旦修改或添加新 代码,就必须重新命名,这将让用到它们的代码都受到影响。资料来源: https://www.infoq.cn/article/2018/02/from-javaee-to-jakartaee。
[^15]: Java One大会从2019年起停办,合并入Oracle CodeOne大会中。
[^16]: 需要使用+XX:+UnlockCommercialFeatures解锁的特性,包括JMC、JFR、NMT、AppCDS和 ZGC等。
[^17]: 资料来源:https://blogs.oracle.com/java-platform-group/oracle-jdk-releases-for-java-11-and-later。
[^18]: JDK 11中仅有的微小差别是OpenJDK少了几个Module(如JavaFX),且不提供安装包,以压缩包 形式发行。但在JDK 12又产生了新的分歧,OpenJDK的Shenandoah垃圾收集器被排除在OracleJDK之 外,详见第4章的相关内容。
[^19]: 这里的商业支持不限定于Oracle公司,如Azul ZingJDK、AdoptOpenJDK等都能提供商业支持。
[^20]: Red Hat此前已经是OpenJDK 6(自2013年起)和OpenJDK 7(自2015年起)的维护者。

1.2 Java技术体系

从广义上讲,Kotlin、Clojure、JRuby、Groovy等运行于Java虚拟机上的编程语言及其相关的程序 都属于Java技术体系中的一员。如果仅从传统意义上来看,JCP官方[^1]所定义的Java技术体系包括了以 下几个组成部分:

  • Java程序设计语言
  • 各种硬件平台上的Java虚拟机实现
  • Class文件格式
  • Java类库API
  • 来自商业机构和开源社区的第三方Java类库

我们可以把Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境,本书中为行文方便,在不产生歧义的地方常以JDK 来代指整个Java技术体系[^2]。可以把Java类库API中的Java SE API子集[^3]和Java虚拟机这两部分统称为 JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。图1-2展示了Java技术体系所 包括的内容,以及JDK和JRE所涵盖的范围。

image-20210914101623411

以上是根据Java各个组成部分的功能来进行划分,如果按照技术所服务的领域来划分,或者按照 技术关注的重点业务来划分的话,那Java技术体系可以分为以下四条主要的产品线:

  • Java Card:支持Java小程序(Applets)运行在小内存设备(如智能卡)上的平台。
  • Java ME(Micro Edition):支持Java程序运行在移动终端(手机、PDA)上的平台,对Java API 有所精简,并加入了移动终端的针对性支持,这条产品线在JDK 6以前被称为J2ME。有一点读者请勿 混淆,现在在智能手机上非常流行的、主要使用Java语言开发程序的Android并不属于Java ME。
  • Java SE(Standard Edition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提 供了完整的Java核心API,这条产品线在JDK 6以前被称为J2SE。
  • Java EE(Enterprise Edition):支持使用多层架构的企业应用(如ERP、MIS、CRM应用)的 Java平台,除了提供Java SE API外,还对其做了大量有针对性的扩充[^4],并提供了相关的部署支持, 这条产品线在JDK 6以前被称为J2EE,在JDK 10以后被Oracle放弃,捐献给Eclipse基金会管理,此后被 称为Jakarta EE。

[^1]: JCP:Java Community Process,就是人们常说的“Java社区”,这是一个由业界多家技术巨头组成的 社区组织,用于定义和发展Java的技术规范。
[^2]: 本书将以OpenJDK/OracleJDK中的HotSpot虚拟机为主脉络进行讲述,这是目前业界占统治地位的 JDK和虚拟机,但它们并非唯一的选择,当本书中涉及其他厂商的JDK和其他Java虚拟机的内容时, 笔者会指明上下文中JDK的全称。
[^3]: Java SE API范围:https://docs.oracle.com/en/java/javase/12/docs/api/index.html。
[^4]: 这些扩展一般以javax.*作为包名,而以java.*为包名的包都是Java SE API的核心包,但由于历史原 因,一部分曾经是扩展包的API后来进入了核心包中,因此核心包中也包含了不少javax.*开头的包名。

第1章 走近Java

世界上并没有完美的程序,但我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过 程。

1.1 概述

Java不仅仅是一门编程语言,它还是一个由一系列计算机软件和规范组成的技术体系,这个技术 体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系统、移动终端、企 业服务器、大型机等多种场合,如图1-1所示。时至今日,Java技术体系已经吸引了600多万软件开发 者,这是全球最大的软件开发团队。使用Java的设备已经超过了45亿,其中包括8亿多台个人计算机、 21亿部移动电话及其他手持设备、35亿个智能卡,以及大量机顶盒、导航系统和其他设备^1

Java能获得如此广泛的认可,除了它拥有一门结构严谨、面向对象的编程语言之外,还有许多不 可忽视的优点:它摆脱了硬件平台的束缚,实现了“一次编写,到处运行”的理想;它提供了一种相对 安全的内存管理和访问机制,避免了绝大部分内存泄漏和指针越界问题;它实现了热点代码检测和运 行时编译及优化,这使得Java应用能随着运行时间的增长而获得更高的性能;它有一套完善的应用程 序接口,还有无数来自商业机构和开源社区的第三方类库来帮助用户实现各种各样的功能……Java带 来的这些好处,让软件的开发效率得到了极大的提升。作为一名Java程序员,在编写程序时除了尽情 发挥Java的各种优势外,还会情不自禁地想去了解和思考一下Java技术体系中这些优秀的技术特性是如 何出现及怎样实现的。认识这些技术运行的本质,是自己思考“程序这样写好不好”的必要基础与前 提。当我们在使用一门技术时,不再依赖书本和他人就能得到这些问题的答案,那才算升华到了“不 惑”的境界。

本书将会与读者一起分析Java技术体系中那些最基础、最重要特性的实现原理。在本章中,笔者 将重点讲述Java技术体系所囊括的内容,以及Java的历史、现状和未来的发展趋势。

前言_深入理解Java虛拟机:JVM高级特性与最佳实践(第3版)

Java是目前用户最多、使用范围最广的软件开发技术,Java的技术体系主要由支撑Java程序运行的 虚拟机、提供各开发领域接口支持的Java类库、Java编程语言及许许多多的第三方Java框架(如 Spring、MyBatis等)构成。在国内,有关Java类库API、Java语言语法及第三方框架的技术资料和书籍 非常丰富,相比而言,有关Java虚拟机的资料却显得异常贫乏。

这种状况很大程度上是由Java开发技术本身的一个重要优点导致的:在虚拟机层面隐藏了底层技 术的复杂性以及机器与操作系统的差异性。运行程序的物理机千差万别,而Java虚拟机则在千差万别 的物理机上面建立了统一的运行平台,实现了在任意一台Java虚拟机上编译的程序,都能在任何其他 Java虚拟机上正常运行。这一极大的优势使得Java应用的开发比传统C/C++应用的开发更高效快捷,程 序员可以把主要精力放在具体业务逻辑,而不是放在保障物理硬件的兼容性上。通常情况下,一个程 序员只要了解了必要的Java类库API、Java语法,学习适当的第三方开发框架,就已经基本满足日常开 发的需要了。虚拟机会在用户不知不觉中完成对硬件平台的兼容及对内存等资源的管理工作。因此, 了解虚拟机的运作并不是普通开发人员必备的,或者说首要学习的知识。

然而,凡事都具备两面性。随着Java技术的不断发展,它已被应用于越来越多的领域之中。其中 一些领域,如互联网、能源、金融、通信等,对程序的性能、稳定性和扩展性方面会有极高的要求。 一段程序很可能在10个人同时使用时完全正常,但是在10000个人同时使用时就会缓慢、死锁甚至崩 溃。毫无疑问,要满足10000个人同时使用,需要更高性能的物理硬件,但是在绝大多数情况下,提升 硬件性能无法等比例提升程序的运行性能和并发能力,甚至有可能对程序运行状况没有任何改善。这 里面有Java虚拟机的原因:为了达到“所有硬件提供一致的虚拟平台”的目的,牺牲了一些硬件相关的 性能特性。更重要的是人为原因:如果开发人员不了解虚拟机诸多技术特性的运行原理,就无法写出 最适合虚拟机运行和自优化的代码。

其实,目前商用的高性能Java虚拟机都提供了相当多的优化参数和调节手段,用于满足应用程序 在实际生产环境中对性能和稳定性的要求。如果只是为了入门学习,让程序在自己的机器上正常工 作,那么这些特性可以说是可有可无的;但是,如果用于生产开发,尤其是大规模的、企业级的生产 开发,就迫切需要开发人员中至少有一部分人对虚拟机的特性及调节方法具有很清晰的认识。所以在 Java开发体系中,对架构师、系统调优师、高级程序员等角色的需求一直都非常大。学习虚拟机中各 种自动运作特性的原理也成为Java程序员成长路上最终必然会接触到的一课。通过本书,读者可以以 一个相对轻松的方式学到虚拟机的运作原理。

本书面向的读者

(1)使用Java技术体系的中、高级开发人员

Java虚拟机作为中、高级开发人员修炼的必要知识,有着较高的学习门槛,本书可作为学习虚拟 机的教材。

(2)系统调优师

系统调优师是最近几年才兴起并迅速流行起来的职业,本书中的大量案例、代码和调优实战将会 对系统调优师的日常工作有直接的参考作用。

(3)系统架构师

保障系统的性能、并发和伸缩等能力是系统架构师的主要职责之一,而这部分与虚拟机的运作密 不可分,本书可以作为他们制定应用系统底层框架的参考资料。

如何阅读本书

本书一共分为五个部分:走近Java、自动内存管理、虚拟机执行子系统、程序编译与代码优化、 高效并发。各个部分之间基本上是互相独立的,没有必然的前后依赖关系,读者可以从任何一个感兴 趣的专题开始阅读,但是每个部分各个章节间则有先后顺序。

这里并没有假定所有读者都在Java领域具备特别专业的技术水平,因此会在保证逻辑完整、描述 准确的前提下,尽量用通俗的语言和案例去讲述虚拟机中与开发关系最为密切的内容。但是,本书毕 竟是在探讨虚拟机的工作原理,不可避免地需要读者有一定的技术基础,而且本书的读者定位是中、 高级程序员群体,对于一些常用的开发框架、Java类库API和Java语法等基础知识点,将假设读者已有 所了解。

本书介绍的Java虚拟机并不局限于某一个特定发行商或者某一款特定虚拟机,只是由于 OracleJDK/OpenJDK在市场占有率上的绝对优势,其中的HotSpot虚拟机不可避免地成为本书主要分 析、讲解的对象,书中在涉及Java虚拟机自身实现相关的内容时,大多将以HotSpot虚拟机为目标对象 来进行讲解。但撰写本书的意图并不是去做HotSpot的源码导读或者解析,书中所讲述的内容多为Java 虚拟机的通用原理,即使读者使用了HotSpot之外的其他Java虚拟机实现,也会有所收获。

最后,非常希望读者能跟随本书的讲解,把与实践相关的内容亲自验证一遍,其中用到的代码清 单可以从华章图书的网站(http://www.hzbook.com/)上下载。

语言约定

开始阅读本书之前,在语言和技术上先与读者建立如下约定:

  • JDK从1.5版本开始,其官方的正式文档与宣传材料中的发行版本号启用了JDK 5、6、7……的新 命名方式;从2018年3月发布的JDK 10起,JDK的开发版本号(如java-version)也放弃了以前1.x的命 名形式,改为按发布的日期时间命名。本书为了行文一致,所有场合统一采用发行版本号来指代所述 的JDK版本。
  • 由于版面原因,本书中的许多示例代码都没有遵循最优的程序编写风格,如使用的流没有关闭 流、直接使用System.out输出日志等,请读者在阅读时注意这一点。
  • 本书讲解中涉及JDK 7以前HotSpot虚拟机、JRockit虚拟机、WebLogic服务器等产品的所有者时, 仍然会使用BEA和Sun公司的名称,而不是Oracle。实际上BEA和Sun分别于2008年和2010年被Oracle公 司收购,现在已经不存在这两个商标了,但是它们毫无疑问都是在Java领域中做出过卓越贡献的、值 得程序员们纪念的先驱企业。
  • 本书第3版撰写于2019年中期,此时JDK 13已有了技术预览版(Early Access),但尚未正式发 布。本书中所有的讲解、讨论都是基于这个时间点的Java技术的,但并不针对特定的JDK版本。如涉 及JDK新版本中加入的功能,或在不同版本中有所变化的特性,笔者都会明确指出JDK的版本号,或专门阐述各个版本间的差异。

内容特色与更新

本书的第2版成文于2011至2012年间,出版于2013年,撰写时是基于早期版本的JDK 7,彼时正值 Oracle全面替代Sun公司领导Java技术发展的起点。经过将近十年的时间,今天JDK版本已经发展到了 JDK 12及预览版的JDK 13,整个Java技术体系一改Sun时代的迟缓作风,出现了许多激烈的变革,也 涌现了不少令人欣喜的新变化、新风潮。我在撰写本书第3版时,期望能把这些新的变化融合到已有的 知识框架中,修改第2版中读者反馈的问题,提升叙述的准确性与可读性,这些期望中的更新使得本书 字数从原有的24万增加到35万。因此,在前言部分,笔者针对每章列举出主要更新的内容,以便阅读 过第2版的读者可以快速定位,获取到新的知识。当然,如果你尚有余暇,不妨从头阅读一次本书,相 信会有与阅读第2版时不一样的体验和收获。

第一部分 走近Java

本书的第一部分为后文的研究和讲解建立了良好的基础。虽然了解Java技术的来龙去脉,以及编 译自己的OpenJDK对于读者理解Java虚拟机并不是必须的,但是这些准备过程可以为走近Java技术和 Java虚拟机提供良好的引导。第一部分只有第1章:
第1章 介绍了Java技术体系过去、现在的情况以及未来的发展趋势,并在实践中介绍了如何自己 编译一个OpenJDK 12。

第3版更新:续写了Java技术发展史,这几年Java世界着实发生了很多值得记录的大事件;完全 重写了第2版对未来Java的展望预测,当时畅想的Java新发展新变化全部如约而至,是时候把聚光灯交 给下一个十年的Java了;OpenJDK开发、编译也发生过不小的变动,本次更新将OpenJDK编译的版本 提升到12。

第二部分 自动内存管理

因为程序员把控制内存的权力交给了Java虚拟机,所以可以在编码的时候享受自动内存管理的诸 多优势,不过也正因为这个原因,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使 用内存的,那排查错误将会成为一项异常艰难的工作。第二部分包括第2~5章:

  • 第2章 介绍了虚拟机中内存是如何划分的,哪部分区域、什么样的代码和操作可能导致内存溢出 异常,并讲解了各个区域出现内存溢出异常的常见原因。
    • 第3版更新:Java运行期数据区域是虚拟机的基础结构,尽管JDK版本在快速发展,这块内容 仍然保持了相对的稳定,主要的变化是JDK 8时期的永久代的落幕和元空间的登场;除此以外,本章 着重修正了第2版中对Java虚拟机栈描述的含糊与偏差之处,还更新了部分测试代码,避免因JDK版本 更迭导致与书中不一样的结果。
  • 第3章 介绍了垃圾收集的算法和HotSpot虚拟机中提供的几款垃圾收集器的特点及运作原理。通 过代码实例验证了Java虚拟机中自动内存分配及回收的主要规则
    • 第3版更新:由于撰写第2版时JDK 7刚刚发布,G1收集器尚无实践数据可查,书中对此讲述得 比较含糊,本次更新完全重写了这部分内容,并重点增加了JDK 11、12中新出现的ZGC和Shenandoah 两款低延迟全并发收集器的详细原理解析,这是垃圾收集器未来的发展方向。对其他与收集器相关的 更新,如统一收集器接口、Epsilon等也都做了对应介绍。此外,针对HotSpot中收集器实现的几个关 键技术点,如解决跨代引用的记忆集与卡表、解决并发标记的增量更新和原始快照算法,还有内存 读、写屏障等技术都增加了专门的小节来进行介绍,以便帮读者在后续深入阅读HotSpot设计与源码时 打下良好的理论基础。
  • 第4章 介绍了随JDK发布的基础命令行工具与可视化的故障处理工具的使用方法。
    • 第3版更新:Java虚拟机的各种监控、管理等辅助工具的功能日益强大,几乎每个版本在这些 工具的数量、功能上都会或多或少有所变化,除了将第2版涉及的工具的变化依照JDK版本进行升级 外,本章还新增了对JDK 9中加入的JHSDB的使用讲解,并增加了对JFR和JMC的工作原理和使用方法 的介绍,以及对部分JDK外部的工具(如JIT Watch)的简要介绍。
  • 第5章 分享了几个比较有代表性的实际案例,还准备了一个所有开发人员都能“亲身实战”的练 习,希望读者能通过实践来获得故障处理和调优的经验。
    • 第3版更新:对案例部分进行了更新和增补,着重补充了与前3章新增内容相对应的问题处理案 例。不过对实战部分,软件版本的落后并未影响笔者要表达的内容,原有的实战目前仍具有相同的实 战价值,在第3版里笔者也并未刻意将Eclipse和HotSpot升级后重写一次。

第三部分 虚拟机执行子系统

执行子系统是虚拟机中必不可少的组成部分,了解了虚拟机如何执行程序,才能更好地理解怎样 才能写出优秀的代码。第三部分包括第6~9章:

  • 第6章 讲解了Class文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用方法, 以实战的方式演示了Class的数据是如何存储和访问的。
    • 第3版更新:笔者认为本章内容更适合以“技术手册”的形式存在,即适合查阅多于适合阅读, 但因为Class文件格式是虚拟机的基础知识,所以尽管枯燥却无法回避。本次更新将Class文件格式的版 本跟进到了JDK 12,《Java虚拟机规范》对Class文件格式进行的增强也会在本章中反映,内容相对琐 碎。例如,为了实现JDK 9的Java模块化系统,属性表中新增了Module、ModulePackages、 ModuleMain-Class三项新属性,常量池中加入了CONSTANT_Module_info和CONSTANT_Package_info 两个常量。为了实现JDK 11新增的嵌套内(Java中的内部类)访问控制的API,属性表中又增加了 NestHost和NestMembers两项属性。为进一步加强动态语言支持,CONSTANT_Dynamic_info常量也在 JDK 11期间加入常量池……
  • 第7章 介绍了类加载过程的“加载”“验证”“准备”“解析”和“初始化”五个阶段中虚拟机分别进行了 哪些动作,还介绍了类加载器的工作原理及其对虚拟机的意义。
    • 第3版更新:随着Class文件格式的发展,类加载的各个过程都发生了一些细节性变动,本章将会按照JDK 12版本的《Java虚拟机规范》的标准来同步更新这些内容。此外,在JDK 9时引入了Java模 块化系统,这是近年来Java技术的一次重要升级,也是对类加载部分影响巨大的一项变革,在本章将 加入专门的小节对其进行讲述。
  • 第8章 分析了虚拟机在执行代码时,如何找到正确的方法、如何执行方法内的字节码,以及执行 代码时涉及的内存结构。
    • 第3版更新:本章讲述的是Java虚拟机执行子系统的概念模型,这部分属于相对稳定的内容, 变化不大,本次主要更新了Java虚拟机对动态类型语言支持的增强。
  • 第9章 通过几个类加载及执行子系统的案例,介绍了使用类加载器和处理字节码的一些值得欣赏 和借鉴的思路,并通过一个实战练习加深读者对前面理论知识的理解。
    • 第3版更新:原有章节所涉及的案例中,程序、类库、工具的版本已经较为陈旧,本次更新对 这些案例涉及的版本进行了升级,以反映在模块化、Lambda表达式、动态语言等新技术出现后它们的 相应变化。

第四部分 程序编译与代码优化

Java程序从源码编译成字节码,再从字节码编译成本地机器码的这两个过程,从整体来看其实等 同于一个传统编译器所执行的编译前端、后端过程。第四部分包括第10~11章:

  • 第10章 分析了Java语言中泛型、主动装箱拆箱、条件编译等多种语法糖的前因后果,并实战练 习了如何使用插入式注解处理器来完成一个检查程序命名规范的编译器插件。
    • 第3版更新:对第2版介绍泛型的小节进行了全文重写,描述了不同语言里泛型实现的方式、 Java泛型出现的历史背景和使用类型擦除来实现泛型所带来的一些限制,并介绍了未来可能会在Java中 出现的值类型等内容。
  • 第11章 讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟 机外部观察和分析即时编译的数据和结果,还选择了几种常见的编译期优化技术进行讲解。
    • 第3版更新:专门增加了介绍提前编译器的章节;由于HotSpot中新的Graal编译器的加入,书 中除了同步增加Graal编译器、JVMCI接口等内容,为了使读者可以在HotSpot编译器上进行实战练 习,在本书第3版中还新增了许多编译器的实战内容。

第五部分 高效并发

Java语言和虚拟机提供了原生的、完善的多线程支持,使得它天生就适合开发多线程并发的应用 程序。不过我们不能期望系统来完成所有与并发相关的处理,了解并发的内幕也是成为一位高级程序 员不可缺少的课程。第五部分包括第12~13章:

  • 第12章 讲解了虚拟机Java内存模型的结构及操作,以及原子性、可见性和有序性在Java内存模型 中的体现;介绍了先行发生原则的规则及使用,以及线程在Java语言之中是如何实现的;还提前介绍了目前仍然在实验室状态的Java协程的相关内容。
    • 第3版更新:重写了原有的对Java内存模型部分过时和过于晦涩的描述,增加了面向Java未来基 于协程的新并发模型的介绍。
  • 第13章 介绍了线程安全所涉及的概念和分类、同步实现的方式及虚拟机的底层运作原理,并且 介绍了虚拟机实现高效并发所做的一系列锁优化措施。
    • 第3版更新:本章主体内容并没有过多变化,但对不少细节进行了修饰,对一些读者疑问较多 的地方进行了补充讲解。

参考资料

本书名为“深入理解Java虚拟机”,但要想真的深入理解虚拟机,仅凭一本书肯定是远远不够的, 读者可以通过以下方式查找到更多关于Java虚拟机方面的资料。笔者在写作此书的时候,也从下面这 些参考资料中得到过很大的帮助。

1.书籍

《Java虚拟机规范》

要学习虚拟机,《Java虚拟机规范》无论如何都是必须读的。这本书的概念和细节描述与Sun的早 期虚拟机(Sun Classic虚拟机)高度吻合,随着技术的发展,高性能虚拟机真正的细节实现方式已经 渐渐与虚拟机规范所描述的方式差距越来越大,如果只能选择一本参考书来了解Java虚拟机,那必然 是这本书。

《Java语言规范》

虽然Java虚拟机并不是Java语言专有的,但是了解Java语言的各种细节规定对虚拟机的行为也是很 有帮助的,它与《Java虚拟机规范》一样都是Oracle官方直接出版的书籍,而且这本书还是由Java之父 James Gosling亲自执笔撰写。

《垃圾回收算法手册:自动内存管理的艺术》

2016年3月由机械工业出版社引进翻译,这是一本真正的教科书式的学术著作,是垃圾收集技术领 域中的唯一必读的书籍。该书从硬件与软件的发展给垃圾回收所带来的新挑战出发,探讨了这些挑战 给高性能垃圾回收器的设计者与实现者所带来的影响,涵盖了并行垃圾回收、增量式垃圾回收、并发 垃圾回收以及实时垃圾回收,描述各种算法与概念。唯一缺点是由于过于专业,所以显得比较晦涩, 不适合作为入门书籍使用。

《Virtual Machines:Versatile Platforms for Systems and Processes》

这是一本虚拟化技术的百科全书,帮助读者理解“虚拟机”一词到底指代什么,有什么不同类型, 大概有哪些实现方法等。此书并不直接针对Java虚拟机,出版于2005年,而且国内并没有中文版,但 即使有这些因素限制,仍然推荐读者阅读此书以建立对虚拟机的全局性观念。

《Java性能优化权威指南》

此书是“The Java”系列(该系列中最出名的《Effective Java》许多人都读过)图书中最新的一本, 但也有一定的历史了。2011年10月出版,2014年3月由人民邮电出版社引进翻译。这本书并非全部都围 绕Java虚拟机展开(只有第3、4、7章直接与Java虚拟机相关),而是从操作系统到基于Java的上层程 序性能度量和调优进行全面介绍。其中涉及Java虚拟机的内容具备一定深度和很好的可实践性。

2.网站资源

联系作者

在本书交稿的时候,我并没有想象中那样兴奋或放松,写作之时那种“战战兢兢、如履薄冰”的感 觉依然萦绕在心头。在每一章、每一节落笔之时,我都在考虑如何才能把各个知识点更有条理地讲述 出来,都在担心会不会由于自己理解有偏差而误导了大家。囿于我的写作水平和写作时间,书中难免 存在不妥之处,所以特地开通了一个读者邮箱(under-standingjvm@gmail.com),大家如有任何意见或 建议欢迎与我联系。相信写书与写程序一样,作品一定都是不完美的,因为不完美,我们才有不断追 求完美的动力。

SQL语句结构

结构化查询语言包含6个部分:

一:数据查询语言(DQL:Data Query Language):

其语句,也称为“数据检索语句”,用以从表中获得数据,确定数据怎样在应用程序给出。保留字selectDQL(也是所有SQL)用得最多的动词,其他DQL常用的保留字有whereorder bygroup byhaving。这些DQL保留字常与其他类型的SQL语句一起使用。

二:数据操作语言(DML:Data Manipulation Language):

其语句包括动词insertupdatedelete。它们分别用于添加,修改和删除表中的行。也称为动作查询语言。

三:事务处理语言(TPL):

它的语句能确保被DML语句影响的表的所有行及时得以更新。TPL语句包括begin transactioncommitrollback

四:数据控制语言(DCL):

它的语句通过grantrevoke获得许可,确定单个用户和用户组对数据库对象的访问。某些RDBMS可用grantrevoke控制对表单个列的访问。

五:数据定义语言(DDL):

其语句包括动词createdrop。在数据库中创建新表或删除表(creat tabledrop table);为表加入索引等。DDL包括许多与人数据库目录中获得数据有关的保留字。它也是动作查询的一部分。

六:指针控制语言(CCL):

它的语句,像declare cursorfetch intoupdate where current用于对一个或多个表单独行的操作。

参考资料

https://www.w3cschool.cn/sql/

第10章 深入使用Mybatis概述

本章要点

  • 一对一映射
  • 一对多映射
  • 多对多映射
  • 动态SQL映射
  • 调用存储过程
  • 事务管理
  • 缓存机制

通过MyBatis的支持,应用程序可以从底层的JDBC中抽离出来,以面向对象的方式进行数据库访问。但面向对象远不止这些内容,比如对象和对象之间的关联关系,这对于客观世界的建模是非常重要的。本章将深入介绍MyBatis的关联映射,也会详细介绍MyBatis的动态SQL查询。

13.2.3 创建持久化实体类

  • 持久化对象之间的关联关系以成员变量的方式表现出来,当然这些成员变量同样需要settergetter方法的支持。持久化类之间的关联关系通常对应数据库里的主、外键约束
  • 除此之外,持久化对象还有自己的普通类型的成员变量,这些成员变量通常对应数据库的字段

下面是6个持久化类的源代码。

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package org.fkit.hrm.domain;
import java.io.Serializable;
import java.util.Date;
public class User
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // id
private String username; // 用户名
private String loginname; // 登录名
private String password; // 密码
private Integer userstatus; // 状态
private Date createDate; // 建档日期
// 无参数构造器
public User()
{
super();
}
// setter和getter方法
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", username=" + username + ", loginname=" + loginname
+ ", password=" + password + ", userstatus=" + userstatus + ", createDate="
+ createDate + "]";
}
}

Dept.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.fkit.hrm.domain;
import java.io.Serializable;
public class Dept
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // id
private String name; // 部门名称
private String remark; // 详细描述
// 无参数构造器
public Dept()
{
super();
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Dept [id=" + id + ", name=" + name + ", remark=" + remark + "]";
}
}

Job.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.fkit.hrm.domain;
import java.io.Serializable;
public class Job
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // id
private String name; // 职位名称
private String remark; // 详细描述
// 无参数构造器
public Job()
{
super();
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Job [id=" + id + ", name=" + name + ", remark=" + remark + "]";
}
}

Employee.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.fkit.hrm.domain;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
public class Employee
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // id
private Dept dept; // 部门
private Job job; // 职位
private String name; // 名称
private String cardId; // 身份证
private String address; // 地址
private String postCode; // 邮政编码
private String tel; // 电话
private String phone; // 手机
private String qqNum; // qq
private String email; // 邮箱
private Integer sex; // 性别
private String party; // 政治面貌
/**
* 使用@ModelAttribute接收参数时 form表单中有日期,Spring不知道该如何转换,
* 要在实体类的日期属性上加@DateTimeFormat(pattern="yyyy-MM-dd")注解
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
private java.util.Date birthday; // 生日
private String race; // 名族
private String education; // 学历
private String speciality; // 专业
private String hobby; // 爱好
private String remark; // 备注
private java.util.Date createDate; // 建档日期
// 无参数构造器
public Employee()
{
super();
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Employee [id=" + id + ", dept=" + dept + ", job=" + job + ", name=" + name
+ ", cardId=" + cardId + ", address=" + address + ", postCode=" + postCode
+ ", tel=" + tel + ", phone=" + phone + ", qqNum=" + qqNum + ", email=" + email
+ ", sex=" + sex + ", party=" + party + ", birthday=" + birthday + ", race=" + race
+ ", education=" + education + ", speciality=" + speciality + ", hobby=" + hobby
+ ", remark=" + remark + ", createDate=" + createDate + "]";
}
}

Notice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.fkit.hrm.domain;
import java.io.Serializable;
public class Notice
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 编号
private String title; // 标题
private String content; // 内容
private java.util.Date createDate; // 发布日期
private User user; // 发布人
// 无参数构造器
public Notice()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Notice [id=" + id + ", title=" + title + ", content=" + content + ", createDate="
+ createDate + ", user=" + user + "]";
}
}

Document.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.fkit.hrm.domain;
import org.springframework.web.multipart.MultipartFile;
import java.io.Serializable;
public class Document
implements Serializable
{
private static final long serialVersionUID = 1L;
private int id; // 编号
private String title; // 标题
private String fileName; // 文件名
private MultipartFile file; // 文件
private String remark; // 描述
private java.util.Date createDate; // 上传时间
private User user; // 上传人
// 无参数构造器
public Document()
{
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Document [id=" + id + ", title=" + title + ", fileName=" + fileName + ", file="
+ file + ", remark=" + remark + ", createDate=" + createDate + ", user=" + user
+ "]";
}
}

13.2.2 设计持久化实体类

面向对象分析,是指根据系统需求提取应用中的对象,将这些对象抽象成类,再抽取出需要持久化保存的类,这些需要持久化保存的类就是持久化对象(PO)。本系统设计了6个持久化类。具体包括:

  • User。对应用户,包括用户编号、用户名、登录名、密码、状态及建档日期等属性
  • Dept。对应部门,包括部门编号、部门名称及详细描述等属性。
  • Job。对应职位,包括职位编号、职位名称及详细描述等属性。
  • Employee。对应员工,包括员工编号、员工名称、身份证号码、地址、邮政编码、电话号码、手机号码、qq号码、邮箱、性别、政治面貌、生日、民族、学历、所学专业爱好、备注及建档日期等属性。
  • Notice。对应公告,包括公告编号、公告标题、公告内容、发布日期等属性。
  • Document。对应文件,包括文件编号、文件标题、文件名、文件描述、上传时间等属性。
  • 在领域模式的设计中,这6个PO对象也应该包含系统的业务逻辑方法,也就是使用领域模型对象来为它们建模;但本应用不打算为它们提供任何业务逻辑方法,而是将所有的业务逻辑方法放到业务逻辑组件中实现。

将所有的业务逻辑方法放到业务逻辑组件中实现,这样系统中的领域对象显得十分简洁,它们都是单纯的数据类,不需要考虑到底应该包含哪些业务逻辑方法,因此开发起来非常便捷;而系统的所有业务逻辑都由业务逻辑组件负责实现,这样可以将业务逻辑的变化限制在业务逻辑层内,从而避免扩散到其他两个层,因此降低了系统的开发难度。
客观世界中的对象不是孤立存在的,以上6个PO类也不是孤立存在的,它们之间存在复杂的关联关系,分析关联关系是面向对象分析的必要步骤。
这6个PO的关系如下:
DeptEmployee之间存在1对N的关系,即一个Dept可以有多个Employee
EmployeeDept之间存在N对1的关系,即一个Employee只属于一个Dept
EmployeeJob之间存在N对1的关系,即一个Employee只能有一个Job
UserNotice之间存在1对N的关系,即一个User可以发布多个Notice
UserDocument之间存在1对N的关系,即一个User可以上传多个Document

13.3.3 部署DAO层

通过前面的介绍不难发现,MyBatis的持久化DAO接口只需要通过SqlSessiongetMapper方法获得对应的接口实例,即可以调用接口的方法完成数据库操作。而SpringMyBatis整合的应用中,由Spring容器负责生成并管理DAO组件

MyBatis社区自己开发了一个Mybatis-Spring中间件用来满足MyBatis用户整合Spring的需求。通过SqlSessionFactoryBean类,可以将SqlSessionFactory纳入IoC容器内。
在使用Spring管理DAO组件之前,必须要为其提供对应的数据源,本应用使用C3PO数据源。

db.properties

1
2
3
4
5
6
7
8
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/hrm_db
dataSource.user=root
dataSource.password=root
dataSource.maxPoolSize=20
dataSource.maxIdleTime = 1000
dataSource.minPoolSize=6
dataSource.initialPoolSize=5

配置了所需的数据源之后,程序就可以在此数据源基础上配置SqlSessionFactory对象。配置SqlSessionFactory的代码如下:

applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<!-- mybatis:scan会扫描org.fkit.dao包里的所有接口当作Spring的bean配置,之后可以进行依赖注入 -->
<mybatis:scan base-package="org.fkit.hrm.dao"/>
<!-- 扫描org.fkit包下面的java文件,有Spring的相关注解的类,则把这些类注册为Spring的bean -->
<context:component-scan base-package="org.fkit.hrm"/>
<!-- 使用PropertyOverrideConfigurer后处理器加载数据源参数 -->
<context:property-override
location="classpath:db.properties"/>
<!-- 配置c3p0数据源 -->
<bean
id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource"/>
<!-- 配置SqlSessionFactory,org.mybatis.spring.SqlSessionFactoryBean是Mybatis社区开发用于整合Spring的bean -->
<bean
id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource"/>
<!-- JDBC事务管理器 -->
<bean
id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!-- 启用支持annotation注解方式事务管理 -->
<tx:annotation-driven
transaction-manager="transactionManager"/>
</beans>

<mybatis:scan base-package="org.fkit.hrm.dao"/>标签,会扫描org.fkit.hrm.dao包下面的所有接口作为SpringBean配置,之后可以进行依赖注入