Java "Foo f = new Foo ()"의 객체 초기화는 본질적으로 C에서 포인터에 malloc을 사용하는 것과 동일합니까?


9

Java에서 객체 생성의 실제 프로세스를 이해하려고 노력 중이며 다른 프로그래밍 언어가 있다고 가정합니다.

Java의 객체 초기화가 C의 구조에 malloc을 사용할 때와 같다고 가정하는 것이 잘못입니까?

예:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

이것이 왜 객체가 스택이 아닌 힙에 있다고 말합니까? 그것들은 본질적으로 데이터에 대한 포인터이기 때문에?


c # / java와 같은 관리되는 언어의 힙에 객체가 생성됩니다. cpp에서는 스택에 객체를 만들 수도 있습니다
bas

Java / C # 제작자가 힙에 객체를 독점적으로 저장하기로 결정한 이유는 무엇입니까?
Jules

나는 단순성을 위해 생각 합니다. 스택에 객체를 저장하고 레벨을 더 깊게 전달하려면 복사 생성자가 포함 된 스택에 객체를 복사해야합니다. 나는 정답 구글하지 않았다, 그러나 나는 확신 당신이 더 만족 대답 자신을 찾을 수 있습니다 (또는 다른 사람이 옆 질문에 자세히 설명합니다)
BAS

java의 @Jules 객체는 런타임 scalar-replacement에 스택에서만 존재하는 평범한 필드로 여전히 "해제"될 수 있습니다 . 그러나 그것은 JIT그렇지 않은 것 javac입니다.
Eugene

"힙"은 할당 된 개체 / 메모리와 관련된 속성 집합의 이름 일뿐입니다. C / C ++에서는 C # 및 Java에서 "스택"및 "힙"이라는 두 가지 속성 집합 중에서 선택할 수 있습니다. 모든 개체 할당에는 동일한 지정된 동작이 있으며 이름은 "heap"으로 지정됩니다. 이러한 속성은 C / C ++ "힙"과 동일하지만 실제로는 그렇지 않습니다. 이것은 구현이 객체 관리를위한 다른 전략을 가질 수 없다는 것을 의미하지는 않으며, 이러한 전략이 애플리케이션 로직과 관련이 없음을 의미합니다.
Holger

답변:


5

C에서는 malloc()힙에 메모리 영역을 할당하고 이에 대한 포인터를 반환합니다. 그게 다야 메모리가 초기화되지 않았으며 메모리가 모두 0이거나 다른 것을 보증하지 않습니다.

Java에서 호출 new은과 같은 힙 기반 할당을 수행 malloc()하지만 추가 편의성 (또는 원하는 경우 오버 헤드)도 많이 얻습니다. 예를 들어 할당 할 바이트 수를 명시 적으로 지정할 필요가 없습니다. 컴파일러는 할당하려는 객체의 유형에 따라이를 파악합니다. 또한 객체 생성자가 호출됩니다 (초기화 방법을 제어하려는 경우 인수를 전달할 수 있음). new반환 되면 초기화 된 객체가 보장됩니다.

그러나 네, 전화 모두의 결과의 끝에 malloc()와 것은 new단순히 힙 기반 데이터의 일부 덩어리에 대한 포인터입니다.

질문의 두 번째 부분은 스택과 힙의 차이점에 대해 묻습니다. 컴파일러 디자인에 대한 과정을 읽거나 책을 읽음으로써 훨씬 더 포괄적 인 답변을 찾을 수 있습니다. 운영 체제에 대한 과정도 도움이 될 것입니다. 스택 및 힙에 대한 SO에 대한 많은 질문과 답변도 있습니다.

그렇게 말하면서, 나는 너무 장황하지 않기를 바라는 일반적인 개요를 줄 것이며 상당히 높은 수준에서 차이점을 설명하는 것을 목표로합니다.

기본적으로 두 개의 메모리 관리 시스템, 즉 힙과 스택을 갖는 주된 이유는 효율성 때문 입니다. 두 번째 이유는 각각이 다른 유형보다 특정 유형의 문제에서 더 낫기 때문입니다.

스택은 개념으로 이해하기가 다소 쉬우므로 스택부터 시작합니다. C 에서이 기능을 고려해 봅시다 ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

위의 내용은 매우 간단합니다. 우리는 이름이 붙은 함수를 정의 add()하고 왼쪽과 오른쪽에 추가합니다. 이 함수는 그것들을 더하고 결과를 반환합니다. 발생할 수있는 오버플로와 같은 모든 대소 문자를 무시하십시오.이 시점에서 논의와 밀접한 관련이 없습니다.

add()기능의 목적은 매우 간단 해 보이지만 수명주기에 대해 무엇을 알 수 있습니까? 특히 메모리 사용이 필요합니까?

가장 중요한 것은, 컴파일러는 데이터 유형이 얼마나 크며 얼마나 많은 데이터를 사용할 것인지에 대한 우선 순위 (컴파일 타임)를 알고 있다는 것입니다. lhsrhs인수는 sizeof(int)4 각 바이트. 변수 resultsizeof(int)입니다. 컴파일러는 add()함수가 4 bytes * 3 ints총 12 바이트의 메모리를 사용함을 알 수 있습니다 .

add()함수가 호출 될 때, 하드웨어 레지스터는 호출 스택 포인터는 스택의 상단을 가리 거기에 주소를해야합니다. add()함수를 실행하는 데 필요한 메모리를 할당하기 위해 모든 함수 입력 코드는 하나의 단일 어셈블리 언어 명령을 실행하여 스택 포인터 레지스터 값을 12 씩 줄입니다. 이렇게하면 스택에 3 개의 스토리지가 생성됩니다. ints하나 각 lhs, rhsresult. 단일 명령을 실행하여 필요한 메모리 공간을 확보하는 것은 단일 클럭이 1 클록 틱 (1 GHz CPU에서 10 억분의 1 초)으로 실행되는 경향이 있기 때문에 속도면에서 엄청난 승리입니다.

또한 컴파일러의 관점에서 배열을 인덱싱하는 것처럼 보이는 변수에 대한 맵을 만들 수 있습니다.

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

다시,이 모든 것이 매우 빠릅니다.

add()기능이 종료 되면 정리해야합니다. 스택 포인터 레지스터에서 12 바이트를 빼서이 작업을 수행합니다. 호출과 비슷 free()하지만 하나의 CPU 명령어 만 사용하며 하나의 틱만 걸립니다. 매우 빠릅니다.


이제 힙 기반 할당을 고려하십시오. 이것은 필요한 메모리 양을 사전에 알지 못할 때 작동 합니다 (즉, 런타임시에만 배울 것입니다).

이 기능을 고려하십시오.

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

addRandom()함수는 컴파일 타임에 count인수 의 값이 무엇인지 알지 못합니다 . 이 때문에 다음 array과 같이 스택에 넣을 때와 같이 정의하려고 시도하는 것이 합리적이지 않습니다 .

int array[count];

경우 count거대 그것은 우리의 스택이 너무 크고 덮어 쓰기 다른 프로그램 세그먼트를 성장의 원인이 될 수 있습니다. 이 스택 오버플로 가 발생 하면 프로그램이 충돌합니다.

따라서 런타임까지 필요한 메모리 양을 모르는 경우을 사용 malloc()합니다. 그런 다음 필요할 때 필요한 바이트 수를 물어볼 수 있고, malloc()그 바이트를 많이 공급할 수 있는지 확인합니다. 그것이 가능하다면, 우리는 그것을 얻습니다. 그렇지 않다면, malloc()실패한 호출을 알려주는 NULL 포인터를 얻습니다 . 그러나 프로그램이 충돌하지 않습니다! 물론 프로그래머는 리소스 할당에 실패하면 프로그램을 실행할 수 없다고 결정할 수 있지만 프로그래머가 시작한 종료는 가짜 충돌과 다릅니다.

이제 효율성을 보러 돌아와야합니다. 스택 할당자는 매우 빠릅니다. 할당하는 명령 하나, 할당 해제 할 명령 하나는 컴파일러에 의해 수행되지만 스택은 알려진 크기의 로컬 변수와 같은 것을 의미하므로 상당히 작습니다.

반면 힙 할당자는 몇 배 더 느립니다. 사용자가 원하는 메모리 양을 공급할 수있는 충분한 여유 메모리가 있는지 확인하려면 테이블을 찾아야합니다. 다른 사람이 해당 블록을 사용할 수 없도록하기 위해 메모리를 공급 한 후에 해당 테이블을 업데이트해야합니다 (이 부기에는 할당자가 할당하려는 것 외에 할당자가 자체적 으로 메모리를 예약해야 할 수도 있음 ). 할당자는 스레드 안전 방식으로 메모리를 공급할 수 있도록 잠금 전략을 사용해야합니다. 그리고 기억이 마침내free()d는 다른 시간에 발생하며 일반적으로 예측할 수있는 순서로 발생하지 않습니다. 할당자는 연속적인 블록을 찾아 다시 합쳐서 힙 조각화를 복구해야합니다. 그것이 모든 것을 달성하기 위해 단일 CPU 명령 이상을 필요로하는 것처럼 들리면 맞습니다! 매우 복잡하고 시간이 걸립니다.

그러나 힙이 큽니다. 스택보다 훨씬 큽니다. 우리는 그들로부터 많은 메모리를 얻을 수 있으며 컴파일 타임에 얼마나 많은 메모리가 필요한지 알지 못할 때 좋습니다. 그래서 우리는 너무 큰 것을 할당하려고 할 때 충돌하는 대신 정중하게 거절하는 관리되는 메모리 시스템의 속도를 상쇄합니다.

귀하의 질문에 대한 답변이 되었기를 바랍니다. 위의 내용 중 어느 것이라도 명확하게 알려면 알려주십시오.


int64 비트 플랫폼에서 8 바이트가 아닙니다. 여전히 4입니다. 컴파일러는 int스택 의 3 분의 1 을 리턴 레지스터로 최적화 할 가능성이 높습니다 . 실제로 두 가지 인수는 모든 64 비트 플랫폼의 레지스터에도있을 수 있습니다.
SS Anne

int64 비트 플랫폼에서 약 8 바이트 문장을 제거하기 위해 답변을 편집했습니다 . intJava에서 4 바이트로 남아있는 것이 맞습니다 . 그러나 컴파일러 최적화에 들어가면 카트가 말보다 앞서 있다고 생각하기 때문에 나머지 대답을 남겼습니다. 그렇습니다.이 시점에서도 정확하지만 질문 은 스택 대 힙에 대한 설명을 요구합니다. RVO, 레지스터, 코드 제거 등을 통한 인수는 기본 개념을 과장하고 기본을 이해하는 데 방해가됩니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.