购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.2 JVM外部有利“辅助”

3.1节讲述JVM的7个核心组成部分,程序的整体运作和实现主要依靠它们。除此之外,JVM还有一些其他较为特殊的功能,如直接内存申请、编译执行、优化和性能提升(方法内联和逃逸分析等),笔者将以上这些功能称为外部的“辅助”。“外部”是为了区分内部核心的七大组件,而“辅助”代表基于核心组件之外做了更多的优化和扩展。

此外,需要注意的是这些外部的“辅助”并不代表着不重要,它们在很多情况下都必须存在且缺一不可。

注意:除本节介绍的内容之外,仍存在其他相关的功能组件一同支撑着JVM的运行生态,读者可以自行收集资料进行学习。

3.2.1 直接内存申请

直接内存不属于直接出现在JVM运行时的数据区,同时其也并非是在虚拟机中明确定义的直接存储管理范围,而是直接向操作系统提出申请直接存储区域。直接内存在软件开发工作中会频繁地被使用。

JVM对堆内存管理分配和调度的处理方式完全是自动化的,但是直接内存却需要手动执行调度管理方式。例如,在进行NIO(New Input/Output)分配数据通道(Channel)和缓冲区(Buffer)时,引入基于数据通道与数据缓冲区方式的I/O实现,通过一个Native函数库直接进行分配堆外内存,进而可以使用在Java堆中的DirectByteBuffer类对象,作为这块堆外内存的引用。

让我们来看一下如何使用ByteBuffer工厂方法分配直接内存,如下所示:

ByteBuffer底层分配内存的方法源码:

可以看出底层采用了DirectByteBuffer对象,在此深入了解其内部原理。其中构造器会进行内存分配,源码如下所示:

通过上面的代码能够明显看出,底层申请使用的Unsafe类的Native方法是申请直接内存,使用直接存储内存能够明显提升类的性能,因为这样减少了类在直接内存和堆内存中的数据来回转换复制的操作。

直接内存对应的内存分配、内存数据读取和内存数据写入的成本相对较高(主要属于操作系统层级的内存控制),但其好处是读写系统性能较高。(由于直接读取存储方式不需要经JVM解释器对其进行地址映射及转化为一个操作系统真正的物理地址,因此直接读取内存速度会比处理JVM堆内存快许多。)

此外,直接内存的申请和回收比较复杂,因此通常建议开发者使用堆内存时,由JVM进行内存管理,而不是直接内存。

如图3.3所示,直接内存无须进行地址映射和转换,可以直接操作系统内存。

图3.3 直接内存与堆内存的加载读取对比

直接内存并不受限于JVM的内存回收速度(依旧可能存在回收内存溢出问题),它直接受限于操作系统的共享物理内存。默认情况下,直接内存几乎没有回收限制(在没有超过本机物理内存的前提下)。

由于没有JVM协助管理直接内存,因此必须由用户自行管理堆外存储,以避免内存泄漏;同时又要防止发生Full GC,造成物理内存被耗光。此时应确定直接内存的最大值,使用JVM参数“-XX:MaxDirectMemorySize”进行控制。如果超过阈值,可以调用System.gc方法触发一次Full GC,清理那些不再使用的直接内存,如下代码所示:

综上,直接内存的特性如下:

(1)减轻了对JVM垃圾处理的压力,堆外内存直接被操作系统管理,这样可以保留一些较小的堆内内存,降低垃圾收集对应用环境的影响。

(2)提高了磁盘及内存的IO读写速率,堆内管理内存在JVM堆中,属于“用户态”;而堆外管理内存则为操作系统堆中管理内存空间,属于“内核态”。因此,在从堆内向硬盘写入大量数据时,数据将被首先复制到堆外,写在存储中,属于内核中的缓冲区,接着由操作系统调度,写数据到硬盘。由此可以看出,使用堆外内存可以减少很多复制步骤。

(3)回收直接内存时,系统情况比较复杂,所以成本会很高,很多时候需要Full GC才能完全进行回收。

(4)分配直接内存时,需要进行一次“用户态”到“内核态”的切换操作,从而用“用户态”的句柄引用“内核态”的内存空间。

3.2.2 编译执行与解释执行

JVM(以HotSpot为例)将翻译字节码文件的方法分为两种,分别是解释执行方式和编译执行方式。

解释执行方式是指边翻译指令边解释运行,但如果JVM发现一个翻译方法被频繁地多次调用,就会将该方法的执行指令提前编译好,在下次运行该方法时直接调用而不需要解释,这种方法称为热点方法。由此可见,编译器的执行策略以一个方法为基本执行单位,称为即时编译(JIT编译器)。

除了JIT编译器以外,还存在另一种编译器,即AOT编译器,其会在程序运行之前就预先产生机器码,从而在程序运行时直接执行。通常使用它时先将一个方法中的所有字节码全部编译成机器码之后再执行。

解释执行的优点是启动效率快,不需要等待编译,翻译一部分就可以执行一部分;缺点是整体执行速度较慢。

编译执行的优点是在编译完成后,实际的运行速度更快;缺点是需要等待编译过程,并且对于JVM的动态性和跨平台性而言,也降低了适配性和兼容性。

目前HotSpot VM中默认采用解释执行和编译执行二者共存的运作处理方式,其先解释执行一系列字节机器码,然后将其中的一些热点代码(多次解释执行、循环等)直接编译成可执行的机器码,下次不必再次进行解释,从而可以使其更高速地运行。

在实际生产环境中,并不是所有的Java应用程序都选择了即时编译的工作模式。如果服务端对代码进行修改的频率不是很高,不如花点儿时间去进行编译,在提高程序的执行效率方面会收到意想不到的效果。

3.2.3 运行进化及升华

就业务的应用角度来说,服务端与软件用户端对代码的运行处理速率与软件启动运行速度的需求往往是不相同的。

例如,对移动端的应用程序而言,如果用户期望程序启动速率更快,那么服务端的应用程序就可能对程序的运行速率有较高的需求,因此在Java 7时HotSpot引入了分层编译器的实现方法,并推出了即时编译器:C1编译器和C2编译器。

C1编译器又称为Client编译器,主要面向对启动时间有一定需求的服务。在每次编译这个时间段,其优化时的策略相对简单,但重点可能会聚焦在对内联源代码执行优化、去虚拟性优化、冗余消除等。C2编译器也可以称为Server编译器,主要面向对执行性能有一定需求的服务,但由于编译优化时间长且每次优化时的策略繁杂,因此主要还是面向对逃逸分析性能的更深层次优化,如栈上分配、标量替换、同步消除等。具体在执行时可以先选择C1编译器,而热点方法会被C2编译器进一步重新编译优化。

即时编译运行速度很快,但如果完全使用即时编译器,JVM将无法掌握所有程序执行时的信息,将导致JVM无法对代码进行很好的优化。另外,如果先进行解释执行的话,将会执行全部代码,但是实际上JVM会记录执行过程中速度过慢且待进一步优化的代码,并针对代码的执行状况做出相关的优化,不仅仅只是编译成机器码。

代码基本遵循二八定律,即80%的热代码不会耗费虚拟机过多的计算资源,而剩下20%的热点代码要耗费虚拟机80%的计算资源。如果编译完所有的机器码并保存在硬盘/内存上,则会占据相当大的空间,因此大部分代码根本不需要消耗很多资源。 e+Gd++jzQggIQbGs1H7H8arQWNyV7pVIpLKV+HLiczKaBEL+pysYQrhHfAGL9DYF

点击中间区域
呼出菜单
上一章
目录
下一章
×