在 Java 编程中常常会遇到数组拷贝操做,通常会有以下四种方式对数组进行拷贝。java
for
遍历,遍历源数组并将每一个元素赋给目标数组。clone
方法,原数组调用clone
方法克隆新对象赋给目标数组,更深刻的克隆能够看以前的文章《从JDK角度看对象克隆》。System.arraycopy
,JVM 提供的数组拷贝实现。Arrays.copyof
,实际也是调用System.arraycopy
。这种状况下是在 Java 层编写 for 循环遍历数组每一个元素并进行拷贝,若是没有被编译器优化,它对应的就是遍历数组操做的字节码,执行引擎就根据这些字节码循环获取数组的每一个元素再执行拷贝操做。算法
使用很简单,好比以下方式进行数组拷贝。编程
int size = 10000;
int[] src = new int[size];
int[] des = new int[size];
System.arraycopy(src, 0, des, 0, size);
复制代码
该方法用于从指定源数组中进行拷贝操做,能够指定开始位置,拷贝指定长度的元素到指定目标数组中。该方法是一个本地方法,声明以下:windows
@HotSpotIntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
复制代码
这个注解是 HotSpot VM 标准的注解,被它标记的方法代表它为 HotSpot VM 的固有方法, HotSpot VM 会对其作一些加强处理以提升它的执行性能,好比可能手工编写汇编或手工编写编译器中间语言来替换该方法的实现。虽然这里被声明为 native 方法,可是它跟 JDK 中其余的本地方法实现地方不一样,固有方法会在 JVM 内部实现,而其余的会在 JDK 库中实现。在调用方面,因为直接调用 JVM 内部实现,不走常规 JNI lookup,因此也省了开销。数组
Java 的 System 类有个静态块在类加载时会执行,它对应执行了 registerNatives
本地方法。bash
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
。app
这里将一些校验源码去掉,留下核心代码,这里由于涉及到内存中指针的移动,因此为了提升赋值操做的效率将起始结束位置转成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
架构上会使用汇编来实现。
再看普通类型对象做为数组元素时候的拷贝操做,这里将一些校验源码去掉,留下核心代码。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 是更加高效的。数组越大致现地越明显。
-------------推荐阅读------------
------------------广告时间----------------
公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。
鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有须要的朋友能够购买。感谢各位朋友。
欢迎关注: