2. arraycopy
배열의 일부를 다른 배열에 복사한다.
JVM 뜯어보기
System.arraycopy() 메서드는 System의 스트림들 다음으로 많이 쓸 수 있는 메서드다. arraycopy() 메서드는 배열 객체의 일부를 다른 배열에 빠르게 복사할 수 있다. 배열을 복사하는 방법은 여러 가지가 있다. 예를 들어 for문을 이용해 배열의 요소를 반복적으로 추가해준다거나 Arrays.stream().toArray(), Arrays.copyOf(), clone()과 같은 메소드를 활용할 수도 있다. arraycopy() 메서드를 이용하면 위의 방법들과 동일한 결과를 얻을 수 있으며, 네이티브 메서드를 활용하므로 큰 배열을 복사할 경우에 비교적 더 빠른 결과를 얻을 수 있다. arraycopy()를 공부해보자.
// src/java.base/java/lang/System.java
public final class System {
@IntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
}
arraycopy() 메서드는 다음과 같이 5개의 파라미터를 가진다. src는 복사할 원본 배열 객체, srcPos는 복사가 시작될 위치, dest는 새로운 배열 객체, destPos는 복사되는 시작 위치, length는 복사될 길이를 의미한다. 쉽게 말해서 (src, srcPos) -> (dest, destPos)로 length만큼 복사하는 것이다. 이 arraycopy() 메서드도 실질적인 부분은 네이티브 메서드로 구현되어 있으므로 해당 코드를 확인해보자.
// src/hotspot/share/prims/jvm.cpp
JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
jobject dst, jint dst_pos, jint length))
// Check if we have null pointers
if (src == nullptr || dst == nullptr) {
THROW(vmSymbols::java_lang_NullPointerException());
}
arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
assert(oopDesc::is_oop(s), "JVM_ArrayCopy: src not an oop");
assert(oopDesc::is_oop(d), "JVM_ArrayCopy: dst not an oop");
// Do copy
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
해당 메서드는 다음과 같이 JVM_Arraycopy라는 함수로 매핑되어 있다. if문에서 각각의 배열 객체가 null일 때 NullPointerException을 던지도록 예외처리를 해주었다. 그 다음으로 각각의 자바 배열 객체를 arrayOop로 변환해 주고 //Do copy 주석이 있는 부분에서 실제로 복사를 수행한다. 해당 코드는 원본 객체인 arrayOop 객체 s에서 klass() 함수를 호출해 'Klass' 객체를 얻을 수 있을 것으로 예상할 수 있다.
하지만 실제로는 원본 객체가 '배열' 객체이므로 'Klass'객체를 확장한 'ArrayKlass'의 서브 클래스인 ObjArrayKlass 혹은 TypeArrayKlass객체를 받는다. '원시 타입 배열은 연속된 메모리 공간에 데이터를 직접 저장하고 참조 타입 배열은 객체에 대한 참조를 저장한다'라는 배열에 대한 기본적인 개념을 기억할 것이다. 즉 klass() 함수를 호출하는 객체가 int[], double[]과 같은 원시 타입 배열이라면 TypeArrayKlass 객체로, String[], Person[]과 같은 참조 타입 배열이라면 ObjArrayKlass 객체가 리턴된다.
각 Klass객체(TypeArrayKlass, ObjArrayKlass)는 둘 다 copy_array() 함수를 가지지만 완벽히 같지 않고 성능 최적화를 위해 조금 다르게 작동한다. 따라서 위 코드에서 각 Klass객체를 리턴받은 뒤 copy_array()를 실행하는 코드는 한 가지 코드로 두 가지 메서드를 실행하는 다형성의 한 예가 되겠다. 일반적인 경우인 ObjArrayKlass의 copy_array()함수를 살펴보자.
// src/hotspot/share/oops/objArrayKlass.cpp
void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
int dst_pos, int length, TRAPS) {
assert(s->is_objArray(), "must be obj array");
if (!d->is_objArray()) {
ResourceMark rm(THREAD);
stringStream ss;
if (d->is_typeArray()) {
ss.print("arraycopy: type mismatch: can not copy object array[] into %s[]",
type2name_tab[ArrayKlass::cast(d->klass())->element_type()]);
} else {
ss.print("arraycopy: destination type %s is not an array", d->klass()->external_name());
}
THROW_MSG(vmSymbols::java_lang_ArrayStoreException(), ss.as_string());
}
// Check is all offsets and lengths are non negative
if (src_pos < 0 || dst_pos < 0 || length < 0) {
// Pass specific exception reason.
ResourceMark rm(THREAD);
stringStream ss;
if (src_pos < 0) {
ss.print("arraycopy: source index %d out of bounds for object array[%d]",
src_pos, s->length());
} else if (dst_pos < 0) {
ss.print("arraycopy: destination index %d out of bounds for object array[%d]",
dst_pos, d->length());
} else {
ss.print("arraycopy: length %d is negative", length);
}
THROW_MSG(vmSymbols::java_lang_ArrayIndexOutOfBoundsException(), ss.as_string());
}
// Check if the ranges are valid
if ((((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length()) ||
(((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length())) {
// Pass specific exception reason.
ResourceMark rm(THREAD);
stringStream ss;
if (((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length()) {
ss.print("arraycopy: last source index %u out of bounds for object array[%d]",
(unsigned int) length + (unsigned int) src_pos, s->length());
} else {
ss.print("arraycopy: last destination index %u out of bounds for object array[%d]",
(unsigned int) length + (unsigned int) dst_pos, d->length());
}
THROW_MSG(vmSymbols::java_lang_ArrayIndexOutOfBoundsException(), ss.as_string());
}
// Special case. Boundary cases must be checked first
// This allows the following call: copy_array(s, s.length(), d.length(), 0).
// This is correct, since the position is supposed to be an 'in between point', i.e., s.length(),
// points to the right of the last element.
if (length==0) {
return;
}
if (UseCompressedOops) {
size_t src_offset = (size_t) objArrayOopDesc::obj_at_offset<narrowOop>(src_pos);
size_t dst_offset = (size_t) objArrayOopDesc::obj_at_offset<narrowOop>(dst_pos);
assert(arrayOopDesc::obj_offset_to_raw<narrowOop>(s, src_offset, nullptr) ==
objArrayOop(s)->obj_at_addr<narrowOop>(src_pos), "sanity");
assert(arrayOopDesc::obj_offset_to_raw<narrowOop>(d, dst_offset, nullptr) ==
objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos), "sanity");
do_copy(s, src_offset, d, dst_offset, length, CHECK);
} else {
size_t src_offset = (size_t) objArrayOopDesc::obj_at_offset<oop>(src_pos);
size_t dst_offset = (size_t) objArrayOopDesc::obj_at_offset<oop>(dst_pos);
assert(arrayOopDesc::obj_offset_to_raw<oop>(s, src_offset, nullptr) ==
objArrayOop(s)->obj_at_addr<oop>(src_pos), "sanity");
assert(arrayOopDesc::obj_offset_to_raw<oop>(d, dst_offset, nullptr) ==
objArrayOop(d)->obj_at_addr<oop>(dst_pos), "sanity");
do_copy(s, src_offset, d, dst_offset, length, CHECK);
}
}
ObjArrayKlass의 copy_array() 함수는 꽤 길다. 하지만 찬찬히 읽어보면 넘겨 받은 파라미터가 적절한지 예외를 처리하는 게 대부분이다. System.arraycopy() 메서드의 파라미터를 보면 어떤 배열 타입을 특정해서 받는 게 아니라 Object 타입 src, dest를 받으므로 해당 파라미터가 배열인지 확인하는 과정이 필요하며 그 과정이 여기 포함되어 있다. 우리는 실제 복사가 수행되는 과정이 궁금하므로 맨 아래 if문만 보면 된다. 해당 if문에서는 UseCompressedOops가 true/false인지에 따라서 객체 포인터를 narrowOop를 사용하는지 oop를 사용하는지가 결정된다. UseCompressedOop는 JVM에 지정된 힙 영역 크기에 따라 압축된 객체 포인터(narrowOop)를 사용할지 말지 결정하는 boolean타입 변수다. UseCompressedOop가 켜져 있으면 자바는 힙 영역 공간 사용을 절약하기 위해 객체 포인터를 압축해서 사용한다. 자세한 내용을 여기를 참조.
이제 복사할 배열과 복사되는 배열의 시작점을 int형으로 표현하는 src_pos, dst_pos를 각각 objArrayOopDesc클래스의 obj_at_offset() 함수를 사용해서, 해당 시작점이 해당 배열의 시작점으로부터 얼마나 떨어져 있는지(offset)를 계산한다. 이를 각각 src_offset, dst_offset으로 저장하며 실질적으로 do_copy()함수를 실행해 복사가 실행된다. 그 이후로는 힙 영역에 접근하고 이런 저런 추가적인 함수들을 세네번 거친 후에 최종적으로 CPU 수준까지 내려가서 각 OS와 CPU에 맞는 복사 함수를 실행한다. 대표적으로 Arm64기반 windows일 때의 함수는 다음과 같다.
// src/hotspot/os_cpu/windows_aarch64/copy_windows_aarch64.hpp
static void pd_disjoint_words(const HeapWord* from, HeapWord* to, size_t count) {
switch (count) {
case 8: to[7] = from[7];
case 7: to[6] = from[6];
case 6: to[5] = from[5];
case 5: to[4] = from[4];
case 4: to[3] = from[3];
case 3: to[2] = from[2];
case 2: to[1] = from[1];
case 1: to[0] = from[0];
case 0: break;
default:
(void)memcpy(to, from, count * HeapWordSize);
break;
}
}
count는 복사할 배열의 길이인데 길이가 8 이하로 짧은 경우에는 memcpy()함수를 실행하지 않고 직접 복사하는 것을 알 수 있다. 이는 속도 최적화를 위함이다. 그 이상의 경우에는 memcpy()함수를 이용해 복사한다. 중요한 점은 memcpy()는 from ~ to로 메모리에 접근해 데이터를 직접적으로 복사하므로 굉장히 빠르다는 것이다. 결론적으로 System.arraycopy()를 이용한 배열 복사는 자바 수준에서 for문을 사용해 복사할 때보다 훨씬 빠른 속도로 복사가 가능하다. 따라서 이 메서드는 JVM 곳곳에서 큰 배열을 빠르게 복사할 때 많이 사용되며 Arrays.copyOf() 메서드가 대표적이다.
사용해보기
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] originalArr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int[] copiedArr = new int[7];
final int DEST_POS = 2;
System.arraycopy(originalArr, 3, copiedArr, DEST_POS, copiedArr.length - DEST_POS);
System.out.println(Arrays.toString(originalArr));
System.out.println(Arrays.toString(copiedArr));
}
}
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
[0, 0, 4, 5, 6, 7, 8]
System.arraycopy() 메서드는 다음과 같이 간단하게 사용할 수 있다. originalArr와 copiedArr를 만들어 주고 origianalArr의 3번 인덱스부터 복사해 copiedArr의 DEST_POS위치부터 저장한다. 하지만 이렇게 작은 배열을 복사하는 경우에는 System.arraycopy() 메서드보다 다른 방법들이 더 빠르다는 결과도 심심찮게 찾아볼 수 있으므로 상황에 따라 적절히 사용할 수 있어야겠다.
'프로그래밍 > Java' 카테고리의 다른 글
[Java] JDK 부수기 - (2) java.lang.System - 4. getProperties (0) | 2023.11.28 |
---|---|
[Java] JDK 부수기 - (2) java.lang.System - 3. console (0) | 2023.11.27 |
[Java] JDK 부수기 - (2) java.lang.System - 1. err, in, out (0) | 2023.11.23 |
[Java] JDK 부수기 - (1) java.lang.Object - 5. toString (0) | 2023.11.21 |
[Java] JDK 부수기 - (1) java.lang.Object - 4. getClass (0) | 2023.11.21 |