Java Overview
1. 객체지향
(1) 객체지향 구성요소
클래스 : 동일 종류의 집단에 속하는 속성과 행위를 정의
객체 : 객체의 행위는 클래스에 정의된 행위에 대한 정의를 공유함으로써 메모리를 경제적사용
메소드 : 클래스로부터 생성된 객체를 사용
메시지 : 객체에게 어떤 행위를 하도록 지시하기 위한 방법
인스턴스 : 클래스에 속한 각각 객체(실제로 메모리상에 할당)
속성 : 한 클래스 내에 속한 객체들이 가지고 있는 데이터 값 들을 단위별로 정의
(2) 객체지향 기본 원칙
캡슐화 : 서로 관련성 많은 데이터와 관련 함수들을 한 묶음으로 처리. 결합도가 낮아 재사용 굿
상속성 : 상위 클래스의 요소들을 하위 클래스에서 재정의 없이 물려 받음
다형성 : 하나 메시지에 대해 각 객체가 가진 고유 방법으로 응답하는 능력(오버로딩, 오버라이딩)
추상화 : 공통 성질을 추출하여 추상 클래스를 설정하는 기법
정보은닉 : 코드 내부 데이터와 메소드 숨김. 공개 인터페이스를 통해서만 접근 가능. Side-Effect 최소화를 위함
(3) 객체지향 설계 원칙 : SOLID
Single Responsibility Principle : 하나의 클래스(객체)는 오로지 한 기능에 대해서만 작동하도록 함
Open Close Principle : 구현이나 개발 시, 기존의 기능을 수정하는 것이 아닌 확장을 통해 구현
- LSP(리스코프 치환원칙) : 부모 객체와 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있다는 원칙이다.
- 잘못된 객체를 상속하거나 올바르게 확장하지 못할 경우, 겉으로 보기엔 정상적이지만 올바른 객체라고 할 수는 없다.
- 수학적으로는 정사각형은 직사각형이다 라는 관계이지만, 객체지향 상속 관점에서는 직사각형이라는 상위 개념 아래에 정사각형이나 있는 관계가 맞다.
Interface Segregation Principle : 클라이언트는 미사용 메소드에 의존하지 않도록, 인터페이스를 분리해야한다
- Dependency Inversion Principle : 클라이언트는 추상화에 의존해야 하며, 구상화된 클래스에 의존 하면 안된다
- 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다.
- 대신 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
2. Java 컴파일 과정
- 들어가기 전 : 자바는 OS에 독립적인 특징을 가짐. 그 이유는 JVM(Java Virtual Machine) 덕분이다
(1) 자바 컴파일 순서
1) 개발자가 자바 소스코드(.java) 작성
2) Java Compiler가 자바 소스파일을 컴파일하고 나면 자바 바이트 코드(.class)로 변환.
바이트 코드는 컴퓨터가 읽을 수 없고 자바 가상 머신이 이해 가능한 코드
3) 컴파일된 바이트 코드를 JVM의 클래스로더(Class Loader)에게 전달
4) 클래스 로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크 하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올림
(2) Class Loader 세부 동작
1) 로드 : 클래스 파일을 가져와서 JVM 메모리에 로드
2) 검증 : 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사
3) 준비 : 클래스가 필요로 하는 메모리를 할당. (필드, 메소드, 인터페이스 등등)
4) 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
5) 초기화 : 클래스 변수들을 적절한 값을 초기화(static 필드)
(3) 실행엔진(Execution Engine)
실행엔진(Execution Engine)은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행. 이때, 실행 엔진은 두가지 방식으로 변경
- 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행. 하나하나 실행은 빠르나, 전체적인 실행 속도가 느림
JIT 컴파일러(Just-In-Time Compiler) : 인터프리터의 단점을 보완하기 위해 도입.
바이트 코드 전체를 컴파일 하여 바이너리 코드로 변경하고 이후에는 해당 메소드를 더 이상 인터프리팅 하지 않고, 바이너리 코드로 직접 실행하는 방식.
바이트 코드 전체가 컴파일된 바이너리 코드를 실행하므로 전체적인 실행속도가 인터프리터 보다 빠름
3. 자바 가상 머신 (Java Virtual Machine)
- 개요 : 시스템 메모리를 관리하면서, 자바 기반 애플리케이션을 위해 이식 가능한 실행 환경 제공
- 기능
1) 자바 프로그램이 어느 기기나 운영체제 상에서 실행될 수 있도록 하는 것
2) 프로그램 메모리를 관리하고 최적화하는 것
- 개발자들이 말하는 JVM은 보통 어떤 기기상에서 실행되고 있는 프로세스, 특히 자바 앱에 대한 리소스를 대표하고 통제하는 서버를 지칭
(1) JVM에서의 메모리 관리
JVM 실행에 있어서 가장 일반적인 상호작용은, 힙과 스택의 메모리 사용을 확인하는 것
실행 과정
1) 프로그램이 실행되면, JVM은 OS로부터 이 프로그램이 필요로 하는 메모리를 할당받음.
JVM은 이 메모리를 용도에 따라 여러 영역에 나누어 관리
2) 자바 컴파일러(JAVAC)가 자바 소스코드를 읽고, 자바 바이트코드(.class)로 변환
3) 변경된 class 파일들은 클래스 로더를 통해 JVM 메모리 영역으로 로딩
4) 로딩된 class 파일들은 Execution Engine을 통해 해석
5) 해석된 바이트 코드는 메모리영역에 배치되어 실질적 수행이 이루어짐.
이러한 실행 과정 속 JVM은 필요에 따라 스레드 동기화나 가비지 컬렉션 같은 메모리 관리 작업을 수행함
- 자바 컴파일러 : 자바 소스코드를(.java)를 바이트 코드(.class)로 변환
- 클래스 로더 : JVM은 런타임 시에 처음으로 클래스를 참조할 때 해당 클래스를 로드하고 메모리 영역에 배치시킴.
- Runtime Data Areas : JVM이 OS 위에서 실행되면서 할당받는 메모리 영역
총 5가지 영역으로 나뉨 : PC레지스터, JVM스택, 네이티브 메서드 스택, 힙, 메서드영역
1) PC 레지스터 : 스레드가 어떤 명령어로 실행되어야 할지 기록하는 부분
(JVM 명령의 주소 가짐) 2) 네이티브 메소드 스택 : Java외의 C나 C++ 로 작성된 코드를 실행
3) JVM 스택 : 지역변수, 매개변수, 메소드 정보, 임시 데이터 등을 저장
4) 힙 : 런타임에 동적으로 할당되는 데이터가 저장되는 영역. 객체나 배열 생성 등
(또한, 힙에 할당된 데이터들은 가비지 컬렉터의 대상이 됨. JVM 성능 이슈에서 가장 많이 언급)
5) 메소드 영역 : JVM이 시작될 때 생성되고, JVM이 읽은 각각의 클래스의 인터페이스에 대한 런타임 상수 풀, 필드 및 메소드 코드, 정적 변수, 메소드의 바이트 코드 등을 보관
- 가비지 컬렉션(Garbage Collection)
자바 이전에는 프로그래머가 모든 프로그램을 관리 했지만 자바에서는 JVM이 프로그램 메모리 관리
가비지 컬렉션은 자바 프로그램에서 사용되지 않는 메모리를 지속적으로 찾아내서 제거하는 역할
(실행 순서 : 미참조된 객체들을 탐색 후 삭제 -> 삭제된 객체의 메모리 반환 -> 힙 메모리 재사용)
4. Garbage Collection(가비지 컬렉션)
(1) 요약
- C/C++ 프로그래밍 에서는 메모리 누수를 막기위해 사용자가 직접 해제해줘야 했음
- JAVA는 JVM이 구성된 JRE(Java Runtime Environment)가 제공되며, 그 구성 요소 중 하나인 Garbage Collection이 자동으로 사용하지 않는 객체를 해제한다
- stop-the-world : GC를 실행하기 위해 JVM이 실행중인 어플리케이션을 멈추는 것임.
어떤 GC 알고리즘을 사용하더라도 ‘stop-the-world’는 발생하게 되는데, 대개의 경우 GC 튜닝은 이 ‘stop-the-world’ 시간을 줄이는 것이라 한다.
- GC를 해도 더 이상 사용 가능한 메모리 영역이 없는데 계속 메모리를 사용하려 하면, OutOfMemoryError가 발생하여 WAS가 다운 될 수 있다.
이를 행(Hang)이라 한다.(서버가 요청을 처리하지 못하는 상태)
(2) Garbage Collection
자바는 개발자가 명시적으로 객체를 해제할 필요가 없음
기본적으로 JVM 메모리는 5가지 영역 (class, stack, heap, native method, PC)로 나뉜다
그 중, GC는 힙 메모리만 다룬다
* GC의 대상이 되는 경우
1) 객체가 NULL (ex String str = null;)
2) 블럭 실행 종료 후, 블록 안에서 생성된 객체
3) 부모 객체가 null인 경우, 포함하는 자식 객체
(GC는 Weak Generational Hypthesis에 기반)
(3) GC의 메모리 해제 과정
1) Marking
- 마킹을 통해 메모리의 사용 여부를 판단.
참조되는 객체는 파란색, 미사용 객체는 주황색임
2) Normal Deletion
참조되지 않은 객체를 제거하고, 메모리를 반환
메모리 Allocator는 반환되어 비어진 블록 참조 위치를 저장했다가, 새 오브젝트가 선언되면 할당하도록 함
3) Compacting
- 퍼포먼스를 향상시키기 위해, 참조되지 않는 객체를 제거하고 또한 남은 참조되어지는 객체들을 묶음. 이로써 여유공간이 생겨나서 메모리 할당 시 더 쉽고 빠르게 가능
4. Generational Garbage Collection 배경
모든 객체를 Marking하고 Compact하는 JVM은 비효율적임
(Y축은 할당된 바이트 수, X축은 바이트가 할당 될 때의 시간)
- 시간이 지날수록 적은 객체만이 남는다. 위와 같은 그래프에 기반한 것이 Weak Generational Hypothesis
5. Weak Generational Hypothesis
- 신규로 생성한 객체의 대부분은 금방 사용하지 않는 상태가 되고, 오래된 객체에서 신규 객체로의 참조는 매우 적게 존재한다는 가설
- 이 가설에 기반하여 자바는 Young 영역과 Old 영역으로 메모리를 분할해서, 신규 생성된 객체는 Young으로, 오랫동안 살아남은 객체는 Old에 보관
6. Generational Garbage Collection
1) Young 영역
새롭게 생성한 객체의 대부분이 이곳에 위치
대부분 객체가 금방 접근 불가상태가 되어 매우 많은 객체가 Young에 있다가 사라짐
이 영역에서 객체가 사라질 때 Minor GC 가 발생한다고 함
2) Old 영역
접근 불가능 상태가 되지 않아 Young에서 살아남는 객체가 이곳으로 복사
대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다는 GC가 적게 발생
이 영역에서 객체가 사라질 때 Major GC 또는 Full GC 라 부름
3) Permanent 영역
Method Area 라고도 함
JVM이 클래스들과 메소드들을 설명하기 위해 필요한 메타데이터를 포함
JDK8 부터는 PermGen은 Metaspace로 교체됨
7. Generational Garbage Collection 과정
1) 어떤 새로운 객체가 들어오면 Eden Space에 할당
2) Eden space가 가득차면, minor garbage collection이 시작됨
3) 참조되는 객체들은 첫번째 survivor(S0)으로 이동, 비 참조 객체는 Eden space가 clear 될 때 반환됨
4) 다음 minor GC때, Eden space에서 같은 일이 발생.
비 참조 객체는 삭제되고 참조 객체는 survivor space로 이동.
그러나 이 케이스에서 참조객체는 두번째 survivor space로 이동하게 됨.
최근 minor GC에서 첫번째 survivor space로 이동한 객체들도 age가 증가하고 S1으로 이동
한번 모든 surviving 객체들이 S1으로 이동하면 S0과 Eden공간은 Clear 된다
이때 주의할 점은 이제 다른 aged 객체들을 survivor 공간에 가지게 되었다는 점
5) 다음 minor GC때, 같은 과정이 반복됨.
그러나 이번엔 survivor space들을 switch 된다. 참조되는 객체들은 S0으로 이동
살아남은 객체들은 aged 된다. 그리고 Eden과 S1 공간은 Clear
6) promotion 과정.
- minor GC 후 aged 오브젝트들이 일정한 age threshold(문지방)을 넘게 되면, 그들은 young generation에서 old generation으로 promotion됨
7) minor GC가 계속되고 계속해서 객체들이 Old Generation으로 이동됨.
8) 전체적인 흐름 요약
- 결국 major GC가 old Generation에 시행되고, old Generation은 Clear 되고, 공간이 Compact 되어진다.
[ 가비지 컬렉션(Garbage Collection)에 의한 시스템 중단 시간을 줄이는 방법 ]
- 옵션을 변경하여 GC의 성능을 높이기
- young 영역과 old 영역의 힙 크기를 높여 GC의 빈도를 줄이는 것
- 객체의 할당과 promotion을 줄이는 것
위의 설명 중에서 힙 크기를 높여 GC의 빈도를 줄이는 해결책이 있습니다. 사실 논리적으로만 생각하면 힙의 크기를 높이면, GC의 실행시간이 길어져서 무의미해진다고 생각이들 수 있습니다. 하지만 Minor GC의 실행시간은 힙의 크기보다는 Collection에서 살아남은 객체의 수에 더욱 지연됩니다. 그렇기 때문에 short-lived 객체를 위한 young 영역의 크기를 높인다면 GC의 실행 시간과 호출 빈도를 모두 줄일 수 있습니다.(하지만 만약 애플리케이션에서 long-lived 객체를 많이 사용한다면, survivor영역으로 복제되는 객체가 많아져 GC에 의한 멈추는 시간이 증가할 수 있습니다.)
출처:
https://mangkyu.tistory.com/94
[MangKyu’s Diary]
GC의 종류
JVM 에는 생각보다 많은 종류의 GC 알고리즘이 있다. 참고로, Java 7, 8은 기본 GC로 Parallel GC를 사용하고, Java 9, 10 은 G1 GC를 사용한다고 한다. Java 11부터는 실험적 기능인 Z GC를 사용할 수 있다. 15부터는 정식 기능으로 출시할 예정인 듯 하다.
Serial GC
순차적인 GC 라는 의미로, Mark-Sweep-Compaction 알고리즘이 한 번에 하나씩만 동작한다. 가장 오래된 GC이며, 요즘에는 사용되지 않고, 사용해서도 안된다. Stop-The-World 시간이 너무 길기 때문.
Parallel GC
Serial GC가 하나의 스레드로 Mark-Sweep-Compaction을 수행한다면, Parallel GC는 여러 개의 스레드로 Mark-Sweep-Compaction을 수행한다. 이로 인해 Stop-The-World 시간이 줄어들게 된다.
Parallel Old GC
Parallel GC와 비슷하나, Mark-Sweep-Compaction 알고리즘 대신 Mark-Summary-Compaction 알고리즘을 사용한다. Summary 작업은 Sweep 작업에 살아있는 객체를 식별하는 작업이 추가된 것이다. (이름만 봐서는 이게 더 옛날 GC같기도..)
CMS GC
앞의 GC 방식보다 더 개선되었으면서, 복잡한 방식이다. Stop-The-World 시간을 최소화 하는데 초첨을 맞췄다. 컨셉은 GC 대상 객체를 최대한 자세히 파악한 후, Stop-The-World 가 발생하는 Sweep 시간을 최소화 하는데 초점을 맞췄다. Low Latency GC라고도 부른다.CMS GC는 총 4단계에 걸쳐 이루어진다.
- Initial MarkGC 과정에서 살아남을 객체를 Root Set에서 가장 가까운 객체만 탐색하며, 참조가 끊겼는지를 확인한다. 이 과정에서 Stop-The-World 가 일어나지만, 탐색 깊이가 짧아 멈추는 시간 역시 짧다.
- ConcurrentMarkInitial Mark 단계에서 GC 대상으로 판별한 객체들을 따라가며 GC 대상인지 추가로 확인한다. 이 과정중에는 Stop-The-World 가 일어나지 않는다.
- RemarkConcurrent Mark 단계의 결과를 검증한다. Concurrent Mark 단계에서 GC 대상으로 추가 확인되었는지, 참조가 제거되었는지 등 확인을 한다. 이때 Stop-The-World가 일어나며, 이 시간을 최대한 줄이기 위해 멀티스레드로 검증작업을 수행한다.
- Concurrent SweepGC 대상으로 판별된 객체들을 멀티스레드로 메모리에서 제거한다. 이때 Stop-The-World가 발생하지 않는다.
단점으로는 하는 일이 많다보니 CPU 부하가 커진다는 것이고, Compaction이 기본적으로 제공되지 않고 필요할 때만 일어나는데, 이때의 Stop-The-World 시간이 다른 GC보다 더 길게 걸릴 수도 있다.
G1 GC (Garbage First)
하드웨어가 발전되어 JVM을 가동하는 메모리의 크기도 점점 커져가는데, 이전까지의 GC들은 큰 용량의 메모리에 적합하지 않다(Root set부터 순차적으로 탐색하기에 용량이 클 수록 탐색 시간이 길어짐).G1 GC는 이런 점을 개선하여, 큰 Heap 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.G1 GC는 앞서 살펴본 Eden, Survivor, Old 영역이 존재하지만, 고정된 크기로 고정된 위치에 존재하지 않는다. 전체 Heap 영역을 Region이라는 특정한 크기로 나눠서, 각 Region의 상태에 따라 역할(Eden, Survivor, Old)이 동적으로 부여된다. 2048개의 Region으로 나뉠수 있으며, 옵션을 통해 1MB~32MB 사이로 지정할 수 있다.
참고 : https://velog.io/@hygoogi/자바-GC에-대해서#cms-gc
5. Call by value와 Call by reference
(1) Call by value : 값에 의한 호출
함수가 호출될 때, 메모리 공간 안에서는 함수를 위한 별도의 임시공간이 생성됨
call by value 호출 방식은 함수 호출 시 전달되는 변수 값을 복사해서 함수 인자로 전달
이때 복사된 인자는 함수안에서 지역적으로 사용되므로 local value 특성임
(2) Call by reference : 참조에 의한 호출
call by reference 호출 방식은 함수 호출 시 인자로 전달되는 변수의 레퍼런스 전달
따라서 함수 안에서 인자 값이 변경되면, argument로 전달된 객체의 값도 변경
(3) Java 함수 호출 방식
자바의 경우, 항상 call by value로 값을 넘긴다.
C/C++과 같이 변수의 주소 값 자체를 가져올 방법이 없음.
따라서 원본 객체의 property까지는 접근 가능하나, 원본 객체 자체는 변경 불가
6. Primitive type & Reference type
(1) Primitive type (기본형 타입)
Java에서는 총 6가지 primitive type 존재
비객체 타입이므로 null 값을 가질 수 없다.
OS에 따라 자료형 길이가 변하지 않음
스택 메모리에 저장된다
1) Boolean : 기본값은 false이며 참과 거짓 저장
2) byte : 이진데이터 다룸
3) short : 정수형 (2byte)
4) int : 정수 연산을 하기위한 기본 타입
5) long : 수치가 int보다도 더 큰 데이터를 다룸. 숫자 뒤에 ‘L’을 붙일 것
6) float, double : 실수 표현. 자바의 실수 기본타입은 double이므로 float에는 알파벳 f를 붙여야 한다.
(2) Reference type (참조형 타입)
Java에서 Primitive type을 제외한 타입들이 모두 Reference type
Java의 최상위인 java.lang.Object 클래스를 상속하는 모든 클래스
class type, interface type, array type, enum type 존재
heap 메모리에 생성된 인스턴스는 메소드나 각종 인터페이스에서 접근하기 위해 JVM의 Stack 영역에 존재하는 Frame에 참조값을 가지고 있어 이를통해 인스턴스 핸들링
(3) String Class
참조형에 속하지만 기본적인 사용은 기본형처럼 사용
그리고 불변(immutable)의 특성을 지님
비교를 할 경우, ==가 아닌 .equals()라는 메소드를 사용해야함
7. 문자열 클래스
(1) String 특징
new 연산을 통해 생성된 인스턴스의 메모리 공간은 불변함
Garbage Collector로 제거되어야 함
문자열 연산 시 새로 객체를 만드는 Overhead 발생
(2) StringBuffer, StringBuilder
- 공통점
- new 연산으로 클래스를 한 번만 만듦(Mutable)
- 문자열 연산시 새로 객체를 만들지 않고, 크기 변경
- StringBuffer와 StringBuilder 클래스의 메소드가 동일
- 차이점
- StringBuffer는 동기화를 지원하기 때문에 Multi-Thread 시에 더 사용됨\
- StringBuilder는 동기화를 지원하지 않으므로 Single-Thread 시에 더 사용
8. Object Class 메소드
toString() : 문자열 형태로 바꾸는 메소드
hashCode() : 객체의 해쉬코드 값을 리턴
wait() : 갖고 있던 고유 lock 해제, Thread를 잠들게 함
notify() : 잠들던 Thread 중 임의의 하나를 깨움
notifyAll() : 잠들어 있던 Thread를 모두 깨움
(wait, notify, notifyAll : 호출하는 스레드가 반드시 고유 락을 갖고 있어야 함)
9. 메모리, 성능을 개선하는 방법
- static 키워드를 사용한다.
- static 키워드를 사용하여 클래스 변수나 메서드를 선언함으로써, 인스턴스를 생성하지 않고도 해당 변수와 메서드에 접근할 수 있다. => static 메서드는 인스턴스 없이 직접 호출될 수 있어, 객체 생성 오버헤드 없이 필요한 기능을 수행할 수 있다.
- static 변수는 클래스가 로드될때 메모리에 단 한번 할당되며, 이후 생성되는 모든 인스턴스에서 해당 변수를 공유하기때문에 메모리 사용을 줄일 수 있다. => 모든 인스턴스에서 공유해야하는 공통 데이터나 유틸리티 함수를 관리할때 유용하다.
- 그 외
- 캐싱 : 자주 사용되는 데이터나 계산 결과를 메모리에 캐시하여 동일한 정보나 결과를 다시 계산하거나 데이터베이스에서 조회하는데 드는 비용 절약 가능
- 데이터 구조 최적화 : 애플리케이션에 적합한 데이터 구조를 선택
- ex) 검색속도 중요할경우 해시 테이블, 요소 삽입, 삭제 성능을 개선하기 위해 연결리스트 사용
- 지연 로딩 : 객체나 데이터를 실제로 사용할때까지 로딩을 지연시킴으로써 초기 로딩시간을 단축하고 필요하지 않은 리소스의 메모리 사용 줄이기
10. Thread 생성 방식
1) Thread 클래스 상속
- 스레드 생성
- java.lang.Thread 클래스를 상속(extends)받아서 run() 메서드 오버라이딩
- 장점 : Thread 클래스 내의 메소드들을 활용하여 다양한 구현 가능
- 단점 : 객체지향 특성 상 다중상속이 안되므로, Thread 클래스 run 메소드의 구현 하나 때문에 상속기능을 사용하는것은 확장성 관점에 있어서는 비효율적
- java.lang.Thread 클래스를 상속(extends)받아서 run() 메서드 오버라이딩
2) Runnable 인터페이스 구현
- 스레드 생성
- java.lang.Runnable 인터페이스로부터 run()메서드를 구현하여 생성 가능
- 참고로 Runnable 인터페이스는 run() 메서드 1개만 가지는 함수형 인터페이스
- 장점 : Interface는 다중상속이 가능하기 때문에, run 메소드에 대한 구현은 Runnable Interface에서 구현 가능하도록 하고, 그 외의 필요한 interface는 사용자가 직접 구현하여 확장 가능
- 단점 : Interface이기 때문에 run 메소드에 대해 반드시 구현(Override)이 되어져야 함