CS

[CS] JAVA

날아 2023. 2. 5. 04:51
자바의 특징
더보기
  • 운영체제에 독립적이다. (JVM)
  • OOP의 특징인 캡슐화, 상속, 다형성, 추상화의 특징이 있다.
  • 보안성이 뛰어나다.
  • GC가 자동적으로 메모리를 관리해준다.
  • 멀티 쓰레드 지원한다. 

 

+ 어느 하드웨어던, 운영체제던 상관없이 컴파일된 코드(바이트코드)가 플랫폼 독립적이다. (어느 플랫폼이든 작성한 소스를 변겨할 필요 없이 다 실행시킬 수 있다.)

JVM은 단순하게 말하자면 컴파일된 코드(바이트코드)를 실행시켜주는 가상의 컴퓨터라고 생각하면 좋다. 

 

자바의 구동원리 (컴파일 순서)
더보기
  1. 프로그램이 실행되면 JVM은 OS로부터 필요한 메모리를 할당받는다. (Runtime Data Area)
  2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 읽어들여 자바 바이트코드(.class)로 변환. 
  3. Class Loader는 동적로딩(Dynamic Loading)을 통해 Class 파일들을 JVM으로 로딩
  4. 로딩된 class 파일들은 Execution engine을 통해 해석
  5. 해석된 바이트코드는 Runtime Data Areas에 배치되어 실질적인 수행이 이루어진다.

자바 바이트 코드

  • 자바 바이트코드는 아직 컴퓨터가 읽을 수 없는 자바 가상 머신이 이해할 수 있는 코드이다. 1바이트 크기의 Opcode와 추가 피연산자로 이루어져있다. 

클래스로더 세부 동작

  • 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드
  • 검증 : 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되어 있는지 검사
  • 준비 : 클래스가 필요로 하는 메모리를 할당 (필드, 메서드, 인터페이스 등)
  • 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
    • 심볼릭 레퍼런스 : 참고하는 클래스의 특정 메모리 주소를 참조관계로 구성한 것이 아닌 참조하는 대상의 이름으로 참조하는 것 
    • 다이렉트 레퍼런스 : 참조하는 클래스의 특정 메모리 주소를 참조하는 것
  • 초기화 : 클래스 변수들을 적절한 값으로 초기화한다. (static 필드)

 Execution engine(실행엔진)은 두 가지 방법으로 바이트코드를 명령어 단위로 변경

  1. 인터프리터 : 바이트코드의 명령어를 하나식 읽어서 해석하고 실행. 하나하나 메서드 실행 속도는 빠르지만 전체적인 실행 속도가 느리다는 단점
  2. JIT(Just-In-Time) 컴파일러 : 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더이상 인터프리팅하지 않고, 네이티브 코드로 직접 실행하는 방식. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트코드 전체가 컴파일된 네이티브코드 를 실행하는 것이기 때문에 전체적인 실행속도는 인터프리팅 방식보다 빠릅니다. 또한 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일 된 코드는 계속 빠르게 수행된다.

JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

 

JVM의 특징
더보기
  • 자바 가상머신이라 불리며 자바소스로부터 만들어진 바이너리파일 즉 [.class]파일을 실행하기 위해 필요하다.
  • java가 OS에 구애받지 않고 재사용가능하게 해줍니다. (어느 기기나 운영체제 상에서도 실행될 수 있게 한다.)
  • 자동메모리관리기법인 Garbage Collection을 수행합니다.
  • JRE : 자바실행환경. JVM으로 자바프로그램을 동작시킬 때 필요한 파일들을 가지고 있다.
  • JDK : 자바 개발을 하기 위해 필요한 환경이다. JRE를 포함한다.

 

JVM구조
더보기

가비지 콜렉터(GC)

  • Heap 메모리 영역에 생성 된 객체들 중에 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다.
  • 객체는 Heap 영역에 저장되고, 스택영역에 이를 가리키는 주소값이 저장된다. 힙 영역에서 자신을 가리키는 주소값이 없으면 참조되고 있지 않다고 판단한다. 
  • 장점 : 메모리 누수를 막을 수 있다. 해제된 메모리에 접근하는 오류와, 해제된 메모리를 한 번 더 해제는 이중해제를 막을 수 있다.
  • 단점 : 메모리 해제 타이밍을 개발자가 정확하게 알기 힘들다(오버헤드) . 실시간이 강조되는 프로그램에서는 GC에게 맞기는 것이 알맞지 않을 수 있다. 

Runtime Data Area의 다섯가지 영역

  • 모든 스레드에서 공유하는 영역 
    • Method Area
      • 메소드 영역에서 자바 프로그램의 클래스 코드, 변수 코드, static, final 변수 등이 생성.
      • 프로그램의 시작부터 종료가지 메모리에 남아있다. 
    • Heap Area
      • new 키워드로 생성한 객체가 저장되는 영역. 동적으로 생성된 객체와 배열이 저장되는 곳으로 GC의 대상이 되는 영역이다. 
      • 메소드가 실행되면 스택 영역에 메소드에 대한 영역이 1개 생긴다. 메소드가 호출될 때 메모리에 할당되고 종료되면 메모리가 해제된다. 
    • Stack Area 
      • 지역변수, 파라미터 등이 생성되는 영역. 동적으로 객체를 생성하면 실제 객체는 Heap에 할당되고 해당 레퍼런스만 스택에 저장된다. 
      • stack은 스레드별로 독자적으로 가진다
  • 각 스레드 별로 생성되는 영역 
    • PC Register : 현재 쓰레드가 실행되는 부분의 주소와 명령어를 저장하고있다.
    • Native Method : 자바외 언어로 작성된 네이티브 코드를 위한 메모리 영역 

+ 각 메모리 영역에 할당되는 시점 

  • Method 영역 : JVM이 동작해서 클래스가 로딩될 때 생성 
  • Stack 영역 : 컴파일 타임 시 할당
  • Heap 영역 : 런타임시 할당 

 

JVM의 GC
더보기
  • JVM의 GC는 Mark And Sweep 의 로직으로 실행된다.
  • 이 로직은 의도적으로 GC를 실행시켜야하고, 애플리케이션 실행과 GC 실행이 병행된다. 
  •  

 

직렬화 
더보기
  • 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트형태로 데이터 변환하는 기술
  • 각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖기 때문에 레퍼런스 타입의 데이터들은 인스턴스를 전달할 수 없다. 따라서 주소값이 아닌 Byte형태로 직렬화된 객체 데이터를 전달해야 한다. 
  • 직렬화된 데이터들은 모두 기본형이다. 
  • 직렬화 대상 : 인터페이스를 상속받은 객체, 기본형 타입의 데이터 (기본형 타입이 아닌 주소값을 지닌 객체들은 바이트로 변환하기 위해 Serializable 인터페이스를 구현해야 한다.)

 

SeiralversionUID를 선언하는 이유

  • JVM은 직렬화와 역직렬화를 하는 시점의 클래스에 대한 버전 번호를 부여하는데, 만약 그 시점에 클래스의 정의가 바뀌어 있다면 새로운 버전 번호를 할당한다. 따라서 직렬화할 때의 버전 번호와 역직렬화를 할 때의 버전 번호가 다르면 역직렬화가 불가능하게 될 수 있기 때문에 이런 문제를 해결하기 위해 SerialVersionUID를 사용한다. 
  • 만약 직렬화할 때 사용한 SerialVersionUID의 값과 역직렬화 하기 위해 사용했던 SVUID가 다르다면 InvalidClassException이 발생할 수 있다. (역직렬화에 실패하는 상황에 대한 예외처리는 필수로 구현한다.)

 

+ 클래스 변경을 개발자가 예측할 수 없을 때는 직렬화 사용을 지양한다.

개발자가 직접 컨트롤 할 수 없는 클래스(라이브러리 등)은 직렬화 사용을 지양한다.

자주 변경되는 클래스는 직렬화 사용을 지양한다. 

직렬화 데이터는 타입, 클래스 메타정보를 포함하므로 사이즈가 크다. 트래픽에 따라 비용 증가 문제가 발생할 수 있기 때문에 JSON 포맷으로 변경하는 것이 좋다. 

 

스택 오버플로우 
더보기
  • Stack Overflow는 Stack 영역의 메모리가 지정된 범위를 넘어갈 때 발생한다.
  • 스택포인터가 스택의 경계를 넘어설 때 일어난다. 호출 스택은 제한된 양의 주소공간을 이루며 프로그램 시작 시 결정된다.

 

Stack 메모리는 보통 지역 변수가 저장되는 영역이다.

함수에서 지역 변수를 선언하면 지역 변수는 Stack 메모리에 할당되고 함수를 빠져나오면 Stack 메모리에서 해제된다.

하나의 프로그램이 실행될 때 수많은 함수를 호출하고 빠져 나오게 되는데 그 때마다 함수에서 사용하는 지역 변수는 Stack 영역에 할당되고 해제되는 것을 반복하게 되며 그에 따라 사용하는 Stack 영역도 변하게 된다.

 

만약 한 함수에서 너무 큰 지역 변수를 선언하거나 함수를 재귀적으로 무한정 호출하게 되면 stack overflow가 발생할 수 있다.

 

stack overflow가 발생하면 컴파일러 옵션에서 stack 영역의 크기를 늘리거나 또는 함수에서 사용하는 지역 변수의 크기를 줄이거나 아니면 지역 변수를 전역 변수로 바꾸어 해결할 수 있다.

 

메모리 누수
더보기
  • 프로그래밍에서 메모리 누수현상은 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상입니다.
  • 자바에서 메모리누수는 더 이상 사용하지 않는 객체가 가비지컬렉션(GC)에 의해 회수되지 않고 누적되는 현상입니다. old영역에 누적된 객체로 인해서 메이저 GC가 빈번히 발생하게 되고 프로그램의 응답속도가 늦어지다 결국 Out of memory 오류로 프로그램이 종료됩니다.

 

메모리 누수 예

  1. Integer, Long 같은 래퍼 클래스(Wrapper)를 이용하여, 무의미한 객체를 생성하는 경우
  2. 맵에 캐쉬 데이터를 선언하고 해제하지 않는 경우
  3. 스트림 객체를 사용하고 닫지 않는 경우
  4. 맵의 키를 사용자 객체로 정의하면서 equals(), hashcode()를 재정의 하지 않아서 같은 키로 착각하여 데이터가 계속 쌓이게 되는 경우
  5. 맵의 키를 사용자 객체로 정의하면서 equals(), hashcode()를 재정의 하였지만, 키값이 불변(Immutable) 데이터가 아니라서 데이터 비교시 계속 변하게 되는 경우
  6. 자료구조를 생성하여 사용하면서, 구현 오류로 인해 메모리를 해제하지 않는 경우

막는 방법

가장 좋은 방법은 참조값을 갖는 변수가 최소한의 유효범위안에 있도록 하는 것입니다. Local변수로 만들 경우 자동으로 GC의 대상이 되는 것을 예로 들 수 있습니다.

 

Wrapper Class Boxing & UnBoxing
더보기
  • 기본 자료형에 대한 객체 표현을 Wrapper Class라 한다.
  • 기본 자료형 -> Wrapper Class로 변환하는 것을 Boxing, 그 반대를 unBoxing이라 한다. 

 

Final 키워드 
더보기
  • 변수나 메소드 또는 클래스가 ‘변경 불가능’하도록 만드는 자바 키워드
  • Final로 선언 시, 무조건 초기화를 해야하며 프로그램 실행 도중 값 수정이 불가능하다. 
  • 사용 이유
    • 클래스, 메서드, 변수가 변하지 않도록 하기 위해
    • 절대 변하지 않느 값, 상수의 개념이 된다.
    • 데이터를 ReadOnly로 만들어, 객체 내부에서 값의 변환이나 가공을 하지 않겠다고 선언하여 데이터를 지킬 때 사용된다.
  • Final변수는 한 번 초기화되면 그 이후에 변경할 수 없다.
  • final 메소드는 다른 클래스가 이 클래스를 상속할 때 메소드 오버라이딩을 금지한다.
  • final 클래스는 다른 클래스에서 이 클래스를 상속할 수 없다. 

 

불변객체
더보기
  • 불변 객체는 객체 생성 이후 내부의 상태가 변하지 않는 객체를 말한다.
  • Java에서는 필드가 원시 타입인 경우 final 키워드를 사용해 불변 객체를 만들 수 있고, 참조 타입일 경우엔 추가적인 작업이 필요하다.

+) 참조타입일 경우 추가적인 작업을 어떻게 해야하나?

참조타입은 일반적으로 객체참조, 배열참조, List 참조 등이 있다.

객체 참조 : 객체를 사용하는 필드의 참조변수도 불변 객체로 변경해야 한다.

배열 : 배열을 받아 copy해서 저장하고, getter를 clone으로 반환하도록 하면 된다. (외부에서의 값 변경을 막는다.)

List: 새로운 List를 만들어 복사하도록 한다. 

 

배열과 리스트는 내부를 복사하여 전달하는데, 이를 방어적 복사라고 한다. 

 

+) 불변객체 사용하는 이유 

  • Thread-Safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다. (공유 자원이 불변이기 때문에 항상 동일한 값을 반환하기 때문이다.)
  • 실패 원자적인 메소드를 만들 수 있다. (어떠한 예외가 발생하더라도 메소드 호출 전의 상태를 유지할 수 있어 예외 발생 전과 똑같은 상태로 다음 로직 처리 가능하다.)
  • 부수효과를 피해 오류를 최소화 할 수 있다. (부수효과: 변수의 값이 바뀌거나 객체의 필드 값을 설정하거나 예외나 오류가 발생하여 실행이 중단되는 현상)
  • 메소드 호출 시 파라미터 값이 변하지 않는다는 것을 보장할 수 있다.
  • 가비지 컬렉션 성능을 높일 수 있다. (가비지 컬렉터가 스캔하는 객체의 수가 줄기 때문에 GC수행 시 지연시간도 줄어든다.)

 

Java가 다중 상속을 지원하지 않는 이유 
더보기
  • 다이아몬드 문제가 발생할 수 있기 때문이다. 예를 들어 Human 클래스에 있는 walk() 메소드를 Female 클래스와 Male 클래스가 모두 구현하고 각각 오버라이딩 했다고 가정할 때, Female과 Male 클래스를 다중 상속받은 Person 클래스의 입장에서는 코드의 충돌이 생긴다. 

 

Static
더보기
  • Static키워드를 사용한다는 것은 어떠한 값이 메모리에 한번 할당되어 프로그램이 끝날 때 까지 그 메모리에 값이 유지된다는 것을 의미한다. 쉽게 말해 특정한 값을 공유해야하는 상황이라면 Static 사용 시 메모리의 이점을 얻을 수 있다. (GC관리 영역 밖에 존재한다)
  • 정적 필드와 정적 메소드는 인스턴스(객체)가 아닌, 클래스에 고정되어, 클래스 로더가 클래스를 로딩해 메모리 영역에 적재할 때, 클래스별로 관리되어 정적 메모리로 사용할 수 있다.
  • 정적 멤버는 클래스가 메모리에 올라갈 때 자동적으로 생성되기 때문에 인스턴스를 따로 생성하지 않아도 호출하여 사용할 수 있다. 따라서 객체 생성 비용을 줄일 수 있다. 단점은 메모리에 올라가는 것이기 때문에 남용할 경우 메모리를 낭비할 수 있다.

 

Java의 main 메서드가 static 인 이유
더보기

메모리가 초기화 된 순간 객체는 하나도 존재하지 않기 때문에 객체 멤버 메서드는 바로 실행할 수 없다. 따라서 객체의 존재 여부에 관계 없이 쓰기 위해 static이어야 한다. static 멤버는 JVM 구동 시 스태틱 영역에 바로 배치된다.

 

Synchronized
더보기
  • 여러 쓰레드가 하나의 자원을 이용하고자 할 때, 한 스레드가 해당 자원을 이용하고 있을 경우 다른 스레드가 접근할 수 없도록 막는 키워드. synchronized 키워드를 이용하면 병렬 상황에서 자원의 접근을 안전하게 하지만, 자원을 이용하지 않는 쓰레드는 락에 의한 병목현상이 발생하게 됩니다.
  • 메소드 synchronized : 한 시점에 하나의 쓰레드만이 해당 메소드를 사용할 수 있다.
  • 변수 synchronized : 한 시점에 하나의 쓰레드만이 해당 변수를 참조할 수 있다.

 

Thread
더보기
  • 하나의 프로세스 내부에서 독립적으로 실행되는 하나의 작업단위를 말하며 세부적으로는 운영체제에 의해 관리되는 하나의 작업(task)를 의미합니다.
  • JVM에 의해 하나의 프로세스가 발생되고 main()안의 실행문들이 하나의 스레드입니다. main() 이외의 스레드를 만드려면 Thread클래스를 상속하거나 Runnable 인터페이스를 구현합니다.

 

String vs String Buffer vs String Builder
더보기

String

  • 불변성을 가진 객체
  • '+' 나 문자열을 수정,삽입하는 경우 새로운 문자열을 생성
  • 장점 
    • String Class의 객체는 불변하기 때문에 단순하게 읽어가는 조회 연산에서는 타 클래스보다 빠르게 읽을 수 있다.
    • Immutable(불변)하기 때문에 멀티쓰레드 환경에서 동기화를 신경 쓸 필요가 없다. (Thread-Safe)
  • 단점
    • 문자열 연산('+', concat 등)이 많이 일어나는 경우, 더이상 참조되지 않는 기존 객체는 GC에 의해 제거되야하기 때문에 성능이 좋지 않다.
    • 문자열 연산이 많아질 때 연산 내부적으로 char 배열을 사용하고, 계속해서 객체를 만드는 오버헤드가 발생하므로 성능이 떨어진다.

String Buffer, String Builder

  • 문자열 객체를 버퍼에 임시로 저장합니다.
  • 수정, 삭제, 삽입 시 버퍼에 저장된 문자열 객체를 수정하고 ToString() 메서드로 문자열을 반환
  • 차이점 
    • 동기화의 유무 : StringBuffer는 각 메서드별로 Synchronized Keyword가 존재하여, Multi-Thread 환경에서도 동기화를 지원하여 Thread-Safe한다.
    • StringBuilder는 동기화를 보장하지 않는다. 하지만 StringBuilder는 Single-Thread 환경에서 동기화를 고려하지 않기 때문에 StringBuffer에 비해 연산처리가 빠르다.

<사용되면 좋을 때>

String : 변하지 않는 문자열을 자주 사용할 경우

String Builder : 단일 스레드 환경과 문자열의 추가, 수정, 삭제 등이 빈번하게 발생하는 경우

String Buffer : 멀티 스레드 환경과 문자열의 추가,수정,삭제 등이 빈번하게 발생하는 경우

 

<String객체가 불변이라 좋은 점?>

1. 캐싱 기능에 의한 메모리 절약과 속도 향상

  • Java에서 String 객체들은 Heap의 String Pool 이라는 공간에 저장되는데, 참조하려는 문자열이 String Pool에 존재하는 경우 새로 생성하지 않고 Pool에 있는 객체를 사용하기 때문에 특정 문자열 값을 재사용하는 빈도가 높을 수록 상당한 성능 향상을 기대할 수 있다.

2. thread-safe

  • String 객체는 불변이기 때문에 여러 쓰레드에서 동시에 특정 String 객체를 참조하더라도 안전하다.

3. 보안기능

  • 중요한 데이터를 문자열로 다루는 경우 강제로 해당 참조에 대한 문자열 값을 바꾸는 것이 불가능하기 때문에 보안에 유리하다.

 

자바 버전 별 특징
더보기

JAVA 8

  • 람다
  • Stream
  • Optional
  • 정적 메소드 참조
  • Date와 Time을 함께 처리하기 위한 LocalDateTime API 등이 추가되었다. 

JAVA 9

  • Modules(패키지의 묶음)
  • 인터페이스 메서드 private 가능
  • 불변 컬렉션 생성 (List.of() / Set.of() / Map.of())
  • Optional API 추가 
    • or() : 값이 없을 경우 Optional 객체를 리턴
    • ifPresentOrElse() : 비어있을 경우 처리할 내용을 추가 
    • stream() : Optional 객체를 Stream 객체로 변환 

JAVA 11

  • String 메소드 추가 
    • Strip() : 문자열 앞, 뒤의 공백 제거 
    • StripLeading() : 문자열 앞의 공백 제거 
    • isBlamk() : 문자열이 비어있거나, 공백만 포함되어 있을 경우 true를 반환 (String.trim().isEmpty()와 결과가 동일)
  • 람다에서 로컬 변수 var 사용 

 

'CS' 카테고리의 다른 글

[CS] 프로그래밍 공통  (0) 2023.01.31