Java和C++性能大比拼

用来运行Java语言的HotSpot VM主要是用C++语言来写的,所以我们在研究JDK时不得不去学习C++这门语言。C++和Java都是面向对象的语言,所以它们常被拿来做比较。本文将从性能的角度对比分析 Java 和 C++,粗略探讨两者在峰值速度、启动时间和启动性能以及内存利用率方面的差异。

1、峰值速度

Java的C2编译器能结合运行时的信息做推断和激进优化,其峰值性能并不比C++差。

C++有接近硬件的操作能力、执行效率和低级控制在性能要求极高的领域中占据一席之地。

所以两者并不能笼统的脱离实际来说性能的高低。不过我们倒是可以列举一些影响峰值速度的点来探讨。

(1)内联

Java无论是C1还是C2编译器,内联都是重点的优化手段之一。Java类是动态加载的,HotSpot VM可以对这些新加载进来的类进行内联优化。不过目前的Java会在运行时调用native,在某些必要的情况下还会调用HotSpot VM内部的函数,这些是没办法内联的,这也是native开销大的原因之一。所以用纯Java写的一些算法也未必比调用native方法慢,比如在JDK8中并不支持Vector API,所以需要通过native来使用本地的向量化指令,但通过native调用时开销绝对不可以忽视。

Project Panama 的目标之一是

通过提供新的API来减少 JNI(Java Native Interface)的使用。

对于C++来说,动态链接库之间不能进行函数内联,否则就没办法让动态链接库单独进行更新和升级。另外动态分派中可被内联的情况也比较少。

(2)动态分派

Java默认的方法都是动态分派,而C++需要通过virtual关键字专门指定,所以Java代码中的虚方法应该比C++中的多很多才对。

HotSpot VM中的虚函数能获取的信息更多,所以优化的手段也比较多,例如通过C2对receiver的类型推导、CHA(类型继承关系分析)以及运行时对receiver的type profile,这些可能让Java方法做单态内联、双态内联、多态内联、守护内联或做内联缓存优化。

C++是纯编译语言,虽然也有一些手段对虚函数做内联,但比起Java来,可用的内联信息更少,所以内联的效果肯定不如Java好。

(3)值类型

Java不支持值类型,而C++支持。Java采用的是托管机制,其运行时动态编译编译和自动内存管理极大的降低了Java使用的门槛,不过Java不建议对底层的内存进行直接操作,所以也无法对底层的内存布局进行操作。有人将这称为导致Java和C语言之间性能差距的最后一个主要障碍。

当我们使用容器时,这种无值类型导致的性能差距可能更加明显,目前Java容器中的数组、链表不能直接保存结构,必须保存引用。所以Java数据结构和STL容器没法比。不过Java可以进行逃逸分析和标量替换,把许多临时对象的内存分配到栈上,这在一定程序上弥补了没有值类型的短板。

(4)额外开销

HotSpot VM的托管技术让它有许多额外的开销,如GC和编译开销。在程序运行起来后,一切GC和编译相关的问题都是Java特有的,C++并不存在这种问题。

Java有许多额外检查,比如GC安全点检查、内存越界检查、类型检查等,这是我们为托管技术和安全性付出的必要代价。

Java中每个基本类型都有对应的包装类,这在一些上下文中需要频繁进行装箱和拆箱操作,C++并不存在这种问题。

总的来说,C++由于可操控性太强,只要代码写的足够好,额外开销是非常少的。对于Java来说,我们不得不做很多的让步。

2、启动时间和启动性能

Java虚拟机的动态特性让它可以根据需要对类进行加载。Java是面向对象的语言(Slogan:everything is object),类数量通常比较大且类加载还是IO操作,严重拖累了Java的启动速度,目前主要是通过CDS(后序的AppCDS、Dynamic CDS)来解决。

HotSpot VM为了照顾启动和峰值性能,默认采用的是分层编译,所以需要在系统运行一段时间后才能逐步由解释执行转化为解释执行 + 编译执行,这也意味着系统需要经过一段时间的预热才能达到性能巅峰

C++ 是静态编译的语言,代码在编译时直接生成机器码,启动即巅峰,其启动时间和启动性能应该要完胜Java才对。

不过随着云原生的到来,Java的启动时间慢和启动性能低的缺点被放大,Java开发者也在努力解决这些问题。例如:

(1)通过CDS来优化类加载的速度,这样可以显著减少类加载、解析和初始化的时间

(2)JDK 9开始提供工具jaotc,同时GraalVM的Native Image都是可以通过静态编译,极大地提升服务的启动速度的方式

(3)CRaC(协调恢复检查点)可以把预热后的 JVM 保存下来,然后快速启动。也就是快照保存和恢复。

更多的启动相关的优化可以看看Project Leyden项目,这个项目专注于缩短 Java 应用程序的启动时间。

另外,Azul JDK的Platform Prime版本提供了Ready Now和Ready Now Orchestrator作为解决 Java 预热问题的解决方案,阿里也在启动上做了一些优化和探索。

3、内存利用率

之前我写过 "历数Java虚拟机GC的种种缺点" 文章,文章中提到过额外分配和浪费的内存,如To Survivor不能用来进行内存分配,卡表需要额外的内存,Java的延迟回收让它白白点用了一段时间内存。

C++具有接近硬件的操作能力,以HotSpot VM为例,它通过重载new和delete运算符、通过C++中的资源获取即初始化技术将资源的分配和释放与对象的生命周期进行绑定等手段,实现了对内存的精确控制。

由于Java对内存的利用率低,所以和C++相比,它的峰值内存和平时内存占用都会显著高于C++程序。

Java的物理内存归还,Lilliput缩小Java对象头、CDS让多个虚拟机共享类元数据以及Valhalla项目对Java值类型的支持,都能够提高Java内存的利用率,减少浪费。

更多文章可访问:JDK源码剖析网