System.arraycopy爲何快

前言

在 Java 編程中常常會遇到數組拷貝操做,通常會有以下四種方式對數組進行拷貝。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方法

該方法用於從指定源數組中進行拷貝操做,能夠指定開始位置,拷貝指定長度的元素到指定目標數組中。該方法是一個本地方法,聲明以下:windows

@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 本地方法。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 分別爲TypeArrayKlassObjArrayKlassapp

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內核設計剖析》

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索