Runtime.availableProcessors() 分析
最近看到一篇文章Docker面对Java将不再尴尬:Java 10为Docker做了特殊优化,里面提到了java10对于docker做了一些特殊的优化。众所周知java的docker容器化支持一直以来都比较的尴尬,由于docker底层使用了cgroups来进行进程级别的隔离,虽然我们通过docker设置了容器的资源限制,但jvm虚拟机其实感知不到这里些限制。比如我们的宿主机可能是8核16G,限定d
最近看到一篇文章Docker面对Java将不再尴尬:Java 10为Docker做了特殊优化,里面提到了java10对于docker做了一些特殊的优化。众所周知java的docker容器化支持一直以来都比较的尴尬,由于docker底层使用了cgroups来进行进程级别的隔离,虽然我们通过docker设置了容器的资源限制,但jvm虚拟机其实感知不到这里些限制。比如我们的宿主机可能是8核16G,限定docker容器为2核4G,在容器中读出来的资源可能还是8核16G,我们平时可能会来读取机器资源来做性能优化,比如核心线程数、最大线程数的设定。这对于一些程序来讲,在docker上跑可能会会带来性能损耗,所幸的是java10已经增加了这些支持,并且有jdk8兼容的计划。
想起最近工作中,在优化程序过程中发现availableProcessors
似乎有较大性能损耗,因此对它进行了详细的了解并做了一些测试。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
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数,这个应该是一个常量值。由此看来,可能有很大的一些误解。由此我产生了两个疑问:
- 1、何为JVM可用核心数?
- 2、为何返回值可变?它是如何工作的?
JVM可用核心数
这个比较好理解,顾名思义为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 |
虽然两个机器的配置相差较大,测试数据比较意义不大,但从测试情况还是可以得出如下结论:
- windows与类linux系统性能差异较大,与具体实现有关
- CPU密集型计算对于该函数性能有较大的影响
- 整体上讲,该函数性能还是比较可以接受的,最长的那次为windows CPU满负荷下 也仅为6us。linux系统下可以降到ns级别。
总结
- 日常工作中,并不太需要注意该函数的调用性能负荷
- 如需使用一般定义成静态变量即可,对于cpu敏感性程序来讲,可以通过类似缓存的策略来周期性获取该值
- 工作中的性能问题可能并不是该函数导致,可能是其他问题导致
更多推荐
所有评论(0)