GC(Garbage Collector)

GC는 아무도 참조하지 않는 객체의 메모리를 해제해주는 기능(가비지 컬렉션)을 한다.

GC 알고리즘 2가지

Reference Counting

  • 각각의 객체는 Reference Counting 을 하며 Reference Count가 0인 객체를 가비지 컬렉션한다.
  • 단점: 순환 참조 문제

Mark And Sweep

  • root 영역에서부터 참조값을 확인하여 참조하지 않는 객체를 가비지 컬렉션한다.
  • 장점: 순환 참조 문제 해결 가능
  • 단점: 애플리케이션과 GC 실행이 병행된다. / 의도적으로 GC를 실행시켜야 한다.
    (가비지 컬렉션의 주체는 GC이므로 의도적으로 GC를 실행시키는건 레퍼런스 카운트도 마찬가지인듯)

JVM 메모리 구조

모든 쓰레드가 공유하는 영역

- 메서드, 힙 영역

 

각 쓰레드마다 고유하게 생성하며 쓰레드 종료 시 소멸하는 영역

 

- 스택, PC Register, Native Method Stack 영역

 

Method 영역

  • 메서드들의 코드를 저장
  • static 변수 등이 속함

Heap 영역

  • new 인스턴스가 속함

Stack 영역

  • 로컬 변수, 중간 연산 결과 등이 속함

Mark And Sweep 알고리즘이 참조값을 확인하는 root 영역

  • 메서드 영역의 static 변수
  • stack 영역의 로컬 변수
  • native method stack 영역의 저수준 언어의 객체

GC 동작 순서

 

자바 8기준의 GC 동작 순서이며, 위의 그림을 참고하자.

 

1. Heap 영역에 위와 같은 구조의 단위 영역을 만든다.

2. 객체 생성 시 Eden 영역에 추가되고 Eden 영역이 다 차면 GC가 일어난다.
(Minor GC)

3. GC 후 남은 객체들은 age bit가 1 증가하고 survival 영역으로 이동한다.
(survival 영역 0, 1 중 둘 중 하나는 비어있고, Minor GC 마다 둘 중의 한 영역으로 이동 시킨다.)

Eden이 처음 다 찼을 때(1번째 Minor GC)
Eden이 두번째로 다 찼을 때(2번째 Minor GC)

4. 위와 같은 과정을 반복하고, age bit가 특정 수치에 도달하면 Old Generation 영역으로 이동한다.
(Promotion 이라고 함)

5. Old Generation 영역이 다 차면 GC가 일어난다.(Major GC)


GC의 종류

여러 종류가 있지만 Pararell, G1 만 간단하게 소개하겠다.

Pararell

자바 8의 default GC 이며, 여러 개의 쓰레드로 가비지 컬렉션을 수행하여 Stop-The-World가 짧다. 

G1

자바 9부터의 default GC 이며

GC 동작 순서는 비슷하지만 힙 영역에 단위 영역 할당 방식에 차이가 있다.

G1 GC는 아래 그림처럼 힙 영역에 Region 이라는 단위로 나누어 메모리를 할당한다.

이 영역들이 Eden, Survival, Old Generation 의 역할을 한다.

기존 GC에서는 힙 영역에 Young, Old Generation 을 포함한 구조의 단위 영역을 만들었다.

따라서 이 단위 영역에서 사용하지 않는 메모리들은 낭비가 된다.(메모리 파편화)

G1 에서는 아래처럼 Region으로 관리하여 메모리 파편화를 줄인다.

 

 

참고자료)

테코톡 GC영상

'언어 > Java' 카테고리의 다른 글

Builder 패턴을 사용해야하는 이유  (0) 2023.09.23

스코프 체인과 호이스팅

이를 이해하기 위해 자바스크립트 엔진에 대해 이해하면 좋은데, 이 글을 참고하자.

함수 레벨 스코프 : var

자바스크립트 실행은 함수에 따라 (A) 컴파일 단계, (B) 수행 단계가 재귀적으로 이뤄진다. 

처음 자바스크립트 실행 시 main() 함수에 대한 (A), (B) 처리를 시작으로 

내부에 새로운 함수 호출이 일어나면 새 함수에 대한 (A), (B) 처리, 그리고 또 내부 함수 호출이 있다면

그 함수에 대한 (A), (B) … 이런식으로 처리를 반복한다.


특정 함수 내 변수 var 의 선언은 본 함수 (A) 컴파일 단계에 정의되기 때문에 변수 var 의 스코프는 함수 레벨이 된다.
if, for 문과 같은 블록 레벨({}) 단위 변수를 위해 ES6 스펙에선 블록 레벨 스코프 const, let이 새로 소개되었다.

스코프 체인

자바스크립트 엔진 실행 과정에서 살펴보았듯 특정 함수에 대한 (B) 수행 단계에서 변수 할당 시,

본 함수의 Heap 영역에 변수 선언이 되어있는지 먼저 검사한다. 

만약 본 함수 내 변수가 선언되어있지 않았다면 해당 함수의 Heap 에서는 변수 선언을 찾을 수 없다. 

이때 해당 함수가 호출되기 이전의 함수로 (hidden) A pointer for previous scope 를 통해 올라가면서 

해당 함수 Heap Scope 에 변수가 선언되었는지 확인한다. 

어떠한 함수에서도 변수 선언이 되어있지 않다면 가장 처음에 호출된 main() 함수까지 올라가면서 검색한다. 

함수 호출 스택에 따라 가장 처음의 main() 함수까지 각 함수 Heap Scope 에 변수 선언 존재여부를 연쇄적으로 Chaining 하며 찾기때문에 이를 스코프 체인 이라고 부른다.

호이스팅

호이스팅은 변수나 함수의 선언이 끌어올려지는 것처럼 보이는 것을 말한다.

 

(A) 컴파일 단계에서 변수를 선언을 먼저하고, 다음 (B) 수행 단계에서 변수를 할당하기 때문에 같은 function-level 이라면 

아래와 같이 변수 선언과 할당을 나누어서 하더라도 자바스크립트 엔진에서는 변수 선언이 먼저 된 것으로 처리된다.

a = 10
var a;
--------------------------
# Global Scope (window)
- a = 10

위 예시처럼 var a 선언이 같은 함수 레벨 내에서 최상단에 ‘말려올라간것’처럼 수행되기도 하지만,

만약 함수 내 변수가 선언되어있지 않았다면 Scope Chain 을 통해 main() 함수까지 올라가면서 변수 선언을 찾는다. 

최종적으로 main() 함수 Heap Scope 에도 선언되어있지 않다면 main() 함수 영역에 변수를 선언해준다. 

main() 에서 호출한 어떤 함수이든 Scope Chain 을 통해 방금 선언해준 변수를 바라볼테니 이는 전역 변수인것이다.

(main() 의 Heap Scope 영역 명칭은 Global Scope (window)이기도 하다.)

특정 함수내에 변수를 할당하였지만 본 변수는 어느 함수에도 존재하지 않는 변수이기에 main() 함수까지 ‘말려올라가서’ 전역 변수를 선언한것이 된다.

변수 선언이 ‘말려올라갔다’는 의미에서 이 모든 경우를 호이스팅 이라고 표현한다.

 

 

 

출처)

 

Javascript 엔진 개요 및 실행 과정으로 살펴보는 Hoisting 과 Closure

자바스크립트자바스크립트는 웹 페이지의 세 요소중 하나입니다. HTML: 웹 페이지(문서) 포맷을 정의하는 마크업 언어 CSS: 웹 페이지(문서)의 디자인 요소에 대한 언어 Javascript: 웹 페이지(문서)와

aaronryu.github.io

 

자바스크립트 엔진

JavaScript 엔진은 Stack 메모리와 Heap 메모리를 사용하며 싱글 스레드로 모든 코드를 수행한다.

 

 

자바스크립트 엔진의 Stack 은 일반 프로그램 언어들의 Stack 과는 다르다.

타 프로그램 언어들은 함수 실행에 따라 Call Stack 에 각 로컬 함수들의 변수 등의 Context 정보들을 다 같이 쌓으며,

로컬 함수에만 국한된 정보들을 갖는다는 이유로 Context 를 Scope 라고도 부른다.

 

자바스크립트 엔진도 Call stack 에 함수 호출 순서를 적재하지만,

변수 및 함수 선언과 할당 정보는 Heap 에 따로 저장하여 Call Stack 에는 본 Heap 에 대한 포인터만 갖고 있다.

구체적으로 정리하면 아래와 같다.

 

  1. Heap: 각 함수 별 선언 및 할당되는 모든 변수 및 함수를 적재하는 메모리 영역
  2. Stack(Call Stack): 함수 실행 순서에 맞게 위 Heap 에 대한 포인터 적재 및 실행

자바스크립트 엔진 실행 과정

자바스크립트 엔진 실행 과정은 (A) JIT 컴파일 단계 (B) 수행 단계 이렇게 두 개로 나뉜다.

(A) 컴파일 과정

컴파일 과정에선 변수 및 함수의 '선언(Declaration)'만 추출하여 Heap 에 적재한다.
변수와 함수의 선언을 자바스크립트 실행 이전에 컴파일로 저장하여 실제 실행 시 변수와 함수 선언 여부를 검색한다.

 

예를 들어 아래 자바스크립트 파일을 처음 실행하게 되면 파일 전체에 컴파일 단계를 수행한다.

var a = 2;
b = 1;

function f(z) {
  b = 3;
  c = 4;
  var d = 6;
  e = 1;

  function g() {
    var e = 0;
    d = 3*d;
    return d;
  }

  return g();
  var e;
}

f(1);

 

1. 자바스크립트 첫 실행을 위한 main() 함수의 Global Scope(window) 영역을 Heap 에 생성한다.

# Global Scope (window)
- 
-

2. 변수 선언 var a을 찾아서 Global Scope(window) 영역에 a 를 적재한다.
3. 변수 할당 b = 1은 할당이므로 본 영역에 b 를 적재하지 않는다.

# Global Scope (window)
- a =
-

4. 함수 선언 function f(z)을 찾아서 Global Scope(window) 영역에 f 를 적재한다.
5. 함수 적재시엔 f 함수의 바이트코드(blob)에 대한 포인터값을 함께 적재한다.

 

자바스크립트 코드를 첫번째 라인에서 20번째 라인까지 컴파일 단계를 마치면 Heap 구성은 아래와 같다.

# Global Scope (window)
- a =
- f = a pointer for f functions bytecode

 

(B) 수행 과정

수행 과정에선 변수의 '할당(Assignment)'값들을 Heap 에 적재하고 함수는 호출 및 실행한다.

 

매 함수 호출때마다 스택에 함수 내 변수 및 함수를 같이 적재하는 스택 베이스 언어과 달리

자바스크립트는 스택에는 함수 호출 순서와 실제 변수 및 함수 정보들은 Heap 에 대한 포인터를 갖는다. 

Heap 에 함수 a() 를 위한 Local Execution Scope 는 a() 함수가 호출되기 이전에 Heap 에 존재했던

Global Scope(window)에 대한 포인터를 갖고있어서 엔진 내에서 아래와 같은 처리가 가능하다.

  • a() 함수 내에서 a = 1 변수 할당 시 먼저 Local Execution Scope 에 a 변수의 선언을 찾고,
    존재하지 않는다면 이전 Global Scope 로 돌아가 검색할 수 있다.
  • a() 함수 실행이 끝나게 되면 Call Stack 을 통해 현재 Heap 영역을 Global Scope 로 다시 되돌린다.


위에서 예시로 살펴본 자바스크립트 파일에 컴파일 단계를 마친 뒤 수행 단계는 아래와 같이 진행된다.

6. 컴파일 이후 아래의 Heap 을 갖고 다시 자바스크립트 파일 코드의 맨 첫번째 라인에서 실행이 시작된다.

# Global Scope (window)
- a =
- f = a pointer for f functions bytecode

7. 변수 할당 a = 2을 찾아서 Global Scope (window) 영역에 변수 a 존재 여부를 확인한다.
8. 변수 a 가 존재하므로 해당 a 에 2 를 할당한다.

# Global Scope (window)
- a = 2
- f = a pointer for f functions bytecode

9. 변수 할당 b = 1을 찾아서 Global Scope (window) 영역에 변수 b 존재 여부를 확인한다.
10. 변수 b 가 선언되어있지 않아 b 선언 및 1 을 할당한다.

# Global Scope (window)
- a = 2
- f = a pointer for f functions bytecode
- b = 1

11. 함수 호출 f(1)을 찾아서 Global Scope(window)영역에서 f() 선언 여부를 확인한다.
12. 함수 f() blob 컴파일 및 수행을 위해 Heap 에 새 Local Execution Scope 영역을 생성한다.

# Global Scope (window)
- a = 2
- f = a pointer for f functions bytecode
- b = 1

# Local Execution Scope for f()
- (hidden) A pointer for previous scope (= Global Scope (window))
- 
-

f(1) 함수 실행 시 새로이 생성된 Local Execution Scope에 다시 컴파일 단계를 통해  변수와 함수를 적재하고 수행 단계을 거친다.

또 f(1) 함수 내부에 또 다른 함수가 있다면 이 과정을 계속해서 재귀적으로 반복한다.

13. 함수 f() 의 컴파일 단계를 마치면 아래와 같다.

# Global Scope (window)
- a = 2
- f = a pointer for f functions bytecode
- b = 1

# Local Execution Scope for function f()
- (hidden) a pointer for previous scope (= Global Scope (window))
- z = 
- d = 
- e =

14. 함수 f() 의 수행 단계를 마치면 함수 f() 내 변수 할당 및 함수 g() 의 Scope 가 생성된다.

# Global Scope (window)
- a = 2
- f = a pointer for f functions bytecode
- b = 3

# Local Execution Scope for function f()
- (hidden) a pointer for previous scope (= Global Scope (window))
- z = 1
- d = 6
- e = 1
- c = 4

# Local Execution Scope for function g()
- (hidden) a pointer for previous scope (= Local Execution Scope for function f())
- e =

 

 

 

출처)

 

Javascript 엔진 개요 및 실행 과정으로 살펴보는 Hoisting 과 Closure

자바스크립트자바스크립트는 웹 페이지의 세 요소중 하나입니다. HTML: 웹 페이지(문서) 포맷을 정의하는 마크업 언어 CSS: 웹 페이지(문서)의 디자인 요소에 대한 언어 Javascript: 웹 페이지(문서)와

aaronryu.github.io

 

async 와 await

async/await 구문은 Promise 객체를 다루는 코드(Promise Chaining 코드 등)를 사람들이 좀더 익숙하게 느끼는

동기 실행 스타일의 코드로 작성할 수 있게 해주는 Syntactic sugar이다.

아래는 promise 를 다루는 코드와 async/await 문법을 비교한것이다. 두 결과는 동일하다.

 

Promise

fetch('https://www.google.com')
    .then((response) => response.text())
    .then((result) => {
        console.log('a');
    })
    .catch((error) => {
    	console.log(error);
    })
    .finally(() => {
    	console.log('exit')
    });

 

async 와 await

async function fetchAndPrint() {
  try {
    const response = await fetch('https://www.google.com');
    const result = await response.text();
    console.log('a');
  } catch(error) {
    console.log(error);
  } finally {
    console.log('exit');
  }
}

fetchAndPrint();

 

Promise 객체가 나온 이유

자바스크립트를 공부하던 중 promise 객체를 공부하면서 promise 객체 이전엔 어떤 식으로 비동기 처리를 했고

어떤 문제점 때문에 promise 객체가 나온 이유에 대해 알아보기로 했다.

 


Promise 객체 이전

promise 객체가 나오기 이전에도 아래 메서드들처럼 비동기 처리를 할 수 있었다.

setTimeout(callback, milliseconds);
addEventListener(eventname, callback);

fetch 함수도 마찬가지로 promise 객체를 반환하지 않았다면 아래처럼 사용했을것이다.

fetch('https://first.com', callback)

만약 promise.then 으로 promise chaining 을 하듯 여러 비동기작업을 순차적으로 처리를 하려면

아래처럼 콜백 헬(callback hell) 이라고 하는 지옥의 피라미드를 만들었을 것이다.

fetch('https://first.com', (response) => {
  // Do Something
  fetch('https://second.com', (response) => {
    // Do Something
    fetch('https;//third.com', (response) => {
      // Do Something
      fetch('https;//fourth.com', (response) => {
        // Do Something
      });
    });
  });
});

지금 봐도 지옥같은 가독성에 Do Something 주석 자리에 실제 코드가 들어있었다면 더 끔찍했을 것이라 예상할 수 있다.

 


Promise 객체 이후

promise 객체가 나온 이후 위와 같은 콜백 헬을 아래처럼 바꿔서 비교적 가독성 높고 깔끔하게 처리할 수 있다.

fetch('https://first.com')
  .then((response) => {
    // Do Something 
    return fetch('https://second.com');
  })
  .then((response) => {
    // Do Something 
    return fetch('https://third.com');
  })
  .then((response) => { 
    // Do Something 
    return fetch('https://third.com');
  });

이 뿐 아니라 promise 객체가 제공하는 문법들 덕분에 비동기 작업 처리 시 더 세밀한 처리를 할 수 있게 되었다.

const 는 과연 불변한가?

자바스크립트를 공부하다가 불변이라는 개념이 나와서 자바의 불변객체가 떠올라 친숙해서 정리해보기로 했다.

const 가 불변한지에 대해 알아보기 앞서 자바스크립트의 변수 선언 방식에 대해 가볍게 알아보자.

 

var, let, const

자바스크립트에서 변수 선언에는 var, let, const 가 있다.

특징은 간단하게 보자면 아래와 같다.

var 는 딱 봐도 변화무쌍해보이고 사용 시 유지보수의 험난함이 예상되는데 역시나 안티패턴이라고 하니 쓰지 않도록 하자.

1. var

  • 변수 재선언 가능
  • 값 재할당 가능
var a = 1;

var a = 2;  // 가능
a = 3;      // 가능

console.log(a);
-> 3 출력

2. let

  • 변수 재선언 불가능
  • 값 재할당 가능
let a = 1;

let a = 2;  // 불가능
a = 3;      // 가능

console.log(a);
-> 3 출력

3. const

  • 변수 재선언 불가능
  • 값 재할당 불가능
const a = 1;

const a = 2;  // 불가능
a = 3;        // 불가능

console.log(a);
-> 1 출력

 


그렇다면 const 는 불변(immutable) 한가?

답은 아니오다. 물론 문법 자체가 그렇듯 const 에 값 재할당은 불가능하다. 

하지만 주목할건 const 의 주소값(= 참조값) 재할당이 불가능한것이지 const 가 Object 라면 아래와 같이

Object 내의 값이나 상태는 얼마든지 변할 수 있다.

const obj = {};

obj['property'] = 1;

console.log(obj);
-> {property: 1} 출력

 

위에서 보았듯 obj 의 내부값이나 상태는 변경 가능하므로 불변하다고 할 수 없다.

Builder 패턴이란?

빌더 패턴은 생성과 관련된 디자인 패턴 중 하나로, 생성과 관련된 문제를 해결하고자 고안된 패턴이다. 

객체를 생성할 때 생성자 패턴, 정적 메소드 패턴, 수정자 패턴, 빌더 패턴 등을 사용할 수 있다.

 


빌더 패턴(Builder Pattern)을 사용해야 하는 이유

@Getter
@ToString
@NoArgsConstructor
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;
    
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

 

객체에서 생성자가 필요할 때 보통 이렇게 코드를 작성하곤 하지만 이는 잠재적 버그를 내포하는 코드이다.

위와 같이 매개변수로 받는 두 필드 값의 타입이 같을 때 만약 순서가 달라지면 어떻게 될까?

누군가 실수로 이 생성자를 이용해 content, title 순으로 매개변수를 입력해도 개발자는 쉽게 발견하기 어렵다.

이런 코드들은 나중에 코드 길이가 길어지면 쉽게 발견하기 어려운 버그가 되곤 한다.

 

[ 빌더 패턴(Builder Pattern)의 장점 ]

  1.  필요한 데이터만 설정할 수 있다.
  2.  가독성을 높일 수 있다.
  3.  불변성을 보장할 수 있다.

 

1. 필요한 데이터만 설정할 수 있다.

@Getter
@ToString
@NoArgsConstructor
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    @Builder
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
PostCreate request = PostCreate.builder()
            .content("글 내용입니다 하하")
            .build();

PostCreate request = PostCreate.builder()
            .title("글 제목입니다")
            .content("글 내용입니다 하하")
            .build();

빌더 패턴을 사용하면 위처럼 원하는 필드에 값을 넣을 수도 있고 필요없으면 해당 필드를 입력하지 않을 수도 있다.

 

2. 가독성을 높일 수 있다.

PostCreate postCreate = new PostCreate("글 내용입니다 하하", "글 제목입니다");

PostCreate request = PostCreate.builder()
            .title("글 제목입니다")
            .content("글 내용입니다 하하")
            .build();            
                      
PostCreate request = PostCreate.builder()
            .content("글 내용입니다 하하")
            .title("글 제목입니다")            
            .build();

맨 위처럼 생성자 패턴을 사용하면 내용과 제목의 순서를 반대로 입력하는 문제가 생길 수 있다.

이로 인해 버그가 발생한다면 이런 버그는 찾아내기가 정말 힘들다.

하지만 빌더 패턴을 사용하면 두번째와 세번째 경우처럼 순서를 반대로 쓰더라도

입력하는 필드값이 명확하므로 문제가 없다.

요즘엔 IDE 가 필드에 해당하는 힌트를 주기는 하지만 입력할 변수가 많아지면 그마저도 효용이 떨어진다.

특히나 혼자 개발하는게 아닌 협업을 하는 상황에서는 이런 가독성의 차이가 극명하게 느껴진다.

 

3. 불변성을 보장할 수 있다.

@Getter
@ToString
@NoArgsConstructor
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    @Builder
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

위처럼 Setter 를 닫아놓고 생성자를 통해서만 객체에 접근이 가능하게 만들면 객체의 불변성을 보장할 수 있다.

불변성을 보장하는건 생성자 패턴과도 공통된 장점이다.

 


결론

객체를 생성하는 대부분의 경우에는 빌더 패턴을 적용하는것이 바람직하다.

하지만 빌더의 남용은 오히려 코드 길이를 비대하게 할 수 있으므로 변경 가능성 및 변수의 개수를 보고 판단하자.

변경 가능성이 전혀 없고 변수의 개수가 적다면 굳이 사용하지 않아도 된다.

 

 

'언어 > Java' 카테고리의 다른 글

GC와 JVM 메모리 영역  (0) 2024.02.14

+ Recent posts