System.arraycopy为什么快

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangyangzhizhou/article/details/79504818

前言

在 Java 编程中经常会遇到数组拷贝操作,一般会有如下四种方式对数组进行拷贝。
* for遍历,遍历源数组并将每个元素赋给目标数组。
* clone方法,原数组调用clone方法克隆新对象赋给目标数组,更深入的克隆可以看之前的文章《从JDK角度看对象克隆》。
* System.arraycopy,JVM 提供的数组拷贝实现。
* Arrays.copyof,实际也是调用System.arraycopy

for遍历

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

arraycopy的使用

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

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

arraycopy方法

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

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

关于@HotSpotIntrinsicCandidate

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

本地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

基本类型和普通类型

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

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--;
    }
  }
}

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

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--;
    }
  }
}

总结

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

————-推荐阅读————

我的2017文章汇总——机器学习篇

我的2017文章汇总——Java及中间件

我的2017文章汇总——深度学习篇

我的2017文章汇总——JDK源码篇

我的2017文章汇总——自然语言处理篇

我的2017文章汇总——Java并发篇

——————广告时间—————-

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以购买。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

欢迎关注:

这里写图片描述

没有更多推荐了,返回首页