实验代码
public class ArrayCopy {
private static void printArray(Object[] arr) {
for (Object obj : arr) {
System.out.println(obj.toString());
}
}
private static class User {
String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "name: " + name;
}
}
public static void main(String[] args) {
int[] intvalues = new int[]{1000, 2000, 3000, 4000, 5000, 6000};
int[] intcopy = new int[intvalues.length];
System.arraycopy(intvalues, 0, intcopy, 0, intvalues.length);
intvalues[0] = 400;
System.out.println("intcopy:" + Arrays.toString(intcopy));
Integer[] values = new Integer[]{1000, 2000, 3000, 4000};
Integer[] copy = new Integer[values.length];
System.arraycopy(values, 0, copy, 0, values.length);
for (int i = 0; i < values.length; i++) {
System.out.printf("values == copy : %s\n", values[i] == copy[I]);
}
values[0] = new Integer(990);
System.out.println("values:" + Arrays.toString(values));
System.out.println("copy:" + Arrays.toString(copy));
User[] array = { new User("aa"), new User("bb") };
User[] targetArr = new User[array.length];
System.arraycopy(array, 0, targetArr, 0, targetArr.length);
printArray(targetArr);
array[0].name = "dd";
printArray(targetArr);
array[0] = new User("mm");
printArray(targetArr);
}
}
上面代码输出的结果如下
//int value array
intcopy:[1000, 2000, 3000, 4000, 5000, 6000]
//Integer array
values == copy : true
values == copy : true
values == copy : true
values == copy : true
values:[990, 2000, 3000, 4000]
copy:[1000, 2000, 3000, 4000]
//User array
name: aa
name: bb
name: dd
name: bb
name: dd
name: bb
调试过程
使用Xcode来调试jdk8,先关注原生类型int的调试过程
栈一红框内会将src、dst等java对象转换为jvm中的oop对象,这里主要是typeArrayOop
和objArrayOop
;
红色区域判断抛异常,栈二中走到了typeArrayClass.cpp
文件中的copy_array
方法
这里有几个分支,不同的类型走不同的处理方式
栈五栈四省略了,就是一个简单的断言加逻辑跳转
栈六到这里开始就是汇编代码了,于是乎在hotspot代码中搜索函数_Copy_conjoint_jshorts_atomic
找到不同平台的实现,继续向下看
找到了核心代码,如图所示
根据调用栈来判断,int、short等基本类型的复制在jvm中的处理类是TypeArrayKlass
,我们可以在hotspot/src/share/vm/oops
里面找到
我们可以在TypeArrayKlass.hpp
文件中找到相关注释,typeArrayOop就是用于基本类型的数组描述;
众所周知,声明数组时int[] values = new int[]{1000, 2000, 3000, 4000}
,会在java栈中创建个引用对象values,指向堆中的数组对象;
当调用System.arraycopy
,核心的代码类似于下面这段函数;
void _Copy_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
if (from > to) {
jint *end = from + count;
while (from < end)
*(to++) = *(from++);
}
else if (from < to) {
jint *end = from;
from += count - 1;
to += count - 1;
while (from >= end)
*(to--) = *(from--);
}
}
因为数组在内存中是顺序排列的,所以通过from++
移动偏移指针来获取数组中的数据,在c语言中数组的头元素可以用from
表示,而数组中第二个元素可以用from+2
表示;
可以看到,*(to++) = *(from++);
或者*(to--) = *(from--);
的含义就是将from数组的每个元素所指向的地址(对象)分别赋值给to;
所以说当我们执行values[0] = 900;
,是将values[0]指针指向的值设置为900,对于基本类型,不存在任何引用关系和所谓的深浅拷贝;
所以会输出
values:[990, 2000, 3000, 4000]
copy:[1000, 2000, 3000, 4000]
继续回到源码中可以看到_Copy_conjoint_jlongs_atomic
方法处理和处理复制short和int类型有些不一样;
void _Copy_conjoint_jlongs_atomic(jlong* from, jlong* to, size_t count) {
if (from > to) {
jlong *end = from + count;
while (from < end)
os::atomic_copy64(from++, to++);
}
else if (from < to) {
jlong *end = from;
from += count - 1;
to += count - 1;
while (from >= end)
os::atomic_copy64(from--, to--);
}
}
是通过atomic_copy64
方法进行copy的,我们知道32位的java虚拟机(这里指hotspot)处理long、double
这种占64位(8字节)的类型时,并不能保证是原子性的,这里也是一个体现;
// Atomically copy 64 bits of data
static void atomic_copy64(volatile void *src, volatile void *dst) {
#if defined(PPC32)
double tmp;
asm volatile ("lfd %0, 0(%1)\n"
"stfd %0, 0(%2)\n"
: "=f"(tmp)
: "b"(src), "b"(dst));
#elif defined(S390) && !defined(_LP64)
double tmp;
asm volatile ("ld %0, 0(%1)\n"
"std %0, 0(%2)\n"
: "=r"(tmp)
: "a"(src), "a"(dst));
#else
*(jlong *) dst = *(jlong *) src;
#endif
}
找到atomic_copy64
的源码,通常而言现在我们都是使用64位的虚拟机,所以复制的操作应该是这行代码*(jlong *) dst = *(jlong *) src
,同样是指针赋值;
// bytes, conjoint, atomic on each byte (not that it matters)
static void conjoint_jbytes_atomic(void* from, void* to, size_t count) {
pd_conjoint_bytes(from, to, count);
}
static void pd_conjoint_bytes(void* from, void* to, size_t count) {
(void)memmove(to, from, count);
}
对于bytes类型的复制则是调用系统函数memmove
,实现其实和上面的差不多;
对于引用类型比如User
和Integer
等,实现方法则是在ObjArrayKlass
类中的copy_array
方法中,可以观察其函数栈;
把核心复制操作do_copy
的代码贴出来
//oop或narrowoop取决于我们的压缩操作。
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 {
// 这里必须确保src和dst内的元素是一致的
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 {
// 类型不一致,需要单独检查子类型
T* from = src;
T* end = from + length;
for (T* p = dst; from < end; from++, p++) {
// 慢操作
T element = *from;
bool element_is_null = oopDesc::is_null(element);
oop new_val = element_is_null ? oop(NULL)
: oopDesc::decode_heap_oop_not_null(element);
if (element_is_null ||
(new_val->klass())->is_subtype_of(bound)) {
bs->write_ref_field_pre(p, new_val);
*p = element;
} else {
const size_t pd = pointer_delta(p, dst, (size_t)heapOopSize);
assert(pd == (size_t)(int)pd, "length field overflow");
bs->write_ref_array((HeapWord*)dst, pd);
THROW(vmSymbols::java_lang_ArrayStoreException());
return;
}
}
}
}
bs->write_ref_array((HeapWord*)dst, length);
}
可以看到,对于引用类型的arraycopy操作代码还是有点多的,需要检查源数组和目标数组的类型是否一致,如果不一致,进行copy操作是比较耗时的;
在类型一致的情况下,走的是以下流程
static void conjoint_oops_atomic(oop* from, oop* to, size_t count) {
assert_params_ok(from, to, LogBytesPerHeapOop);
pd_conjoint_oops_atomic(from, to, count);
}
static void pd_conjoint_oops_atomic(oop* from, oop* to, size_t count) {
#ifdef AMD64
assert(BytesPerLong == BytesPerOop, "jlongs and oops must be the same size");
_Copy_conjoint_jlongs_atomic((jlong*)from, (jlong*)to, count);
#else
assert(HeapWordSize == BytesPerOop, "heapwords and oops must be the same size");
// pd_conjoint_words is word-atomic in this implementation.
pd_conjoint_words((HeapWord*)from, (HeapWord*)to, count);
#endif // AMD64
}
void _Copy_conjoint_jlongs_atomic(jlong* from, jlong* to, size_t count) {
if (from > to) {
jlong *end = from + count;
while (from < end)
os::atomic_copy64(from++, to++);
}
else if (from < to) {
jlong *end = from;
from += count - 1;
to += count - 1;
while (from >= end)
os::atomic_copy64(from--, to--);
}
}
static void pd_conjoint_words(HeapWord* from, HeapWord* to, size_t count) {
memmove(to, from, count * HeapWordSize);
}
最后执行的函数是和基本类型一样,属于指针赋值;
所以如果数组中保存的对象是User
对象,User
对象内有个String
类型的字段name
,那么当我们执行array[0].name = "dd";
时,是将User(name:aa)
对象中name指向的常量改成了dd
;
所以打印values和copy时都能看到dd字符;当执行array[0] = new User("mm");
时和上面Integer类似,是将values[0]指向新建的User对象,copy是不会受到影响的所以打印输出不变;对于引用类型,数组内的对象是浅拷贝;
结论
System.arraycopy
在JDK里面大量的使用,jvm肯定是会对其进行专门的优化,性能方面肯定是好过对数组进行for循环然后分别clone(),对于基本类型,不存在深浅拷贝,但是对于引用类型,源码中体现了对数组内的元素拷贝属于浅拷贝(指针赋值,所以在执行==比较地址时是相等的);
参考
https://blog.csdn.net/wangyangzhizhou/article/details/79504818
网友评论