Java 10大优点—Part1—Java编译器


在我加入Eigenharp仪器后,从此告别了Java开发。新工作需要开发跨平台的实时音频软件,要求处理大量数据输出和低延迟。开发工作基本上使用C++完成,用到了Juce函数库和用CPython编写的一些胶水代码(glue code)。我被安排开发音乐演奏软件。在做技术选型时,我意识到了那些在Java世界里习以为常的事情(其实不那么简单)。

但有哪些事情让Java变得很棒?列出Java很棒的10个原因并不困难。你可以已经注意到了其中的一些,然而在开发选型时大声说出Java的好处是很有技巧的。下面是我钟爱Java的几个理由:

  •     Java编译器
  •     Core API
  •     开源
  •     Java内存模型
  •     高性能虚拟机(VM)
  •     字节码(Bytecode)
  •     聪明的IDE
  •     性能分析工具
  •     向后兼容性
  •     成熟与创新

我们从Java编译器开始,后面的几点会在接下来的文章中详细讨论。

Java编译器的优异之处

Java编译器可能是作为开发者遇到的第一个平台组件。在开始学Java语言时,需要用它来编译你的“Hello World”程序,通过它将你的源代码转为可执行程序。

没有字节码(bytecode)就没有Java编译器。除了字节码本身的优点(后面的文章会有讨论),这种中间形式还支持运行时JIT(即时编译)。

JVM的即时编译所向无敌

你可能认为JIT只是一种性能改进,让解析器的运行速度匹配本地程序。但实际上,JIT的速度比本地程序更快。如果像C++那样直接编译成本地程序,需要在编译时进行静态优化(static optimization)。这种做法过早地考虑了性能优化,与在发布时带有运行时监控功能的做法大相径庭。提前优化改变了很多指令,这与你在源代码中表达的内容形成了差距,程序的实际执行结果可能会与代码中的逻辑有所差别。

遇到这种情况,通常的做法是人为调整优化级别,期望以此得到可以正确运行的程序。在深入编译器细节之前,通常无法知道分析器(profiler)会对代码的执行产生怎样的影响,当前的优化级别是否正确。有时唯一可行的做法是通过变更集合(change set)及时回退到之前的工作,此时你的代码与验证过的问题之间已经不再有任何联系。随着项目日益庞大,通常优化级别会不再那么激进,这种情况通常无法找出更高的优化级别为什么会引入问题。

优化开关的“黑盒”特性使得这种情况愈加糟糕。下面是你在clang手册里可以找到的所有内容,希望你能从中幸运地发现有用的信息:

代码生成选项
-O0 -O1 -O2 -Os -Oz -O3 -Ofast -O4
请指定需要使用的优化级别
-O0 表示“没有优化”:该级别编译速度最快,并且生成的代码可调式性最佳。
-O2是中间级别的优化,可以生成最优化的代码。
-Os与-O2类似,可以额外减少代码大小。
-Oz与-Os(以及-O2)类似,但是代码大小会进一步减小。
-O3与-O2类似,但是编译过程稍长生成的代码大小也稍大(这样可以使得代码执行速度更快)。
-Ofast具备所有-O3优化功能,但是由于优化程度过大可能与一些语言标准不兼容。
在某些平台下,-O4会开启链接时优化(link-time optimization);对象文件会以LLVM bitcode文件格式存储,所有优化在链接时执行。
-O1优化位于-O0和-O2之间。

专注你的代码,而不是编译器结构

由于Java编译器的唯一工作是将源代码转为字节码,使用javac命令变得非常简单。通常你所有需要关心的就是,设置正确的classpath信息、选择兼容的VM版本、确认class文件存储的位置,所有的3个编译选项就是: -classpath、-target和-d。

C++的选项更多也更复杂。下面展示了相对简单的一个g++编译器调用。里面包括了一些项目相关的flag以及头文件-I flag,类似javac的classpath。这些都是C++的标准习惯,通常也是进行模块化和提供平台独立构建的唯一办法。

1
2
3
4
5
6
7
8
9
g++-4.2 -o tmp /obj/eigend-gpl/piagent/src/pia_buffer .os -c -arch i386
-DDEBUG_DATA_ATOMICITY_DISABLED
-DPI_PREFIX=\" /usr/pi/Python .framework /Versions/2 .5\"
-mmacosx-version-min=10.6 -ggdb -Werror -Wall -Wno-deprecated-declarations
-Wno- format -O4 -fmessage-length=0 -falign-loops=16 -msse3 -DALIGN_16
-DBUILDING_PIA -fvisibility=hidden -fPIC -Isteinberg -Ieigend-gpl /steinberg
-Ieigend-gpl -I. -I /usr/pi/Python .framework /Versions/2 .5 /include/python2 .5
-Itmp /exp
eigend-gpl /piagent/src/pia_buffer .cpp

最大的问题在于每个编译器都有不同的选项。G++与Clang不同,与C++编译器也不同,此外还有Visual Studio C++编译器等等。对常用的命令行参数,这些编译器使用不同的命名,各自支持不同版本的C++标准或标准的不同子集。此外,它们还提供特定编译选项。

如果期望获得最佳性能,你需要从每个编译器的数百个编译选项中艰难地搜寻,有时还需要具备目标硬件平台的底层知识。更糟糕的是,你需要提前做好准备期望编译选项可以正常运行在所有平台或处理器架构上,否则当程序运行失败后没有任何办法进行追踪。每次发布我都提心吊胆,因为找出客户报告中软件崩溃的原因是我的主要职责。有几次,我们不得不在同一个处理器上不断调整编译器选项,重现问题并生成稳定的二进制。

没有静态链接,也没有动态链接,只有运行时链接

当你生成本地二进制时,你可以选择如何链接自己的二进制或者模块。静态链接将所有内容打包到一个可执行文件里,这样无法独立更新函数库,也不能生成更大的二进制文件。运行时链接更容易发布,而且没有动态链接的上述问题。在实际开发中,一个大型产品单独发布静态链接生成的可执行程序是不现实的。即使你将构建拆分成多个模块或者调用第三方函数库,迟早还是需要面对动态链接问题。

动态链接非常复杂而且会引入很多麻烦。除了代码内部的可见性(private、public等),你需要单独编译自己的API,确保创建的动态链接库导出正确的符号(symbol)。另一方面,你需要为实际使用函数库的代码导入这些符号。如果使用了同一个函数库的头文件和客户端,需要在所处的编译环境下为你的API声明需要传入的正确参数。除此之外,在MacOSX、Linux、Windows上动态链接也有着不同的含义。你需要整理代码,为不同的平台使用不同的宏定义(macro)。当然,可能还需要维护一个通用的代码库(codebase)以支持所有平台。马上你就会发现,实际的编译过程中编译器会将链接应用到代码的每个声明,有时你最终使用的class结构会由编译器规定。

即使一切就绪,还需要编译很多共享函数库——尤其是在Windows平台上,每个用户都需要面对DLL版本不兼容问题。尽管在新版本的Windows上这个问题有所改善,但你的二进制还是绑定到了特定版本的DLL,一个看似很小的API变化都会让动态链接不再兼容。要解决这个问题,可以将这些DLL作为应用程序的私有绑定,或者在运行时动态加载。如果使用动态加载,需要格外注意在应用程序内部进行连接和符号解析。而实际上你不应该关心这些细节。

Java通过运行时链接绕过了所有这些问题。所有内容都是动态的,通过字节码导出的符号与包没有直接关联,基本上不需要关心链接过程。如果确实有需要,你还可以动态地加载类或方法,但是这种情况很少出现。字节码的符号表示是非常稳定的,即使class的版本不断演变之前编写的代码还是可以直接运行,除非函数库的作者故意修改API。

接下来

这是Java 10大有点第一部分。请留言或者通过tweet @gbevin联系我。下一篇会讨论Core API,相信会给你留下更深的映像。不要换台哦!

-- 扫描加关注,微信号: importnew --
Java 10大优点—Part1—Java编译器
 
原文链接: zeroturnaround 翻译: ImportNew.com 唐尤华
译文链接: http://www.importnew.com/6268.html
转载请保留原文出处、译者、译文链接和上面的微信二维码图片。]

你可能感兴趣的:(java,jvm,编译器,java优点)