【java】System.arraycopy为什么快

387 篇文章 481 订阅 ¥19.90 ¥99.00

在这里插入图片描述

1.概述

转载:System.arraycopy为什么快

作者:RednaxelaFX 链接:知乎大佬

转载这篇文章的原因是,我看Java源码,发现有好多地方用到了System.arraycopy,比如hashMap,当时很纳闷的是,为何改变一个值就要拷贝,而且数据量那么大,不会消耗性能吗?为啥不是改变一下数据地址之类的。

然后就去研究了一下System.arraycopy为什么快

2.前言

在 Java 编程中经常会遇到数组拷贝操作,一般会有如下四种方式对数组进行拷贝。

  1. for遍历,遍历源数组并将每个元素赋给目标数组。
  2. clone方法,原数组调用clone方法克隆新对象赋给目标数组,更深入的克隆可以看之前的文章《从JDK角度看对象克隆》。
  3. System.arraycopy,JVM 提供的数组拷贝实现。
  4. Arrays.copyof,实际也是调用System.arraycopy。

2.1 for遍历

这种情况下是在 Java 层编写 for 循环遍历数组每个元素并进行拷贝,如果没有被编译器优化,它对应的就是遍历数组操作的字节码,执行引擎就根据这些字节码循环获取数组的每个元素再执行拷贝操作

2.2 clone方法

2.3 arraycopy的使用

使用很简单,比如如下方式进行数组拷贝。

int size = 10000;
int[] src = new int[size];
int[] des = new int[size];
System.arraycopy(src, 0, des, 0, size);

2.3.1 arraycopy方法

该方法用于从指定源数组中进行拷贝操作,可以指定开始位置,拷贝指定长度的元素到指定目标数组中。该方法是一个本地方法,声明如下:

@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

2.3.2 关于@HotSpotIntrinsicCandidate

这个注解是 HotSpot VM 标准的注解,被它标记的方法表明它为 HotSpot VM 的固有方法, HotSpot VM 会对其做一些增强处理以提高它的执行性能,比如可能手工编写汇编或手工编写编译器中间语言来替换该方法的实现。虽然这里被声明为 native 方法,但是它跟 JDK 中其他的本地方法实现地方不同固有方法会在 JVM 内部实现,而其他的会在 JDK 库中实现。在调用方面,由于直接调用 JVM 内部实现,不走常规 JNI lookup,所以也省了开销

2.3.3 本地arraycopy方法

Java 的 System 类有个静态块在类加载时会执行,它对应执行了 registerNatives 本地方法。

public final class System {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

而在对应的 System.c 中的 Java_java_lang_System_registerNatives方法如下,可以看到有三个本地方法绑定到 JVM 的固有方法了,其中一个就是 arraycopy,它对应的函数为(void *)&JVM_ArrayCopy

JNIEXPORT void JNICALL
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}

#define OBJ "Ljava/lang/Object;"
static JNINativeMethod methods[] = {
    {"currentTimeMillis", "()J",              (void *)&JVM_CurrentTimeMillis},
    {"nanoTime",          "()J",              (void *)&JVM_NanoTime},
    {"arraycopy",     "(" OBJ "I" OBJ "II)V", (void *)&JVM_ArrayCopy},
};

那么通过以上就将arraycopy方法绑定到下面的JVM_ArrayCopy函数,前面的逻辑主要用于检查源数组和目标数组是否为空,为空则抛空指针;接着分别将源数组对象和目标数组对象转换成arrayOop,即数组对象描述,assert用于判断它们是否为对象;最后的s->klass()->copy_array才是真正的数组拷贝操作。

JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
                               jobject dst, jint dst_pos, jint length))
  JVMWrapper("JVM_ArrayCopy");
  // Check if we have null pointers
  if (src == NULL || dst == NULL) {
    THROW(vmSymbols::java_lang_NullPointerException());
  }
  arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
  arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
  assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
  assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
  // Do copy
  s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END

2.3.4 基本类型和普通类型

上面说到的通过s->klass()->copy_array完成拷贝操作,处理过程根据Java的不同类型其实有不同的处理,数组根据里面元素类型可分为基本类型和普通类型,对应到 JVM 分别为TypeArrayKlassObjArrayKlass

2.3.4.1 TypeArrayKlass

这里将一些校验源码去掉,留下核心代码,这里因为涉及到内存中指针的移动,所以为了提高赋值操作的效率将起始结束位置转成char*,log2_element_size就是计算数组元素类型长度的log值,后面通过位移操作快速计算位置。而array_header_in_bytes计算第一个元素的偏移。

void TypeArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d, int dst_pos, int length, TRAPS) {
  ....
  int l2es = log2_element_size();
  int ihs = array_header_in_bytes() / wordSize;
  char* src = (char*) ((oop*)s + ihs) + ((size_t)src_pos << l2es);
  char* dst = (char*) ((oop*)d + ihs) + ((size_t)dst_pos << l2es);
  Copy::conjoint_memory_atomic(src, dst, (size_t)length << l2es);
}

接着到Copy::conjoint_memory_atomic函数,这个函数的主要逻辑就是判断元素属于哪种基本类型再调用各自的函数因为已经有起始和结尾的指针,所以可以根据不同类型进行快速的内存操作。这里以整型类型为例,将调用Copy::conjoint_jints_atomic函数。

void Copy::conjoint_memory_atomic(void* from, void* to, size_t size) {
  address src = (address) from;
  address dst = (address) to;
  uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;

  if (bits % sizeof(jlong) == 0) {
    Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong));
  } else if (bits % sizeof(jint) == 0) {
    Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint));
  } else if (bits % sizeof(jshort) == 0) {
    Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort));
  } else {
    // Not aligned, so no need to be atomic.
    Copy::conjoint_jbytes((void*) src, (void*) dst, size);
  }
}

conjoint_jints_atomic函数主要是调用pd_conjoint_jints_atomic函数,该函数在不同的操作系统有自己的实现,这里看下windows_x86的实现,

static void conjoint_jints_atomic(jint* from, jint* to, size_t count) {
    assert_params_ok(from, to, LogBytesPerInt);
    pd_conjoint_jints_atomic(from, to, count);
  }

主要逻辑是分成两种情况复制:向前复制和向后复制并且是通过指针遍历数组来赋值,这里进行的是值拷贝,有些人称之为所谓的“深拷贝”。

static void pd_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
  if (from > to) {
    while (count-- > 0) {
      // Copy forwards
      *to++ = *from++;
    }
  } else {
    from += count - 1;
    to   += count - 1;
    while (count-- > 0) {
      // Copy backwards
      *to-- = *from--;
    }
  }
}

对于long、short、byte等类型也是做类似的处理,但在某些操作系统的某些cpu架构上会使用汇编来实现。

2.3.4.2 ObjArrayKlass

再看普通类型对象作为数组元素时候的拷贝操作,这里将一些校验源码去掉,留下核心代码。UseCompressedOops标识表示对 JVM 中Java对象指针压缩,主要表示用32位还是64位作为对象指针。这里忽略它,直接看未压缩的情况,即会调用do_copy<oop>函数。

void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
                               int dst_pos, int length, TRAPS) {
  ...
  if (UseCompressedOops) {
    narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
    narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
    do_copy<narrowOop>(s, src, d, dst, length, CHECK);
  } else {
    oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
    oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
    do_copy<oop> (s, src, d, dst, length, CHECK);
  }
}

这块代码较长,同样地,我去掉一部分代码,留下能说明问题的一小部分代码。这里会进行s==d的判断是因为源数组和目标数组可能是相等的,而如果不相等的情况下则要判断源数组元素类型是否和目标数组元素类型一样,如果一样的话处理也做类似处理,另外这里还添加了是否为子类的判断。以上两种情况核心赋值算法都是Copy::conjoint_oops_atomic

template <class T> void ObjArrayKlass::do_copy(arrayOop s, T* src,
                               arrayOop d, T* dst, int length, TRAPS) {

  BarrierSet* bs = Universe::heap()->barrier_set();
  if (s == d) {
    bs->write_ref_array_pre(dst, length);
    Copy::conjoint_oops_atomic(src, dst, length);
  } else {
    Klass* bound = ObjArrayKlass::cast(d->klass())->element_klass();
    Klass* stype = ObjArrayKlass::cast(s->klass())->element_klass();
    if (stype == bound || stype->is_subtype_of(bound)) {
      bs->write_ref_array_pre(dst, length);
      Copy::conjoint_oops_atomic(src, dst, length);
    } else {
      ...
    }
  }
  bs->write_ref_array((HeapWord*)dst, length);
}

该函数也跟操作系统和cpu架构相关,这里看windows_x86的实现,很简单也是直接通过指针遍历赋值,oop是JVM层的对象类,而且该类也没有重写operator=操作符的,默认情况下是拷贝地址的,所以它们还是指向同一块内存,这反应到 Java 层也是这样的。即所谓的“浅拷贝”。

static void conjoint_oops_atomic(oop* from, oop* to, size_t count) {
    pd_conjoint_oops_atomic(from, to, count);
}
static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
  if (from > to) {
    while (count-- > 0) {
      *to++ = *from++;
    }
  } else {
    from += count - 1;
    to   += count - 1;
    while (count-- > 0) {
      // Copy backwards
      *to-- = *from--;
    }
  }
}

4.案例

4.1 System.arrayCopy与for对比

package JCF.ArrayList;
 
import java.util.Date;
 
public class ArrayCopyCompare {
    public static void main(String[] args) {
        int length = 1000000;
        //init
        System.out.println("array length : "+length);
        int[] array = new int[length];
        for(int i = 0 ; i < array.length ; i ++){
            array[i] = i;
        }
         
        //use method by system
        long begin1 = new Date().getTime();
        int[] arrayCopyBySystem = new int[length];
        System.arraycopy(array, 0, arrayCopyBySystem, 0, array.length);
        long end1 = new Date().getTime();
        System.out.println("use time by system method : "+(end1 - begin1));
         
        //use method normal
        long begin2 = new Date().getTime();
        int[] arrayCopyByNormal = new int[length];
        for(int i = 0 ; i < arrayCopyByNormal.length ; i ++){
            arrayCopyByNormal[i] = array[i];
        }
        long end2 = new Date().getTime();
        System.out.println("use time by narmal method : " +(end2 - begin2));
    }
}

常用的数组复制方法就按照循环赋值的防范, 下面列举有不多长度数组的效率比较:

array length : 10000
use time by system method : 0
use time by narmal method : 0

array length : 100000
use time by system method : 0
use time by narmal method : 2

array length : 1000000
use time by system method : 3
use time by narmal method : 5

array length : 10000000
use time by system method : 25
use time by narmal method : 29

array length : 100000000
use time by system method : 279
use time by narmal method : 293

所以在数组长度不大时,两者效率可以忽略, 但是数组长度变大时,System.arrayCopy()还是有效率优势的, 但是感觉效率提升没有想象的大

4.2 copy2

package com.java.data.array;

/**
 * @author: chuanchuan.lcc
 * @date: 2021-01-11 20:51
 * @modifiedBy: chuanchuan.lcc
 * @version: 1.0
 * @description:
 */
public class ArrayCopyDemo1 {

    public static final int size = 1000000;

    public static void copyByArrayCopy(String[] strArray){
        Long startTime = System.currentTimeMillis();
        String[] destArray = new String[size];
        System.arraycopy(strArray,0,destArray,0,strArray.length);
        //printArr(destArray);
        Long endTime = System.currentTimeMillis();
        System.out.println("copyByArrayCopy cost time is "+(endTime-startTime));
    }

    public static void copyByLoop(String[] strArray){
        Long startTime = System.currentTimeMillis();
        String[] destArray = new String[size];
        for(int i = 0;i<strArray.length;i++){
            destArray[i] = strArray[i];
        }
        //printArr(destArray);
        Long endTime = System.currentTimeMillis();
        System.out.println("copyByLoop cost time is "+(endTime-startTime));
    }

    public static void copyByClone(String[] strArray){
        Long startTime = System.currentTimeMillis();
        String[] destArray = strArray.clone();
        Long endTime = System.currentTimeMillis();
        System.out.println("copyByClone cost time is "+(endTime-startTime));
    }

    public static void main(String args[]){
        String arr1[] = new String[size];
        for(int i=0;i<size;i++){
            arr1[i] = "this is a test"+i;
            //arr1[i] = "shishangzhiyoumamahaoyouamdehaizixiangkuaibaotoujinmamadehuaibaoxingfuxiangbuliaoshishangzhiyoumamahaomeimadehaizixianggencaolikaimamadehuaibaoxingfunalizhaoshishangzhiyoumamahaomeimadehaizibuzhidaoyaoshitazhidaomengliyehuixiao"+i;

        }
        String arr2[] = new String[size];
        for(int i=0;i<size;i++){
            arr2[i] = "this is a test"+i;
            //arr2[i] = "shishangzhiyoumamahaoyouamdehaizixiangkuaibaotoujinmamadehuaibaoxingfuxiangbuliaoshishangzhiyoumamahaomeimadehaizixianggencaolikaimamadehuaibaoxingfunalizhaoshishangzhiyoumamahaomeimadehaizibuzhidaoyaoshitazhidaomengliyehuixiao"+i;

        }
        String arr3[] = new String[size];
        for(int i=0;i<size;i++){
            arr3[i] = "this is a test"+i;
            //arr3[i] = "shishangzhiyoumamahaoyouamdehaizixiangkuaibaotoujinmamadehuaibaoxingfuxiangbuliaoshishangzhiyoumamahaomeimadehaizixianggencaolikaimamadehuaibaoxingfunalizhaoshishangzhiyoumamahaomeimadehaizibuzhidaoyaoshitazhidaomengliyehuixiao"+i;

        }
        copyByClone(arr1);
        copyByLoop(arr2);
        copyByArrayCopy(arr3);

    }
    public static void printArr(String[] strArray){
        for(String str:strArray){
            System.out.println(str);
        }
    }
}

测试结果如下

数据量:1000000
copyByClone cost time is 2
copyByLoop cost time is 9
copyByArrayCopy cost time is 1

数据量:5000000
copyByClone cost time is 19
copyByLoop cost time is 27
copyByArrayCopy cost time is 12


5.总结

System.arraycopy为 JVM 内部固有方法,它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。数组越大体现地越明显。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值