GC란? ( Garbage Collections )

|

GC( garbage collector )

GC란?

  • 메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당됐던 메모리 영역 중에서 필요없게 된 영역을 해제하는 기능이다. 더이상 사용할 수 없게 된 영역이란, 어떤 변수도 가리키지 않게 된 영역을 의미한다.

  • 즉 짧게 설명하면 참조되지 않는 메모리 이것을 가비지로 표현합니다. 이 쓰레기 메모리들이 더이상 메모리를 점유하고 있지 않도록 해제 하는 것을 의미합니다.

GC의 역활

  • GC는 메모리 내 객체를 관리하기 위해서 크게 두가지 역할을 해주고 있습니다.
    • 사용되지 않는 객체 찾기
    • 이러한 객체들을 메모리 상에서 정리해주기
  • 그럼 GC는 어떤식으로 사용되지 않는 객체를 알수 있을까요? 기본적으로는 참조를 통해서 확인할 수 있습니다.
  • 자바에서는 NEW 연산자를 통해서 객체를 생성합니다.
  • GC는 언제 작동할까요? 알기 쉽게 메모리가 부족할 때 작동합니다.
User user = new User();

여기서 user는 메모리에 생성된 User을 가르키는 포인터 변수입니다. 그래서 오른쪽에 생성된 메모리 상의 대입연산자(=)을 통해서 user가 참조할 수 있도록 한것입니다. 그런데 예를 들면, 이 user ( 포인터 변수 ) 가 새로운 객체를 가르 키게 되면, 기존에 메모리에 생성되었던 User 객체는 더 이상 접근 할 방법이 없게 됩니다.

이렇게 GC는 객체와 포인터 간의 참조관계를 기록해서 이를 바탕으로 포인터 변수가 더이상 가르키지 않는 객체들을 제거하고, 그 메모리를 프로그램이 사용할 수 있도록 비워줍니다.

하지만 참조 관계만 보면 모든 쓰레기 객체들을 찾아낼 수 있을까요? 그렇지 않습니다. 리스트 자료구조의 경우, 각각의 객체들이 다음 리스트를 가리키고 있습니다.

이렇게 기차처럼 연결된 리스트의 경우에 만약 가장 맨 앞에 있는 객체를 가르키는 포인터가 다른 객체를 가르키게 되는 순간, 이 머리 객체에 연결된 객체들은 미아가 됩니다.

따라서 이러한 참조는 있으나, 프로그램에서 접근할 수 가 없으면 아무런 소용이 없기 때문에, 이러한 객체들도 관리를 해주어야 합니다.

GC는 주기적으로 메모리를 돌며 이러한 객체들을 찾아서 제거합니다.

그리고 GC중 몇몇은 객체가 있던 자리에, 덩그러니 남은 메모리 공간들을 한데모아 메모리 단편화을 해결해줍니다. 여전히 사용되고 있는 객체는 사용되고 있는 것들끼리, 그 사이사이에 남아있는 빈 메모리 공간들은 빈 공간끼리 모아서 큰 공간을 만들어줍니다.

그래서 GC는 작동방식에 따라서 4가지의 종류가 있습니다. 주로 JVM이 돌아가는 환경에 따라서 적절한 GC가 달라집니다. 이러한 일들을 처리하는 방식도 다르고, 이에 따라 성능도 달라지게 됩니다.

아무도 메모리를 건들지 않는 상황에서 GC 혼자 메모리를 모두 사용하게 되면 좋겠지만, 컴퓨터 메모리는 한시도 쉴 틈 없이 사용되고, 자바 기반의 프로그램들은 모두 멀티 쓰레드를 구현하고 있기 때문에, 수많은 메모리들을 읽고, 쓰고 있습니다.

따라서 GC가 작동하는 방식은 자바 프로그램( 쓰레드 ) 와 GC 쓰레드가 메모리를 두고 어떤 방식으로 누가 얼마나 접근할 것인지를 결정하게 됩니다.

그래서 GC가 작동할 때 기본적으로 다른 쓰레드들은 모두 메모리에 접근할 수 없도록 해주어야합니다. ( 그렇게 하지 않으면 GC가 메모리를 확보 하나 마나 소용이 없게 됩니다)

이런 GC를 제외한 모든 쓰레드가 ‘얼음’ 상태를 stop-the-world라고 합니다. 이러한 stop-the-world는 프로그램의 성능에 커다란 영향을 끼치기 때문에, 이 stop-the-world 상태를 얼마나 줄이느냐가 GC를 튜닝하는데 주된 고려사항이 됩니다.

GC의 메모리 구조 및 작동방식

  • Generational GC

David ungar라는 분이 84년 ‘Generation Scavenging: A Non-disruptive High Performance Storage Reclamation Algorithm’이라는 한 논문을 발표합니다. 가설을 하나 제시하면서 Generational GC를 소개합니다. “대부분의 객체는 일찍 죽는다.” 라는 가설입니다. 실제 통계로도 생성된 객체의 98%의 객체가 곧바로 쓰레기 객체가 된다고 합니다.

이러한 경험을 바탕으로 Generational GC가 디자인됩니다. 힙을 Young Generation 영역과, Old Generation 영역으로 나눈 뒤에, Young Generation 영역을 주기적으로 청소하고, 상대적으로 오랜 기간 사용되는 객체는 Old Generation 으로 보내버리는 것이 기본 원리입니다.

이렇게 되니 GC는 매번 힙 전체를 청소하는 것이 아니라 Young 영역 위주로 청소를 하다가, Old 영역에 메모리가 부족하게 될 때만 Old 영역 청소를 하게 됩니다.

장점은 첫번째로, Young은 Old보다 사이즈가 작고 힙 공간의 일부분 이기 때문에 GC가 전체영역을 처리하는 것보다 시간이 적게 걸립니다. 즉 stop-the-world로 애플리케이션이 중지되는 시간이 짧아집니다.

두번째는 Young 영역을 한번에 모두 비우기 때문에, 이 Young 영역에 연속된 여유가 공간이 만들어집니다. 만약 GC가 군데군데 골라서 객체를 제거 했다면, 메모리 파편화 현상으로 인해서 연속된 큰 데이터가 들어갈 공간이 부족해졌을 겁니다.

그런데 Young 영역에 있던 객체들이 Old 영역으로 계속 옮겨지다 보면 언젠가 Old 영역도 가득 차게 될 건데 이때 쓸모없는 객체들을 모두 제거될겁니다.

여기서 Young 영역을 정리하는 것을 minor GC라고 하고 Old 영역을 정리하는 것을 full GC라고 합니다.

GC는 Serial, Parallel, CMS, G1 GC라는 이름으로 4가지 형태가 존재합니다.일단 GC가 Young 영역을 청소 할 때는 이 4가지 종류에 상관없이 모두 stop-the-world 상태가 발생합니다.

하지만 Old generation 영역을 청소하는 방법은 서로 다릅니다. 즉, 어떻게 다르게 청소하느냐에 따라 위에서 말한대로 4가지 종류로 구분됩니다.

  • MAJOR GC와 FULL GC의 차이

Major GC 는 Tenured 영역( = Old 영역) 을 청소합니다. 그렇지만 Full GC는 Heap 메모리 전체영역을 청소합니다. ( Young and Old )

image

  • Heap 메모리 구조

크게는 Young / Old ( Tenured ) / Permanent 영역으로 나뉘어 집니다. 그렇지만 Permanent 영역은 거의 사용되지 않는 영역이므로 실제로는 Young 영역과 Old 영역으로 나뉘어집니다. 그리고 Young 영역은 Eden 영역과 두개의 Survivor 영역으로 나뉩니다.

일단 메모리에 객체가 생성되면, Eden 영역에 객체가 지정됩니다. Eden 영역에 데이터가 어느정도 쌓이면, 이 영역에 있던 객체가 어디론가 옮겨지거나 삭제됩니다. 이 때 옮겨지는 위치가 Survivor 영역입니다.

두개의 Survivor 영역 사이에 우선 순위가 있는 것은 아니지만 이 두개의 영역중 한군데는 반드시 비어있어야 합니다. 그 비어 있는 Eden 영역에 있던 객체가 할당됩니다.

Eden에서 survivor 둘 중 하나의 영역으로 할당되고, 할당된 survivor 영역이 차면 Eden 영역에 있는 객체와 꽉찬 survivor 영역에 있는 객체가 비어 있는 survivor 영역으로 이동합니다.

그러다가 더 큰 객체가 생성되거나 더이상 Young 영역에 공간이 남지 않으면, 객체들은 Old 영역으로 이동합니다.

GC의 종류

  • Serial Garbage Collector

가장 간단한 GC입니다. 주로 32비트 JVM에서 돌아가는 싱글쓰레드 어플리케이션 에서 사용됩니다. 특별히 지정하지 않을 경우 기본 GC로 지정되어 있습니다.

단순 하게 작동해서, Young 영역을 정리하는 마이너 GC때도 stop-the-world, 풀 GC할 때도 올 스탑입니다.

그리고 동작하는 GC도 싱글쓰레드로 돌아갑니다. 그렇기 때문에 데스크탑 같이 클라이언트 혼자 돌아가는 어플리케이션에서나 적합한 구조입니다.

  • Parallel Collector(=Throughput Collector)

이 GC는 64비트나 JVM이나 멀티 CPU 유닉스 머신에서 기본 GC 설정이 되어있습니다. 그리고 마이너 GC와 풀 CG 모두 멀티쓰레드를 사용합니다. 위의 싱글쓰레드로 돌아가던 GC보다는 훨씬 빠릅니다. 여러쓰레드가 작동하기 때문에 이름도 Parallel 입니다. 그렇지만 마이너, 풀 CG모두 올스탑인건 Serial GC와 동일합니다.

  • CMS Collector

이 GC는 풀 CG의 올스탑(stop-the-world) 상태를 줄여 볼 수 없을까? 라는 고민에서 출발한 GC입니다. Parallel GC와 동일하게 멀티쓰레드로 마이너 CG를 합니다. 그렇지만 풀 CG에서는 거의 올스탑이 발생하지 않습니다. 이유는 어플리케이션이 작동하는 와중에, 백그라운드에서 쓰레드를 만들어서 Old 영역에 쓸모없는 객체들을 찾아서 지속적으로 제거합니다.

즉, CMS의 가장 큰 장점은 풀 CG에서 올스탑이 거이 발생하지 않습니다. 단점은 백그라운드에서 지속적으로 풀 CG를 실행하니 CPU리소스를 많이 잡아먹습니다. 또한 중간 중간에 Old 영역에 쓸모 없는 객체들을 제거하기 때문에 메모리가 군데군데 비어지기 때문에 메모리 파편화가 발생하게 됩니다.

  • G1 Collector

힙 영역이 매우 큰 머신을 돌리기에 적합한 GC입니다. 대신 CMS의 단점을 극복했습니다. 힙에 영역이라는 개념을 도입하고, 힙을 여러개의 Region으로 나눕니다. 몇 몇 Region은 Young 영역으로 쓰이고, 나머지 몇몇은 Old 영역으로 쓰입니다. ( 자동으로 알아서 Young과 Old를 나눠서 쓴다 )

Young 영역과 Parallel이나 CMS처럼 멀티쓰레드로 정리를 합니다. 그리고 Old 영역에 해당하는 Region이 여러개 있는데 CMS 처럼 백그라운드에서 풀 GC를 합니다. 그런데 CMS와의 차이점은 중간중간 쓸모없는 객체들을 제거하는게 아니라 통째로 한 구역을 정리합니다.

참조가 없는 객체들은 지우고, 사용중인 객체는 다른 Region으로 고스란히 복사합니다. 이로인해서 CMS에서 발생했던 메모리 파편화 현상이 발생하지 않습니다.

Spring Boot 사용이유, Spring 과 차이점

|

Spring Boot를 왜 사용할까?

개인적으로 사용하면서 크게 느껴진 점 3가지

설정의 표준화, 자동화

  • 예를 들면, FreeMaker를 사용해야 한다면 ViewResolver를 Bean으로 등록하고, prefix suffix를 지정해줘야한다. 하지만 boot 같은 경우는 classpath를 기본적으로 읽어들이기 때문에 따로 지정하지 않고, classpath에 폴더를 위치시키면 된다.
  • AutoConfigration으로, 많은 설정들을 기본적으로 지원해준다. 많이 쓰이고 범용적인 보일러 플레이트에 가깝다
  • 기존 spring에서 xml 설정이나 java Config로 설정 파일을 읽어들인 것처럼, @SpringBootApplication이 대신 프로젝트 패키지에 있는 애너테이션을 찾아서 명시된 대로 Bean으로 등록해준다. 기존처럼 따로 component Scan을 지정할 필요가없다. 이유는 @SpringBootApplication이 @ComponentScan과 @EnableAutoConfiguration을 포함하고 있기 때문이다

배포가 간단하고, Tomcat 내장서버를 포함하고 있다

  • 톰켓의 버전이나, 여러 설정 xml 값, maven/gradle 같은 build툴을 다시 설치해야 했지만, Boot의 경우 그럴 필요가없다.
  • 내장 톰켓이 버전관리, 배포면에서 용이하다. 또한 성능관련해서도 저어언혀 떨어지지 않는다. 실제 운영서버로도 많이 사용하고 있다.
  • 다만 상대적으로, 톰캣 하나에 설정파일이 많거나 컨텍스트를 여러개 생성해야 하는 경우는 외장 톰켓을 고려하는게 나을 수 있다.

의존성 관리가 편리하다

  • 의존성에 대한 호환성을 고려하지 않아도 된다. 맨날 maven 사이트 들어가서 몇버전까지 지원되는지 확인하고, 이 라이브러리가 호환이 어떻게 되는지 하루종일 들여다 볼 필요가 없다.
  • 예를 들면, SpringBoot의 의존성 시리즈인 starter의 경우엔 사용하고 싶은 의존성이 Freemarker라면 spring-boot-starter-freemarker만 추가하면 그외에 어떤 의존성도 필요없다.

잘못된 오해

  • Spring 과 Spring Boot는 전혀 다른 프레임워크가 아니다.
  • Spring Boot는 Spring의 Best Practice를 제공해주고 편의성을 향상시킨것 뿐이지, Spring에서 안되던게 Spring Boot에서는 작동하는 그런 방식이 아니다.

  • 참고
    • https://d2.naver.com/helloworld/5626759
    • https://jojoldu.tistory.com/43

프로그래머스 문제풀이, 숫자야구

|

프로그래머스 - 숫자 야구

문제 설명

문제설명 바로가기

  • 제한사항
    • 질문의 수는 1 이상 100 이하의 자연수입니다.
    • baseball의 각 행은 [세 자리의 수, 스트라이크의 수, 볼의 수] 를 담고 있습니다.
  • 푼방식
    • 숫자야구에서 쓸수있는 수를 만든다. 모든 자릿수의 숫자가 겹치면 안됨, 0도 올수없음.
    • 만든 숫자와, 2차원 배열로 주어진 숫자를 통해서 값을 비교해서 스트라이크와 볼의 갯수를 구한다.
    • 볼 - 스트라이크값을 반드시 해주어야한다. 예를 들면 답이 132일 경우, 주어진 값이 123이면 1스트라이크 3ball이 아니다.
    • 1 strike, 2ball이다. contains로 구하면 이런 경우를 생각해서 볼에서 스트라이크 값을 빼야한다
    • 모든 baseball 배열에서 주어진 스트라이크 볼 값과 동일 해야함

public class Programmers42841 {
    public static void main(String[] args) {
        Programmers42841 program = new Programmers42841();
        System.out.println(program.solution(baseball));
    }

    public int solution(int[][] baseball) {
        int answer = 0;
        // 일단은 겹치지 않는 숫자를 만들자
        List<String> list = new ArrayList<>();

        int s = 0;
        int b = 0;
        int s1 = 0;
        int b1 = 0;

        for(int i = 1; i < 10; i++) {
            for(int j = 1; j < 10; j++) {
                for(int k = 1; k < 10; k++) {
                    if(i != j && j != k && i != k) {
                        list.add(String.valueOf(i * 100 + j * 10 + k));
                    }
                }
            }
        }
        for(int i=0; i<list.size(); i++){
            String temp = list.get(i);
            int cnt = 0;
            for (int j = 0; j < baseball.length && cnt < baseball.length; j++) {

                s = baseball[j][1];
                b = baseball[j][2];
                s1 = strike(temp,Integer.toString(baseball[j][0]));
                b1 = ball(temp,Integer.toString(baseball[j][0])) - s1;
                if(s1 == s && b == b1)
                    cnt++;
                // 베이스볼 같은 경우, 베이스의 볼의 숫자 모두와 일치하는 경우의 수만 정답
                // 123, 356, 327, 489를 통해서 얻은 스트라이크와 볼 갯수가 모두 일치하는 숫자만 정답..!
                // 하나라도 일치하는 경우의 수로 구할경우 정답이 나오지 않는다.. 이런 디테일이 중요함!!
                if(cnt == baseball.length)
                    answer++;
            }
        }
        return answer;
    }

    public static int strike(String num, String target) {
        int cnt = 0;
        for(int i = 0; i < target.length(); i++) {
            cnt = num.charAt(i) == target.charAt(i) ? cnt + 1 : cnt;
        }
        return cnt;
    }

    public static int ball(String num, String target) {
        int cnt = 0;
        for(int i = 0; i < target.length(); i++) {
            cnt = num.contains(String.valueOf(target.charAt(i))) ? cnt + 1 : cnt;
        }
        return cnt;
    }
}

ORM이란?

|

ORM이란?

  • ORM(Object-relational mapping)을 단순하게 표현하면 객체와 관계와의 설정이라 할 수 있다. 그렇다면 과연 객체와 관계라는 것이 의미하는 것은 무엇일까? 지극히 기초적인 이야기지만 개발자가 흔히 사용하고 있는 관계형 데이터베이스를 의미한다.

왜 이런 개념이 나왔을까?

  • 그렇다면 도대체 무엇이 문제여서 객체와 관계형 데이터베이스 간의 매핑을 지원해주는 Framework나 Tool들이 나오는 것일까? 굳이 이런 새로운 개념들이 나오게 되는 이유는 “Back to basics(기본에 충실하자)” 을 지키기 위해서라고 볼 수 있다. 즉, 보다 OOP다운 프로그래밍을 하자는데부터 출발한 것이다.
  • 그럼 과연 무엇이 문제였던 것일까? 우리가 어떤 어플리케이션을 만든다고 하면 관련된 정보들을 객체에 담아 보관하게 된다.
  • 즉, 테이블(Table)에 객체가 가지고 있던 정보를 입력하고, 이 테이블들을 “join”과 같은 SQL 질의어를 통해 관계 설정을 해 주게 된다. 여기서 문제는 객체 모델과 관계형 테이블간에 불일치가 존재한다.

어떤식으로 해결할까?

  • 데이터베이스 <-> 매핑 <-> Object
    • 객체를 통해 간접적으로 데이터베이스 데이터를 다룰 수 있다.
  • ORM을 통해 객체간의 관계를 바탕으로 SQL을 자동으로 생성하여, 불일치를 해결한다.
  • Persistant API라고도 할 수 있다. Ex) JPA, Hibernate 등

장점

  • 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 있게 도와준다.
    • 선언문, 할당, 종료 같은 부수적인 코드가 없거나 급격히 줄어든다.
    • 각종 객체에 대한 코드를 별도로 작성하기 때문에 코드의 가독성을 올려준다.
    • SQL의 절차적이고 순차적인 접근이 아닌 객체 지향적인 접근으로 인해 생산성이 증가한다.
  • 재사용 및 유지보수의 편리성이 증가한다.
    • ORM은 독립적으로 작성되어있고, 해당 객체들을 재활용 할 수 있다.
    • 때문에 모델에서 가공된 데이터를 컨트롤러에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데 유리하다.
    • 매핑정보가 명확하여, ERD를 보는 것에 대한 의존도를 낮출 수 있다.
  • DBMS에 대한 종속성이 줄어든다.
    • 대부분 ORM 솔루션은 DB에 종속적이지 않다.
    • 종속적이지 않다는것은 구현 방법 뿐만아니라 많은 솔루션에서 자료형 타입까지 유효하다.
    • 프로그래머는 Object에 집중함으로 극단적으로 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요된다.
    • 또한 자바에서 가공할경우 equals, hashCode의 오버라이드 같은 자바의 기능을 이용할 수 있고, 간결하고 빠른 가공이 가능하다.

단점

  • 완벽한 ORM 으로만 서비스를 구현하기가 어렵다.
    • 사용하기는 편하지만 설계는 매우 신중하게 해야한다.
    • 프로젝트의 복잡성이 커질경우 난이도 또한 올라갈 수 있다.
    • 잘못 구현된 경우에 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.
    • 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는등 별도의 튜닝이 필요한 경우가 있다.
    • DBMS의 고유 기능을 이용하기 어렵다. (하지만 이건 단점으로만 볼 수 없다 : 특정 DBMS의 고유기능을 이용하면 이식성이 저하된다.)
  • 프로시저가 많은 시스템에선 ORM의 객체 지향적인 장점을 활용하기 어렵다.
    • 이미 프로시저가 많은 시스템에선 다시 객체로 바꿔야하며, 그 과정에서 생산성 저하나 리스크가 많이 발생할 수 있다.

참고

  • https://gmlwjd9405.github.io/2019/02/01/orm.html
  • http://www.incodom.kr/ORM#h_702209f3f35878a32ee91352ddc6bbe7

ContextSwitching이란?

|

정확히 알기 위해서 따로 포스팅으로 정리했다.

Context Switching 이란?

  • CPU가 하나의 프로세스가 실행중인 상태에서 현재 프로세스의 상태값을 저장하고, 다른 프로세스의 상태값을 읽어 들이는 행위

왜 Context Switching을 사용하는가?

  • 만약 어떤 일을 처리할 때, 하나의 일이 모두 끝날때까지 기다리는건 비효율적인 일입니다.
  • Context Switching을 사용하지 않으면, 하나의 프로세스가 끝날때까지는 다른 프로세스를 실행할 수 없습니다.
  • CPU가 CS을 통해서 여러개의 프로세스를 돌아가면서 실행하기 때문에, 사용자 입장에서는 동시에 처리하는 것과 같은 효과를 누릴 수 있습니다.

Context Switching의 작동원리

  • Context Switching은 다음과 같은 상황에서 일어납니다.
    • 입출력을 요청할 때 ( I/O 요청 )
    • CPU의 사용시간이 만료됐을 경우
    • 부모프로세스를 통해 자식 프로세스를 생성할 때 ( FORK 함수 이용 )
    • 인터럽트가 처리될때까지 기다릴때
  • 해당 OS 스케쥴러의 우선 순위 알고리즘을 통해서 순서가 정해진대로 CS가 발생합니다( OS마다 다를수 있다는 얘기 )

  • Context Switching은 PCB( Process Control Block )에 저장됩니다
    • 즉 스케쥴러의 우선순위에 따라서 PCB에서 정보를 읽어들여서 실행됩니다.
  • PCB의 저장정보
    • 프로세스의 상태 : 생성, 준비, 수행, 대기, 중지
    • 프로그램 카운터 : 프로세스가 다음에 실행할 명령어 주소
    • 레지스터 : 스택, 색인정보 등
    • 해당 프로세스의 id

Context Switching의 비용

  • Context Switching이 자주 일어날수록, CPU 자원 소모는 커질수밖에 없습니다( 오버헤드 발생 ).
    • Cache 초기화
    • Memory Mapping 초기화
    • Kernel이 항상 실행되어 있어야됨( 메모리가 접근해야함 )

Process 와 Thread 중 왜 Thread를 더 많이 사용할까?

  • CPU 자원소모가 적게 든다 (Context Switching 비용이 Process가 더 많이 듭니다)
    • Thread는 Stack 영역만 초기화를 하면 됩니다.
    • 그러나 Process 같은 경우 Chache, Memory Mapping까지 모두 초기화를 해주어야합니다.
  • 데이터 공유가 어렵다
    • 프로세스의 경우 데이터를 공유하려면 IPC 통신을 사용해야한다
    • 스택같은 경우는 힙 메모리를 서로 공유한다 ( 별다른 통신 필요 X )