前言
在 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 分别为 TypeArrayKlass 和 ObjArrayKlass
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 是更加高效的数组越大体现地越明显
来源: https://juejin.im/post/5aa32725f265da2373140df3