编译OpenJDK12——Windows 11系统

调试OpenJDK12见文章:CLion调试OpenJDK12—Windows 11系统

前言:

本文中大量文字内容均摘自《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》第一章1.6小节。

  • 想要窥探Java虚拟机内部的实现原理,最直接的一条路径就是编译一套自己的JDK,通过阅读和跟踪调试JDK源码来了解Java技术体系的运作,虽然这样门槛会比阅读资料更高一点,但肯定也会比阅读各种文章、书籍来得更加贴近本质。此外,Java类库里的很多底层方法都是Native的,在了解这些方法的运作过程,或对JDK进行Hack(根据需要进行定制微调)的时候,都需要有能自行编译、调试虚拟机代码的能力。
  • 现在网络上有不少开源的JDK实现可以供我们选择,但毫无疑问OpenJDK是使用得最广泛的JDK,我们也将选择OpenJDK来进行这次编译实战。

1.获取源码

  • 编译源码之前,我们要先明确OpenJDK和OracleJDK之间、OpenJDK的各个不同版本之间存在什么联系,这有助于确定接下来编译要使用的JDK版本和源码分支,也有助于理解我们编译出来的JDK与Oracle官方提供的JDK有什么差异。
  • 从前面介绍的Java发展史中我们已经知道OpenJDK是Sun公司在2006年年末把Java开源而形成的项目,这里的"开源"是通常意义上的源码开放形式,即源码是可被复用的,例如OracleJDK、Oracle OpenJDK、AdoptOpenJDK、Azul Zulu、SAP SapMachine、Amazon Corretto、IcedTea、UltraViolet等都是从OpenJDK源码衍生出的发行版。但如果仅从"开源"字面意义(开放可阅读的源码)上讲的话,其实Sun公司自JDK 5时代起就曾经以JRL(Java Research License)的形式公开过Java的源码,主要是开放给研究人员阅读使用,这种JRL许可证的开放源码一直持续到JDK 6 Update 23才因OpenJDK项目日渐成熟而终止。如果拿OpenJDK中的源码跟对应版本的JRL许可证形式开放的Sun/OracleJDK源码互相比较的话,会发现除了文件头的版权注释之外,其余代码几乎都是相同的,只有少量涉及引用第三方的代码存在差异,如字体栅格化渲染,这部分内容OracleJDK采用了商业实现,源码版权不属于Oracle自己,所以也无权开源,而OpenJDK中使用的是同样开源的FreeType代替。
  • 当然,笔者说的"代码相同"必须建立在两者共有的组件基础之上,OpenJDK中的源码仓库只包含了标准Java SE的源代码,而一些额外的模块,典型的如JavaFX,虽然后来也是被Oracle开源并放到OpenJDK组织进行管理(OpenJFX项目),但是它是存放在独立的源码仓库中,因此OracleJDK的安装包中会包含JavaFX这种独立的模块,而用OpenJDK的话则需要单独下载安装。
  • 此外,在JDK 11以前,OracleJDK中还会存在一些OpenJDK没有的、闭源的功能,即OracleJDK的"商业特性"。例如JDK 8起从JRockit移植改造而来的Java Flight Recorder和Java Mission Control组件、JDK 10中的应用类型共享功能(AppCDS)和JDK 11中的ZGC收集器,这些功能在JDK 11时才全部开源到了OpenJDK中。到了这个阶段,我们已经可以认为OpenJDK与OracleJDK代码实质上已达到完全一致的程度(严格来说,这里"实质上"可以理解为除去一些版权信息(如java-version的输出)、除去针对Oracle自身特殊硬件平台的适配、除去JDK 12中OracleJDK排除了Shenandoah这类特意设置的差异之外是一致的)。
  • 根据Oracle的项目发布经理Joe Darcy在OSCON大会上对两者关系的介绍(全文地址:https://blogs.oracle.com/darcy/resource/OSCON/oscon2011_OpenJDKState.pdf)也证实了OpenJDK和OracleJDK在程序上是非常接近的,两者共用了绝大部分相同的代码(如图1-7所示,注意图中的英文提示了两者共同代码的占比要远高于图形上看到的比例),所以我们编译的OpenJDK,基本上可以认为性能、功能和执行逻辑上都和官方的OracleJDK是一致的。
  • 下面再来看一下OpenJDK内部不同版本之间的关系,在OpenJDK接收Sun公司移交的JDK源码时,Java正处于JDK 6时代的初期,JDK 6 Update 1才刚刚发布不久,JDK 7则还完全处于研发状态的半成品。OpenJDK的第一个版本就是来自于当时Sun公司正在开发的JDK 7,考虑到OpenJDK 7的状况在当时完全不足以支持实际的生产部署,因此又在OpenJDK 7 Build 22的基础上建立了一条新的OpenJDK 6分支,剥离掉所有JDK 7新功能的代码,形成一个可以通过TCK 6测试的独立分支,先把OpenJDK 6发布出去给公众使用。等到OpenJDK 7达到了可正式对外发布的状态之后,就从OpenJDK 7的主分支延伸出用于研发下一代Java版本的OpenJDK 8以及用于发布更新补丁的OpenJDK 7 Update两条子分支,按照开发习惯,新的功能或Bug修复通常是在最新分支上进行的,当功能或修复在最新分支上稳定之后会同步到其他老版本的维护分支上。后续的JDK 8和JDK 9都重复延续着类似的研发流程。通过图1-8(依然是从Joe Darcy的OSCON演示稿截取的图片)可以比较清楚地理解不同版本分支之间的关系。
  • 到了JDK 10及以后的版本,在组织上出现了一些新变化,此时全部开发工作统一归属到JDK和JDK Updates两条主分支上,主分支不再带版本号,在内部再用子分支来区分具体的JDK版本。OpenJDK不同版本的源码都可以在它们的主页(http://openjdk.java.net/)上找到,在本次编译实践中,笔者选用的版本是OpenJDK 12。
    获取OpenJDK源码有两种方式。一是通过Mercurial代码版本管理工具从Repository中直接取得源码(Repository地址:https://hg.openjdk.java.net/jdk/jdk12),获取过程如以下命令所示:
		hg clone https://hg.openjdk.java.net/jdk/jdk12
  • 这是直接取得OpenJDK源码的方式,从版本管理中看变更轨迹也能够更精确地了解到Java代码发生的变化,但弊端是在中国访问的速度实在太慢,虽然代码总量只有几百MB,无奈文件数量将近十万,而且仓库没有国内的节点。以笔者的网络状况,不科学上网的话,全部复制到本地需要耗费数小时时间。另外,考虑到Mercurial远不如Git常用,甚至普及程度还不如SVN、ClearCase以及更古老的CVS等版本控制工具,对于大多数读者,笔者建议采用第二种方式,即直接在仓库中打包出源码压缩包,再进行下载。
    读者可以直接访问准备下载的JDK版本的仓库页面(譬如本例中OpenJDK 12的页面为https://hg.openjdk.java.net/jdk/jdk12/),然后点击左边菜单中的"Browse",将显示如图1-9的源码根目录页面。
  • 此时点击左边的"zip"链接即可下载当前版本打包好的源码,到本地直接解压即可。在国内使用这种方式下载比起从Mercurial复制一堆零散文件要快非常多。笔者下载的OpenJDK 12源码包大小为171MB,解压之后约为579MB。

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源码是为了供技术专家进行研究之用)"那么夸张,但是如果读者是第一次编译,那在上面耗费一整天乃至更多的时间都很正常。
  • 笔者在本次实战中演示的是在64位Windows 11平台下编译x64版的OpenJDK(也就是64位的JDK)。另外编译涉及的所有文件都必须存放在NTFS格式的文件系统中,因为FAT32格式无法支持大小写敏感的文件名。官方文档上写道:编译至少需要512MB的内存和600MB的磁盘空间。如果读者耐心很好的话,512MB的内存也可以凑合使用,不过600MB的磁盘空间仅仅是指存放OpenJDK源码和相关依赖项的空间,要完成编译,600MB肯定是无论如何都不够的。这次实战中所下载的工具、依赖项、源码,全部安装、解压完成最少("最少"是指只下载C++编译器,不下载VS的IDE)需要1GB的空间。
  • 对系统的最后一点要求就是所有的文件,包括源码和依赖项目,都不要放在包含中文或空格的目录里面,这样做不是一定不可以,只是这样会为后续建立CYGWIN环境带来很多额外的工作。这是由于Linux和Windows的磁盘路径差别所导致的,我们也没有必要自己给自己找麻烦。

3.构建编译环境

  • 准备编译环境的第一步是安装一个CYGWIN(下载地址:http://www.cygwin.com/中文站点:Cygwin中文站点)。这是一个在Windows平台下模拟Linux运行环境的软件,提供了一系列的Linux命令支持。需要CYGWIN的原因是,在编译中要使用GNU Make来执行Makefile文件(C/C++程序员肯定很熟悉,如果只使用Java,那把这个东西当成C++版本的ANT看待就可以了)。

  • 安装Cygwin环境详见:Windows环境运行Linux命令——Cygwin安装
    配置环境变量时:Cygwin的环境变量位置尽量放到前面一点,要放到**%SystemRoot%\system32**前面,否则后面执行bash configure命令时会报错configure: Your path contains Windows tools (C:\Windows\system32) before your unix (cygwin or msys) tools。

  • 建立编译环境的第二步是安装编译器。JDK中最核心的代码(Java虚拟机及JDK中Native方法的实现等)是使用C++语言及少量的C语言编写的,官方文档中说它们的内部开发环境是在Microsoft Visual Studio C++中进行编译的,同时也是在Microsoft Visual Studio C++中测试过的,所以最好只选择这个编译器进行编译。

  • 这里选择Microsoft Visual Studio C++2017(VC2017)编译器,安装前需要提前把Windows 11 SDK安装好,否则无法编译C/C++代码。

  • VC2017编译器的详细安装过程见Microsoft Visual Studio C++2017+Windows 11 SDK环境,安装的时候注意安装路径尽量不要有空格,笔者提供的图片中路径是带有空格的,这里只是对整个安装过程做了详细描述,真正安装的时候请不要使用有空格的路径,这点要特别注意。

  • 另外VC2017编译器语言包请选择使用英文OpenJDK的源码包中是根据英文名称去寻找Windows环境下VC2017链接路径,如果使用中文,在编译校验的过程中会报错找不到Visual Studio

    安装完成,配置好环境变量,在cmd中执行cl命令验证一下语言包是英文的。

  • VC2017配置环境变量时,尤其要注意配置一下VC2017中bin目录的环境变量,因为在CYGWIN中也有一个连接器link.exe,但是只有VC2017中的连接器可以完成OpenJDK的编译,配置环境变量时必须保证VC2017连接器变量在CYGWIN的bin目录之前,否则在编译校验的过程中会调用CYGWIN的连接器link.exe导致报错。

  • 准备JDK编译环境的第三步就是下载一个已经编译好的JDK。这听起来也许有点滑稽——要用鸡蛋孵小鸡还真得必须先养一只母鸡呀?但仔细想想,其实这个步骤很合理:因为JDK包含的各个部分(Hotspot、JDK API、JAXWS、JAXP……)有的是使用C++编写的,而更多的代码则是使用Java自身实现的,因此编译这些Java代码需要用到一个可用的JDK,官方称这个JDK为Bootstrap JDK。而编译OpenJDK 12的话,Bootstrap JDK必须使用JDK 11或之后的版本,笔者选用的是JDK 11.0.18版本。JDK的安装与环境变量配置读者自行完成,这部分是Java的基础知识,对本文的读者来说应该没有难度,笔者不再详述。

4.依赖检查

  • 需要下载的编译环境和依赖项目都齐备后,我们就可以按照默认配置来开始编译了,但通常我们编译OpenJDK的目的都不仅仅是为了得到在自己机器中诞生的编译成品,而是带着调试、定制化等需求,这样就必须了解OpenJDK提供的编译参数才行,这些参数可以使用"bash configure–help"命令查询到,笔者对它们中最有用的部分简要说明如下:
    • –with-debug-level=:设置编译的级别,可选值为release、fastdebug、slowde-bug,越往后进行的优化措施就越少,带的调试信息就越多。还有一些虚拟机调试参数必须在特定模式下才可以使用。默认值为release。
    • –enable-debug:等效于–with-debug-level=fastdebug。
    • –with-native-debug-symbols=:确定调试符号信息的编译方式,可选值为none、
      internal、external、zipped。
    • –with-version-string=:设置编译JDK的版本号,譬如java-version的输出就会显示该信息。 这个参数还有–with-version-=的形式,其中part可以是pre、opt、build、major、minor、security、patch之一,用于设置版本号的某一个部分。
    • –with-jvm-variants=[,…]:编译特定模式(Variants)的HotSpot虚拟机,可以多个模式并存,可选值为server、client、minimal、core、zero、custom。
    • –with-jvm-features=[,…]:针对–with-jvm-variants=custom时的自定义虚拟机特性列表(Features),可以多个特性并存,由于可选值较多,请参见help命令输出。
    • –with-target-bits=:指明要编译32位还是64位的Java虚拟机,在64位机器上也可以通过交叉编译生成32位的虚拟机。
    • –with-= :用于指明依赖包的具体路径,通常使用在安装了多个不同版本的Bootstrap JDK和依赖包的情况。其中lib的可选值包括boot-jd、freetype、cups、x、alsa、libffi、jtreg、libjpeg、giflib、libpng、lcms、zlib。
    • –with-extra-=:用于设定C、C++和Java代码编译时的额外编译器参数,其中flagtype可选值为cflags、cxxflags、ldflags,分别代表C、C++和Java代码的参数。
    • –with-conf-name=:指定编译配置名称,OpenJDK支持使用不同的配置进行编译,默认会根据编译的操作系统、指令集架构、调试级别自动生成一个配置名称,譬如"linux-x86_64-server release",如果在这些信息都相同的情况下保存不同的编译参数配置,就需要使用这个参数来自定义配置名称。
  • 以上是configure命令的部分参数,其他未介绍到的可以使用"bash configure–help"来查看,所有参数均通过以下形式使用:
		bash configure [options] 
  • 解压下载好的OpenJDK 12源码,进入解压后的源码文件夹中,打开控制台(cmd.exe)输入bash进入Bourne Again Shell环境,执行bash configure命令。
  • configure命令承担了依赖项检查、参数配置和构建输出目录结构等多项职责,如果编译过程中需要的工具链或者依赖项有缺失,命令执行后将会得到明确的提示,并且给出该依赖的安装命令,这比编译旧版OpenJDK时的"make sanity"检查要友好得多。
  • 现在编译FastDebug版、仅含Server模式的HotSpot虚拟机,命令应为:
		bash configure --enable-debug --with-jvm-variants=server
  • 初次编译校验报错:
    Cannot locate a valid Visual Studio or Windows SDK installation on disk, nor is this script run from a Visual Studio command prompt.Try setting --with-tools-dir to the VC/bin directory within the VS installation or run “bash.exe -l” from a VS command prompt and then run configure from there.
  • 报错原因是在\jdk12\build.configure-support\generated-configure.sh文件中需要调用vcvars64.bat、vcvarsx86_amd64.bat和vcvarsx86_amd64.bat,但是找不到这些文件。
  • 在Microsoft Visual Studio C++2017(VC2017)中它们在Microsoft_Visual_Studio\Community\2017\VC\Auxiliary\Build文件夹下面。
  • 报错建议使用 --with-tools-dir指定VC/bin的目录,或者在VS中使用bash命令运行configure。这里笔者选择第一个建议,使用–with-tools-dir指定VC/bin的目录。
    进入VC2017安装目录,找到VC目录,在VC目录下运行bash命令获取在bash命令下的文件路径:/cygdrive/d/Microsoft_Visual_Studio/Community/2017/VC/Auxiliary/Build
  • 带上–with-tools-dir参数重新编译校验JDK源码:
    bash configure --enable-debug --with-jvm-variants=server --with-tools-dir=“/cygdrive/d/Microsoft_Visual_Studio/Community/2017/VC/Auxiliary/Build”
  • 再次报错,原因是这个版本的构建脚本有点问题,通过–with-tools-dir来配置Visual Studio的路径不起作用,因为脚本设置的相关变量被清除了,所以需要修改jdk12\make\autoconf\toolchain_windows.m4文件(这里只做了注释,官方做法是删掉,实际上没有区别,可以忽略。这个问题是在OpenJDK12+33之后才改的,但OpenJDK12已经停止维护了,非LTS)。
		# 修改前,205VS_ENV_CMD=""
		# 修改后,205行
		#VS_ENV_CMD=""
  • 修改
  • 修改后继续带上–with-tools-dir参数编译校验,仍然报错:
    Could not find any dlls in /cygdrive/d/Microsoft_Visual_Studio/Windows_Kits/10/Redist/ucrt/DLLs/x64
  • 带着报错内容Could not find any dlls去jdk12\make\autoconf\toolchain_windows.m4文件中查找在698行发现了报错代码,从代码中可以看到$with_ucrt_dll_dir这个变量(看名称是一个文件的路径名称)就是Could not find any dlls后面半部分的报错信息。
  • 在701行发现这个$with_ucrt_dll_dir变量赋值给了UCRT_DLL_DIR变量,这个变量在714行被赋值了一个字符串地址"$CYGWIN_WINDOWSSDKDIR/Redist/ucrt/DLLs/$dll_subdir",很明显$CYGWIN_WINDOWSSDKDIR与Cygwin目录地址有关联。
  • 根据报错去Windows SDK目录下寻找/Windows_Kits/10/Redist/ucrt/DLLs/x64地址,发现这个地址中少了一级Windows SDK的版本号路径,真实路径为:\Windows_Kits\10\Redist\10.0.22621.0\ucrt\DLLs\x64。
  • 修改代码714行改为Windows SDK DLLs的真实路径,添加一级版本号10.0.22621.0(读者根据自己的真实版本号修改)路径。
  • 再次执行编译校验命令,终于通过校验,编译参数配置成功。
    bash configure --enable-debug --with-jvm-variants=server --with-tools-dir=“/cygdrive/d/Microsoft_Visual_Studio/Community/2017/VC/Auxiliary/Build”
  • 执行make clean命令清除较旧的配置:
  • 在configure命令以及后面的make命令的执行过程中,会在"build/配置名称"目录下产生如下目录结构。不常使用C/C++的读者要特别注意,如果多次编译,或者目录结构成功产生后又再次修改了配置,必须先使用"make clean"和"make dist-clean"命令清理目录,才能确保新的配置生效,编译产生的目录结构以及用途如下所示:

5.进行编译

  • 依赖检查通过后便可以输入"make images"执行整个OpenJDK编译了,这里"images"是"product-images"编译目标(Target)的简写别名,这个目标的作用是编译出整个JDK镜像,除了"product-images"以外,其他编译目标还有:
  • 执行"make images"··· ··
  • 报错在jdk12\test\hotspot\gtest\utilities\test_json.cpp文件中有乱码,导致编译失败。文件test_json.cpp是UTF-8编码,里面带有非ASCII字符集的符号,357-373行,可能是linux和windows的差异(编译器对编码处理的差异)。
  • 使用Notepat++修改文件格式。
  • 保存为Utf8-with-bom后,查找并替换☃字符为普通字符(因为是test的关系,没有处理的很谨慎,把⛄字符换成了一个字母‘T’)。
  • 执行make clean继续编译,执行"make images"··· ···
  • 继续报错,打开jdk12\src\hotspot\share\compiler\methodMatcher.cpp,在240行后面添加代码:
  		#pragma warning(disable: 4819)
 		#pragma warning(disable: 4778)
  		#pragma warning(disable: 4474)
  • 修改
    在这里插入图片描述
  • 执行make clean继续编译,执行"make images"··· ···
  • 继续报错:warning C4819: The file contains a character that cannot be represented in the current code page (936). Save the file in Unicode format to prevent data loss. 报错中涉及两个文件afblue.c和afscript.h,将两个文件格式改为UTF-8-BOM。
    • 根据报错信息找到下述文件,将格式改为UTF-8-BOM:
    • 文件路径:jdk12\src\java.desktop\share\native\libfreetype\src\autofit\afblue.c
    • 文件路径:jdk12\src\java.desktop\share\native\libfreetype\src\autofit\afscript.h
      在这里插入图片描述
    • 执行make clean继续编译,执行"make images"··· ···
  • 继续报错:warning C4819: The file contains a character that cannot be represented in the current code page (936). Save the file in Unicode format to prevent data loss. 仍然是编码问题,根据报错信息找到文件ttload.c和ttobjs.c。
    • 文件路径:jdk12\src\java.desktop\share\native\libfreetype\src\sfnt\ttload.c
    • 文件路径:jdk12/src/java.desktop/share/native/libfreetype/src/truetype/ttobjs.c
  • 执行make clean继续编译,执行"make images"··· ···
  • 继续报错:warning C4996: ‘ID2D1Factory::GetDesktopDpi’: Deprecated. Use DisplayInformation::LogicalDpi for Windows Store Apps or GetDpiForWindow for desktop apps. 原因是使用了过时的函数。
    • 文件jdk12\src\java.desktop\windows\native\common\awt\systemscale\systemScale.cpp这里使用了废弃的函数ID2D1Factory::GetDesktopDpi。
  • 添加一行代码。
		#pragma warning(disable: 4996)
  • 添加

  • 执行make clean继续编译,执行"make images"··· ···

  • 终于编译成功,图中的乱码是Java报错使用了过时的API。

  • 查看编译日志,终于编译成功,进入编译好的JDK目录jdk12\build\windows-x86_64-server-fastdebug\jdk\bin。

  • 输入java -version查看编译的JDK版本。

参考:

本文为作者(难拳)原创,转载请注明出处。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值