最近看到一篇文章 Docker面对Java将不再尴尬:Java 10为Docker做了特殊优化 ,里面提到了java10对于docker做了一些特殊的优化。众所周知java的docker容器化支持一直以来都比较的尴尬,由于docker底层使用了cgroups来进行进程级别的隔离,虽然我们通过docker设置了容器的资源限制,但jvm虚拟机其实感知不到这里些限制。比如我们的宿主机可能是8核16G,限定docker容器为2核4G,在容器中读出来的资源可能还是8核16G,我们平时可能会来读取机器资源来做性能优化,比如核心线程数、最大线程数的设定。这对于一些程序来讲,在docker上跑可能会会带来性能损耗,所幸的是java10已经增加了这些支持,并且有jdk8兼容的计划。
想起最近工作中,在优化程序过程中发现 availableProcessors
似乎有较大性能损耗,因此对它进行了详细的了解并做了一些测试。
/** * Returns the number of processors available to the Java virtual machine. * * <p> This value may change during a particular invocation of the virtual * machine. Applications that are sensitive to the number of available * processors should therefore occasionally poll this property and adjust * their resource usage appropriately. </p> * * @return the maximum number of processors available to the virtual * machine; never smaller than one * @since 1.4 */ public native int availableProcessors(); 复制代码
jdk文档中这么写到,返回 jvm虚拟机可用核心数 。并且后面还有一段注释: 这个值有可能在虚拟机的特定调用期间更改 。我们平时对于此函数的直观印象为: 返回机器的CPU数,这个应该是一个常量值 。由此看来,可能有很大的一些误解。由此我产生了两个疑问:
这个比较好理解,顾名思义为JVM可以用来工作利用的CPU核心数。在一个多核CPU服务器上,可能安装了多个应用,JVM只是其中的一个部分,有些cpu被其他应用使用了。
返回值可变这个也比较好理解,既然多核CPU服务器上多个应用公用cpu,对于不同时刻来讲可以被JVM利用的数量当然是不同的,既然如此,那java中是如何做的呢? 通过阅读jdk8的源码,linux系统与windows系统的实现差别还比较大。
linux 实现 int os::active_processor_count() { // Linux doesn't yet have a (official) notion of processor sets, // so just return the number of online processors. int online_cpus = ::sysconf(_SC_NPROCESSORS_ONLN); assert(online_cpus > 0 && online_cpus <= processor_count(), "sanity check"); return online_cpus; } 复制代码
linux 实现比较懒,直接通过sysconf读取系统参数,_SC_NPROCESSORS_ONLN。
windows 实现 int os::active_processor_count() { DWORD_PTR lpProcessAffinityMask = 0; DWORD_PTR lpSystemAffinityMask = 0; int proc_count = processor_count(); if (proc_count <= sizeof(UINT_PTR) * BitsPerByte && GetProcessAffinityMask(GetCurrentProcess(), &lpProcessAffinityMask, &lpSystemAffinityMask)) { // Nof active processors is number of bits in process affinity mask int bitcount = 0; while (lpProcessAffinityMask != 0) { lpProcessAffinityMask = lpProcessAffinityMask & (lpProcessAffinityMask-1); bitcount++; } return bitcount; } else { return proc_count; } } 复制代码
windows系统实现就比较复杂,可以看到不仅需要判断CPU是否可用,还需要依据CPU亲和性去判断是否该线程可用该CPU。里面通过一个while循环去解析CPU亲和性掩码,因此这是一个CPU密集型的操作。
通过如上分析,我们基本可以知道这个操作是一个cpu敏感型操作,那么它的性能在各个操作系统下表现如何呢?如下我测试了该函数在正常工作何cpu满负荷工作情况下的一些表现。测试数据为执行100万次调用,统计10次执行情况,取平均值。相关代码如下:
public class RuntimeDemo { private static final int EXEC_TIMES = 100_0000; private static final int TEST_TIME = 10; public static void main(String[] args) throws Exception{ int[] arr = new int[TEST_TIME]; for(int i = 0; i < TEST_TIME; i++){ long start = System.currentTimeMillis(); for(int j = 0; j < EXEC_TIMES; j++){ Runtime.getRuntime().availableProcessors(); } long end = System.currentTimeMillis(); arr[i] = (int)(end-start); } double avg = Arrays.stream(arr).average().orElse(0); System.out.println("avg spend time:" + avg + "ms"); } } 复制代码
CPU 满负荷代码如下:
public class CpuIntesive { private static final int THREAD_COUNT = 16; public static void main(String[] args) { for(int i = 0; i < THREAD_COUNT; i++){ new Thread(()->{ long count = 1000_0000_0000L; long index=0; long sum = 0; while(index < count){ sum = sum + index; index++; } }).start(); } } } 复制代码
系统 | 配置 | 测试方法 | 测试结果 |
---|---|---|---|
Windows | 2核8G | 正常 | 1425.2ms |
Windows | 2核8G | CPU 满负荷 | 6113.1ms |
MacOS | 4核8G | 正常 | 69.4ms |
MacOS | 4核8G | CPU满负荷 | 322.8ms |
虽然两个机器的配置相差较大,测试数据比较意义不大,但从测试情况还是可以得出如下结论: